Description
On vous demande d’auditer ce serveur web sandboxé.
nc challenges.france-cybersecurity-challenge.fr 2058
Note : le binaire à exploiter n’a pas accès à Internet.
Translation:
We ask you to audit this sandboxed web server.
nc challenges.france-cybersecurity-challenge.fr 2058
Note : the binary to exploit has no access to Internet.
The source code
The source code is available with this challenge. We have the following files:
audit.c
/audit.h
- Contains the
audit
function. Apparently just logging things. If it’s here, it might be useful at some point, so don’t forget to check it out.
- Contains the
base64.c
/base64.h
- Contains the
b64_decode
function. By looking at the function signature, it seems like there is no check for the output buffer size. It smells like a buffer overflow 👀.
- Contains the
debug.h
- Define a macro for logging debug messages. There may be some format string in there, so don’t forget to check it out.
http.c
/http.h
- Contains the main functions for parsing the HTTP request and sending the response. I think this is just about programming the web server and nothing really interesting. Might still be useful to check it out in order to understand better the code.
worker.c
/worker.h
- Seems to contain the code that manages how the web server is actually running.
httpd.c
- Contains the main function for the web server (calling the
worker
function) and the sandbox activation.
- Contains the main function for the web server (calling the
Test the code
We have an overview of the code. But we still don’t know what it does. So, let’s try to run the code.
|
|
And know, the program is hanging. It seems like no socket is created, which seems weird for a web server. By looking at the code, we can see the program just use the standard input and output.
|
|
Now we can communicate with the server through a web browser, on http://localhost:8080.
Authentication
When we open the page, we are asked for a username and a password.
First thing every hacker would do is to type admin
and admin
in the form. And of course, it works 🙃.
But we still don’t have the flag, so we need to search for a better vulnerability.
You said base64?
As we seen in the source code, there is a b64_decode
function:
|
|
And as I said before, we don’t give the size of the output buffer to the function. There are two possible scenarios:
- The size is hardcoded in the function (which is not a good idea).
- The function doesn’t care about the size of the output buffer (which is even worse).
I don’t want to read the entire source code, so let’s just read the important parts and try to overflow the buffer.
|
|
Here, in the worker.c
file, we can see that the creds
buffer is 0x100 bytes long. Let’s try to overflow it, by sending this request:
GET / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4AYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQ==
The base64 encode the string admin:admin\0aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...aaaaaaaaa
(where the \0
is the null terminator). It’s probably useless to send admin:admin
, but since I didn’t really read the source code yet, I prefer to send it in order to get logged in if this is useful.
|
|
And we got no answer, so it seems that the program has crashed as we expected.
Just a buffer overflow 😏
We found a vulnerability, so let’s try to exploit it.
|
|
[*] '/home/lucas/CTF/FCSC/Pwn/httpd/write_up/httpd'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] '/home/lucas/CTF/FCSC/Pwn/httpd/write_up/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] '/home/lucas/CTF/FCSC/Pwn/httpd/write_up/ld-linux-x86-64.so.2'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
Sadly, the program has every security feature enabled, so we can’t just send a buffer overflow. The only way I see in order to bypass ASLR / PIE / Canary is to bruteforce everything byte by byte in order to leak the data we need.
By looking a bit more at the source code, we find some code about the keepalive
feature:
|
|
The main loop (in the parent process):
|
|
And the requests are handled in child processes (forked from the parent process):
|
|
So we can bruteforce everything like we wanted if we add the header Connection: keep-alive
.
|
|
[+] Offset: 264
[+] Canary: 0x87f031db50917e00
[+] RIP: 0x6160213fb89e
So, we bruteforced the canary and the return address. You can see that I have hardcoded the least significant byte of the canary to 0x00, and the one from the return address to 0x9e (which can be obtained by disassembling the program).
We can now get the base address of our program, from the leaked return address:
|
|
[*] Base: 0x6160213f9000
THE RCEEEEE !!!!!!!!!!
Yeah ! We got the RCE ! We can execute code on the remote server by overwriting the return address with the address of the function we want to execute ! This is awesome !!!
But …
The sandbox …
Yes, we can execute functions .. but this is still sandboxed.
According to the seccomp configuration, we can only execute the following syscalls:
SYS_read
SYS_write
SYS_sigreturn
SYS_exit
SYS_brk
This was my first time dealing with seccomp, so I didn’t really know what I could do about these restrictions …
The wrong way
After a bit of research I found that the strict mode of seccomp is what we have here, except for SYS_brk
. I then spent some hours searching why is SYS_brk
disabled in the strict seccomp configuration. Is it dangerous ? Can we exploit something using it ?
I never got an answer about this, so I think there is no specific reason for this. They just enabled the essential syscalls, and didn’t care about the rest.
The right way
I realized, after some hours, that the seccomp configuration is not applied everywhere. The parent process needs to fork itself in order to handle the requests. So, the sandbox cannot be applied to the parent process.
And what if there is another vulnerability ? One that can be exploited in the parent process, from a child process ?
Finally reading the source code ?
The parent process does few things, so if there is a vulnerability, it should be easy to find.
Main loop:
|
|
There are 3 things:
request
: the function that handles the requests- Need to be checked, there might be a vulnerability in it
DEBUG
: the macro that prints some debug information- As I said before, we need to check for format string vulnerabilities, but in the given code, it seems like it is correctly used
audit
: the function that logs some information- Do you remember when I said that this should be useful at some point ? Let’s see if we can find a vulnerability in it. I’m pretty confident about this, so let’s check it first.
The audit function
The audit function is pretty simple:
|
|
It manages the msg
buffer, and the prio
variable, in order to print the correct message after each loop.
I don’t see any buffer overflow on the msg
buffer, so I think it’s pretty safe.
But at the end of the function, we have a syslog
call. I don’t know this function, I never used it, so let’s read the manpage:
|
|
Hum … are you telling me that there is a format string vulnerability ? Let’s try in a custom program if we can exploit this using %n
like we do all the time if printf
:
|
|
And the program prints 6
, so the format string works as expected.
Triggering the format string
We have to control the content of the msg
buffer, passed to syslog
, in order to trigger the format string.
There are multiple cases:
- Normal connection:
- The message is
LOGIN <username>
(the username is alwaysadmin
since this is the only user we have)
- The message is
- Signal:
- The message is
SIGNAL <signal>
, we do not control the signal number, and even if we did, this would not allow us to trigger the format string.
- The message is
- Unknown:
- The message is
UNKNOWN <status>
, we do not control the status, and even if we did, this would not allow us to trigger the format string.
- The message is
So, the only way to trigger the format string is to control the username. It is stored in the shared
structure, which is shared between the parent and the child processes. If we could use our buffer overflow to trigger a ROP chain that would allow us to control the username, we would be able to trigger the format string, and then control the return address of the parent process, which would allow us to execute arbitrary code.
THE RCEEEEE !!!!!!!!!! (for real this time ?)
Leaking the libc base address
We need to know where the libc is loaded in memory. We will use this information to find the address of a one gadget (a single gadget that trigger execve("/bin/sh", NULL, NULL)
) and the address of the stack (from the _environ
symbol) in order to overwrite the return address.
We also could have checked for the libc version, and if it is old enough, we could have tried to use __free_hook
or __malloc_hook
to trigger the one gadget, but this might be harder, and this is not reliable for every libc, so I tend to avoid it nowadays when this is possible.
|
|
[*] Loaded 14 cached gadgets for './httpd'
[*] free: 0x74fe3b0d1740
[*] libc: 0x74fe3b03a000
[*] environ: 0x7ffc03bccbd8
Now, we can get the address of the shared
structure, and where the return address is stored.
|
|
[*] shared: 0x74fe3b227000
[*] username: b'admin\x00'
Now, let’s search for a one gadget:
|
|
I already tested all of them, and the last one is working, so let’s use it in our exploit.
|
|
And exploit the format string in order to overwrite the return address.
|
|
[*] Loaded 193 cached gadgets for './libc.so.6'
Now, we overwrote the return address, so we just have to get out of the main loop, by setting the keepalive
variable to false
, ie: not including the Connection: keep-alive
header in the request.
|
|
[+] Flag: FCSC{d87c69143541ae0d3e43f8d65bff7072646cdc781167b89aedf0146cb20ed3cd}
Conclusion
This was a great challenge. When I saw it, I instantly wanted to solve it.
But of course, when I solve a challenge, the most important thing is what I learned from it.
What I learned
First thing, I didn’t know seccomp. I watched some videos and read some documentation, and I learned how powerfull it is if you want to create a sandbox. I don’t know if there is some ways to escape the sandbox by disabling it if it is not configured correctly, but I think I will try to read more documentation on this topic.
But the most important thing, that made me waste a lot of time, was the fact that I got stuck on the wrong idea. I wanted to use brk
in order to bypass the sandbox, but I couldn’t. But during the recon, I found the audit
function and I already knew that it would probably be useful (nothing is useless when you want to solve a challenge, when a piece of code is here, it is probably for a reason). So next time, when I need to search for what I can do, I will go back to what I found in the recon phase, instead of rushing headlong towards the first idea that I have