一、PHP弱类型比较(松散比较 ==)
1.1 字符串 → 数字转换
原理:字符串与数字 == 时,字符串提取开头的数字(直到非数字),无数字则为0。
积累清单(直接可用):
| 目标 | 传入值示例 | 说明 |
|---|---|---|
| $var == 0 为真 | abc,0e123,0abc | abc→0,0e123→0 |
| $var == 1 为真 | 1abc,1e0,+1 | 提取数字1 |
| $var == “0” 为真 | 0e123,abc,0 | 注意 “0e123” == “0” 也为真(两边都转数字) |
1.2 布尔值比较
原理:true == x 时,x 转换为布尔值。除 0、""、null、[]、“0” 外均为真。
积累清单:
| 条件 | 绕过payload |
|---|---|
| if($var == true) | 1,abc,[1],true(字符串) |
| if($var == false) | 0,""(空字符串),null(不传或传null字符串? 注意:“null” 为真,应传空) |
1.3 NULL 比较
原理:NULL == x 时,x 为 “"、0、false、[] 等均成立。
积累清单:
| 条件 | 绕过 payload |
|---|---|
| $var == NULL | var=(空字符串),var=0,var=false |
1.4 in_array() / array_search()
原理:默认松散比较(第三个参数默认为 false),类型转换后判断。
积累清单:
// 目标数组
$allow = [0, 1, 2];
// 绕过:传入 "abc" 因为 "abc" == 0 → true,认为在数组内
// 绕过:传入 "1abc" 因为 "1abc" == 1 → true
常见绕过值:
若数组含 0:abc、0e123、null(作为字符串? 注意 “null” 不转换,需传空字符串?空字符串 == 0 为真)
若数组含 1:1abc、true(字符串 “true” 转数字0?不对,“true” == 1 为 false,因为 “true” 无数字→0。应传 “1abc”)
万能绕过:若数组元素为字符串如 “admin”,可传 0?因为 0 == “admin” 为 true(字符串无数字→0)。所以用数字0可绕过任何非数字字符串的in_array。
1.5 数组与字符串比较
原理:md5($_GET[‘a’]) == md5($_GET[‘b’]) 且无类型限制时,传数组使 md5([]) 返回 NULL。
积累清单:
| 场景 | payload |
|---|---|
| md5($a) == md5($b) | a[]=1&b[]=2 |
| md5($a) === md5($b) | 同上(NULL === NULL 为真) |
| sha1($a) == sha1($b) | a[]=1&b[]=2 |
注意:PHP 会报 Warning,但不影响结果。
二、MD5 碰撞(哈希绕过)
2.1 0e 碰撞(松散比较 ==)
原理:两个字符串的 MD5 值都以 0e 开头且后面全为数字,则 == 比较时均视为 0,判定相等。
积累清单:直接复制以下字符串到参数中。
全字母类(用于 ctype_alpha)
| 字符串 | MD5 值 |
|---|---|
| QNKCDZO | 0e830400451993494058024219903391 |
| aabg7XSs | 0e087386482136013740957780965295 |
| aabC9RqS | 0e041022518165728065344349536299 |
全数字类(用于 is_numeric 或纯数字限制)
| 字符串 | MD5 值 |
|---|---|
| 240610708 | 0e462097431906509019562988736854 |
| 129581926211651571912466741651878684928 | 0e395398819683822636810253424488792 |
| 314282422 | 0e784681856360276065847126705975 |
字母数字混合类(用于 ctype_alnum)
| 字符串 | MD5 值 |
|---|---|
| s878926199a | 0e545993274517709034328855841020 |
| s155964671a | 0e342768416822451524974117254469 |
| s214587387a | 0e848240448830537924465865611904 |
SHA1 0e 碰撞(用于 sha1 且 ==)
| 字符串 | SHA1 值 |
|---|---|
| aaroZmOk | 0e66507019969427134894567494305185566735 |
| aaK1STfY | 0e76658526655756207688271159624026011393 |
2.2 真正 MD5 碰撞(严格比较 ===)
原理:两个不同内容产生完全相同的 MD5 二进制/十六进制值。需要工具生成。
工具:fastcoll(生成两个不同文件,MD5 相同)
在线示例(短字符串碰撞较少,通常用二进制数据)。CTF 中若遇 === 且不能传数组,可能需要文件上传或 base64 编码的碰撞块。常用碰撞对(十六进制表示)可从网上获取,例如:
d131dd02c5e6eec4 693d9a0698aff95c 2fcab58712467eab 4004583eb8fb7f89
55ad340609f4b302 83e488832571415a 085125e8f7cdc99f d91dbdf280373c5b
d8823e3156348f5b ae6dacd436c919c6 dd53e2b487da03fd 02396306d248cda0
e99f33420f577ee8 ce54b67080a80d1e c69821bcb6a88393 96f9652b6ff72a70
与另一个略有差异的块。但这种长度较长,不适用于 GET 参数。实际做题时优先考虑数组绕过。
2.3 数组绕过(适用于 === 且无类型检查)
原理:md5([]) 返回 NULL,NULL === NULL 成立。
// 目标代码
if (md5($_GET['a']) === md5($_GET['b'])) { ... }
// 攻击
?a[]=1&b[]=2
同样适用于 sha1。
三、常见函数限制及其绕过对照表
| 函数/条件 | 要求 | 可用的绕过字符串/值 |
|---|---|---|
| ctype_alpha($s) | 全字母 | QNKCDZO, aabg7XSs, aabC9RqS |
| ctype_alnum($s) | 字母或数字 | s878926199a, QNKCDZO, 240610708 |
| is_numeric($s) | 数字字符串 | 240610708, 129581926211651571912466741651878684928, 0e123 |
| is_string($s) | 字符串 | 任意字符串,注意数组不行 |
| preg_match(’/^\d+$/’, $s) | 纯数字 | 同上数字碰撞串 |
| preg_match(’/^[a-z]+$/i’, $s) | 纯字母 | 同上字母碰撞串 |
四、做题快速排查步骤
拿到一个涉及比较的 PHP 题目,按顺序检查:
比较运算符是 == 还是 ===?
如果是 ==,优先考虑 0e 碰撞(哈希)或类型转换(如字符串 “abc” == 0)。
如果是 ===,考虑数组绕过或真正碰撞。
有无类型限制?
ctype_alpha → 必须用全字母碰撞串。
is_numeric → 必须用全数字碰撞串。
无限制 → 数组绕过最省事。
比较的是哈希函数?
md5、sha1 均可使用上述碰撞表或数组。
其他哈希(如 hash(‘md5’, …))同样适用。
能否传数组?
GET/POST 传 a[]=1 可行,则优先数组绕过(适用 ===)。
是否还有其他过滤(如长度、特殊字符)?
检查碰撞串长度是否满足。若长度限制很小(如小于5),则0e碰撞可能不存在,需另寻其他弱类型比较(如 “0” == “0e123”)。
五、本地生成新碰撞的脚本(应急用)
若已知碰撞表不满足过滤条件(例如要求首字符为 x 且全字母),可跑脚本:
import hashlib
import itertools
import string
# 生成纯字母组合(长度可调)
for length in range(1, 8):
for combo in itertools.product(string.ascii_letters, repeat=length):
s = ''.join(combo)
md5_val = hashlib.md5(s.encode()).hexdigest()
if md5_val.startswith('0e') and md5_val[2:].isdigit():
print(f"{s} -> {md5_val}")
数字碰撞遍历更简单:for i in range(10000000): 即可。
六、经验总结
松散比较 ==:记住 0e 碰撞表 和 字符串转数字为0 的规则。
严格比较 ===:记住数组绕过 ?a[]=1&b[]=2。
遇到限制:从表中筛选符合字符集的碰撞串。
没有现成碰撞:跑脚本或换思路(如利用 “abc” == 0 配合 in_array)。