📗 CTF Writeup
9th Place in 3108 CTF 2024 + Writeup
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!
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!
RESarawak KitaIlmu HisabAsal Nama SabahWebzZzZzSelangorkuSelangorku V2Wordle Bahasa UtaqaBawangSultan yang HilangMerdekaHang Tak Tidur Lagi?Kapla Harimau SelatanCryptoTanpa Nama 3Syah SesatPandak LamMesej RashiaKekacauan HurufForensicsKontrasPahlawan LagendaPangkalanDaerah Sabah & SarawakOSINTTinggi MatPrivacy MattersMalayan UnionJalan Jalan DesaPerigiTinggi LagiMiscSembunyiSembunyi V2CerCariMakanan PopularCordiniSejarah N9Jauh Bono UmohnyoSambungan TelefonMamu Kasi Tau
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: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:Figured out that
addtwonumber()
will trigger merdeka()
, so I started renaming some variables to understand the flow better:We got 2 conditions to trigger
merdeka()
:1st condition cannot be true, so let’s focus on the 2nd condition:
1st_user_input < 0
2nd_user_input < 0
sum_of_user_input > 0
Analysis:
- Both
1st_user_input
and2nd_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
and2nd_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.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:
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 scenarioCan 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>
- After searching them one by one (please remember to sort it by newest, don’t be a clown like me), a review can be found in Restoran Nasi Kandar Line Clear
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: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
You will get a huge chunk of Base64 encoding, so just decode it, and you will see the flag in the
password
field:3108{m4r1_k1t4_w4rg4_n3g4r4}
Hang Tak Tidur Lagi?
Login page source code gave credentials as
tuah:tuah
, so just use that to login:After login, this is what you see:
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)Cookie value:
KRCU2RKOI5DVKTSH
(TEMENGGUNG)Cookie value:
KBCU4R2IKVGFKICCIVHEIQKIIFJES%3D%3D%3D
(PENGHULU BENDAHARI)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:
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 valuehttps://127.0.0.1
X-Custom-Header
with the valueSm9ob3IganVnYSBkaWtlbmFsaSBzZWJhZ2FpIEdfX19fX19fIG9sZWggb3JhbmcgU2lhbQ==
(Base64-encoded string)- Decoding the string gives you a question, where the answer is Gangganu
- Hence the
X-Custom-Header
value would beGangganu
- 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
andX-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:
3108{d941697cea9e3f341864780b68416961}
Crypto
Imma go thru these real quick lol
Tanpa Nama 3
Just XOR each binary string in
binary_str
with xor_str
Or you could just
print()
lol3108{S1MPL3_CRPYT0_CHALLENGE}
Syah Sesat
Vigenere decode with given key → Reverse
3108{GAMBUS_BUDAYA_LAMA}
Pandak Lam
ROT13 →
grep
3108{k3b4ngk1tanp4hl4w4n}
Mesej Rashia
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
- 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 theoffset
and then take moduloq
to ensure the result remains within the range[0, q-1]
- Reverse the Shuffling
- Purpose: During the creation of the
secret_key
, the order of values was shuffled based onoriginal_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 tooriginal_order
, the sequence of values before the shuffling was applied is recovered
- 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
byq
and adding the current digit, we build the original integer representation of the flag
- 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
:
- Rename the file as
.zip
, change the file header to50 4B 03 04
- Extract it, you will find 4 pictures:
- Challenge description mentioned that you only have to focus on the third one, so let’s do
binwalk --dd='.*' 3.jpg
:
- Rename 0x43F7C into a RAR file
cp 43F7C 43F7C.rar
- Extract it
- You will get a password protected
file.zip
, and aDaerah_Sabah&Sarawak.txt
wordlist
- Let
zip2john
do the password cracking onfile.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
- Do Unicode Steganography with Zero-Width Characters on the text file (
flag2.txt
) after extractingflag2.rar
: https://330k.github.io/misc_tools/unicode_steganography.html - Get the 2nd part of the flag :
_0n3_1n_M4l4ys14!}
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
Found that its kbbsteak Terengganu from the Insta story, but picture was cut in half: https://scontent.cdninstagram.com/o1/v/t16/f1/m78/2B4A39F1C9C6448BD0AC4073CF255EB4_video_dashinit.mp4?efg=eyJ2aWRlb19pZCI6bnVsbCwidmVuY29kZV90YWciOiJpZy14cHZkcy5zdG9yeS5jMi1DMy5kYXNoX2Jhc2VsaW5lXzFfdjEifQ&_nc_ht=scontent.cdninstagram.com&_nc_cat=106&ccb=9-4&oh=00_AYCQ2aUR9_X9HZd3dvgdTsBOJiMnCjM0weQlD-j4wQ3hNw&oe=66D2C5C8&_nc_sid=9ca052
Time to find his existence in the Google review from the shop itself:
3108{J4g4_pr1v4cy_4nd4!}
Malayan Union
- Steghide → https://www.youtube.com/watch?v=rrBCZSnjCgo
- Sort by new comments → "Jadi disinilah merupakan titik permulaan segalanya"
aHR0cHM6Ly91ZmlsZS5pby9jYmRvdzF2MQo=
- Decode the Base64 string → https://ufile.io/cbdow1v1
- 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
- Based on the challenge description, its time to find reviews! This time, I found it on Facebook: https://www.facebook.com/mzmkotakayang/reviews
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
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}