Description
The SOAP stock-check endpoint fails to sanitize XML entities. Inject an external entity to have the backend disclose /etc/passwd.
Setup
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.
# 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># 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>Solution
Walk me through it- Step 1Set up Burp and intercept the requestInstall 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. - Step 2Sanity-check XXE with /etc/hostnameReplace 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
stockCheckroot with&xxe;as theproductIdvalue. 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,
SYSTEMtakes a single URI and the parser fetches it directly.PUBLICtakes a public identifier plus a system identifier (the URI) for catalog-based resolution. For XXE you almost always useSYSTEMbecause 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.
- Step 3Swap to /etc/passwd for the flagOnce /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 namedxxewhose 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_PROCESSINGin JAXP,resolve_entities=Falsein 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.