📗 CTF Writeup

9th Place in 3108 CTF 2024 + Writeup

date
Aug 31, 2024
slug
3108ctf24
author
status
Public
tags
3108CTF
Reverse Engineering
Web
Cryptography
Digital Forensics
OSINT
CTF
summary
I secured 9th place in the 3108 CTF and this is my writeup for most of my solved challenges. Enjoy :D
type
Post
thumbnail
Screenshot 2024-10-08 181045.png
category
📗 CTF Writeup
updatedAt
Oct 8, 2024 10:49 AM
Wrapped up the 3108 CTF: Kembara Tuah 2024 by Bahtera Siber Malaysia during National Day and secured 9th place out of 902 players! 🥳 It was a solo CTF, and honestly, I really liked some of the challenges!
notion image
 
I’ve also put together a write-up on the challenges I solved (not all though, I’m a bit lazy 😂), so enjoy! This is the most brief write-up I’ve ever done, and apologies for the lack of screenshots, because halfway through, my PC browser decided to stop loading, so I ended up typing the rest on my phone asdfghjkl :’D
 
A special shoutout to the challenge creator for “Hang Tak Tidur Lagi?” because it was creative in how it blended history and also web exploitation knowledge very well. Certainly a gem 👍🏻
 
💡
If the code doesn't show up, just refresh!


RE


Sarawak Kita

By treating the Microsoft Document file as a zip, you'll find a file called vbaProject.bin. Running strings on it reveals some suspicious commands (You can actually see the flag at this step, but I was just being extra lol)
 
When your good ol friend calc.exe is in a microsoft document:
notion image
notion image
If its a Malware + Word Document, then probably = Macrovirus, so all hail olevba!
# Creating a new virtual environment named venv using Python and activating it virtualenv -p python3 venv . venv/bin/activate pip3 install oletools olevba vbaProject.bin > olevba.log # vbaProject.bin was inside the doc cat olevba.log
Output:
olevba 0.60.2 on Python 3.11.9 - http://decalage.info/python/oletools =============================================================================== FILE: vbaProject.bin Type: OLE ------------------------------------------------------------------------------- VBA MACRO ThisDocument.cls in file: vbaProject.bin - OLE stream: 'VBA/ThisDocument' - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Option Explicit Private Declare Function ShellExecute Lib "shell32.dll" Alias "ShellExecuteA" ( _ ByVal hwnd As Long, _ ByVal lpOperation As String, _ ByVal lpFile As String, _ ByVal lpParameters As String, _ ByVal lpDirectory As String, _ ByVal lpShowCmd As Long) As Long Dim command command = "MwAxADAAOAB7AEsAdQBjAGgAMQBuAGcAXwAxAGIAdQBfAE4AMwBnADMAcgAxAF8AUwA0AHIANAB3ADQAawB9AA==""""" Shell.Run command, 0, True Sub AutoOpen() Call ShellExecute(0, "Open", "calc.exe", "", "", 1) End Sub +----------+--------------------+---------------------------------------------+ |Type |Keyword |Description | +----------+--------------------+---------------------------------------------+ |AutoExec |AutoOpen |Runs when the Word document is opened | |Suspicious|Open |May open a file | |Suspicious|Shell |May run an executable file or a system | | | |command | |Suspicious|Run |May run an executable file or a system | | | |command | |Suspicious|ShellExecute |May run an executable file or a system | | | |command | |Suspicious|ShellExecuteA |May run an executable file or a system | | | |command | |Suspicious|shell32 |May run an executable file or a system | | | |command | |Suspicious|command |May run PowerShell commands | |Suspicious|Call |May call a DLL using Excel 4 Macros (XLM/XLF)| |Suspicious|Lib |May run code from a DLL | |Suspicious|Base64 Strings |Base64-encoded strings were detected, may be | | | |used to obfuscate strings (option --decode to| | | |see all) | |IOC |shell32.dll |Executable file name | |IOC |calc.exe |Executable file name | +----------+--------------------+---------------------------------------------+
See that Base64 string MwAxADAAOAB7AEsAdQBjAGgAMQBuAGcAXwAxAGIAdQBfAE4AMwBnADMAcgAxAF8AUwA0AHIANAB3ADQAawB9AA==? Decode it and you would get the flag:
3108{Kuch1ng_1bu_N3g3r1_S4r4w4k}

