[WolvCTF 2023] Writeup Web
Last updated
Last updated
Trước khi bắt đầu đây là source code dành cho các bạn: link
Bài này cho source nhưng mà khá basic, nó dính lỗi XSS:
Payload:
Sau đó submit link cho bot->flag; dùng burp suite để bắt request lấy link submit.
Bài này thì mọi thứ giống bài 1 nhưng có điều lúc này cookie được set http only, như trước đây mình học tập có một người anh đã nói với mình là nếu đã set http only thì chỉ có leak nó ra gián tiếp từ một trang các chứ không thể leak ra trực tiếp được.
Chúng ta sẽ vào phân tích những đoạn code:
Chú ý đoạn code trên có một endpoint debug ở đây nó sẽ kiếm tra biến allowDebug trong config xem cho phép hay không khi đó nếu true thì req.headers (Tất nhiên nó sẽ kèm cả cookie).
Xem xét tiếp file docker-compose:
Yaahh! Ở đây allowDebug: true , vậy mọi việc đơn giản rồi.
Ý tưởng ở đây sẽ khai thác dữ liệu từ một endpoint khác bằng cách exploit từ endpoint có vuln XSS. Có thể hiểu Cross-origin resource sharing.
Từ ý tưởng trên mình sẽ có một đoạn payload JS API, mình sử dụng fetch(Mọi người có thể dùng những thứ khác như Ajax, HML HTTP Request ....)
Mình sẽ giải thích chi tiết đoạn payload trên cho ai chưa biết tới JS:
Đoạn code này sử dụng phương thức fetch để gửi một yêu cầu HTTP GET đến địa chỉ 'https://zombie-201-tlejfksioa-ul.a.run.app/debug'. Sau đó, nó sử dụng phương thức then để xử lý phản hồi từ máy chủ.
Trong phương thức then thứ nhất, nó trích xuất nội dung của phản hồi bằng cách gọi phương thức text() trên đối tượng response. Phương thức text() sẽ trả về một Promise chứa dữ liệu văn bản của phản hồi.
Trong phương thức then thứ hai, nó sử dụng dữ liệu được trả về từ phương thức text() để tạo một địa chỉ URL mới. Địa chỉ URL này sẽ chứa một tham số "c" với giá trị là dữ liệu văn bản được trả về từ phản hồi.
Sau đó, nó sử dụng phương thức concat() để ghép chuỗi địa chỉ IP của yêu cầu vào địa chỉ URL mới và sử dụng đối tượng document để chuyển hướng trình duyệt đến địa chỉ URL mới này.
Nếu có lỗi xảy ra trong quá trình gửi yêu cầu hoặc xử lý phản hồi, đoạn code sử dụng phương thức catch để xử lý ngoại lệ và ghi log lỗi ra console.
Mình đã đọc qua code thì chắc chắn debug không dính CSP khi đó cách này là khả thi nhất.
Chúng ta sẽ lại lấy link submit cho con bot->flag
Bài này giống những bài nhưng những endpoint debug không thể truy cập được.
Vẫn như lời một người anh đã nói với mình muốn đọc được cookie có http only thì hãy xem file nào chứa nó và có thể leak ra hay không.
Trong file index.js có đoạn này
Và file Docker có vị trí rõ cho chúng ta biết vị trí file config.
Vậy là chúng ta có vị trí file chứa flag: /ctf/app/config.json
Payload sẽ như bài 201 nhưng có một cái quan trọng nếu muốn truy cập file từ máy của chính mình thì dùng giao thức file://
Yah cũng có một cách khác là sử dụng constructor của JS để return env
Payload:
Mình sẽ giải thích qua như này:
this.constructor
là một tham chiếu đến hàm constructor của đối tượng hiện tại.(Dễ hiểu nó là Object)
this.constructor.constructor
là một tham chiếu đến hàm constructor của hàm constructor của đối tượng hiện tại(Dễ hiểu là function của object).Trong đoạn code này,
this.constructor.constructor('return process.env.FLAG')()
được sử dụng để truy cập đến hàm constructor của hàm constructor của đối tượng hiện tại. Sau đó,('return process.env.FLAG')()
được sử dụng để trả về giá trị của biến môi trườngprocess.env.FLAG
.
Có một payload khác là đánh cắp header kèm cookie khi server trả về browser.
Khi một cookie được thiết lập là http-only, nó sẽ không thể truy cập được từ các client-side script như JavaScript, và chỉ có thể được gửi đến server thông qua các yêu cầu HTTP. Tuy nhiên, việc thiết lập cookie là http-only không có tác dụng trong việc ngăn chặn việc gửi cookie từ server đến client thông qua các phản hồi HTTP.
Khi một yêu cầu được gửi đến server, server sẽ trả về phản hồi HTTP chứa các thông tin header như cookie, các thông tin về cache, mã trạng thái và các thông tin khác. Tùy thuộc vào cách server được cấu hình, nó có thể trả về các header chứa cookie đã được thiết lập là http-only.
Trong trường hợp của đoạn mã trên, khi một yêu cầu được gửi đến trang web https://zombie-301-tlejfksioa-ul.a.run.app/
bằng phương thức fetch
, server sẽ trả về phản hồi HTTP chứa các thông tin header, bao gồm header chứa cookie đã được thiết lập là http-only. Tuy nhiên, cookie đó không thể truy cập được từ client-side script, do đó, nó không thể được đánh cắp bởi các script độc hại.
Sau đó, đoạn mã tiếp tục sử dụng phương thức fetch
để gửi dữ liệu của phản hồi HTTP đến một địa chỉ URL khác bằng cách sử dụng thuộc tính credentials
được thiết lập là include
. Nếu server của địa chỉ URL này đã được cấu hình để chia sẻ cookie giữa các tên miền khác nhau (cross-domain) bằng cách thiết lập header Access-Control-Allow-Credentials: true
, thì cookie có thể được gửi đến địa chỉ URL khác thông qua phương thức fetch
.
Tóm lại, việc thiết lập cookie là http-only không thể ngăn chặn việc gửi cookie từ server đến client, nhưng nó có thể ngăn chặn việc truy cập cookie từ các client-side script. Việc sử dụng thuộc tính credentials
được thiết lập là include
trong phương thức fetch
chỉ cho phép chia sẻ cookie giữa các tên miền khác nhau nếu server của địa chỉ URL đích đã được cấu hình để chia sẻ cookie giữa các tên miền khác nhau.
Bài này cũng có thể solve bằng payload của bài 301 truy cập thẳng file config của server chứa flag
Chúng ta sẽ không lấy được flag ở payload 2 vì biến môi trường lúc này không chứa flag.
File config.json sẽ dạng:
Ở đây chúng ta sẽ nghiên cứu lại một xíu thì Đoạn mã trên sử dụng thư viện zombie
trong Node.js để tạo một trình duyệt không đầu (headless browser).
Chịu khó tìm zoombie.js ở git truy cập vào file CHANGELOG.md để xem các thay đổi thì thấy rằng:
Zombie phiên bản cuối cùng là từ năm 2018, có thể chúng ta sẽ tìm thấy một số 1 day https://github.com/assaf/zombie/blob/master/CHANGELOG.md
Ở đây một lúc research thì chúng ta có một POC có thể đọc file - thực tế nó là vuln của nodeJS nhưng zoombie cũng gặp phải:
Oke nhưng từ dây chúng ta có thể xây dựng một payload khác
Payload này có thể dùng RCE được nếu không biết vị trí chứa flag:
Chúng ta sẽ giải thích cách hoạt động đoạn payload nay nhé:
Dòng đầu mình đã giải thích ở Zoombie 301, nó chỉ khác ở trên return ra ENV còn đây là process.
Trong Node.js biến toàn cục process (Global) tức là mọi thứ đều có quyền truy cập đến. Bạn không cần khai báo hay import chúng bởi phương thức require() mà vẫn có thể sử dụng chúng một cách trực tiếp. Các đối tượng này có thể là các Module, các hàm, các chuỗi hoặc các đối tượng.(Có thể hiểu nó là một tiến trình tương đương 1 ứng dụng dược chạy)
Đoạn code tiếp, process.mainModule
được sử dụng để xác định module chính được sử dụng trong ứng dụng Node.js. require('child_process')
được sử dụng để yêu cầu module child_process
. execSync('cat config.json')
được sử dụng để thực thi lệnh cat config.json
và trả về kết quả. toString()
được sử dụng để chuyển đổi kết quả trả về thành chuỗi .
child_process
là một module trong Node.js, cho phép tạo ra các tiến trình con (child process) để thực hiện các tác vụ bất đồng bộ (asynchronous) mà không ảnh hưởng đến tiến trình chính (main process).
Vì NodeJS hay là thực hiện xử lý bất đồng bộ nghĩa là nó sẽ thực hiện các tác vụ nhẹ trước rồi sẽ tới tác vị nặng vì vậy chúng ta dùng execSync:
execSync
là một phương thức của đối tượng child_process
trong Node.js. Phương thức này được sử dụng để thực thi một lệnh đồng bộ và trả về kết quả của lệnh đó.
Phương thức execSync
sẽ chặn luồng thực thi cho đến khi lệnh được thực thi xong và trả về kết quả. Nếu lệnh trả về một mã lỗi, phương thức sẽ ném ra một ngoại lệ
Bài này chúng ta sẽ gửi một url tới bot. Và chúng ta sẽ xem qua source code của chall này.
Ở đây pritvate-server sẽ chạy ở locale, chúng ta sẽ phân tích code private:
Ở đây ý tưởng chúng ta sẽ dùng một đoạn css nói ở prefix, nó gọi là CSS Injection
Ý tưởng ở đây chúng ta sẽ bypass tất cả điều kiện của server private và lấy giá trị của biến css.
Payload sẽ như sau:
Phân tích qua cách thức hoạt động của đoạn payload trên:
Trong đoạn payload, chúng ta đang sử dụng một đường dẫn http://0:1337/css
để lấy nội dung CSS. Trong đường dẫn này, chúng ta đang truyền tham số prefix
với giá trị p {--flag:"
. Điều này sẽ làm cho đoạn CSS trả về chứa một biến --flag
với giá trị bằng rỗng và tiếp đó được nối với giá trị của biến FLAG
trong đoạn code server.
Tiếp theo, chúng ta sử dụng JavaScript để lấy giá trị của biến --flag
từ id
#payload
. Điều này có thể được thực hiện bằng cách sử dụng hàm getComputedStyle
để lấy các thuộc tính CSS của phần tử và sau đó sử dụng getPropertyValue
để lấy giá trị của biến --flag
.
Cuối cùng, chúng ta sử dụng hàm fetch
để gửi giá trị của biến --flag
đến một địa chỉ IP bất kỳ để lấy giá trị của Flag.
Tóm lại, đoạn payload này sử dụng lỗ hổng CSS Injection để lấy giá trị của biến --flag
từ đoạn CSS trả về và sau đó gửi giá trị này đến một địa chỉ IP bất kỳ để lấy giá trị Flag.
Tới đây có nhiều bạn có thể sẽ thắc mắc tại sao nếu dùng p{flag: }
hoặc p{color: }
.... thì không thể truy xuất được giá trị flag
trả về.
Vì, thứ nhất flag
ở trong css nó không có, tiếp theo color
hay background-color
, font-size
... tất cả đều là thuộc tính trong CSS và nó đã có sẵn nên khi bạn gọi tới nó thì nó sẽ trả về giá trị mặc định tại endpoint CSS, thay vào đó bạn phải khai báo một biến ở CSS --ten-bien
lúc này nó sẽ là biến và nó có giá trị rỗng(nhớ kỹ là rỗng là nó không hề có trong endpoint CSS) khi đó chúng ta có thể lấy được flag như đoạn giải thích payload trên.
Ban đầy mình chạy thẳng file payload.html lên local sau đó dùng ngrok chạy payload lên internet, nhưng không thể exploit.
Vì: ngrok sử dụng protocol https nhưng local mình dùng http và thẻ link của mình cũng dùng http://0:1337..và ip-request cũng http nên nó chặn script mặc dùng vẫn hiển thị nội dung nhưng script không được thực hiện.
Sau đó mình dùng python deploy lên trực tiếp với địa chỉ là ip máy nhưng server không nhận, sau đó thử build bằng flask cũng không được.
Sau một lúc mình mò ở request-ip mình có mục response và sửa được nên mình đã up payload lên đó và. Lúc này ip-request chứa đoạn payload và nhận response của challenge.
Bây giờ chỉ cần gửi link cho bot và nhận flag:
Bài này cho source code, nhưng trước khi tới source code chúng ta sẽ interface có gì.
Ở đây sẽ có một endpoint opt sẽ cho chúng ta một đoạn code và có một biến ad cho chúng ta truyền bất kỳ giá trị nào.
Bây giờ chúng ta sẽ xem qua source code của chall:
Chúng ta xem xét đoạn code opt này:
Endpoint opt này bạn có thể điền thêm giá trị ở biến ad và sau đó render ra opt. Có điều đáng nói ở trên đã được set header CSP nên khi đó chịu, không thể khai thác XSS hoặc Cors, như trên có đoạn cmt chỉ rằng opt là endpoint mà bot visit. À opt ở đây sẽ được random với 12 chữ cái a-zA-Z0-9
Ở đây mình nghĩ có thể dùng cách khai thác CSS Injection:
Brute-foce 12 ký tự sẽ ra nhưng có điều khá lâu có một bài về kỹ thuật CSS Injection ở giải ctf khác, có thể xem ở đây: link
Nếu có đủ opt của bot khi đó sẽ có flag. Nhưng chúng ta sẽ xem xét tới một vuln khác ở đây:
Đây là một hàm visitUrl, nó tạo một trình duyệt ẩn danh mới và mở một trang web với đường dẫn được cung cấp. Sau đó, nó tìm kiếm phần tử đầu tiên của trang web là một thẻ input và lấy giá trị của thuộc tính “value” của nó. Nếu không tìm thấy phần tử input, giá trị của biến otp sẽ là null. Cuối cùng, nó đóng trang web và trình duyệt ẩn danh và trả về giá trị của biến otp. Nếu có bất kỳ lỗi nào xảy ra trong quá trình này, chúng sẽ được bắt và xử lý trong khối try-catch-finally cuối cùng.
Yaahh nếu bạn đọc đoạn trên cũng hiểu vuln ở đâu rồi chứ nếu chưa thì xem xét kỹ hơn:
Đoạn code trên là một chức năng JavaScript được sử dụng để trích xuất giá trị của input từ một trang web đã cho.
Đầu tiên, biến otp
được khởi tạo với giá trị null
. Chức năng này sử dụng thư viện Puppeteer để tạo một trình duyệt đơn giản. Sau đó, chức năng sử dụng phương thức setUserAgent()
để thiết lập user agent là "puppeteer".
Tiếp đó, chức năng này sử dụng phương thức goto()
để truy cập đến địa chỉ url đã cho. Tham số đầu tiên của phương thức này là địa chỉ url cần truy cập. Tham số thứ hai là một đối tượng chứa các tùy chọn, trong đó timeout
được thiết lập là 20000 (20 giây) và waitUntil
được thiết lập là 'networkidle2', cho phép trang web đợi cho đến khi mạng không có hoạt động nào trước khi tiếp tục.
Sau khi đã truy cập vào trang web, chức năng này sử dụng phương thức $eval()
để trích xuất giá trị của thẻ input
đầu tiên trên trang web. Đối số đầu tiên của phương thức này là một chuỗi CSS selector để xác định phần tử HTML cần được truy cập. Trong trường hợp này, phần tử HTML là thẻ input
đầu tiên trên trang web. Đối số thứ hai của phương thức này là một hàm chứa các thao tác để lấy giá trị thuộc tính của phần tử HTML. Trong trường hợp này, chúng ta sử dụng phương thức getAttribute()
để lấy giá trị của thuộc tính "value" của phần tử HTML đó.
Sau khi trích xuất được giá trị của intput
, chức năng đóng trình duyệt và kết thúc phiên làm việc bằng các lệnh page.close()
và ctx.close()
. Cuối cùng, giá trị của biến otp
được trả về. Nếu không tìm thấy giá trị của trường nhập liệu, giá trị của biến otp
sẽ vẫn là null
.
Bạn đọc kỹ phần mình bôi màu nhé. Tiếp xem đến endpoint visit:
Tiếp theo xem endpoint index /
Tổng kết lại:
Giá trị otp sẽ được hàm visitUrl lấy ở input đầu tiên thì ở endpoint /
cũng có input, khi đó nếu không có giá trị otp thì nó sẽ tự gắn session.opt = opt. Nếu tìm cách chuyển hướng về /
Vậy nếu khi đó chúng ta chỉ cần submit thì có thể lấy được flag, nhưng phải trong cùng session.
Sau khi bot truy cập trang, nó sẽ tìm thẻ input đầu tiên và trích xuất giá trị của nó và trả về dưới dạng OTP.
Khi đó để có flag thì
Đoạn payload của chúng ta sẽ như này:
Trong đoạn payload, yêu cầu GET tới endpoint /visit được gửi đến trang web của ứng dụng với một tham số truy vấn (query parameter) ad
chứa một đoạn mã HTML. Tuy nhiên, đoạn mã này không phải là mục đích chính của cuộc tấn công, mà chỉ là một cách để tạo ra một yêu cầu đến ứng dụng web mà không cần sự tương tác của người dùng.
Sau khi yêu cầu GET /visit đã được gửi, payload tiếp tục gửi một yêu cầu GET tới endpoint /flag với một tham số truy vấn otp được đính kèm. Nhưng vì đã sử dụng một đối tượng session để thực hiện cả hai yêu cầu GET này, ứng dụng web sẽ xác định đây là hai yêu cầu được thực hiện bởi cùng một người dùng và sẽ cho phép yêu cầu /flag được thực hiện mà không cần xác thực lại người dùng.