Lion Cipher Lab
Exploit and fix a PHP type-juggling authentication bypass
- AppSec / DevSecOps
- Intermediate
- ~45 min
- Free · no account required
Why it matters
PHP still runs a large share of the web, and the single most PHP-specific footgun is the loose comparison operator. When you write `==` or `!=`, PHP does not compare the two values as they are -- it TYPE-JUGGLES them: it picks a common type, silently coerces BOTH operands to it, and only then compares. That is convenient right up until the values are secrets. A login that checks `md5($password) == $stored_hash`, an API gate that checks `$token == $SECRET`, a reset flow that checks `strcmp($code, $real) == 0` -- every one of these can be walked straight past by an attacker who never learns the…
What you'll learn
- explaining what PHP TYPE JUGGLING is: the loose operators == and != silently coerce both operands to a common type before comparing, so values of different types can test equal
- performing the 'magic hash' (0e) attack: finding two different strings whose md5/sha1 both render as 0e[only digits], which == compares as 0 == 0 -> true, and a password whose hash is 0e... that equals the integer 0
- abusing string-vs-int coercion in a token check (0 == 'any-non-numeric-string' is true on PHP 7) and boolean coercion (true == 'any-non-empty-string' is true on every version)
- abusing strcmp/array behaviour: strcmp() given an array returns NULL (PHP 7) and NULL == 0 is true, so an array input slides past a strcmp(...) == 0 secret check; and sending a JSON body with a non-string type to force the same juggling
- building a small self-contained PHP login / token-verification endpoint that compares secrets with ==, != and strcmp, entirely on your own machine
- measuring the break honestly with a harness: N crafted inputs that bypass the vulnerable endpoint N/N, 0/N against the fixed endpoint, while M legitimate credentials still succeed on both -- reporting the exact N, M and PHP version
- proving why you must never rely on the engine version as a defence: running the SAME vulnerable code on PHP 8 vs PHP 7 and recording which bypasses the version change happens to block and which it does not
- applying the fix: strict === comparison, hash_equals() for secret/token comparison, password_verify() for stored password hashes, and strict type / JSON-schema checks that reject anything that is not the expected type
- naming the limits of each fix: strict_types does NOT change == ; hash_equals is for timing and needs string arguments (an array throws); a byte-exact comparison is necessary but you still need type validation on the input
Steps in this lab
- The one idea: == in PHP compares after it coerces types
- Get PHP running (local install OR the Docker image)
- See the juggling for yourself (before any auth code)
- Build the vulnerable auth endpoint
- Exploit it: six bypasses, no password -- measure N/N
- The version trap: run the SAME code on PHP 8 (honest negative)
- Fix it: strict ===, hash_equals(), password_verify(), type checks
- Re-measure: run the identical attack against the fix
- The limits of the fix -- what each piece does NOT do
- Do it better -- the code-review reflex