Ilmu Hisab

Need to trigger merdeka() because that is where the flag will show:
notion image
 
Figured out that addtwonumber() will trigger merdeka(), so I started renaming some variables to understand the flow better:
notion image
notion image
 
We got 2 conditions to trigger merdeka():
notion image
 
1st condition cannot be true, so let’s focus on the 2nd condition:
  1. 1st_user_input < 0
  1. 2nd_user_input < 0
  1. sum_of_user_input > 0
 
Analysis:
  • Both 1st_user_input and 2nd_user_input are negative
  • The sum of 2 negative numbers is always negative, so the 3rd condition (sum_of_user_input > 0) seems impossible under normal circumstances.
  • However, if the variables 1st_user_input and 2nd_user_input are represented in a way that allows an overflow (e.g., in a system with fixed-width integers), then this might be possible
  • In systems where integers have a maximum and minimum value, adding 2 large negative numbers might cause an overflow, resulting in a positive sum
 
If the system uses 32-bit signed integers, the maximum value is 2,147,483,647 and the minimum value is -2,147,483,648. If an overflow occurs, the sum of 2 negative numbers might wrap around to a positive number:
  • Let 1st_user_input = -2,147,483,648 (the most negative number in a 32-bit signed integer)
  • Let 2nd_user_input = -1
Their sum would be -2,147,483,649, which under normal circumstances would be negative. However, in a 32-bit signed integer system, this could wrap around to 2,147,483,647, a positive value.
notion image
3108{n0mb0r_k3r4mat}

Asal Nama Sabah

I focused on check_flag() because that is where the flag will show, so I renamed some variables like usual.
Looks like a XOR function by the ^ symbol:
notion image
 
notion image
So, I used CyberChef to perform the XOR operation. I converted the hex string 5d505d591a20552e47293d325c3e3159291c to its original form, then XORed it with the key which is namaasalsabah.
3108{S4B4H_S4PP4H}

Web


zZzZz

  • Answer for the Sejarah question was Laksamana Bentan
  • Output: 0x33z0x31z0x30z0x380x7bz0x37z0x30z0x30z0x650x66z0x34z0x61z0x37z0x39z0x39z0x350x39z0x360x31z0x350x62z0x360x37z0x650x61z0x35z0x32z0x39z0x37z0x65z0x37z0x32z0x350x63z0x300x36z0x65z0x7dz
  • Decode output from hex → Flag
3108{700ef4a79959615b67ea5297e725c06e}

Selangorku

Bypass 403 Forbidden error by using curl, since the curl user agent was allowed in this scenario
Can find the flag by doing:
3108{S3lang0r_temp4t_kelahiran_ku}

Selangorku V2

Same solution with Selangorku, its just that it has way more endpoints, so I just scripted the solution lol
#!/bin/bash # Base URL BASE_URL="https://1b267619c4.bahterasiber.my" # Array of endpoints ENDPOINTS=( "" "/hulu_langat/Kajang.html" "/hulu_langat/Bangi.html" "/hulu_langat/Semenyih.html" "/hulu_langat/Cheras.html" "/klang/Kapar.html" "/klang/Pelabuhan_Klang.html" "/klang/Meru.html" "/klang/Pandamaran.htm" "/kuala_langat/Banting.html" "/kuala_langat/Teluk_Datok.html" "/kuala_langat/Jugra.html" "/kuala_langat/Jenjarom.html" "/kuala_langat/Morib.html" "/kuala_selangor/Tanjong_karang.html" "/kuala_selangor/Ijok.html" "/kuala_selangor/Sekinchan.html" "/kuala_selangor/Bestari_jaya.html" "/petaling/Subang_jaya.html" "/petaling/Petaling_jaya.html" "/petaling/Shah_alam.html" "/petaling/Damansara.html" "/petaling/Puchong.html" "/sabak_bernam/Sabak.html" "/sabak_bernam/Sekinchan.html" "/sabak_bernam/Sungai_besar.html" "/sabak_bernam/Sungai_air_tawar.html" "/sepang/Dengkil.html" "/sepang/Cyberjaya.html" "/sepang/KLIA.html" "/gombak/Batu_caves.html" "/gombak/Rawang.html" "/gombak/Selayang.html" "/hulu_selangor/Batang_kali.html" "/hulu_selangor/Serendah.html" "/hulu_selangor/Bukit_beruntung.html" ) # Loop through each endpoint and curl the content for endpoint in "${ENDPOINTS[@]}"; do url="${BASE_URL}${endpoint}" echo "Checking URL: $url" # Curl the URL and grep for "3108{" response=$(curl "$url" | grep "3108{") # If grep finds the string, print the result if [[ ! -z "$response" ]]; then echo "Found '3108{' at $url" echo "$response" else echo "'3108{' not found at $url" fi done
chmod +x script.sh
3108{D1_s1ni_t3mp4t_S4ya_m3mb3s4r}

