Đây là một chall về ssrf khá hay. Có source. Thực ra ssrf ở đây cũng có vẻ không đúng lắm, vì ta dễ dàng ssrf, tuy nhiêm cái khó của bài là việc bypass để phù hợp logic của bài. Bắt đầu nhé!!! I. Review code Web có 3 endponit: * Endponit /: Đây là một endpoint khá đặc biệt, khi nếu như ta gửi req tới tất cả các endpoint còn lại, hay chính ở hắn. Thì nó vẫn sẽ được goị đầu tiên, chứ không phải hàm xử lý endpoint đó được xử lý đầu tiên. Endpoint này check thử, nếu như tham số flag ở query(method GET) không được định nghĩa thì nó sẽ rediect về "/flag?flag=guess_the_flag" còn ngược lại nếu định nghĩa rồi thử next, tức là đi tiếp tới endpoint đó. * Endpoint /flag Endpoint này là ảo ma nhất, đơn giản chỉ kiểm tra nếu tham số flag= FLAG, FLAG là chuỗi flag ta đang tìm ấy, thì show FLAG ra. ![meme-chong-nanh-1](https://hackmd.io/_uploads/SyEN6zMmJx.jpg) Biết rồi tìm làm gì!!!! * Endpoint /ssrf: Đây là endpoint mà ta sẽ khai thác. Đầu tiên nó lấy req.url, tức là ví dụ nếu localhost:3000/ssrf?flag=1 thì req.url là đoạn "/ssrf?flag=1" Sau đó, từ req.url và biến LOCALHOST với LOCALHOST là: ![image](https://hackmd.io/_uploads/Hyu66GGXJg.png) Sẽ tạo ra biến url: ![image](https://hackmd.io/_uploads/BJRyRMz7kx.png) Nếu console.log ra ta sẽ thấy url dạng như này: ![image](https://hackmd.io/_uploads/rkYPRGGXJx.png) Nó là một đối tượng của URL, gồm các tham số của một url cơ bản, được lưu dưới dạng json. Sau khi có biến url, nó sẽ check hostname, và protocol để chắc chắn rằng, ta không dùng OOB và protocol khác để khai thác ví dụ như file, hay ftp. Qua hai lệnh if rồi, chall thay đổi pathname thành "/flag" và thêm một tham số flag với value là FLAG (chuỗi FLAG ta đang tìm), sau đó ssrf bằng việc fecth đến url đó. Tức là nó mặc định rằng luôn luôn sẽ về endpoint /flag và có một tham số flag=FLAG. **II. Phân tích:** **1. Vấn đề:** Thứ nhất để khai thác ssrf, thứ nhất vì các tham số hostname và protocol đều là những thứ ta không kiểm soát được( lấy từ biến LOCALHOST), nên việc OOB hay dùng các protocol khác là rất khó. Nó còn check lại nữa. Thứ hai, vào thẳng /flag :)))))))), thôi bỏ đi Thứ ba, khai thác /ssrf, đầu tiên ta thấy là mặc định sẽ ssrf vào /ssrf. Tuy nhiên, nếu: * Ta không định nghĩa biến flag ở query tức là đơn giản chỉ là /ssrf hoặc ssrf/abc=abc gì đó thì sao. Tất nhiên nó sẽ không qua được hàm ở /, và nó sẽ redirect về /flag: ![image](https://hackmd.io/_uploads/B1BK-XMXyl.png) * Nếu ta định nghĩa flag, tất nhiên nó sẽ vượt qua được hàm ở /, tuy nhiên khi đó, flag sẽ biến thành một mảng, nên nếu có vào được /flag thì cũng không có được: ![image](https://hackmd.io/_uploads/H1t1MmGmJe.png) Như bạn thấy, searchParams sẽ là một mảng của flag. Và khi vào /flag , nó sẽ so sánh mảng này với chuỗi FLAG. Và tất nhiên sẽ trả về false, vì đơn giản === trong js phải là cùng kiểu thì may ra mới có cơ hội là true( đó là chưa kể so sánh nội dung). ![image](https://hackmd.io/_uploads/r1cSGXfXJx.png) **2. Giải pháp:** Chính vì con đường đi như vậy, nên ta phải có cách nào đó. Định nghĩa biến flag để vượt qua endpoint /, nhưng khi vào được endpoint thì nó sẽ không có tác dụng hay liên quan đến biến flag, hay đúng hơn flag phải là một biến, và giá trị của nó là chuỗi FLAG. Như thế khi so sánh trả về True. Và lấy được flag. Tuy nhiên, việc này coi bộ khó, thứ nhất để qua được / phải có định nghĩa flag, mà nếu đã định nghĩa thì thành mảng mất. Chán thật. Hết giải, đọc ở discord thấy người ta giải. Hay phết. Qua phần mới nói cho xung... 3. Khai thác Đọc docs của express js, ta thấy: ![image](https://hackmd.io/_uploads/ryJ18QfQJe.png) Đoạn này là nói về việc parser query( tức là việc xử lý các tham số đầu vào bằng phương thức GET ấy, ở js req.query là GET, req.body là POST). Thì nó nói rằng, có hai cách để xử lý, cách 1 là xử lý bằng querystring của Node, cách 2 là xử lý bằng qs, một thư viện cũng của Node luôn thì phải. Tuy nhiên, nếu để default(cột 3) thì nó dùng qs. Đọc source qs trên github, ở file parse.js, dòng 99, 100: ![image](https://hackmd.io/_uploads/HJuaL7zQkl.png) **Phân tích kỹ lương** Do để phân tích hết các trường hợp thì dài quá, nên mình sẽ phân tích mỗi payload thôi nhé: ![image](https://hackmd.io/_uploads/rybgtp4QJl.png) Đầu tiên, ta phải xác định, luồng đi của code. [+] Khi gửi req, các hàm nằm trong qs sẽ xử lý về các tham số GET, sau đó trả về ở req.url [+] Sau đó, khi về ssrf, sẽ tạo ra đối tượng url từ URL và fetch(url). Thì nó sẽ chạy lại một vòng nữa ở các hàm trong qs. Vì nó ssrf mà. [+] Để dễ dàng, tôi sẽ gọi lần vào các hàm trong qs là Thread1, và lần thứ 2 là Thread2. Đặt breakpoint trong qs, ta sẽ phân tích : ![image](https://hackmd.io/_uploads/rykCR3N7yx.png) Ở Thread1, trong qs, sẽ chứa các hàm trong các file js để xử lý. Đặc biệt trong hàm parseQueryStringValues(), là hàm xử lý chủ yếu các query parse. Hàm này nhận vào hai tham số, str là chuỗi query nằm trên url, và options là một số những gì liên quan đến một số cái liên quan đến kiểu chữ các kiểu: ![image](https://hackmd.io/_uploads/r1PYkpE71e.png) Chú ý dòng 59, sau khi đã loại bỏ các ký tự đặc biệt ở dòng 58, thì dòng 59 sẽ replace %5B/gi và %5D/gi thành [], chủ yếu để xử lý mảng. Sau đó xử lý, mình sẽ focus vào phần for ở dòng 80. Đầu tiên, nó sẽ tìm index của ']=', sau đó gán nó vào bracketEqualsPos để xác định vị trí để lấy key, sau đó xác định pos dựa trên bracketEqualsPos, chính là vị trí để lấy value. ![image](https://hackmd.io/_uploads/SJjigTVX1g.png) ![image](https://hackmd.io/_uploads/B1xRx6EXkl.png) Sau đó lên value và key từ đó: ![image](https://hackmd.io/_uploads/HJR1b64X1l.png) ![image](https://hackmd.io/_uploads/BklZbT4QJl.png) Trả về obj: ![image](https://hackmd.io/_uploads/ryImZTE71e.png) Sau khi các hàm trong qs xử lý xong, bạn sẽ thấy: ![image](https://hackmd.io/_uploads/Hk_Kz6NQ1g.png) Mọi thứ vẫn ok, vẫn là flag[=]=1111. Sau đó, code sẽ chạy đến /ssrf ![image](https://hackmd.io/_uploads/rJvM76VmJe.png) Thì trước khi vào qs, ta phải xem thử url đó có gì: ![image](https://hackmd.io/_uploads/ryDj7p4m1g.png) Tại sao lại thế, bởi, mặc dù req.url vẫn là giống như trên. Tuy nhiên vì khi tạo đối tượng từ class URL, nó sẽ sử dụng query string, chứ không phải hàm ở do qs. Vậy nên url.searchParams sẽ tìm dấu = , và phân tách sau dấu = là value, trước là key. Bạn có thể đọc ở đây: https://github.com/defunctzombie/node-url/blob/master/url.js Sau đó, chạy vào Thread2: ![image](https://hackmd.io/_uploads/S1tZSpV7yx.png) ![image](https://hackmd.io/_uploads/B1gmrp47Jl.png) Sau đó, bạn thấy dòng 59, mặc dù nó replace %5B và %5D, nhưng %3D thì không. Sau khi replace thì chuỗi str nó sẽ là: ![image](https://hackmd.io/_uploads/ByytrpNQJx.png) Sau đó, nó được tách thành parts rồi nhảy vào hàm for(): Và tất nhiên trong chuỗi đó sẽ không tìm đươc ']=' và bracketEqualsPos là -1, và pos sẽ là lấy từ vị trí dấu = đầu tiên: ![image](https://hackmd.io/_uploads/rkRRrp4Q1g.png) ![image](https://hackmd.io/_uploads/SJtlLaEm1e.png) Sau khi xử lý ta thấy: ![image](https://hackmd.io/_uploads/H1yQ86E7ke.png) Nó biến thành là flag[ : ]=1111. Và sau đó, thì req.query.flag sẽ mang giá trị là FLAG thôi. ![image](https://hackmd.io/_uploads/BJ_a_64mJe.png) III. Exploit Thực ra đến đó là xong r, chỉ cần gửi flag[=]= là OK. Thì tất nhiên biến flag vẫn được định nghĩa ngay ban đầu, nhưng sau khi parse thì nó là flag[ .... Quá hay Tham khảo: Docs express:https://expressjs.com/en/api.html qs:https://github.com/ljharb/qs/blob/32e48a2f94f3a433dd69bf011356616c5e81f1a5/lib/parse.js#L99C9-L100C86