Magic Hash Authentication Bypass
Magic hashes are a specific subclass of type juggling that deserves its own page because the grep patterns and the test inputs are different enough to be worth a dedicated checklist.
Why
A string matching /^0+e\d+$/ is interpreted by PHP as scientific notation
when one side of an == comparison is numeric: 0 * 10^digits = 0. Two
different inputs whose md5 (or sha1, crc32, etc.) hashes are both
0e<digits-only> collide under ==. No preimage attack needed — these
collisions are pre-discovered and listed publicly.
md5("240610708") === "0e462097431906509019562988736854"; // ← all digits after 0e
md5("QNKCDZO") === "0e830400451993494058024219903391"; // ← all digits after 0e
"240610708" == "QNKCDZO"; // false (strings differ)
md5("240610708") == md5("QNKCDZO"); // true (both juggle to 0)Any check shaped if (md5($input) == $expected) where $expected is a magic
hash is bypassable with any other magic-hash input.
Search patterns
# Hash function output compared with ==
rg -n '(md5|sha1|hash|crc32)\s*\(' --type=php | rg ' == '
# Variants seen in the wild
rg -n '(md5|sha1|hash)\s*\([^)]+\)\s*==' --type=php
rg -n '==\s*(md5|sha1|hash)\s*\(' --type=phpAnything compared with === or hash_equals(...) is safe from this class.
Everything else is a flag.
Test inputs (known magic-hash strings)
For MD5:
| Input | MD5 output |
|---|---|
240610708 | 0e462097431906509019562988736854 |
QNKCDZO | 0e830400451993494058024219903391 |
aabg7XSs | 0e087386482136013740957780965295 |
aaaXIA | 0e000045469718506715568090931435 |
0e215962017 | 0e291242476940776845150308577824 (output of md5 on its own output — chains break) |
For SHA-1:
| Input | SHA-1 output |
|---|---|
aaroZmOk | 0e66507019969427134894567494305185566735 |
aaK1STfY | 0e76658526655756207688271159624026011393 |
Larger lists circulate on GitHub; these are enough for the first round of testing. If the application uses a different hash function, generate one:
for i in $(seq 1 1000000); do
h=$(echo -n "$i" | md5sum | cut -d' ' -f1)
[[ "$h" =~ ^0+e[0-9]+$ ]] && echo "$i -> $h"
doneAudit focus
For every hash comparison performed with ==:
- What is the secret? Session token, password hash (yes, still common in custom auth), CSRF token, signature, license key, password-reset token, payment-callback HMAC.
- Does attacker input feed either side? Either the input being hashed or the value compared against — both sides matter.
- Is the expected value already a magic hash? If you can pick the
expected value to be a known
0e…hash (e.g. an installation that lets the admin set a token), you don’t even need a collision — you just need any of the canonical magic inputs. - Even with
===, is the comparison constant-time?===short-circuits on the first differing byte and leaks length / prefix timing. For real secret comparison,hash_equals()is the right call.
In WordPress plugins and one-off PHP apps, md5($_GET['key']) == $stored
is the most common shape. Always test with ?key=240610708 after grepping
md5( in any plugin source.
Fix
// Bad
if (md5($_GET['token']) == $stored_hash) { … }
// Better
if (md5($_GET['token']) === $stored_hash) { … }
// Best
if (hash_equals($stored_hash, md5($_GET['token']))) { … }For password storage specifically, password_hash + password_verify —
never raw md5 / sha1.