← Back to list

Java Script Kiddie 2 PicoCTF(CyLab)

Challenge ini bernama Java Script Kiddie 2 dan kategorinya adalah web exploitation. Dari deskripsi challenge, petunjuk utamanya adalah…

Kaguai10 · 2026-06-22 01:50 · 0 claps · 6.1 min read
#picoctf #cylab #web-exploitation #javascript #ctf-writeup
Open on Medium ↗
Wiki topics: 🌐 · Web Development

Java Script Kiddie 2 PicoCTF(CyLab)

challange

challange

Challenge ini bernama Java Script Kiddie 2 dan kategorinya adalah web exploitation. Dari deskripsi challenge, petunjuk utamanya adalah kalimat “The image link appears broken… twice as badly…”. Kalimat ini mengarah ke sesuatu yang berhubungan dengan gambar yang rusak, dan karena challenge web biasanya menyembunyikan logika penting di sisi client, langkah paling masuk akal adalah membuka halaman challenge lalu melihat source HTML dan JavaScript-nya. Kenapa Javascript juga disinggung disini karena dari judul tantangannya saja sudah menyinggung ‘Java Script Kiddie 2’ berarti ada hubungannya tentang javascript. Dan tantangan ini adalah tantangan lanjutan dari tantangan sebelumnya yaitu ‘Java Script Kiddie’. Okay lanjut……

URL challenge yang dianalisis adalah:

http://fickle-tempest.picoctf.net:63946/

Ini bukti halaman challenge dari picoCTF(CyLab). Dari awal sudah kelihatan clue-nya mengarah ke gambar yang rusak.

web

web

Ketika halaman web challenge dibuka, tampilannya sangat sederhana. Hanya ada input, tombol submit, dan satu area gambar. Gambar itu tidak langsung muncul karena datanya memang belum tersusun dengan benar.

Bagian pentingnya bukan dari tampilan halaman, tetapi dari JavaScript di source halaman. Di sini saya melihat ada kode yang mengambil data dari endpoint /bytes, lalu mencoba menyusun data itu menjadi gambar PNG. Kode yang ditemukan seperti ini:

  <script>
   var bytes = [];
   $.get("bytes", function(resp) {
    bytes = Array.from(resp.split(" "), x => Number(x));
   });

   function assemble_png(u_in){
    var LEN = 16;
    var key = "00000000000000000000000000000000";
    var shifter;
    if(u_in.length == key.length){
     key = u_in;
    }
    var result = [];
    for(var i = 0; i < LEN; i++){
     shifter = Number(key.slice((i*2),(i*2)+1));
     for(var j = 0; j < (bytes.length / LEN); j ++){
      result[(j * LEN) + i] = bytes[(((j + shifter) * LEN) % bytes.length) + i]
     }
    }
    while(result[result.length-1] == 0){
     result = result.slice(0,result.length-1);
    }
    document.getElementById("Area").src = "data:image/png;base64," + btoa(String.fromCharCode.apply(null, new Uint8Array(result)));
    return false;
   }
  </script>

Dari kode ini terlihat bahwa halaman mengambil data dari /bytes. Data itu bukan file PNG biasa yang bisa langsung dibuka, tetapi daftar angka desimal yang dipisahkan spasi. Angka-angka itu dimasukkan ke array bernama bytes. Setelah user memasukkan key sepanjang 32 karakter dan menekan tombol submit, fungsi ‘assemble_png()’ akan menyusun ulang angka-angka tersebut menjadi PNG, lalu hasilnya ditampilkan di elemen ‘<img id=”Area”>’.

Hal penting pertama adalah nilai LEN = 16. Artinya, JavaScript menganggap data sebagai deretan byte yang dibagi menjadi blok berukuran 16 byte. Supaya lebih gampang dipahami, bayangkan semua angka dari /bytes disusun seperti tabel. Satu baris isinya 16 angka. Setelah angka ke-16, lanjut ke baris berikutnya.

Hal penting kedua adalah cara key dipakai. Walaupun key harus memiliki panjang 32 karakter, JavaScript ternyata tidak memakai semua karakternya. Di dalam loop, nilai geser atau shift diambil dari:

