快速入门字符串逃逸——序列化和反序列化(未完待续)
快速入门字符串逃逸——序列化和反序列化(未完待续)
大家经常会在代码审议的时候看到unserialize()
或许有些人会跟我一样没有仔细深入的了解它,只是会运用它而已。
那么为了我们以后的学习和记忆我们还是得要认识一下这个老客户的。
第一篇unserialize()
1.读取规则——php是怎么读取它的内容的?
例子:把php想象成扫描仪
a:1:{s:4:"name";s:3:"cat";}
看到
a:1:扫描仪进入“数组模式”,它心里记下:“我要找 1 对键值对”。看到
{:开始读数组内部。看到
s:4:进入“字符串读取模式”。重点来了: 看到数字4后,它会找引号"。从引号后的第一个字符开始,它往后数 4 个字节。
数完 4 个后:它睁开眼,必须看到一个收尾的引号
"。如果没看到,直接报错;如果看到了,这个字符串读取成功。看到
;:表示这个字段结束,准备读下一个。
衍生知识点
1.那我们这里就可以涉及一个知识点——反序列化逃逸
1 | s:N....... |
若遇到 字符串替换 ,那么我们的php会怎么读呢?它是不是还是会很死板地读n个,反正不是只说了n个嘛?
而在后面,或许抓住它的盲点我们可以塞点魔法进去
情况 1:变长逃逸(把代码“挤”出来)
假设代码里有个过滤器:把 1 换成 66(1位变2位)。
构造输入:
你直接在输入框里填:111111111111111111111";s:3:"vip";s:1:"1";}
(注意:这里我填了 21个1,因为后面的代码全长 21位)
序列化:
PHP 帮你生成:s:42:"111111111111111111111";s:3:"vip";s:1:"1";}";
注意: 此时因为 s:42,它数了 21个1 和后面 21位代码,一共 42 个字符。
过滤器替换:
1 变成了 66。
字符串变成:
1 | s:42:"666666666666666666666666666666666666666666";s:3:"vip";s:1:"1";}"; |
(这里的 6 一共有 42 个)
PHP 反序列化:
- 扫描仪看到
s:42,开始往后数 42 个。 - 它数完 42 个字符时,正好落在6上面
- 剩下的
;s:3:"vip";s:1:"1";}
因为解析器认为变量已经在引号那里“读完了”,所以它会把剩下的分号、s、vip 全部当成下一条指令去解析。
情况 2:变短逃逸(把结构“吞”进去)
假设代码里有个过滤器:把 no 删掉(2位变0位)。
构造输入:
- 输入框 1 (User):填入
nononononononononono(10个no) - 输入框 2 (Pass):填入
A";s:4:"pass";s:3:"666";}
序列化:
a:2{s:4:"user";s:20:"nononononononononono";s:4:"pass";s:28:"A";s:4:"pass";s:3:"666";}";}
注意: 此时因为 s:20,所以它数了 10个 n 和 10个 o,一共 20 个字符。
过滤器替换: no 变成了 空。
字符串变成: a:2:{s:4:"user";s:20:"";s:4:"pass";s:28:"A";s:4:"pass";s:3:"666";}";}
PHP 反序列化:
- 扫描仪看到
s:20,开始往后数 20 个。 - 它数完 20 个字符时,正好落在你写的那个
A上。- 为什么?因为前面的
no没了,它必须往后借位,把中间的墙";s:4:"pass";s:28:"(19位)和你的A(1位)凑在一起数够 20 个。
- 为什么?因为前面的
- 剩下的
";s:4:"pass";s:3:"666";}就被丢在了外面,变成了真正的代码!
题外话:为什么不是输入11个no?
a:2{s:4:"user";s:22:"nonononononononononono";s:4:"pass";s:28:"A";s:4:"pass";s:3:"666";}";}
数到第22个的时候刚好是A后面的;,此时php反序列化发现没有引号和前面的引号闭合就结束了,会解析错误
总结:变长 vs 变短
| 类型 | 原理 | 你的操作 |
|---|---|---|
| 情况 1:变长逃逸 | 1个变2个,位置不够了。 | 挤出去:多出来的字符填满 s:N,把 Payload 顶出引号。 |
| 情况 2:变短逃逸 | 2个变0个,位置空出来了。 | 吞进来:名额不满 s:N,把后面的“墙”拉进引号。 |
计算逻辑:
1 | [替换产生的长度差] × [重复次数] = [你要挤出或吞掉的字符总长度] |
2.学过的反序列化题目类型总结:
数据流篡改
代码执行

