X marks the spot picoCTF 2021 Solution

Published: April 2, 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 1
    Test for XPath injection
    Observation
    I noticed the challenge hint explicitly called out XPath and the login page accepted free-form text input, which suggested the backend was embedding user-supplied strings directly into an XPath expression and could be broken with a mismatched quote.
    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: anything
    bash
    curl -X POST http://<server>/ --data "name=' or 'x'='x&password=pass"
    What didn't work first

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

    SQL comment syntax (--) has no equivalent in XPath, so the trailing condition is never silenced. The backend XPath parser sees the literal - characters as part of the string and the query either errors or fails to match. XPath tautologies must close with a balanced string like or 'x'='x rather than a comment.

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

    XPath string literals can be delimited by either single or double quotes, but the delimiter must match what the application used in its query template. If the server-side query wraps the input in single quotes, injecting double quotes does not break out of the string literal and the server returns a normal login failure with no injection effect. Observe the error or difference in response when using a single quote to identify the correct 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. The payload ' or '*'='x turns the query into:

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

    The * matches any node, and the contains(.) function tests whether the current node contains a substring. These are the building blocks of an XPath injection exploit.

  2. Step 2
    Brute-force the flag character by character
    Observation
    I noticed the server returned a distinct 'right path' success message only when the injected payload matched, confirming a boolean oracle, which suggested I could use XPath's contains() function to extract 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(., 'picoCTF{h') would also work for prefix extraction, but contains() succeeds here because the script always grows from the known picoCTF{ left anchor, making the substring unique in practice. The real danger is switching to contains() without the left anchor - searching for a bare character like 'a' matches anywhere in any node's text and gives false positives for characters that appear in other fields like 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 uses a case-sensitive check such as 'right path' and the server response varies (for example 'Right Path' or a slightly different phrase), the oracle always returns false and the flag never grows. Printing r.text for the first few requests confirms the exact success string before automating the loop.

    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.

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 follows the same root cause as SQL injection: user input is concatenated into a query language expression rather than supplied as a bound parameter, allowing an attacker to alter the query's logic. Boolean-blind injection, where the application's true/false response leaks secret data one bit at a time, applies equally to SQL, XPath, LDAP, and NoSQL query languages. The fix in every case is parameterized queries or allowlist input validation before any user value reaches a query constructor.

Related reading

Want more picoCTF 2021 writeups?

Useful tools for Web Exploitation

What to try next