嘿嘿,时隔几个月没有发公众号文章了,最近在学习,没有怎么看公众号(公众号也是随缘发,哈哈哈哈(狗头🐶保命~)),这几个月正好学了学PHP反序列化,文章之前发在了我的小博客(没几篇文章😆),正好发一下公众号文章 🤣(划水),额,学习的时候总结的笔记,佬们别喷俺。😵
⚡概念
在学PHP字符串逃逸之前先了解一下原理是什么,字符串逃逸的原理其实就是让字符串变成可以执行的序列化代码。在序列化和反序列化这个中间过程中,序列化字符增加或减少后,再去反序列化可能会发生属性逃逸。
额,可能会有点不太理解,没关系,进一步解释
⚡了解字符逃逸前需要的补充
一般我们进行反序列化时候常规方式是这样的
class test{
public $aumg = '111';
public $id = 'aaasystem();';
}
$a = serialize(new test());
echo $a;
var_dump(unserialize($a));
先去实例化一下对象类,然后进行序列化/反序列化的操作,输出结果分别如下:
O:4:"test":1:{s:2:"id";s:12:"aaasystem();";}
object(test)#1 (1) {
["id"]=>
string(12) "aaasystem();"
}
那么正常的是这样进行一个操作,那么我们就不正常,我们就想让这个test()
类去反序列化的时候输出点其他的类属性,就比如我想要反序列化输出时加一个类属性,或者直接修改存在的类属性,那么需要怎么操作呢?可以先想一想,再来继续往下看。
一般字符串逃逸会有一个类似于替换字符串的功能函数,例如php中的str_replace()
,以下面的代码来展开讲解:
讲解逃逸前先来补充一下知识点:
🌼坑一、
举例
class test{
public $id = 'lslsls';
public $ss = '123';
}
$a = serialize(new test());
$b = str_replace('ls','pwd',$a);
var_dump($b); //这里使用var_dump()方法来抑制报错,错误直接返回false,正确直接输出反序列
$id
值为字符串ls
,这段代码很容易理解,就不解释了,运行一下,输出结果为:string(59) "O:4:"test":2:{s:2:"id";s:6:"pwdpwdpwd";s:2:"ss";s:3:"123";}"
输出了一个总共有59个字符拼接成的序列化字符串,并且发现类的$id
属性的值已经被字符串pwd
进行了替换,,序列化出来的结果再去进行反序列化,能不能执行呢?思考一下。
进行反序列化:
class test{
肯定不能,输出结果为:
public $id = 'lslsls';
public $ss = '123';
}
$a = serialize(new test());
$b = str_replace('ls','pwd',$a);
$c = unserialize($b);
var_dump($c);
因为序列化之后的结构是这样的:
O:类名的长度:类名:类中属性个数:{第一个类属性的种类:类属值的字符长度:"类属性名称";第一个类属性值的种类:类属值的字符长度:"类属性值名称";}
为什么反序列化不出来对比一下哪里出错了
可以发现类属性的值与他对应的字符串长度不一致,本应为9,但是反序列化时候是6,所以输出的值为false
🌼坑二、
还是以下面的代码举例:
class test{
public $id = 'lslsls';
public $ss = '123';
}
$a = serialize(new test());
var_dump($a);
输出结果为:
string(56) "O:4:"test":2:{s:2:"id";s:6:"lslsls";s:2:"ss";s:3:"123";}"
这个一定可以反序列化,但是我们用这个序列化的结果再去构造一下:
O:4:"test":2:{s:2:"id";s:6:"lslsls";s:2:"ss";s:3:"123";}:s:4:"wang";s:1:"q";};
想一想这个反序列化后得到的结果会是什么?
反序列化代码:
class test{
输出结果为:
public $id = 'lslsls';
public $ss = '123';
}
$a = serialize(new test());
$b = 'O:4:"test":2:{s:2:"id";s:6:"lslsls";s:2:"ss";s:3:"123";}:s:4:"wang";s:1:"q";}';
$c = unserialize($b);
var_dump($c);
可以看到,我们构造的 :s:4:"wang";s:1:"q";}
并没有被反序列化,这是为什么呢?
这是因为在序列化之后的结果,也就是O:4:"test":2{s:2:"id";s:6:"lslsls";s:2:"ss";s:3:"456";}
这一部分如果没有一点差错,结构完整,可以被反序列化的情况下;}
就是反序列化的结束。当然,这里并不是说只要是;}
就是反序列化的结束,可以举一个例子:
O:4:"test":2:{s:2:"id";s:4:"ls;}";s:2:"ss";s:3:"456";}
假如这个序列化的结果成立,那么这种说法(;}
就是反序列化的结束) 就是错误的,为什么这样说,因为如果不能反序列化(输出结果如果为bool(false)),证明到了第一个$id
类属性值这个地方的;}
反序列化就已经不在进行了,因为序列化结构已经出错了,这里不懂的可以向上翻,上面介绍了这是为什么;那么如果不出错,则就是直接可以反序列化出来所有属性(包括类属性ss
和提所对应的值123
)
完整代码如下:
class test{
运行结果为:
public $id = 'ls;}';
public $ss = '123';
}
serialize(new test());
$b = 'O:4:"test":2:{s:2:"id";s:4:"ls;}";s:2:"ss";s:3:"456";}';
$c = unserialize($b);
var_dump($c);
这里就不再陈述为什么没有反序列化出来ss
为123
的原因了,可以自己去动手试一下序列化类之后,在反序列化之前修改属性值是以原有的类中的属性值为结果还是反序列化之前修改后的类属性值为结果,这里判断;}
是不是结束是以前面对应的长度决定的,这里也就是s:4:
这个部分。
好啦,以上都是为了这个字符逃逸做铺垫,下面就开始正式讲解PHP反序列化之字符逃逸啦~😚
⚡字符逃逸—字符增加
具体什么是字符逃逸呢?字符逃逸我感觉就是属性逃逸(这个逃逸还有别的嘛欢迎大佬们斧正),假如我们有这样一个需求,我想让$ss
的值变成F0R
,那么可以这样构造(先看怎么个事,然后在进行解释):
我在下面会进行代码拆分,以至于方便每一步的理解。
假如代码是下面这样的:
class test{
public $id = 'lslsls';
public $ss = '123';
}
$a = serialize(new test());
$b = str_replace('ls','pwd',$a);
$c = unserialize($b);
var_dump($c);
可以看到,这个代码将类对象类进行了序列化,并且在后面进行了字符串的过滤,然后在进行反序列化,我们将代码拆分来看,先来看序列化之后是什么结果:
序列化代码:
class test{
public $id = 'lslsls';
public $ss = '123';
}
$a = serialize(new test());
echo $a;
输出结果为:
O:4:"test":2:{s:2:"id";s:6:"lslsls";s:2:"ss";s:3:"123";}
再来看一下字符替换之后是什么样:
字符替换代码:
class test{
public $id = 'lslsls';
public $ss = '123';
}
$a = serialize(new test());
$b = str_replace('ls','pwd',$a);
echo $b;
输出结果为:
O:4:"test":2:{s:2:"id";s:6:"pwdpwdpwd";s:2:"ss";s:3:"123";}
可以看到$id
的值被替换成了pwdpwdpwd
然而对应的字符长度还是6,实际上却是9,现在反序列化肯定是不行的,那么逃逸出来的字符也就是pwdpwdpwd
的最后一个pwd
,前面两个pwd
还是字符的形式,但是现在最后的pwd
属于功能性字符了,发现一个ls
对应一个pwd
从而逃逸出一个字符,那么我们是不是可以将逃逸字符构造一下使这个逃逸出的字符刚好够我们特意构造好的序列化字符的长度,进而成为功能字符,进行反序列化,可以这样理解,如果把最后的pwd
转换成n个pwd
,
使逃逸出来的pwd
的长度刚好把后面的字符串";2:s:"ss";s:3:"F0R";}
逃逸出来,使整个序列化字符在s:3:"F0R";}
的;}
处刚好结束,后面的;s:2:"ss";s:3:"123";}
就不再被反序列化了,这个不明白的可以向上翻,有讲到这个知识点,从而达到了修改类属性ss
的值,这个其实就叫做属性逃逸了,就是字符逃逸。
那么具体怎么实现呢?下面以基础的来讲。直接假如我们可以修改$id
的值,那么我们需要多少个ls
才可以恰好让构造的;s:2:"ss";s:3:"123";}
逃逸出来进而被反序列化呢,想一想,一个ls
替换成一个pwd
逃逸出来1个d
也就是一个字符,我们只需要将逃逸出来的d
填充字符串;s:2:"ss";s:3:"123";}
的长度就可以,数一数一共21位,也就是需要21个d
,21个pwd
,进而推出需要21个ls
,好啦,大致思路我们已经知道,下面就是构造payload啦~
代码:
class test{
public $id = 'lslslslslslslslslslslslslslslslslslslslslsls";s:2:"ss";s:3:"F0R";}';
public $ss = '123';
}
$a = serialize(new test());
$b = str_replace('ls','pwd',$a);
echo $b;
将代码拆开输出一下:
序列化代码:
class test{
public $id = 'lslslslslslslslslslslslslslslslslslslslslsls";s:2:"ss";s:3:"F0R";}';
public $ss = '123';
}
$a = serialize(new test());
echo $a;
结果为:
O:4:"test":2:{s:2:"id";s:66:"lslslslslslslslslslslslslslslslslslslslslsls";s:2:"ss";s:3:"F0R";}";s:2:"ss";s:3:"123";}
这里id
对应值的字符串总长度为66
字符串替换代码:
class test{
public $id = 'lslslslslslslslslslslslslslslslslslslslslsls";s:2:"ss";s:3:"F0R";}';
public $ss = '123';
}
$a = serialize(new test());
$b = str_replace('ls','pwd',$a);
echo $b;
输出结果为:
O:4:"test":2:{s:2:"id";s:66:"pwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwd";s:2:"ss";s:3:"F0R";}";s:2:"ss";s:3:"123";}
这里id
对应值的长度还是66,那么我们构造的";s:2:"ss";s:3:"F0R";}
是不是可以被正常反序列化了呢,直接反序列化一下就知道啦
反序列化代码:
class test{
public $id = 'lslslslslslslslslslslslslslslslslslslslslsls";s:2:"ss";s:3:"F0R";}';
public $ss = '123';
}
$a = serialize(new test());
$b = str_replace('ls','pwd',$a);
var_dump(unserialize($b));
运行结果为:
可以看到ss
被成功反序列化,此时逃逸就成功了,这就是字符逃逸的字符增加类型,还有一个字符减少类型
⚡字符逃逸—字符减少
他们的原理都是一样的,只不过字符减少就是具有字符替换的功能函数,从字符串长度长的替换成字符串长度短的,进而达到字符串可以变成功能字符,进而可以被成功反序列化。
举例代码:
class test{
public $id = 'pwdpwdpwd';
public $ss = '123";s:2:"ss";s:3:"F0R";}';
}
$a = serialize(new test());
$b = str_replace('pwd','ls',$a);
var_dump(unserialize($b));
序列化代码:
class test{
public $id = 'pwdpwdpwd';
public $ss = '123";s:2:"ss";s:3:"F0R";}';
}
$a = serialize(new test());
echo $a;
输出结果为:
O:4:"test":2:{s:2:"id";s:9:"pwdpwdpwd";s:2:"ss";s:25:"123";s:2:"ss";s:3:"F0R";}";}
字符替换代码:
class test{
public $id = 'pwdpwdpwd';
public $ss = '123";s:2:"ss";s:3:"F0R";}';
}
$a = serialize(new test());
echo str_replace('pwd','ls',$a);
输出结果为:
O:4:"test":2:{s:2:"id";s:9:"lslsls";s:2:"ss";s:25:"123";s:2:"ss";s:3:"F0R";}";}
可以看到原来的pwdpwdpwd
被替换成了lslsls
,但是值对应的长度还是9,实际为6,思考一下,如果想要修改ss
的值为F0R
,我们需要多少pwd
?可以根据上面讲的增多的思路,想一想这种类型怎么进行逃逸,拓展一下来实现下这个字符较少类型的字符逃逸。
如果想要实现修改ss
的值,是不是只要让已经存在的对应序列化字符吞并到id
的值中。让原本为功能字符的";s:2:"ss";s:25:"123
转变为id
字符串形式的值,这样在后面对应的";s:2:"ss";s:3:"F0R";}";}
不就是功能字符了嘛。
还是推一下,一个pwd
替换成一个ls
,于是便有一个字符的空位,";s:2:"ss";s:25:"123
一共有25个字符,所以我们需要25个ls
,然后需要25个pwd
,推完了,开始构造payload啦~😋
class test{
public $id = 'pwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwd';
public $ss = '123";s:2:"ss";s:3:"F0R";}';
}
$a = serialize(new test());
$b = str_replace('pwd','ls',$a);
$c = unserialize($b);
var_dump($c);
将代码拆开:
序列化代码:
class test{
public $id = 'pwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwd';
public $ss = '123";s:2:"ss";s:3:"F0R";}';
}
echo serialize(new test());
运行结果为:
O:4:"test":2:{s:2:"id";s:60:"pwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwd";s:2:"ss";s:25:"123";s:2:"ss";s:3:"F0R";}";}
字符替换代码:
class test{
public $id = 'pwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwd';
public $ss = '123";s:2:"ss";s:3:"F0R";}';
}
$a = serialize(new test());
echo str_replace('pwd','ls',$a);
输出结果为:
O:4:"test":2:{s:2:"id";s:60:"lslslslslslslslslslslslslslslslslslslsls";s:2:"ss";s:25:"123";s:2:"ss";s:3:"F0R";}";}
这里类属性id
的值到ss";s:25:"123
的123
这个地方就刚好凑够60个字符了,后面的s:2:"ss";s:3:"F0R";}
本来是字符串类型,现在逃逸了出来,从而转变成了可以被反序列化的功能字符了,验证一下:
执行反序列化代码:
class test{
输出结果为:
public $id = 'pwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwd';
public $ss = '123";s:2:"ss";s:3:"F0R";}';
}
$a = serialize(new test());
$b = str_replace('pwd','ls',$a);
$c = unserialize($b);
var_dump($c);
这就是字符逃逸的字符减少类型啦!🤗
⚡总结
其实,字符逃逸就是这个事,写的多少有点啰嗦,但是就是这个意思。🐽
2023,与君共勉,继续攀登
原文始发于微信公众号(朱厌安全团队):PHP反序列化—字符逃逸
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论