TryHackMe — Anonymous Playground Writeup — ROP Tutorial

Sohail Saha
9 min readDec 10, 2020
We are Anonymous.
We are Legion.
We do not forgive.
We do not forget.
Expect us.

Introduction

Hey folks! This is my first write-up ever, and I chose TryHackMe’s ‘Anonymous Playground’ room created by Nameless0ne. The foothold part was a bit annoying because I am impatient at times, but the privesc was awesome.

The creator designed this room to have 2 users and a root user, and sequentially escalate through them, but I found a way to directly escalate to root from foothold. So, if you are searching for how the creator intended this room to be solved, please look for other write-ups. But, if you are willing to try a little harder, then hang on with me.

Also, if anything in this room seems overwhelming for you, please take some time to research and study for it. My Field-Manual repo has some stuff too.

You are a fool only if you do not try.

Prerequisite Knowledge

Tools Used

Getting Foothold

Getting foothold is interesting here. This part is very CTF-ish, and not much real-world oriented, but nonetheless, it requires careful observation.

I started off by doing a portscan to find all open ports. Only 2 ports were open:

  • Port 22: ssh
  • Port 80: http

So, I started enumerating with the webpage. I arrived at this…

The webpage at Port 80

I checked the page source html code, but did not find anything useful. Then I checked the ‘/robots.txt’, and found an interesting looking directory ‘/zYdHuAKjP’.

Next, I saw what cookies (if any) had been set by the website, and saw a cookie named ‘access’ which had a ‘denied’ value. I changed it to values like ‘granted’, ‘allowed’, but nothing happened.

Next, I visited the Operativespage, and found the usernames of some people who are quite well-known in the infosec community. I took note of these names, but it didn’t turn out to be useful later. I bruteforced that cookie’s value with these names but nothing happened.

I then headed to that directory I found earlier. Note that I didn’t do directory busting here because that directory name looked exciting enough.

The /zYdHuAKjP directory

So I figured that something interesting is right here, somewhere. I checked the source, and found a very sneaky hint after a few minutes of carefully reading.

Observe that ‘granted’ has been bolded (although the font used in the website does not show it :/)

So I thought that it is telling me to set the value of that cookie to ‘granted’. And I was right!! Unlike that on the homepage, this time, I got something interesting…

Looks like an encoded string

This is the part where it got the hardest for me; I tried very, very hard at decoding this manually after having tried and failed at finding an already existing encoding that gives results like these. This didn’t look like anything I had seen before. Frustrated, I checked the hint on THM, and I got this…

Interesting…

Let me be honest - I could not understand how this was supposed to work. I thought up things like: the ‘smallest’ letter from each pair of letters would be the decoded value, and such. Decoding like these didn’t yield any actual username/passwords, and I looked at the walkthroughs of others, and then I found it.

The decoding scheme is this:

Small alphabet, Capital alphabet = Alphabet that is at position Small alphabet + Capital alphabet

For instance, ‘z’ is the 26th and ‘A’ is the 1st alphabet, and their sum is 27 (26+1), and the ‘27th’ alphabet (imagine the list of alphabets arranged in a circular loop) is ‘a’. The case does not matter here. So seeing this, I wrote up a little script to decode this string:

And, I got this decoded string:

Some credentials it seems…

This looks like a username:password, so I used this to login using ssh, and succeeded!!

Privilege Escalation to Root

This part is where this walkthrough will differ from all other ones you find out there, and you’ll shortly know why.

Upon logging in and listing the directory contents, I immediately saw that there is a binary with SUID set on it — ‘hacktheworld’, and the owner is root. I understood that this binary would be the way to privesc.

I also saw a note there in a text file that reads:

Hey Magna,

Check out this binary I made! I’ve been practicing my skills in C so that I can get better at Reverse
Engineering and Malware Development. I think this is a really good start. See if you can break it!

P.S. I’ve had the admins install radare2 and gdb so you can debug and reverse it right here!

Best,
Spooky

So this confirmed my thought, and I decided to take the binary offline and try to reverse it there. Yes, the box has radare2 installed, but still, I decided to test it out on my machine first.

I assume that you already know your way with radare2, so I won’t be explaining the meanings of each command here, neither will I explain much of assembly language except for what is relevant here. If it gets tough, scroll way up and find the resources I linked to.

Analyzing with radare2 shows that there is an interesting function — ‘call_bash’, plus some other juicy imports as well, like ‘system’, ‘setuid’.