Wordle Bahasa Utaqa

Just do wordle according to picture given in the challenge description
3108{h4ng_m3m4ng_s3mp0i}

Bawang

  • Onion link based from the challenge title: tmdjl5kyfzimrsrkkjisxybwb7664epxizxfz6hbivkg6k4a3x2svrad.onion
  • Found login.js:
    • function validateForm() { var username = document.getElementById("username").value; var password = document.getElementById("password").value; // The correct username and password (base64 encoded) var correctUsername = "bawang"; var correctPassword = "bWVtYmF3YW5namVrZWpl"; // Base64 encoded password // Check if the username matches if (username !== correctUsername) { alert("Invalid username or password"); return false; } // Encode the input password to base64 var encodedPassword = btoa(password); // Check if the password matches if (encodedPassword !== correctPassword) { alert("Invalid username or password"); return false; } return true; // Allow the form to submit }
  • By decoding the password in Base64, you will get your credentials:
    • User: bawang
    • Password: membawangjekeje
  • The website will give you a list of restaurant coordinates:
    • <h2>Welcome, bawang!</h2> <h2>Hang rasa nasi kandaq mana paling surrr?</h2> <div class="button-group"> <button onclick="showText('output1')">Deen Maju</button> <p id="output1" class="output-text">5°24'35.8"N 100°19'41.4"E</p> <button onclick="showText('output2')">Line Clear</button> <p id="output2" class="output-text">5°25'11.7"N 100°19'57.1"E</p> <button onclick="showText('output3')">Sulaiman</button> <p id="output3" class="output-text">5°24'49.0"N 100°18'46.9"E</p> </div>
3108{surrr_punya_tobat_jumpa}

Sultan yang Hilang

Found an API in the source code:
<script> const sultanYears = [1763, 1795, 1800, 1835, 1837, 1886, 1890, 1899, 1920, 1944, 1960, 1979, 2010]; sultanYears.forEach(year => { fetch(`/api/v1/sultan/${year}`) .then(response => response.json()) .then(data => { const list = document.getElementById('sultan-list'); const listItem = document.createElement('li'); if (data.error) { listItem.textContent = `${data.error}`; } else { listItem.textContent = `${data.nama}`; } list.appendChild(listItem); }) .catch(error => console.error('Error:', error)); }); </script>
 
Compared the given list in Wikipedia and found that Sultan Muhammad III is missing, so I just went to call the API for it:
{ "flag": "3108{putera_sulong_Sultan_Ahmad}", "id": 1889, "nama": "Sultan Muhammad III", "tahun_pemerintahan": "1889-1890" }
3108{putera_sulong_Sultan_Ahmad}

Merdeka

In the source code, the setPage function stores a Base64-encoded version of the provided page string in a cookie and then reloads the page:
<script> function setPage(page) { const encodedPage = btoa(page); document.cookie = "page=" + encodedPage + ";path=/"; location.reload(); } </script>
The path=/ attribute makes the cookie accessible to all pages on the website, rather than being restricted to a specific directory or page. This means that any script or page within the website can read and use the cookie, including those handling sensitive files.
 
I tried doing LFI by referencing a sensitive file like /etc/passwd, and encoded it using Base64 and URL encoding:
notion image
 
Great! Now time to try and get a config.php file since it might be included in a PHP include or require statement. If a site is vulnerable to LFI, there's a high likelihood it's also susceptible to PHP wrappers. By using a wrapper like php://filter/(filter type)/resource=index.php, you can potentially leak PHP code:
php://filter/convert.base64-encode/resource=config.php
💡
In this case, the source code would still be exposed even without the filter, but aNyWays
notion image
 
You will get a huge chunk of Base64 encoding, so just decode it, and you will see the flag in the password field:
notion image
3108{m4r1_k1t4_w4rg4_n3g4r4}

Hang Tak Tidur Lagi?

Login page source code gave credentials as tuah:tuah, so just use that to login:
notion image
 
After login, this is what you see:
notion image
The default cookie value after login is a Base32 + URL Special Char Encoding: JRAUWU2BJVAU4QI%3D (LAKSAMANA)
 
Seeing that the cookie used LAKSAMANA as its value, and it is also bolded in the picture above, this challenge definitely has something to do with cookies and roles.
 
The challenge description gave a strong hint as it bolded the words “Pembesar Berempat”. By going through some Sejarah knowledge, these are the Pembesar Berempat:
  • Bendahara
  • Penghulu Bendahari
  • Temenggung
  • Laksamana
 
Now all you have to do is change the cookie value based on the roles by doing Base32 + URL Special Char Encoding, and you will get parts of the flag from different roles.
 
Cookie value: IJCU4RCBJBAVEQI%3D (BENDAHARA)
notion image
notion image
 
Cookie value: KRCU2RKOI5DVKTSH (TEMENGGUNG)
notion image
 
Cookie value: KBCU4R2IKVGFKICCIVHEIQKIIFJES%3D%3D%3D (PENGHULU BENDAHARI)
notion image
3108{1d0R_s4nGa7l4h_Bah4y4!}

Kapla Harimau Selatan

Couldn’t solve this at first, until they release the source code, which you can find it from a comment:
notion image
notion image
 
To successfully trigger the code that reveals the flag, you need to provide the correct HTTP headers in the request. Here is some explanation of the code for context:
  • The script initializes two key headers:
    • Origin with the value https://127.0.0.1
    • X-Custom-Header with the value Sm9ob3IganVnYSBkaWtlbmFsaSBzZWJhZ2FpIEdfX19fX19fIG9sZWggb3JhbmcgU2lhbQ== (Base64-encoded string)
    • Decoding the string gives you a question, where the answer is Gangganu
      • notion image
    • Hence the X-Custom-Header value would be Gangganu
  • The script constructs the corresponding keys for the headers from the incoming HTTP request using the $_SERVER superglobal:
    • HTTP_ORIGIN
    • HTTP_X_CUSTOM_HEADER
  • The script checks if the request contains these 2 headers (Origin and X-Custom-Header) and if their values match the expected ones
  • If both conditions are satisfied, the script outputs the flag :D
 
So, just add the 2 headers mentioned and tadaaaaaa:
notion image
3108{d941697cea9e3f341864780b68416961}

Crypto

Imma go thru these real quick lol

Tanpa Nama 3

notion image
Just XOR each binary string in binary_str with xor_str
💡
Or you could just print() lol
3108{S1MPL3_CRPYT0_CHALLENGE}

Syah Sesat

Vigenere decode with given key → Reverse
3108{GAMBUS_BUDAYA_LAMA}

Pandak Lam

ROT13 → grep
3108{k3b4ngk1tanp4hl4w4n}

Mesej Rashia

notion image
lol
3108{substitute_cipher_text}

Kekacauan Huruf

Challenge:
import random from Crypto.Util.number import bytes_to_long, long_to_bytes q = 64 # Read the flag from a file flag = open("flag.txt", "rb").read() flag_int = bytes_to_long(flag) # Add random padding padding_length = random.randint(5, 10) padding = random.getrandbits(padding_length * 8) flag_int = (flag_int << (padding_length * 8)) + padding # Generate the secret key secret_key = [] while flag_int: secret_key.append(flag_int % q) flag_int //= q # Shuffle the secret key original_order = list(range(len(secret_key))) random.shuffle(original_order) shuffled_secret_key = [secret_key[i] for i in original_order] # Add a random offset to each value in the secret key offset = random.randint(1, q) shuffled_secret_key = [(x + offset) % q for x in shuffled_secret_key] # Save the secret key and offset with open("secret_key.txt", "w") as f: f.write(f"secret_key = {shuffled_secret_key}\n") f.write(f"offset = {offset}\n") f.write(f"padding_length = {padding_length}\n") f.write(f"original_order = {original_order}\n") print("Secret key, offset, and original order saved to secret_key.txt")
 
Had a script to do this:
import random from Crypto.Util.number import long_to_bytes # Given values secret_key = [54, 38, 12, 47, 37, 37, 53, 22, 6, 38, 62, 22, 10, 54, 19, 41, 43, 53, 0, 62, 63, 28, 63, 63, 22, 10, 7, 37, 63, 53, 44, 8, 10, 42, 35, 43, 42, 63, 37, 21, 4, 19, 45, 21, 19, 18, 3, 62, 53, 24, 2, 62, 18, 35, 41, 14, 53, 3, 37, 63, 55, 62, 5] offset = 50 padding_length = 9 original_order = [9, 20, 6, 12, 22, 38, 14, 24, 53, 52, 61, 29, 45, 11, 57, 44, 8, 46, 55, 59, 31, 2, 51, 43, 21, 27, 17, 40, 15, 58, 0, 26, 19, 36, 60, 28, 48, 39, 34, 50, 7, 16, 56, 30, 10, 49, 13, 3, 5, 42, 41, 47, 37, 4, 32, 33, 62, 1, 18, 23, 25, 35, 54] q = 64 # Step 1: Reverse the offset original_secret_key = [(x - offset) % q for x in secret_key] # Step 2: Reverse the shuffling reordered_secret_key = [0] * len(original_secret_key) for i, original_pos in enumerate(original_order): reordered_secret_key[original_pos] = original_secret_key[i] # Step 3: Reconstruct the flag integer flag_int = 0 for value in reversed(reordered_secret_key): flag_int = flag_int * q + value # Step 4: Convert to bytes and remove padding flag_bytes = long_to_bytes(flag_int) flag = flag_bytes[:-padding_length] # Remove padding print(flag.decode()) # Output the flag
  1. Reverse the offset
      • Purpose: The offset was added to each value in the secret_key during encryption
      • For each value in the secret_key, subtract the offset and then take modulo q to ensure the result remains within the range [0, q-1]
  1. Reverse the Shuffling
      • Purpose: During the creation of the secret_key, the order of values was shuffled based on original_order
      • original_order tells us the new positions of the original values
      • By placing each value from original_secret_key back into its original position according to original_order, the sequence of values before the shuffling was applied is recovered
  1. Reconstruct the Flag Integer
      • Purpose: The secret_key was encoded as a base-q number (base-64) during encryption
      • Iterate through the reordered_secret_key in reverse order, treating each value as a digit in a base-q (base-64) number
      • By multiplying the current flag_int by q and adding the current digit, we build the original integer representation of the flag
  1. Convert to Bytes and Remove Padding
      • Purpose: The integer representation of the flag was converted to bytes, but extra padding was added during encryption to obfuscate the length
      • Convert the integer back to bytes using long_to_bytes
      • Then, slice off the padding bytes from the end of the byte sequence
3108{9546880676d3788377699aad794c5a44}

Forensics


Kontras

  • Upload redacted PDF into google drive
  • Open it as Google Doc
  • Highlight time :D
  • Anyways the flag was in white font lol
3108{Peghak_Darul_ridzuAn}

Pahlawan Lagenda

The flag explains the process lol
3108{gr3p_15_@w3s0m3_l4ks4m4n4}

Pangkalan

  • Follow TCP Packet
  • You will get: Mw==MQ==MA==OA==ew==bWlrZQ==YWxwaGE=bGltYQ==YnJhdm8=YWxwaGE=dGFuZ28=dGFuZ28=fQ==
  • Which decoded, is 3108{mikealphalimabravoalphatangotango}
  • Decode the NATO phonetic alphabet
3108{malbatt}

Daerah Sabah & Sarawak

  • When in doubt, consult file:
    • notion image
  • Rename the file as .zip, change the file header to 50 4B 03 04
  • Extract it, you will find 4 pictures:
    • notion image
  • Challenge description mentioned that you only have to focus on the third one, so let’s do binwalk --dd='.*' 3.jpg:
    • notion image
  • Rename 0x43F7C into a RAR file
    • cp 43F7C 43F7C.rar
    • Extract it
    • You will get a password protected file.zip, and a Daerah_Sabah&Sarawak.txt wordlist
  • Let zip2john do the password cracking on file.zip wahoo
    • zip2john file.zip > file.hash
    • john file.hash --wordlist=Daerah_Sabah\&Sarawak.txt
  • Password for file.zip: LubokAntu
3108{S4B4H_27_D43RAH_S4R4W4K_40_D43R4H}

OSINT


Tinggi Mat

  • Aperisolve the picture given in the challenge zip file
    • MERDEKA118 is the password for 2nd rar file (flag2.rar), extracted from Aperisolve output
    • Get the 1st part of the flag from Aperisolve output: 3108{th3_t4ll3st
3108{th3_t4ll3st_0n3_1n_M4l4ys14!}

Privacy Matters

Just use the same username from TikTok in Instagram and found more stuff from his Instagram highlights
 
 
Time to find his existence in the Google review from the shop itself:
3108{J4g4_pr1v4cy_4nd4!}

Malayan Union

  • Sort by new comments → "Jadi disinilah merupakan titik permulaan segalanya" aHR0cHM6Ly91ZmlsZS5pby9jYmRvdzF2MQo=
  • Fix the header of the picture to FF D8 FF E0 00 10 4A 46 49 46 00 01
  • Shows a picture of Istana Besar Johor
3108{istana_besar}

Jalan Jalan Desa

  • Threw it to Google lens, and it shows that its Kota Kayang Museum
3108{Muzium_Bersejarah_Perlis}

Perigi

Belanda (Poisoned the Hang Li Poh well for the 2nd time)
3108{th3_k1ngs_w3ll_st4ys_0n}

Tinggi Lagi

Threw it to Google lens, and it shows that its Tradewinds Square Kuala Lumpur
3108{3.15,101.71}

Misc

Time to speed thru round 2

Sembunyi

Whitespace esolang
3108{S3jarah_Ters3mbunyi_P4hang}

Sembunyi V2

  • Get the decoding of spaces and tabs
  • Change spaces and tabs into binary: S=0, T=1
  • Decode the binary
3108{putih_dan_hitam_dalam_negeri_pahang}

CerCari

Just find the important year of Sabah which was when it gained self-governance and federated into Malaysia
3108{S4b4h_1963}

Makanan Popular

strings Makanan | grep "3108"
3108{L4KS4_S4R4W4K}

Cordini

Can’t see the flag in Discord app, but can see it in the browser version of Discord lmao
3108{kibarkanlah_jalur_gemilang}

Sejarah N9

Brute force it then you will see a pattern
3108{tigasatukosonglapan}

Jauh Bono Umohnyo

3108{rembau_most_wanted}

Sambungan Telefon

Draw the numbers on your numpad, then if some alpahbets didn’t make sense (e.g. JANG), then just google something like “HOBIN football Negeri Sembilan” lol
notion image
3108{HOBINJANGHOBIN}

Mamu Kasi Tau

Reverse the audio using an online audio editor (Since it sounds like he is speaking in reverse), and tembak what the dude is trying to say HAHAHAH and put it rapat-rapat xD
3108{peningtelinga}