Willow
Lab Info
| Detail | Info |
|---|---|
| Room | Willow |
| Platform | TryHackMe |
| Difficulty | Hard |
| Type | CTF / NFS Enumeration + RSA Key Reconstruction + Sudo Mount PrivEsc + Steganography |
| Knowledge Required | NFS, RSA (modular exponentiation), John the Ripper, Linux Sudo Rules, Steghide |
Tools Used
Nmap / RustScan — Port scanning
showmount/mount -t nfs— NFS export enumeration and mountingGobuster / dirsearch — Directory enumeration on the web server
CyberChef (Hex → Text) — Decoding the homepage's hidden message
Python (
pow(c, d, n)) or an online RSA calculator — Reconstructing the private key from raw RSA parametersssh2john + John the Ripper — Cracking the reconstructed key's passphrase
steghide — Extracting the actual root flag from an image file
First Impressions
MuirlandOracle's rooms have a reputation for this exact flavor of "nothing is what it looks like," and Willow delivers fully — there's no traditional user.txt, the root.txt you eventually read is a taunt rather than the flag, and the actual privilege escalation path is a single, oddly specific sudo rule that has no entry on GTFOBins at all. Genuinely one of the more creative rooms out there: the core skill being tested is reconstructing a working RSA private key from nothing but its raw mathematical parameters, by hand, before you can even attempt to log in.
Recon
nmap -A -T4 -p- <ip>
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
111/tcp open rpcbind
2049/tcp open nfs
NFS running alongside a web server is the first thing worth noting — worth checking what's actually exported before anything else.
NFS — An Open Export With RSA Parameters Inside
showmount -e <ip>
Export list for <ip>:
/var/failsafe *
The * means anyone can mount it, no host restriction at all.
mkdir /mnt/willow
mount -t nfs <ip>:/var/failsafe /mnt/willow -o nolock
ls -la /mnt/willow
A single file, rsa_keys:
cat /mnt/willow/rsa_keys
Public Key Pair: (23, 37627)
Private Key Pair: (61527, 37627)
Recognizable immediately as RSA parameters — a public exponent/modulus pair (e, n) and a private exponent/modulus pair (d, n). Not a usable key file on its own yet — just the raw numbers a key would be built from.
The Website — A Wall of Numbers
Gobuster doesn't turn up much extra on port 80 — the homepage itself is the payload. Viewing it (or fetching it directly and converting from hex) reveals a message:
Hey Willow, here's your SSH Private key -- you know where the decryption key is!
2367 2367 2367 2367 2367 9709 8600 28638 18410 ...
Followed by hundreds of space-separated integers. Each number here isn't a coincidence — it's an individual RSA-"encrypted" byte, and the rsa_keys file from the NFS share is exactly what's needed to decrypt them back into plaintext.
Reconstructing the Private Key — RSA by Hand
Standard RSA decryption is just modular exponentiation: m = c^d mod n. Applying that to every number in the sequence, one at a time, using the recovered d and n:
d = 61527
n = 37627
with open("rsa.txt") as f:
numbers = [int(x) for x in f.read().strip().split(" ")]
decoded = "".join(chr(pow(c, d, n)) for c in numbers)
with open("id_rsa", "w") as out:
out.write(decoded)
The output isn't a decoded English sentence — it's the actual PEM-formatted RSA private key itself, headers and all (-----BEGIN RSA PRIVATE KEY----- and an AES-128-CBC-encrypted body), reconstructed byte by byte purely from its own mathematical parameters. A genuinely satisfying "aha" once it clicks: the numbers on the webpage were never a message to read, they were ciphertext for a file to rebuild.
Cracking the Key's Passphrase
chmod 600 id_rsa
Attempting to SSH with the freshly rebuilt key confirms it's passphrase-protected (the standard Proc-Type: 4,ENCRYPTED header gives it away even before trying). Converting and cracking it is routine from here:
ssh2john id_rsa > id_rsa.hash
john id_rsa.hash --wordlist=/usr/share/wordlists/rockyou.txt
Cracks quickly against rockyou.
Logging In — A Legacy Key Type Wrinkle
ssh -i id_rsa willow@<ip>
On a modern OpenSSH client, this can fail outright with something like no mutual signature supported or sign_and_send_pubkey errors — this key type predates current default-disabled legacy RSA signature algorithms. The fix is forcing them back on explicitly:
ssh -i id_rsa -o PubkeyAcceptedAlgorithms=+ssh-rsa -o HostkeyAlgorithms=+ssh-rsa willow@<ip>
(Older OpenSSH clients may only need the older flag name, -o PubkeyAcceptedKeyTypes=ssh-rsa.) Enter the cracked passphrase when prompted — in as willow.
No user.txt — Just an Image
ls -la /home/willow
No flag file at all — instead, user.jpg sitting in the home directory. First sign that the room isn't going to hand anything over the normal way; this file gets revisited once there's an actual password to try against it with steganography.
scp -i id_rsa willow@<ip>:/home/willow/user.jpg .
PrivEsc — A Very Specific Sudo Rule
sudo -l
(ALL : ALL) NOPASSWD: /bin/mount /dev/*
An oddly narrow rule — willow can run mount, but only against something matching /dev/*. No direct GTFOBins entry covers this exact restriction, so the next move is just checking what's actually sitting in /dev:
ls -la /dev | grep "^b"
brw-rw---- 1 root disk 202, 5 hidden_backup
brw-rw---- 1 root disk 202, 0 xvda
brw-rw---- 1 root disk 202, 1 xvda1
...
hidden_backup stands out immediately next to the expected disk device names — a block device that clearly isn't part of the normal system layout. Since the sudo rule allows mounting any device node under /dev/, mounting this one specifically is the obvious next move:
mkdir /home/willow/bcp
sudo mount /dev/hidden_backup /home/willow/bcp
ls -l /home/willow/bcp
-rw-r--r-- 1 root root 42 creds.txt
cat /home/willow/bcp/creds.txt
root:<recovered_password>
willow:<recovered_password>
Root's actual password, sitting on a filesystem that was never mounted by default — accessible purely because the sudo rule allowed mounting arbitrary device nodes, with no restriction on where they get mounted or who can then read the result.
The Twist — root.txt Isn't the Flag
su root
cat /root/root.txt
The file exists, but its contents are a taunt rather than a flag — something to the effect of "this would be too easy... I actually gave you the root flag some time ago... you've got my password now, go find your flag." The room is telling you directly: the actual flag was handed over earlier, disguised as something else — which points straight back at user.jpg.
Extracting the Real Flag — Steghide With Root's Password
steghide extract -sf user.jpg
Enter passphrase:
Using root's just-recovered password as the steganography passphrase (not a login credential at all in this context) extracts the actual flag file cleanly from inside the image.
cat root.txt
THM{redacted}
Flag Summary
| Flag | Method |
|---|---|
| "User flag" | No standalone file — recovered by mounting NFS for RSA parameters, reconstructing a PEM private key via modular exponentiation from a webpage's number sequence, cracking its passphrase with John, then SSHing in as willow |
| "Root flag" | sudo mount abused against a hidden block device to recover root's password, then that same password used as a steghide passphrase to extract the real flag hidden inside user.jpg |
(Actual flag string, RSA parameter values, and recovered passwords are per-deployment and are not reproduced verbatim here — the room's parameters are randomized per instance, confirmed by differing values across multiple independent writeups of this same room.)
Key Takeaways
RSA parameters found separately from their ciphertext are meant to be combined, not read in isolation. The
rsa_keysfile and the webpage's number sequence look unrelated at first glance — recognizing that one is the key material and the other is ciphertext encrypted with that key is the entire puzzle."Encrypted with RSA" doesn't only mean "a message got scrambled" — RSA can encrypt arbitrary binary data, including a private key file itself. The reconstructed plaintext here wasn't a sentence to read, it was PEM-formatted key material, byte for byte.
A narrow, oddly specific sudo rule (
/bin/mount /dev/*) not appearing in GTFOBins doesn't mean it's unexploitable — it means the exploit is specific to what's actually sitting on the system rather than a documented generic trick. Checking/devfor anything that doesn't belong is the natural next step whenevermountshows up insudo -l.A "final" flag that reads as a taunt instead of a real flag is a deliberate signal, not a bug or a wrong turn. MuirlandOracle-style rooms use this pattern specifically to force re-examining something dismissed earlier (
user.jpg) rather than continuing to dig for a conventional file that was never going to appear.The same recovered credential can serve two completely different purposes in the same room — root's password worked first as a login credential, then again as a steganography passphrase entirely unrelated to authentication. Worth testing a recovered secret against every locked artifact still outstanding, not just the login prompt it was "obviously" meant for.
Day 29 of 30 ✅ — See You Tomorrow