Take a look at the functions listed

So, I disassembled the main function, and saw what was going on:

Disassembled main

So you see that running the binary would only print out “Who do you want to hack? ”, and take an input from you with gets(), then exit the program. There is no call to the interesting call_bash(). However, as you might know, gets() is vulnerable to Buffer Overflows, so in theory, we can overflow the buffer that gets() stores the input text in, and have control over the RIP.

I tested out my theory and sent a pattern of 200 bytes as input, to see what are the overflow offsets. I used pwn’s cyclic tool to generate the pattern; it’s very, very handy. After sending the pattern text, this is the register dump.

Notice the rbp, it’s our pattern!

Also, let’s check out the stack (by viewing the contents of the address contained in the rsp)…

Notice our pattern

So, in essence, we overflowed the stack all the way to the rbp and beyond. Using pwn’s cyclic to find out the pattern offsets shows that rbp is at offset +64, and rsp is at offset +72. In other words, after the buffer overflow, the stack pointer ‘fell’, and is now currently just after rbp (you’d know this AND the reason behind this if you understand how a program loads up and runs in the memory). Also, since rbp is at +64 and we’re working with a x64 binary (8 byte addresses), so, rip is at offset +72 (64+8). Note that both rsp and rip is at the same offset. Sometimes, you won’t see this in other binaries; often the rsp is a few bytes after the rip. Here, however, they are both at the same place on stack.

Now, let’s take a look at the interesting function — ‘call_bash()’:

Disassembled call_bash()

You see that some lines would get printed out each after 1 second delays, followed by a setuid call that sets the uid to 1337, followed by system(“/bin/sh”) call. In essence, you would get a shell as the user with uid of 1337. Now, it’s clear that the creator wants us to jump to this function’s address from overflowing the rip, and escalate to uid 1337, which is pretty neat. We’d have to escalate from there to other users and/or root.

However, here is where I deviate from the others. I thought to myself that this function is too good to just elevate to uid 1337. If we could just jump NOT to the function itself, but right at the address where setuid() is called, then we can get a shell as root. Of course though, we’d need to pass 0 to setuid(), which is not in the binary. Note that this setuid() call is at address 0x004006c4.

If you’re familiar with x64 calling convention, you’d know that to make the setuid(0); call, you would have to put 0 in the rdi. There are many ways of doing this:

xor rdi,rdi; ret
mov rdi,0; ret
push 0; pop rdi; ret

The ‘ret’ is so that the next instruction can fire off. These all are ROP Gadgets (if found).

I tried to find the first two, but there weren’t any in the binary itself, and since ASLR is enabled, I could not work with addresses in other .so files (I could’ve though, but it would have been very, very complicated, but possible, and that is where I’d have gone if I hadn’t tried the third one). Searching for the third one brought just one result; I was overjoyed to see that!

ROP gadget — pop edi; ret

Note the address of this ROP gadget, 0x400773

Now, let’s summarize our findings so far:

rip -> +72
ROP gadget that sets rdi (setuid param) to 0 -> 0x400773
setuid() call -> 0x4006c4

Since we are working on a x64 binary, the flow would be:

  1. The rip is to be overwritten with the address of the ROP gadget
  2. 0x00000000 is to be pushed on stack (at rsp, the top of the stack)
  3. The ROP gadget is then invoked, which pops the top of stack (0), and transfers this value to rdi (therefore, setting rdi to 0), then calls ret, which now would cause the instruction at the address on the new (increased) rsp to be invoked.
  4. Therefore, this address should be that of the setuid() call that we took note of earlier.
  5. After setuid(), the binary then calls system(), and gives us a shell.

This gives us the payload structure which must be written exactly in this manner on the stack:

72 bytes junk -> 0x00400773 (pop rdi,ret) -> 0x00000000 (for setuid) -> 0x004006c4 (setuid)

“This is all but theory, will it work?” Well, there’s only one way to know…

For the scripting part, I always use pwn library. Here’s the script I wrote.

And now, the moment of truth…

And look at that, we have root! Now being root, gathering the flags should be easy — just waltz into all the users’ home directories.

Conclusion

The takeaway from this box is that whenever you see a custom binary with an SUID bit set, you should always try harder to judge the extent of things that you can do with it. Knowledge of assembly language, and how a program works is, thus, very important.

--

--

Sohail Saha

🚀 Frontend developer @Polygon 👨‍💻 Creator at #DevvingItWithSohail 🎮 Gamer