July 12, 2026
Whiterose (THM) Walkthrough
Big spoiler warning up front, this room is straight out of the Mr. Robot episode "409 Conflict," so if you haven't watched that far, go do…

By L4ZZ3RJ0D
7 min read
Big spoiler warning up front, this room is straight out of the Mr. Robot episode "409 Conflict," so if you haven't watched that far, go do that first and come back. Also if you're new here and you see a terminal in any of these screenshots, that's Ghostty, not some fancy custom build, just my terminal of choice these days.
We get dropped straight into the Mr. Robot universe here, help Elliot take down the Deus Group, and along with the usual user.txt and root.txt, this room also wants us to dig up Tyrell Wellick's phone number.
They were even kind enough to hand over a starting login, Olivia Cortez:olivi8, before we'd done a single thing.
Started with the standard nmap sweep.
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.7 (Ubuntu Linux; protocol 2.0)
80/tcp open http nginx 1.14.0 (Ubuntu)22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.7 (Ubuntu Linux; protocol 2.0)
80/tcp open http nginx 1.14.0 (Ubuntu)SSH and a web server, so obviously the web is where the actual fight is going to happen. Loaded up the site.
"Under maintenance," very convincing, very much not the whole story. Checked the page source and any JS files hanging around, completely empty, nothing to grab onto. So directory brute force was next, except the common wordlists weren't turning up anything either, and I also noticed there was a UDP port open, which felt like it might be worth a detour.
nmap -sU -sCV cyprusbank.thm
PORT STATE SERVICE VERSION
68/udp open|filtered dhcpcnmap -sU -sCV cyprusbank.thm
PORT STATE SERVICE VERSION
68/udp open|filtered dhcpcNever actually poked a DHCP client service before, so I went and read up on it. Turns out you can't just netcat your way into a DHCP conversation, it's connectionless UDP and expects properly formed DHCP packets, offer, ack, the whole handshake. Tried crafting some with scapy, also tried dhcping directly against it.
sudo dhcping -c 192.168.161.231 -s cyprusbank.thm
no answersudo dhcping -c 192.168.161.231 -s cyprusbank.thm
no answerNothing. Total dead end, and a whole detour for absolutely no payoff. Back to the website then, and this time instead of directory brute forcing I ran vhost fuzzing instead, and that's where things finally started moving.
www [Status: 200, Size: 252, Words: 19, Lines: 9, Duration: 47ms]
admin [Status: 302, Size: 28, Words: 4, Lines: 1, Duration: 51ms]www [Status: 200, Size: 252, Words: 19, Lines: 9, Duration: 47ms]
admin [Status: 302, Size: 28, Words: 4, Lines: 1, Duration: 51ms]Added both to /etc/hosts first, learned that lesson the hard way after previously assuming a vhost was just down when really I just hadn't pointed my hosts file at it yet.
Threw the credentials we were handed at the start straight at the admin login, Olivia Cortez:olivi8, and walked right in.
Inside the admin panel now, and the plan was simple, find Tyrell's phone number, then find a shell. Except every phone number field was hidden, so search wasn't going to cut it. Poked around the other endpoints instead.
Spotted a message endpoint that looked promising, and my first thought was stored XSS, since the search feature was useless and getting an admin's cookie felt like the natural way in. But before jumping straight to that assumption, I ran everything through a proxy first to actually watch how requests behaved, and the settings endpoint just kept returning 200 OK no matter what headers I threw at it. That left the chat endpoint as the next thing worth checking.
Sending a chat message handed back an id value in the response, and an id value sitting right there in plain sight is basically an engraved invitation to try IDOR. So I did.
And look at that, my assumptions were actually holding up for once. Felt pretty good about myself for about five seconds there. Used the leaked credentials sitting in those older messages to get into the settings endpoint properly.
Got in, grabbed the mobile number, and picked up another set of admin creds along the way, Gayle Bev:REDACTED. One box ticked.
Now for the actual shell. Went digging around that settings endpoint expecting a file upload feature, something juicy to drop a webshell through.
Instead I got a customer page. A customer page?. All that effort for phone number access I already technically had, and this is what's hiding behind it. Poked around the admin section some more looking for a missing directory, found nothing, got mildly annoyed, took an actual break, came back, and out of pure boredom just changed a parameter name on that same settings endpoint. And that's when it finally gave something up.
An error message revealing it was running through an eval somewhere, which is never a word you want to see in someone else's stack trace, and a reference to an .ejs file sitting right there too. EJS templates take JavaScript and data and turn them into rendered HTML server-side, which meant this was very likely heading toward server-side template injection
Went searching for EJS SSTI specifically and found a github repo describing exactly this bug.
The example payload they used just popped a calculator, cute for a proof of concept, but the same structure works for arbitrary command execution.
You can find the payload from this github.
Adapted it to fit what this app actually expected.
It worked, and I wasn't entirely sure why at first, so I actually asked an AI to break down what each part of the payload was doing. Here's the simple version.
Think of the settings page like a form at a bank counter. Normally you fill in your name and maybe a note, and the bank just prints it exactly as you wrote it, nothing more. This app had a hidden setting buried in that same form, one meant only for developers, that basically says "instead of just printing what the customer wrote, let the customer hand us actual instructions and we'll carry them out."
That's what flipping "client mode" on does, it switches the form from "just display this text" to "treat this text as something to execute." Then the next part of the payload is where the actual instruction goes, and in this case the instruction was simply "open a program on this computer and run a command for me." Since the app never checked whether that instruction was something a normal customer should be allowed to send, it just carried it out like an obedient assistant who never asks questions.
So in plain terms, a form that was only supposed to accept a name ended up accepting a full command instead, because nobody taught it the difference between "words to display" and "orders to follow."
Time to actually get a shell out of this. Tried the usual direct bash and bind shell one-liners first and they just kept erroring out on me for reasons I never fully chased down. So instead I wrote out a proper bash reverse shell script, spun up a quick Python HTTP server to host it, and used the SSTI to have the target itself wget the file down and execute it.
Shell landed. Started enumerating for a way up to root.
sudo -l showed something interesting straight away, this user could run sudoedit against an nginx config file, no password required. Never actually abused sudoedit before, so this meant learning something genuinely new mid-box. Did some reading and found the trick, sudoedit opens whatever editor you've set in your EDITOR environment variable, and if you point that at a completely different file than the one you're technically authorized to edit, the editor still opens with root's permissions behind it.
export EDITOR="nano -- /etc/passwd"
sudoedit /etc/nginx/sites-available/admin.cyprusbank.thmexport EDITOR="nano -- /etc/passwd"
sudoedit /etc/nginx/sites-available/admin.cyprusbank.thmBut before running that, I needed a password hash ready to append to the passwd file.
openssl passwd -6 lazzer
$6$1WlH9nIMRaJjNGp.$ayqwgDXHqtSFAVkC7077VTVsNpNcfaKfUoF2eW9oGaLbRRsFhNBtNQK.TY/UlA4Kdbaw2Fw0YLq17q4xvKNfU0openssl passwd -6 lazzer
$6$1WlH9nIMRaJjNGp.$ayqwgDXHqtSFAVkC7077VTVsNpNcfaKfUoF2eW9oGaLbRRsFhNBtNQK.TY/UlA4Kdbaw2Fw0YLq17q4xvKNfU0Then the actual line to drop in, a brand new user with UID 0, which as anyone who's touched Linux permissions knows means root no matter what the username says.
lazzer:$6$1WlH9nIMRaJjNGp.$ayqwgDXHqtSFAVkC7077VTVsNpNcfaKfUoF2eW9oGaLbRRsFhNBtNQK.TY/UlA4Kdbaw2Fw0YLq17q4xvKNfU0:0:0:root:/root:/bin/bashlazzer:$6$1WlH9nIMRaJjNGp.$ayqwgDXHqtSFAVkC7077VTVsNpNcfaKfUoF2eW9oGaLbRRsFhNBtNQK.TY/UlA4Kdbaw2Fw0YLq17q4xvKNfU0:0:0:root:/root:/bin/bash
Ran it, switched to the new lazzer user, and just like that, root.
What actually happened here, worth spelling out since it wasn't obvious to me at first either, sudoedit lets a user edit a specific whitelisted file as root without a password. But sudoedit opens that file using whatever program is set as your EDITOR, and it trusts that environment variable completely. Point EDITOR at nano with a totally different target file baked into the command, and when sudoedit launches your editor with root privileges, it hands you a root-owned nano session pointed wherever you told it to go instead of the file it thinks it's protecting. Add a new root-equivalent user to /etc/passwd, save, and you've just written to a root-only file using a permission you were only ever supposed to use on one specific nginx config.
This one had a bit of everything, a dead-end DHCP rabbit hole to remind me that not every open port is a way in, an IDOR that leaked admin creds through chat history, a genuinely fresh SSTI chain through EJS's client mode, and a sudoedit misconfiguration I'd never actually exploited hands-on before this box. Solid room start to finish, and the Mr. Robot theming made the whole thing feel like it actually mattered instead of just being another box with a Linux logo slapped on it.
Happy hacking, see you in the next room, hopefully one where the DHCP port actually goes somewhere for once :)