tomghost
Target: tomghost (10.129.182.160) Room type: beginner boot-to-root — an outdated Apache Tomcat AJP connector chained into a Linux privilege-escalation misconfiguration. Final result: root shell, both flags.
This writeup explains not just what was run, but why — every command is broken down piece by piece, and every vulnerability is explained conceptually so the logic is followable, not just the keystrokes. The whole box hinges on one idea worth internalising: a single unpatched service (Tomcat 9.0.30) leaks a credential, and from there the box is a chain of "one secret unlocks the next user."
1. Environment
No /etc/hosts entry is needed here — the room is driven entirely by IP from the TryHackMe AttackBox. Two machines are in play:
- Attacker (AttackBox): 10.129.73.186
- Target (tomghost): 10.129.182.160
Everything in an AttackBox block runs on your machine; everything in a Target block runs inside the compromised shell.
2. Phase 1: Network Recon
Full port sweep first
AttackBox
Why this shape, and not -A -T5 -p- in one go: the common beginner habit is to fire a single heavyweight scan (version detection, OS detection and NSE scripts) across all 65,535 ports at the most aggressive timing. That is both slow and unreliable over a VPN — -T5 shrinks timeouts so aggressively that packets get dropped and real ports get missed. The professional pattern is two passes: a fast sweep to find which ports are open, then targeted enumeration on only those.
Command breakdown:
- nmap — Network Mapper, the industry-standard port and service discovery tool.
- -p- — scan all 65,535 TCP ports, not just Nmap's default ~1,000. Vulnerable services love hiding on non-standard ports.
- --min-rate 5000 — send at least 5,000 packets per second. This is what actually makes the scan fast, and it is more reliable than -T5 because it controls send-rate rather than blindly collapsing timeouts.
- -oN ports.txt — save normal-format output to a file, so you have a permanent record.
Result:
Targeted service and script scan
AttackBox
- -p 22,53,8009,8080 — only the ports we already found open, so this pass is fast.
- -sV — version detection. This is the key output; we need Tomcat's exact build number.
- -sC — default NSE scripts, which give ssh-hostkey, ajp-methods and http-title.
Result (trimmed):
What the output tells us, read like a pentester:
- 53 tcpwrapped — DNS responding to the handshake but revealing nothing usable. Dead end; ignore it.
- 8080 Apache Tomcat 9.0.30 — the version is the vulnerability. The AJP fix landed in 9.0.31, so this build is one release below the patch.
- 8009 ajp13 exposed to the network — the AJP connector should normally be bound to localhost. Its exposure is the misconfiguration; the version is the flaw. Port 8080's web UI is a rabbit hole — the door is 8009.
3. Phase 2: The AJP Vulnerability — Ghostcat (CVE-2020-1938)
What it is. AJP (Apache JServ Protocol) is a binary protocol Tomcat uses to talk to a front-end web server. Tomcat implicitly trusts AJP requests more than it trusts HTTP ones, including request attributes that control internal servlet routing. Ghostcat abuses three of those attributes — javax.servlet.include.request_uri, path_info and servlet_path — to make Tomcat serve any file under the webapp root as if it were a plain static resource.
Why it matters. That gives an unauthenticated arbitrary file read of anything inside the deployed app, most importantly WEB-INF/web.xml, the deployment descriptor, which is exactly where secrets tend to live. If an attacker can also get a file uploaded into the webapp, the same primitive escalates to remote code execution — not needed on this box. NVD scores it CVSS 3.1 9.8 CRITICAL.
How attackers use it. Point a Ghostcat PoC at the AJP port (8009) and request WEB-INF/web.xml. No login, no web-app interaction — the AJP connector hands the protected file straight over.
How defenders detect and prevent it. Patch Tomcat to 9.0.31 or later, disable the AJP connector if it is not used, or bind it to loopback with a secret / requiredSecret. Detection: AJP connections from non-loopback source IPs, or IDS signatures matching the Ghostcat attribute strings.
Real-world relevance. Ghostcat hit an enormous install base in 2020 because AJP was frequently left network-exposed by default. It remains a staple internal-network finding wherever legacy Tomcat lingers behind a load balancer.
Confirm a public exploit exists
AttackBox
- 48143.py — the standalone Python AJP file-read PoC. This is the one we want, because running it manually teaches the mechanics.
- 49039.rb — the Metasploit module. Ignore it here; we are doing this by hand.
4. Phase 3: Ghostcat File Read → web.xml
Snag worth noting: searchsploit -m 48143 is supposed to mirror the exploit into the current directory, but it silently did not land here. Rather than fight it, locate the source copy directly. On this AttackBox it lives under /opt/exploitdb/, not the older /usr/share/exploitdb/ path many guides assume.
AttackBox
- find / -name "48143.py" 2>/dev/null — search the whole filesystem for the file; 2>/dev/null discards the permission-denied noise so only real hits print.
- python2 48143.py <target> — the PoC was written for Python 2 (old print statements and socket handling). The AttackBox has python2, so run it there rather than porting to Python 3. The script opens an AJP13 connection to 8009 and requests WEB-INF/web.xml by default via the Ghostcat attribute trick, so no arguments are needed beyond the IP.
Result — the leaked descriptor:
The <description> block hands over a username:password pair for the skyfuck account. That is the foothold credential.
Observe here: why would valid SSH credentials sit in a web deployment descriptor? They would not, in a sane deployment — this is the CTF handing you the pivot. In a real engagement, web.xml more commonly leaks database connection strings, API keys or LDAP bind credentials. The class of finding, secrets in a protected config file that is now readable, is entirely real; only the neatness is contrived.
5. Flag 1: SSH Foothold (skyfuck) + user.txt
AttackBox
Once the prompt reads skyfuck@ubuntu:~$, orient yourself and grab the user flag:
Target — skyfuck
- id — confirm which user and groups you landed as (uid=1002(skyfuck)).
- ls -la — skyfuck's home holds two files that matter: credential.pgp (an encrypted message) and tryhackme.asc (a PGP private key). Note them; they are the pivot to merlin.
- cat /home/merlin/user.txt — the flag lives in merlin's home but is world-readable, so skyfuck can read it.
Result:
→ Flag 1 (user.txt) captured. Task 1 (+50).
6. Phase 4: PGP Key Crack → Lateral Movement to merlin
The reasoning a pentester applies: an encrypted message (credential.pgp) sitting right next to a private key (tryhackme.asc) is a self-contained puzzle. We cannot read the message directly, and the private key is itself passphrase-protected. So the chain is: crack the key's passphrase offline, import the key, decrypt the message, read the next user's credentials.
Pull both files back to the AttackBox
AttackBox
Direction matters with scp. scp user@target:/remote/path . only makes sense when you are the client and the target is remote to you, so run it from the AttackBox, not from inside the target's shell. Running it from inside the box tells the target to copy from itself to itself, which is pointless. A classic mix-up that wastes a minute if you are not watching your prompt.
Convert the private key to a crackable hash, then crack it
AttackBox
- gpg2john — extracts the S2K (string-to-key) hash that protects the private key into a format John understands. The output header confirms it: gpg, OpenPGP / GnuPG Secret Key, SHA1 S2K, AES256 cipher.
- john --wordlist=… — dictionary attack with rockyou. Cracks in under a second.
Result:
Critical distinction: what John recovered is the passphrase for the PGP key, not merlin's login password. The (tryhackme) suffix is just the key's user-ID label, not part of the secret. We use the passphrase in the next step to decrypt the message, and that decrypted output is merlin's actual password. Confusing these two is the most common place people get stuck on this box.
Import the key and decrypt the message
AttackBox
- gpg --import — loads the private key into your keyring so gpg can use it. pinentry will prompt for the passphrase to unlock the secret key.
- gpg --decrypt — decrypts the message with that now-unlocked key.
Two expected quirks: a cosmetic WARNING: cipher algorithm CAST5 not found in recipient preferences (decryption still succeeds), and if pinentry misbehaves in a headless terminal, force an inline prompt with gpg --pinentry-mode loopback --decrypt credential.pgp.
Result:
Switch to merlin
Do this on the box from your existing skyfuck session. You are already there, and in real engagements you frequently only have a shell as one user with no SSH for the next, so su is the reliable pivot.
Target — skyfuck to merlin
7. Flag 2: Privilege Escalation (sudo + zip) to Root
First move on any new user is always the same: enumerate sudo rights.
Target — merlin
merlin can run zip as root, with no password. That is game over.
What it is. zip is not a "safe" binary. It has a -TT option that specifies an external command to test an archive after it is built. When zip runs as root via this sudo grant, that test command executes as root too — a textbook GTFOBins abuse.
Why it matters. The vulnerability is not in zip's code, it is in the sudoers policy. Any binary that can shell out or run a sub-command becomes an instant root shell the moment it is granted passwordless sudo. zip, vim, find, tar, awk and dozens more are all on that list.
How defenders detect and prevent it. Never NOPASSWD a GTFOBins-listed binary. If merlin genuinely needs to zip something, scope it far more tightly or wrap it in a controlled script. Detection: sudo logs showing zip … -TT, or a root shell whose parent process is zip.
Real-world relevance. Over-broad sudoers entries are one of the most common Linux privesc findings in professional engagements, usually introduced for convenience during deployment and never tightened.
The GTFOBins zip escalation
Target — merlin
- TF=$(mktemp -u) — generate a random unused filename (-u produces the name only, it does not create the file). Just a throwaway name for the archive.
- sudo zip $TF /etc/hosts — build an archive containing any readable file; /etc/hosts is a convenient one.
- -T — after building, test the archive's integrity.
- -TT 'sh #' — override the test command with sh. The trailing # comments out the archive filename that zip would otherwise append, so only sh runs — and because the whole line is sudo'd, that shell spawns as root.
Result:
Target — root
→ Flag 2 (root.txt) captured. Task 2 (+100). Box complete.
8. Full Attack Chain Summary
| # | Stage | Vulnerability Class | Technique Used |
|---|---|---|---|
| — | Recon | — | nmap -p- --min-rate 5000, then targeted -sV -sC |
| — | Service enum | Outdated software (Tomcat 9.0.30) | Version detection flags a Ghostcat-vulnerable build |
| 1 | Unauth file read | AJP request-attribute injection — CVE-2020-1938 | Ghostcat PoC reads WEB-INF/web.xml |
| 1 | Credential access | Secrets in config file (CWE-538) | skyfuck creds leaked in the <description> block |
| 1 | Foothold | Valid-account reuse | SSH as skyfuck, then world-readable user.txt |
| — | Lateral movement | Cracked PGP passphrase (CWE-522) | gpg2john and John, decrypt credential.pgp, su merlin |
| 2 | Privilege escalation | Over-broad sudoers entry (CWE-269 / GTFOBins) | sudo zip -TT 'sh #' spawns a root shell |
No single flaw alone reached root. The chain moved from an unpatched service, to a leaked credential, to a cracked key, to a misconfigured sudo grant — each link handing over exactly what the next one needed.
9. Key Lessons
- Version strings are vulnerabilities. Tomcat 9.0.30 on the -sV output was the entire finding, one release below the Ghostcat patch. Always map exact versions against known CVEs before touching anything else.
- Split your scans. A fast --min-rate sweep to find open ports, then targeted -sV -sC on only those, is faster and more reliable than one heavyweight -A -T5 -p- pass that drops packets over a VPN.
- The exposed port is often the misconfiguration. AJP on 8009 should have been localhost-only. Whenever a service meant to be internal is reachable from your position, that exposure is worth investigating before the version even matters.
- A key next to a ciphertext is a puzzle to solve, not two random files. An encrypted blob and its passphrase-protected private key sitting together is a standard crack-then-decrypt-then-pivot pattern.
- Track exactly what each secret unlocks. The value John cracked was the key passphrase, not merlin's password — merlin's password came from the decrypted message. Conflating the two is the top stall point on this box.
- Watch your prompt with scp. user@target:/path . only makes sense run from the attacker box. If your prompt says skyfuck@ubuntu, you are on the target and telling it to copy from itself, a silent time-waster.
- sudo -l is the first thing you run as any new user. One line, NOPASSWD: /usr/bin/zip, ended this box. Any sudo-granted binary that can execute a sub-command is a root shell, and GTFOBins tells you exactly how.