Last updated
Last updated
Rope is an insane machine from hackthebox that will have to exploit an arbitrary file read to download the binary of the running web server and perform reverse engineering to it. With that, you will need to exploit format strings in order to overwrite the GOT table with the address of system()
and pass arguments to it through the HTTP method to spawn a shell as john. After it, you will need to check the files that can be executed as other users. Doing that, you will see that the file that can be ran as r4j uses a library that your current user can write. After writing a malicious shared library in the location and getting shell as r4j, you will have access to the file a process that is being ran as the root user. Downloading it, the attack will exploit a buffer overflow with almost protections enabled by exploit the fork()
calls on the program to brute the canary.
Doing a nmap port scanning, we can discover the ports 22 and 9999 opened.
Opening the page in a web browser, it shows us a login page.
Fuzzing the paths with the "fuzz-Bo0m" wordlist, we can find an interesting file named "run.sh"
Accessing it, gives to us the name of the webserver that it is running on CWD.
With an attempt to access it directly, we can download the binary.
## Shell as john
Running a checksec on the binary, we can see that it has canary, NX and PIE enable with partial RELRO.
Opening the file in IDA to get a dissasembled version of the binary, it's possible to check the program's main function.
This part of the program is just setting the binary to listen to the **9999** port, create a new process with "**fork()**" and call the "**process()**" within it.
Before perform actions with the data, the "process()" function calls the "parse_request()" to, what seems, parse the request obtained from the user.
The "**parse_request()**" can be resumed in the following pseudo code:
This means that we can pass a header named "Range" with two numbers that the program will perform something with them further. Let's go back to the process function.
After parsing the data, the program call one function, "serve_static()" if the requested path is a file, or "handle_directory_request", if it is a directory.
Reading the disassemble of server_static, it's possible to see how's the program logic.
Basically, the program is sending us some headers and calling "sendfile(out_fd, in_fd, range_start, range_end-range_start)", which "in_fd" being "open(req_data.path)".
This means that, theoretically, we can just pass the full path of any file on the machine that the user running the program can read and it's value will return to us. Testing this theory with "/etc/passwd", we can get the file successfully.
That's great. For now, let's go back to process.
Before return to the main function, the program calls "*log_access(int status, in_addr con, *req_data)**".
At the line 12, we can clearly see a format string bug, where our path is sent to "printf()" without any sanitisation. This bug can give to us a read and a write primitive to the binary memory.
As after the format string vulnerability the program calls "puts()" with an argument that we control "req_data->method" and the binary only has partial RELRO, we can perform a GOT overwrite to overwrite the GOT index of puts with the address of "system" on glibc. So, instead of calling "puts(req_data->method)" it will call "system(req_data->method)".
But, as the binary has PIE enabled, and the system (probably) has ASLR, we have to find a way to leak an address and calculate the base addresses by using them.
We could use the format string bug to achieve this, but it only prints to the output of the program, so no data is sent to us.
One interesting directory in linux is the "/proc/<pid>" directory. It has some files that contains information about process with the requested PID, or about the current process if the directory accessed is "self".
One of these files is the "/proc/\self/maps" because it contains the memory mapping of the program and the shared libraries.
This means that, if we can access this file, we have the base address of the binary and libc. So, we can use our arbitrary file read to read this file, right?
Trying to access the file, gives to us an empty response. Why?
We can see in the "Content-length" header that the program identified the length of the file being 0. Running the command "stat" on "/proc/self/maps" in our machine gives to us that, in fact, the operational system identifies this file having the length of 0.
So, we need a way to pass the correct length to the program. And we have it. When calling "sendfile()", the program sends the start offset and how bytes it will read as the arguments received through the "Range" header.
Trying request the file again but with the new headers, the mapping file is returned.
With all these information, we are ready to write our exploit.
At first, we need to write a function to download the mapping.
After that, we will need to calculate the "system()" function in libc. We currently do not have the glibc file, so let's download with our bug.
Finally, we can send our malicious command to download a reverse shell and execute it.
After executing the exploit, we got a reverse shell connection.
As the goal of these writeups is to write only about memory corruption, I will be skipping how the details of how to get shell as this user. As a short description, you will need to list the commands that you can run as other users by using sudo -l
. Doing that, you will find that you can execute /usr/bin/readlogs
as r4j. Checking the shared libraries of the file, you can check that the binary uses the /lib/x86_64-linux-gnu/liblog.so
and you can write to it.
By decompiling the readlogs
file, you will discover that it calls a function named printlog
. So, you will need to create a shared object file with a function named as printlog
that calls system()
from stdlib.h
and copy this file to the location of liblog.so
. After that, you can just call sudo -u r4j readlogs
and execute what you put on the shared object.
With a shell as r4j, we can start enumerating the machine in order to search some way to escalate our privileges up. Using the command ss -tulpn
, it's possible to see a running process that is listening the port 1337 on the machine. Let's download the file to our machine.
Opening it in IDA, we can see how the program works.
It setups a socket listening on the port 1337 and starts a loop to handle the receiving connections. When a connection is received, the program calls the sub_14EE
function sending the file descriptor of the connection to it.
In the function start, it calls the fork
function, effectively forking the process and creating a new thread. After it, the program writes Please enter the message you want to send to admin:
to the connection and calls the sub_159A
function.
At this function, we clearly have a buffer overflow vulnerability, where the function recv
writes 1024 bytes to a buffer that only supports 56 bytes. So, what we can do with this?
The program has almost all the protections enabled, just having partial reallocation read-only as the least level of protection.
We will need a way to find or leak the canary, in order bypass the stack smashing protection feature, and leak the instruction pointer register (RIP), to locate ourselves in the binary and calculate the offset in order to redirect the execution flow to a ROP chain. And how could we leak them?
If you paid attention, you said that I've mentioned that the program calls fork()
to each received connection. When this function is called, the current stack is copied to the new process. This implies that, for every new process, the same canary value is used.
As the canary protection works by checking if the value overwritten on the canary address stored in the stack is equal than the value stored earlier in the stack (where we can't write), if we sent one byte and the connection do not crash, we can assume that the byte we sent is correctly part of the canary. Let's test this theory.
Before running the program in GDB, we must set the detach-on-fork option to off and follow-fork-mode to child.
After that, we will need to setup a breakpoint after the buffer overflow. Which is at the 0x15CC offset.
After sending 56 bytes followed by a "\x69" to localhost:1337 with python3 -c 'from pwn import *; import sys; sys.stdout.buffer.write(b"A"*56 + b"\x69")' | nc localhost 1337
, we can reach the breakpoint.
First, we can check the current canary with pwndbg's canary command.
0x1e89ca993f04e900
is our value. Now, reading the 10 QWORDS in the stack, it's possible to see the canary with only the last byte tampered.
As the canary value was changed to a different value, if we continue the execution we should see a stack smashing message.
One interesting behaviour is that if we come back to the server response, it is possible to see that, as the process was exited, no response came.
Now, as the canary stack value always will end with a null byte, let's send 56 As followed by a zero.
We received a different response. In this time, the server answered us with a "Done.
" string. And as we can tell what bytes were correct, it's possible to brute the canary (and other things).
You can see that the delay parameter is set to True
if port is equal to 1337. The reason to this be in the code is because you will need to forward the port from the machine. And as SSH's port forwarding is kind of buggy, you will need to apply a little delay before receiving the response.
The canary had change because the program was restarted, but attaching GDB to the new instance we can see that the found canary is correct.
Now, we have the canary. To keep continuing exploring the binary, we should overwrite the instruction pointer with the address of the start of our ROP chain. The executable was compiled with PIE, so we do not have static address to overwrite the return address with. We need to find a way to get it and calculate the other offsets with it.
Let's inspect the binary stack and search for the return address.
We can see that 16 bytes after the end of the buffer we have what seems a pointer to an instruction. Let's finalise the current function and see if the RIP is equal that value.
Nice, there it is. So, if we use the same methodology we used to brute the canary but this time writing the return address it should work as well but not so reliable. As we will write the return address, if we overwrite it with any executable address, it should not crash. We can prevent this by overwriting the last byte (or the last 12 bits) with the parts that we already have (0x62).
Before bruting the return address, we must to get RBP in order to get our payload working. We can use our function to do this without any problems.
After run the updated exploit, we got the return address.
To calculate the elf base address from this we just need to subtract 0x1562 from this value.
With the libs
commands from pwndbg, we can verify that this is indeed the base address.
Running it and printing the received value, we got a value.
Checking what the value is with pwndbg, we can state that it is the printf()
address.
Now we can use this value to get the libc base address.
With the lib address, we can calculate any function address and a ROP chain to execute them. If we execute execve("/bin/sh")
to spawn a shell, the shell input would go to the default stdin and the output would go to the default stdout, which have 0 and 1 as file descriptors, respectively.
To redirect the shell to us, we must use the dup2
function to copy these file descriptors to the file descriptor of the current connection before calling execve()
itself.
Running the entire exploit, we can get a shell spawned from the machine.
Fortunately, gives to us an interface to explore format strings bugs named .
Now it's easy to get the libc base address by using the technique to call write()
from PLT to write the printf()
GOT address to the socket.