This post spoils a CTF challenge … Don’t read if you want to try it !
ECW (European Cyber Week) is a Jeopardy student CTF challenge. It is organized by Thales, Airbus and the Bretagne region. I had a great time solving these challenges, especially reverse and pwn ones. Hurry to hand over this !
Sudo not being very secure, as the leader of the cyber-digital world, we created a replacement
Admire the result: ssh -p 10022 <username>@challenge-ecw.fr
We have a simple CTF setup :
- 3 users : ecw (us), adm and root
- 2 binaries :
~/mysudo(owned by adm, can be executed by ecw, suid) and
/tmp/getflag(owned by root, can be executed by adm, suid)
- 4 files owned by adm in our home directory :
/tmp/getflag read the flag from
/proc/1/environ, so we need to get a shell as adm, and then execute
File output :
I played a bit with the binary to understand how it works, here are examples of output we can get :
Let’s reverse it to have a better understanding of it !
We can notice is that the binary uses the mruby lib, which is an embedded Ruby bytecode interpreter in C. The 4
.mrb files are compiled Ruby scripts.
There are several interestings parts in the binary. The first one is about retrieving the user password and loading
The second one is about checking the password.
The last one is about loading the requested
mrb script and executing it with the same privileges as it’s owner.
Let’s first find the password : the program takes our input with the
get_password function, passes it to
main.mrb's encode then xors the output with
0xfe and finally compare it with the
I didn’t reverse the
main.mrb's encode function (because it was really linear : for a given byte you only had one possible output), I just sent to it all possible bytes from
0xff, in order to create a mapping with the inputs and the associated outputs of the function.
After applying the xor to the map I made, I just had to read the conditions to finally extract the password : ADMsystem42$$$$.
With the great password, we can now execute the mrb scripts :
To get the flag, we can’t simply create an mrb script which calls
/bin/sh because we will have a shell with our own user since we are the owner of our custom script.
But, there is a buffer overflow vulnerability in the
get_password function : the user input can have an arbitrary size so we can overflow the stack and overwrite other variables. The most interesting variable we can overwrite is mrb_payload, which is the content of the mrb script passed as an argument.
So, the idea is to use an adm owned mrb script as an argument and input a very long password to overwrite this script data with our custom mrb payload.
Our mrb payload will then be executed as the adm user, since the script used as argument is owned by the adm user.
The only problem is that our mrb payload is mixed with our password so it will be encrypted as the rest of the password.
We need to decrypt our payload like if it was an encrypted data, and send the result to avoid this side effect :
encrypt(payload) => garbage
encrypt(decrypt(payload)) => payload
The final problem (an easier one) was that the
get_password function checks that it is executed in a real tty, to avoid piping stdin and stdout …
Python comes with a handy
pty module, which allow us to bypass the
I finally made two scripts : one which generates the payload and one which send the payload to the data through a tty.
generate.py, which generates a payload with the extracted mapping, based on
shell.mrb, which is a simple ruby shell compiled with the mrb compiler
And here is the final
#!/usr/bin/python3 import pty, os i = 0 with open("payload.bin", "rb") as f: payload = f.read() def read(fd): global i, payload data = os.read(fd, 10) if i == 0: os.write(fd, payload) i += 1 return data pty.spawn(["/app/mysudo","/app/id"], read)
We now have an adm shell, just run
/tmp/getflag and get this damn flag !
Feel free to tell me what you think about this post :)