shifter = Number(key.slice((i*2),(i*2)+1));

Untuk i = 0, yang diambil adalah karakter key posisi 0. Untuk i = 1, yang diambil adalah karakter key posisi 2. Untuk i = 2, yang diambil adalah karakter key posisi 4. Polanya terus seperti itu sampai i = 15. Jadi yang benar-benar dipakai hanya karakter pada posisi genap. Karakter pada posisi ganjil hanya seperti pengisi agar panjang key tetap 32 karakter.

Contohnya, jika bagian shift yang benar adalah:

9501897572837677

maka key 32 karakter yang valid bisa dibuat dengan menyisipkan satu karakter pengisi setelah setiap digit. Saya memakai 0 sebagai karakter pengisi, sehingga key menjadi:

90500010809070507020803070607070

Karakter yang benar-benar dibaca oleh JavaScript adalah karakter ke-0, 2, 4, 6, dan seterusnya. Jika karakter ganjilnya diganti dengan angka lain pun gambar tetap bisa terbentuk, selama karakter genapnya sama.

Bagian intinya adalah mencari nilai shift untuk tiap kolom. Dari kode ini:

result[(j * LEN) + i] = bytes[(((j + shifter) * LEN) % bytes.length) + i]

dapat dipahami bahwa setiap kolom digeser sendiri-sendiri. Untuk kolom i, byte hasil pada baris j diambil dari baris ‘j + shifter’ pada kolom yang sama. Karena shifter cuma satu digit angka, nilai yang perlu dicoba hanya 0 sampai 9. Jadi ini bukan brute force key 32 karakter secara penuh. Yang dicari hanya 16 angka shift, dan masing-masing angka cuma punya kemungkinan 0 sampai 9.

Supaya lebih mudah tergambar, bayangkan data /bytes sebagai tabel seperti ini:

baris 0:  b00  b01  b02  b03  ...  b15
baris 1:  b16  b17  b18  b19  ...  b31
baris 2:  b32  b33  b34  b35  ...  b47
...

Untuk kolom 0, JavaScript tetap mengambil byte dari kolom 0, tetapi barisnya digeser. Untuk kolom 1, JavaScript tetap mengambil byte dari kolom 1, tetapi shift-nya bisa berbeda. Begitu juga sampai kolom 15. Jadi PNG yang rusak ini sebenarnya bukan dienkripsi berat, tetapi kolom-kolom byte-nya diputar.

Supaya bisa tahu susunan yang benar, saya butuh patokan. Di sinilah format PNG membantu. Setiap file PNG valid selalu punya header awal yang sama. Jadi kalau kita tahu awal file PNG harus seperti apa, kita bisa mencocokkannya dengan data /bytes.

89 50 4E 47 0D 0A 1A 0A

Setelah signature PNG, biasanya langsung ada chunk pertama bernama IHDR. Struktur awal PNG lengkapnya menjadi:

89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52

Dalam bentuk ASCII, byte 49 48 44 52 adalah teks ‘IHDR’. Jadi 16 byte pertama file PNG valid sudah bisa diketahui. Ini sangat pas dengan challenge-nya, karena JavaScript memakai LEN = 16. Artinya, 16 byte pertama PNG tepat sejajar dengan 16 kolom yang sedang dicari shift-nya. Untuk setiap kolom, saya tinggal mencari di baris mana byte yang cocok dengan header PNG.

Sebagai contoh, byte pertama PNG harus 0x89 atau desimal 137. Pada data /bytes, untuk kolom 0, nilai 137 ditemukan pada baris shift 9. Maka shift untuk kolom 0 adalah 9. Byte kedua PNG harus 0x50 atau desimal 80. Pada kolom 1, nilai 80 ditemukan pada baris shift 5. Maka shift untuk kolom 1 adalah 5. Proses yang sama dilakukan sampai kolom 15.

Hasil pencarian shift berdasarkan header PNG adalah seperti ini:

Dari tabel tersebut, shift finalnya adalah:

