Web - File Storage

Tuần này ta sẽ đến với một challenge có vẻ tương đối khó (mình để ý chall này mở được hơn 3 tháng rồi mà tính đến thời điểm writeup này được đăng lên mới có hơn 10 người bypass được thôi). Bản thân mình cũng phải lên kênh chat của Cookie Hân Hoan trên discord để cầu cứu chứ một mình thì khó mà qua được ải này.
Sau ít phút phân tích, ta nhận thấy con web trong challenge này cung cấp cho user truy cập vào 4 endpoint api:
/: Giao diện Home
/mkfile: Thực hiện chức năng tạo file
/readfile?filename=…: Thực hiện đọc nội dung file
/test: Gồm hai chức năng được gọi là ‘rename’ và ‘reset’
Suy nghĩ một chút sẽ nhận ra được dấu hiệu của lỗi ‘Directory Traversal’ có thể được khai thác từ query param filename. Ví dụ trong trường hợp filename=JonaSon, lúc này server tìm file tại địa chỉ: __dirname/storage/JonaSon(__dirname chính là thư mục hiện tại của file app.js). Như vậy ta có thể nhập liên tục các chuỗi “…/” để tìm và đọc file flag.txt.
Tất nhiên thực tế không đơn giản như vậy, để ý line 58 của file app.js:

Server có thực hiện validate đối với ký tự ‘.’, mình mất nửa ngày để research xem liệu có lỗ hổng nào tồn tại trong các module được import vào file app.js không, cuối cùng vẫn là rơi vào ngõ cụt. Phải mất một quãng thời gian khá lâu sau đó mình mới nhận ra được phần nào nguyên nhân, rằng bên cạnh ‘Directory Traversal’, ta cần phải chú ý đến một khái niệm khác.
Đó chính là: Prototype Pollution
Hiểu một cách đơn giản thì mỗi object trong javascript đều có một thuộc tính, gọi là prototype, thuộc tính này trỏ tới một object. Ví dụ như khi truy cập vào thuộc tính filename của object read, nếu như thuộc tính filename không tìm được trong object read thì thuộc tính filename sẽ được tìm tiếp trong prototype của object read, còn nếu vẫn không tìm được thì sẽ trả về undefined.
Có thể tìm hiểu chi tiết và cụ thể hơn về prototype qua đường link: Object prototypes - Learn web development | MDN
Để truy cập vào prototype của object file, sử dụng syntax: file.proto

  • Lưu ý: file.__proto__ === read.__proto__ (vì cả hai đều là instance của class ‘Object’)

Quay trở lại với endpoint ‘/readfile’, trong trường hợp query param filename=JonaSon, server sẽ trỏ vào file[‘JonaSon’], lưu giá trị của file[‘JonaSon’] vào biến filename, nếu filename == null thì sẽ trả về nội dung của file __dirname/storage/fake(read[‘filename’] == fake).
Tận dụng những gì đã biết về Prototype Pollution, ta có thể nghĩ đến việc ghi đè thuộc tính filename của object read. Và đối với challenge này, ý tưởng đấy là khả thi. Nên nhớ rằng file app.js có function setValue() dùng để thay đổi các giá trị được lưu trong object file, và như vừa mới được lưu ý, ghi đè lên prototype của object file cũng đồng nghĩa với việc ghi đè lên prototype của object read.
Vấn đề bây giờ sẽ là làm thế nào để ghi đè lên prototype của object file ?
Để trả lời câu hỏi này, mình đã phải nhờ đến sự support từ bên ngoài, và dưới đây là file ảnh mà anh đã gửi cho mình:

![Screenshot_from_2023-08-01_14-08-49|690x387]
upload://8Y9REvFFM4sq8btDRU2yOqX8SBo.png)

Ta có thể điều khiển cho server chạy code: setValue(file,‘__proto__.filename’, ‘…/…/…/…/…/flag.txt’) bằng cách nhập url:
server/test?func=rename&filename=__proto__.filename&rename=…/…/…/…/…/flag.txt
Bước tiếp theo là truy cập vào endpoint ‘/readfile’(không nhập query param filename để filename == null).
Tuy nhiên sau một khoảng thời gian khoảng hơn tiếng đồng hồ, mình vẫn không thể nào tìm được flag. Phải mất rất lâu mình mới nhận ra rằng thuộc tính filename thuộc prototype chỉ được truy cập vào khi thuộc tính filename không được tìm thấy trên object read, tuy nhiên ngay khi truy cập vào endpoint /, read[‘filename’] đã được set value bằng ‘fake’.
Lưu ý rằng endpoint /test còn cung cấp thêm chức năng reset để thay đổi giá trị của biến read, biến read lúc này sẽ trở thành một object rỗng, không còn lưu thuộc tính nào.
Tóm lại ta cần truy cập vào các đường dẫn(theo thứ tự):
‘server’/test?func=reset
‘server’/test?func=rename&filename=__proto__.filename&rename=…/…/…/flag.txt
‘server’/readfile
*Lưu ý: rename không phải mặc định là …/…/flag.txt, mỗi thay đổi một giá trị mới cho rename cần truy cập vào endpoint /test?func=reset để set lại biến read.

Writeup tới đây có lẽ cũng tương đối dài rồi,link facebook mình để dưới comment,mọi người có thể ib mình để trao đổi kỹ hơn về chall này(có lẽ sẽ có nhiều người vẫn thắc mắc tại sao lại nhập filename=__proto__.filename).
Và không quên cảm ơn anh Ngọc Trường đã support em qua ải này rất nhiệt tình ^^.

2 Likes

Sr mn. Mình fix lại chút là chỗ query rename chỉ cần 2 dấu ‘.’ thôi.