Write Up này mình viết sau khi cuộc thi đã kết thúc, với niềm tin là giải sẽ vẫn còn mở instance sau khi end, nhưng không =))). Nó thực sự kết thúc và không được mở instance nữa nên mình sẽ chủ yếu khai thác ở local thôi.
Challenge cấp cho mình src code và Dockerfile để có thể dựng được local. Mục tiêu chính của bài này sẽ là đăng nhập vào được website thì sẽ có flag.
Sau khi thử đăng nhập và ăn ngay thông báo "Browser not supported". Mình đã đi vào và xem xét src code:
Challenge có 2 api chính, là control_api được xử lý bằng nim (code lạ quá) và user_api xử lý bằng golang
Vậy nên chủ yếu mình sẽ phải làm việc với 2 file là main.go và main.nim. Khi gửi request đăng nhập lên thì nó sẽ được xử lý ở main.nim, sau đó gửi về main.go để xử lý
Ban đầu thì mình chưa biết về flow request nó sẽ đi như vậy, nên mình cũng chỉ đọc lướt file nim rồi tập trung vào khai thác ở golang để bypass vì nhận thấy đoạn đăng nhập dính sqli khá rõ ràng, nhưng mình đã không làm được vì hàm xử lý filter sqli ở file nim là không thể bypass bằng request body. Sau đó thì mình có đi hỏi những anh em bạn bè (rất cảm ơn bro Chanze vì đã hint mình siêu mạnh) thì mình mới hiểu được flow của chương trình và solve được challenge này.
Thứ mình cần phải đọc kĩ trước tiên phải là file main.nim
-> nơi đầu tiên request được xử lý sau khi gửi lên ở server
Đầu tiên mình cần để ý hàm filter sqli:
Mình được đưa vào một string, thì hàm này sẽ check xem kí tự được duyệt có phải là từ a->z, A->Z, hoặc 0->9 không. Nếu vi phạm sẽ trả về true và bị đánh giá là có chứa SQL Injection ngay -> không cho phép nhập cả dấu cách, nghĩa là chỉ có kí tự thường -> filter siêu chặt .
Sau khi khai báo hàm kiểm tra, thì mình sẽ nói đến hàm xử lý request POST đến entrypoint /user:
Hàm lấy giá trị của username và password vào 2 biến tương đương. Sau đó kiểm tra giá trị đầu vào của 2 biến này bằng hàm checkSQLi vừa rồi, nếu có sẽ trả về thông báo "Malicious input detected" và không thực hiện tiếp nữa
Sau đó nó lấy UserAgent của request bằng cách decode url giá trị user agent(sussy). Khởi tạo một json từ giá trị username và password từ body request, gửi đến entrypoint /login tại server internal http://127.0.0.1:9090
với post body request là đoạn JSON vừa khởi tạo, nếu như response trả về khác 200 sẽ gửi nội dung body được strip cho client (nghĩa là nếu như 400 sẽ trả về 400, hoặc 500 sẽ trả về 500). Còn nếu như status 200 sẽ trả về msg là flag
Hàm này sẽ xử lý và trả về kết quả theo dạng json {"msg": "..."}
-> Nội dung sẽ được đưa vào bên trong msg và trả về cho mình.
Tại main.go, đập vào mắt mình ngay lập tức là website sử dụng sqlite3 để làm dbms, và có danh sách các user-agent được cho phép (lí do tại sao đăng nhập bình thường dính ngay lỗi browser :D)
Server khởi tạo struct User có 3 giá trị ID, username, password tương ứng trong database để thuận tiện trong việc gán giá trị và lấy thông tin
Sau đó họ tạo ra 10 tài khoản được random 32 ký tự hex, riêng mật khẩu được lưu trữ dưới dạng hash theo thuật toán bcrypt bằng hàm bcrypt.GenerateFromPassword
Tiếp đến là đoạn xử lý request login được gửi đến từ file .nim:
Họ check header user-agent trước, nếu như có chứa một trong các user-agent được cho phép đã đề cập ở bên trên thì sẽ cho qua, còn nếu không sẽ trả về lỗi và kết thúc chương trình.
Tiếp đến khởi tạo một object User với nội dung là đoạn json được decode về dạng key-value tương ứng với dạng struc user vừa tạo trước đó
Gán password vừa lấy được vào biến userPassword, thực hiện tìm kiếm user bằng câu lệnh SQL "SELECT * FROM users WHERE username='" + user.Username + "';"
-> nối chuỗi username vào câu lệnh này. Fetch lấy giá trị của hàng đó cho vào giá trị của các object User -> user.Password lúc này sẽ password đã được hash. Tiếp tục so sánh hash của password mình nhập vào với giá trị hash password tương ứng ở trong database. Nếu sai sẽ trả về lỗi Invalid password or password
, còn nếu 200 thì trả về Login successful -> file nim thấy status 200 sẽ trả về flag
Vậy là khi mình POST /user, file nim sẽ đứng ra xử lý request, check body param, tạo 1 request POST /login đến internal service tại cổng 9090 để xử lý request -> file main.go, tại đây, request tiếp tục được xử lý để xem có tồn tại username và password hash có giống nhau không, sau đó trả response về file nim. File nim dựa vào status để xem nếu status 200 sẽ gửi flag về cho user, nếu status khác sẽ trả về response chứa lỗi tương ứng, ví dụ như 400 Bad Request,…
Như đã nói ở trên, đoạn nối chuỗi để login rất ngon nên mình đã cố gắng để sqli bằng body request ở POST /user, mình thử xài mảng và một số cách nữa nhưng đều thất bại, hàm filter SQLinjection là quá chặt . Đây không phải là cách mình có thể solve được chall này
Sau đó mình có chú ý đến đoạn decode url user-agent, chả có lý do gì nó cần phải decode URL, ngoại trừ khi mình cần xuống dòng -> nên mình đã nghĩ đến việc code injection để ném cái đoạn thông báo gửi flag về ngay đằng sau câu lệnh tạo user-agent, nhưng cách cũng không được vì mình không escape khỏi đoạn chuỗi khai báo được, và khi gửi lên nó không hoạt động như vậy
Sau khi được Chanze hint thì mình mới hiểu mục đích của việc nó cho decode url là gì, không phải là code injection chèn một đoạn code nim vào, mình sẽ lợi dụng decode url để thực hiện crlf injection, chèn payload vào phần khai báo json body của request đến /login. Vì để ý mình sẽ thấy đoạn khai báo user-agent cách chỗ khởi tạo đoạn jsson 2 dòng -> %0d%0a%0d%0a
Một điều mãi mình mới hiểu là hàm filter sqli chỉ áp dụng với body request của POST /user, chứ không phải body request của POST /login, nên nếu như chèn được đoạn body json để file nim gửi đi thì mình có thể SQL injection thoải con gà mái rồi
Khi mà có thể sqli và chèn vào một json tùy ý, thì đoạn json của mình sẽ được ghi lên trên đoạn json mà code Nim gen ra, từ đó phát sinh ra 1 vấn đề mới là content-length, nếu như content-length không đủ nó sẽ không lấy hết json, từ đó render lỗi và kết quả là status 400. Thế nên để truyền vào được json tùy ý mình còn phải control giá trị content length nữa.
Content-Length là header được tự tạo khi gửi request mà mình đã cố thử sử dụng crlf để chèn thêm nhưng không thể được, nên mình cần phải kiểm soát nó bằng cách khác. Sau một ngày bí xị thì anh của mình đã hỏi mình một câu là: Em nghĩ xem cái content length cũ nó tính ở đâu ra
-> đúng, content length cũ (tức là content length của POST /user) được quyết định dựa trên độ dài của body request đến /user, request đến /login sẽ dựa vào dữ liệu nhận được (cũng chính là body request đến /user) để tạo ra content-length của /login. Tức là mình có thể sử dụng body để quyết định xem request đến /login sẽ có content-length là bao nhiêu.
Tất cả mọi thứ đã đầy đủ, vậy giờ cách khai thác của mình sẽ như sau:
Invalid password
chứ để or thì mình không biết bao giờ mới sqli thành công =))Giờ mình sẽ tự gen ra đoạn hash đơn giản bằng golang nhằm phục vụ việc set password:
Mình có thêm cả đoạn check lại password nữa cho bó cẩn =))), để xem liệu mình có gen hash đúng không
Mình có được hash cho password "a" là
Giờ xem xét câu lệnh SQL nào:
Để UNION SELECT thì mình cần phải để đúng số cột của bảng users, bảng này chứa 3 thuộc tính id int, username text và password text nên Payload của mình lúc này sẽ chọn cả 1 số int bất kì để giống với cột id nữa:
Đã có payload json rùi, giờ mình lấy length và gửi payload thui
Length của payload là 151
Encode URL đoạn json nào:
Lụm flag:
Deploy instance, và mình bê y nguyên những gì đã viết ở đây vào instance thì mình có được flag:
Flag: HTB{d0_th3_d45h_0n_th3_p4r53r}
Challenge này được solve bởi teammate của mình Hồng Nam, mình đã tham khảo cách làm và sẽ thực hiện khai thác lại trên local để hiểu rõ hơn về chall.
Ngoài ra mình cũng được các anh hint cho một số keyword để khai thác chall này:
Challenge cung cấp chức năng đổi tên tài khoản, xem các mặt hàng và thêm các sản phẩm mình thích vào một list được gọi là WishList: (code giao diện đẹp wá)
Đây là lần đầu mình được tiếp cận với code C# nên khá là ngợp vì số lượng file của nó, nhưng có một số folder chính mà mình chú ý đến trong những kha khá nhiều file src code của challenge:
Nhìn sơ qua ban đầu thì mình xác định dbms của challenge vẫn là SQLite ở file Nexus_Void.csproj:
Tại HomeControllers.cs, mình thấy chức năng Setting -> đổi tài khoản có khả năng dính SQL Injection vì nó chỉ nhét giá trị username mình control vào mà không phòng bị gì cả, lúc đầu đọc mình đã bỏ qua nó vì nghĩ ID là thuộc tính mình không control được nên mà quên mất mình có thể comment chỗ đó lại mà chèn vào một câu query khác -> stacked query(đây cũng là lần đầu mình stack query trên SQLite vì mình cứ nghĩ sqlite không hỗ trợ stacked query)
Sau khi biết nó có thể sqli, mình đã thử ngay chức năng đọc file của SQLite là readfile()
nhưng không có kết quả, có lẽ việc SQL injection là chưa đủ để lấy được flag, nên mình tiếp tục đọc đến các hàm xử lý của WishList:
Đây là chức năng thêm vào wishlist sản phẩm mình yêu thích, cụ thể là khi add một sản phẩm vào favourite sẽ trigger hàm này với tham số truyền vào người bán (sellerName) và tên sản phẩm (name). Đầu tiên nó fetch lấy thông tin của sản phẩm đó bằng ID user, sau đó so sánh xem liệu sản phẩm này đã tồn tại ở wishlist hay chưa, nếu chưa thì nó tiến hành khởi tạo object ProductModel, thêm các thuộc tính của mặt hàng này vào object, serialize object bằng helpers SerializeHelper, gán nó vào giá trị serializedData. Rồi thêm vào bảng Wishlist id, tên người dùng và data là serializedData. Tức bảng Wishlist sẽ chứa một cột data chứa dữ liệu được serialize về object thuộc kiểu ProductModel. Vậy thì sure kèo là để hiện ra trang web sẽ thực hiện deserialize giá trị này -> insecure deserialization
Chính xác luôn, nó lần lượt deserialize dữ liệu có trong cột data của bảng để show ra view, nếu không sẽ trả về null. Mình thấy có 2 chức năng trigger đến deser là truy cập đến wishlist và remove sản phầm, mình nghĩ để trigger deser thì xóa đi hay truy cập đến nó cũng như nhau nên ở đây mình sẽ khai thác deser bằng cách truy cập đến Wishlist.
Oke, đọc đến hàm này có vẻ như mình sẽ phải khác thác theo deser rồi, mình chuyển sang đọc về các function Helpers được sử dụng để xem chúng serialize như thế nào:
Helper này sử dụng JsonConvert Serialize để serialize một object về dạng một string json, string json sau đó được xử lý qua hàm EncodeHelper.Encode -> tiếp tục là một helpers nữa phục vụ việc encode và decode Base64 rồi mới trả về.
Điều cần lưu ý là để serialize input buộc là một object ProductModel nên mình nghĩ sẽ không thể nào mình chèn payload vào được ở hàm này, hay nhờ webserver serialize payload của mình được
Hàm deserialize thì làm ngược lại, từ chuỗi json string nó decode Base64 trước rồi deserialize về object ban đầu, và gán nó trở lại vào 1 object kiểu ProductModel là products
Hmmm , vậy tức là nó deserialize trước, rồi gán nó vào một object kiểu ProductModel? Nếu như mình có thể control được string input thì hoàn toàn có thể khai thác được deserialize tùy ý nhỉ
Sau đó mình xem liệu deserialize thì sẽ deserialize cái gì. Mình được một người anh là Tuấn Anh chỉ rằng có một helpers thực hiện câu lệnh command tùy ý theo input vào, nó là StatusCheckHelper.cs
Ngon chưa, helpers này nhận đầu vào là một string, sau đó nhét command vào /bin/bash -c
để thực thi câu lệnh dưới một process được ẩn. Sau khi thực thi sẽ lưu output tại biến output và có thể được show ra nếu như ta .output
Chức năng sử dụng thằng này là ở /status và /uptime để thực thi sẵn bash script có sẵn trong tmp. Ở entrypoint này không có cơ hội cho mình khai thác vì trỏ đến là nó tạo mới lại object StatusCheckHelper nên không có chuyện nó nhận object của mình
Sau khi nhận thấy có 3 chỗ chứa lỗi SQLi, Deser và Command Execution. Mình vẫn chưa biết làm như thế nào để website nhận chuỗi deserialize của mình. Thứ mình cần là payload nằm trong cột data của bảng wishlist cơ
Khoan đã, mình có SQLi stacked query mà, vậy thì mình có thể tạo một câu lệnh UPDATE bảng để chèn thông tin vào . Sau đó thì mình nghĩ không cần thiết đến việc có show ra output hay không, vì mình có thể out bound ra ngoài
Tổng kết lại, quy trình exploit sẽ như sau:
Mọi thứ đã đầy đủ, chỉ còn thiếu mỗi payload, mình sẽ lấy helpers của source code để thử xem payload serialize có hoạt động đúng theo những gì mình nghĩ không, mình demo trên Windows nên sẽ phải chỉnh sửa một số chỗ ở StatusCheckHelpers
StatusCheck.cs
Program.cs
Hehe, outbound ra ngoài thành công
Vậy thì mình đã build được thành công payload, giờ mình xem là docker có curl hay không để tiện sử dụng:
Có vẻ như không có curl rồi, vậy thì mình dùng wget cũng okela
Sửa lại câu lệnh command mình muốn để gen ra payload:
Base64 encode lại payload, mình cần sửa lại tyoe về đường dẫn đến helpers như trong challenge:
SQLi vào entrypoint /Home/Setting để update payload vào cột data của bảng Wishlist
Truy cập vào wishlist và ngồi đợi flag về thui, khi hàm thực hiện deserialize và đến đoạn gán vào object ProductModel thì chắc chắn sẽ xảy ra lỗi, tuy nhiên nó vẫn sẽ được thực thi:
Solved