SOAP picoCTF 2023 Solution

Published: July 19, 2023

Description

The SOAP stock-check endpoint fails to sanitize XML entities. Inject an external entity to have the backend disclose /etc/passwd.

Install Burp Community, configure your browser to use the Burp proxy, and accept the Burp CA so HTTPS targets work.

In Burp Proxy, turn Intercept on. Click any Details button on the challenge site and the POST stops in your hands.

Right-click the intercepted request and Send to Repeater so you can iterate without re-clicking the page.

bash
# Sanity check first: file:///etc/hostname proves XXE works without touching sensitive files
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///etc/hostname"> ]>
<stockCheck><productId>&xxe;</productId></stockCheck>
bash
# Once XXE is confirmed, swap to /etc/passwd to grab the flag
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>
<stockCheck><productId>&xxe;</productId></stockCheck>
XXE in a SOAP endpoint, classic web-app territory. The web challenges and real-world bug patterns guide covers XXE, SSRF, and the family of XML-driven server bugs in more depth.
  1. Step 1Set up Burp and intercept the request
    Install Burp Community, point your browser proxy at 127.0.0.1:8080, install the Burp CA cert, then enable Proxy → Intercept → On. Clicking any Details button pauses the POST mid-flight.
    Learn more

    Burp Suite sits between your browser and the server, intercepting HTTP/HTTPS requests so you can read and modify them. Three pieces have to be in place before Intercept does anything useful: (1) Burp running and listening on 127.0.0.1:8080 by default, (2) your browser configured to use that as its HTTP/HTTPS proxy (FoxyProxy or system proxy settings), and (3) Burp's CA certificate installed in the browser's trust store so HTTPS targets do not trigger TLS errors.

    Once Intercept is on, every request the browser makes hits Burp first and waits for you. Click a Details button on the challenge page; in the Proxy tab a POST appears containing the SOAP <stockCheck><productId>...</productId></stockCheck> body. Right-click the request and pick Send to Repeater: that copies the full request (headers, cookies, body) into Burp's Repeater tab where you can edit and resend without going back through the browser. Forward the original from Intercept so the page does not stall.

  2. Step 2Sanity-check XXE with /etc/hostname
    Replace the productId child with an XXE entity pointing at /etc/hostname first. Less sensitive, smaller response, and either it works or you know XXE is filtered before you waste time on /etc/passwd.
    bash
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///etc/hostname"> ]>
    <stockCheck><productId>&xxe;</productId></stockCheck>
    Learn more

    The full body replacement matters: paste the XML declaration, the DOCTYPE with the entity definition, and the stockCheck root with &xxe; as the productId value. Just prepending the DOCTYPE to the existing body works only if the server is permissive about extra processing instructions. Replacing the whole thing is the dependable form.

    SYSTEM vs PUBLIC. In a DOCTYPE entity declaration, SYSTEM takes a single URI and the parser fetches it directly. PUBLIC takes a public identifier plus a system identifier (the URI) for catalog-based resolution. For XXE you almost always use SYSTEM because you control the URI and want the parser to fetch it verbatim.

    A successful response echoes the file contents inside the response body where the productId would normally appear. /etc/hostname is one short line (the container hostname), so the answer is unmistakable: a single word printed back to you. If you see an XML parse error or the original Product not found, XXE is either filtered or the response does not echo the parsed entity.

  3. Step 3Swap to /etc/passwd for the flag
    Once /etc/hostname comes back, change file:///etc/hostname to file:///etc/passwd. The flag is appended to /etc/passwd by the challenge.
    bash
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>
    <stockCheck><productId>&xxe;</productId></stockCheck>
    Learn more

    The DOCTYPE declaration <!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]> tells the parser: define an entity named xxe whose value is the contents of /etc/passwd. When the parser later resolves &xxe; in the document body, it substitutes the file contents. The server then includes that productId value in its response and you get the file back.

    /etc/passwd is world-readable on Linux: one line per user with username, UID, GID, home directory, and shell. CTF authors typically append the flag to that file for predictable XXE proofs. In real engagements the same primitive is used to read app config (/etc/nginx/nginx.conf), private keys, AWS credentials, or source code, so the technique generalises straight from CTF to production-impact bugs.

    The fix on the defender side is to disable external entity processing in the XML parser: FEATURE_SECURE_PROCESSING in JAXP, resolve_entities=False in lxml, etc. JSON APIs do not have an equivalent reference mechanism and avoid the whole class of bug.

Flag

picoCTF{XML_3xtern@l_3nt1t1ty_4db...}

Any file path works, but /etc/passwd proves the XXE and includes the picoCTF token in the response.

Want more picoCTF 2023 writeups?

Useful tools for Web Exploitation

Related reading

What to try next