9501897572837677

Lalu karena JavaScript membaca shift dari posisi genap key, saya bentuk key 32 karakter dengan menaruh 0 sebagai filter di antara digit shift:

90500010809070507020803070607070

Ada sedikit detail penting: beberapa byte header seperti 0x00 atau 0x0A bisa muncul lebih dari satu kali pada kolom yang sama. Jadi pada beberapa kolom, pencarian berdasarkan header saja bisa menghasilkan lebih dari satu kandidat. Untuk memastikan pilihan yang benar, saya tidak hanya berhenti di header PNG. Saya juga memvalidasi struktur chunk PNG dan CRC-nya.

PNG terdiri dari beberapa chunk. Pada hasil yang benar, chunk awal harus IHDR, lalu ada chunk data gambar seperti IDAT, dan diakhiri oleh IEND. Setiap chunk memiliki CRC untuk memastikan datanya valid. Saya menggunakan skript python untuk memastikan apakah key nya sudah benar atau belum. Berikut solver.py nya:

from itertools import product
from io import BytesIO
from PIL import Image

data = list(map(int, open("bytes").read().split()))
header = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR"

pilihan = [
    [geser for geser in range(10) if data[geser * 16 + kolom] == nilai]
    for kolom, nilai in enumerate(header)
]

def susun_png(geser):
    hasil = [0] * len(data)

    for kolom, g in enumerate(geser):
        for baris in range(len(data) // 16):
            asal = ((baris + g) * 16) % len(data) + kolom
            hasil[baris * 16 + kolom] = data[asal]

    return bytes(hasil).rstrip(b"\x00")

for geser in product(*pilihan):
    png = susun_png(geser)

    try:
        Image.open(BytesIO(png)).verify()
    except Exception:
        continue

    print("shift:", "".join(map(str, geser)))
    print("key  :", "".join(str(x) + "0" for x in geser))
    break

Untuk menjalankannya kita perlu mendapatkan isi bytes di endpoint /bytes lalu kita simpan kedalam sebuah file bernama bytes. Kalian dapat melakukannya seperti ini:

curl -O http://fickle-tempest.picoctf.net:63946/bytes

Lalu kita dapat menjalankan script tersebut dan mendapatkan keynya

Yaps hasilnya sama sesuai yang kita lakukan sebelumnya key nya adalah:

90500010809070507020803070607070

Selanjutnya key-nya tersebut dapat kita kirimkan ke web tantangan tadi untuk mendapatkan sebuah image.

Yaps rupanya kita mendapatkan gambar QR. Selanjutnya kita dapat melakukan scan QR untuk mendapatkan flag.

Yaps, di sini kita sudah berhasil mendapatkan flag. Flag tersebut didapatkan dengan cara menganalisis halaman web challenge, terutama source code dan script JavaScript yang berjalan di sisi client. Dari hasil analisis tersebut, ditemukan sebuah endpoint bernama /bytes yang berisi kumpulan angka acak. Angka-angka tersebut sebenarnya dapat disusun kembali menjadi sebuah gambar, tetapi kita harus menemukan key yang benar terlebih dahulu. Setelah key berhasil ditemukan, key tersebut dimasukkan ke halaman web challenge. Hasilnya, gambar yang sebelumnya rusak berhasil berubah menjadi sebuah QR code. Ketika QR code tersebut discan, kita mendapatkan flag yang dicari:

picoCTF{c1f4f2d57d0538d4e9e2ab53e3d1fedc}

Terimakasih telah membaca semoga bermanfaat dan membantu.


메타데이터
post_id
a5d71f0e64dc
slug
java-script-kiddie-2-picoctf-cylab-a5d71f0e64dc
url
https://medium.com/@arifkaguai10/java-script-kiddie-2-picoctf-cylab-a5d71f0e64dc
canonical_url
https://medium.com/@arifkaguai10/java-script-kiddie-2-picoctf-cylab-a5d71f0e64dc
author_url
https://medium.com/@arifkaguai10
status
ok
fetched_at
2026-06-23 03:48:11