Cuối tuần này câu lạc bộ của mình có tham gia vào giải AKASEC CTF 2024 và cũng đạt được 1 số thành quả nhất định, trong giải thì mình không tự solve được bài nào, mình vẫn chậm trong việc spot lỗi và bypass exploit mà chủ yếu toàn là học hỏi hướng đi và cách giải của các teammates. Nhưng mình cũng học được những kiến thức mới từ teammates và các anh khóa trước nên không sao, kiến thức +1.
Về các web challenge của giải này mình thấy khá hay và đa dạng các loại ngôn ngữ, team mình đã cố gắng hết sức những vẫn không clear được web mà vẫn còn 1 challenge rust, khá tiếc vì đấy là challenge mình dành hầu hết thời gian cho nó mà không tìm được hướng đi đúng đắn. Âu cũng là bài học kinh nghiệm để sau mình không dính quá sâu vào rabbithole mà không lùi ra kịp nữa =))
Chia sẻ dài dòng rồi, mình bắt đầu thôi.
Đây là một challenge như mình thấy thì không có gì ngoài upload và login ở đây cả.
Nhưng mà mình có cả link bot nữa, nên mình đoán nó sẽ là upload file to XSS
Route upload file xử lý khá đơn giản, filename được truyền vào db cùng với tên người dùng, và có thể xem qua endpoint /view/file-name
Tại const upload
trên file được check mime type để xem có đúng là pdf hay không:
Flag nằm tại route /flag
, nhưng chỉ truy cập được từ local:
Mình thấy việc check này lỏng quá, hoàn toàn có thể upload file khác để report bot xss đến route /flag
, sau đó ném kết quả về requestrepo hộ mình.
Mình upload thử trước 1 file html:
Nếu trỏ sang view thì sẽ không trigger được xss vì file được xem bằng pdf.js, file nằm trong thư mục uploads/
nên trỏ đến /uploads/file-1718003466650.html
sẽ trigger xss:
Mình craft một file html để xss:
Gửi cho bot với route /uploads/file-1718003739434.html
và đợi flag về thôi, lưu ý mình cần gửi với đúng định dạng url của con bot:
Bot nhanh quá gửi cái requestrepo có request luôn:
Flag: AKASEC{PDF_1s_4w3s0m3_W1th_XSS_&&_Fr33_P4le5T1n3_r0t4t333d_loooool}
Sau khi lượn lờ ở kênh discord thì có vẻ intended của bài là CVE của pdf.js thì phải :">
Challenge cho cái giao diện làm mình nghĩ đến vuln SSRF classsic là fetch URL nhưng mà thử 1 hồi không giòn nên mình đi xem source code:
Đoạn fetching cũng chỉ đơn thuần là trỏ đến URL bằng một GET request, đọc nội dung và hiển thị render ra nội dung đó trong template html:
Ảo nhất là quả route đến /flag
cho quả so sánh 1=0 thì in ra flag =)))
Cái route flag coi như bỏ đi sau khi mình đọc đến dòng điều kiện =))).
Sau khi nghịch SSRF không ra cái gì, mình chuyển qua xem lib nó có gì sus không thì thấy có thư viện net/http/pprof
là không được dùng trong code.
Đi tìm hiểu về thư viện này thì mình thấy có một số path khá hay, cụ thể là debug/pprof/
-> Ref
Trỏ đến thì mình thấy giao diện giải thích khá chi tiết một số path tiếp theo và công dụng:
Trong đó mình để ý path cmdline sẽ hiển thị câu lệnh khởi tạo tiến tình hiện tại, là tiến trình web golang này (cũng tương tự như /proc/self/cmdline).
Khoan, flag được đưa vào như một tham số chạy web, vậy mình chỉ cần đọc nó là có flag rồi, cần gì route /flag
kia nữa.
Câu lệnh chạy webapp trong Dockerfile đã thể hiện điều mình vừa nói:
Cũng không SSRF lắm =))))
Flag: AKASEC{r0t4t3d_p20x1n9_f002_11f3_15n7_92347_4f732_411____}
Đây là một chall web code bằng ruby với rất nhiều thư mục :)), mình cũng không rành về thằng ruby này lắm nên việc review source mất kha khá thời gian và thực sự là mình cũng chưa hiểu hết cách hoạt động của thằng này
Challenge cho mình nhập tên tại endpoint /join
và sẽ hiển thị tên của những người đã nhập tên tại endpoint /home
Khi gọi POST request đến /join
, web server xử lý req thông qua hàm create trong join_controller.rb
:
/home
authenticity_token
và username
:username
mới được đưa vào, authenticity_token
đã bị filter:Tại route /home
phục vụ phương thức GET, admin sẽ có thể redirect đến latest/url:
get '/users/latest', to: "users#latest"
Trong routes.rb đã nêu đường dẫn có thể trỏ đến của challenge:
/flag
nằm trong def flag của users_controllerRoute /flag
chỉ có thể trỏ đến thông qua local, hàm kiểm tra địa chỉ có phải 127.0.0.1 thì mới cho phép truy cập, không sẽ hiển thị thông báo Access Denied:
Nên mình nghĩ là cần phải lên admin rồi tìm cách SSRF trỏ đến route flag tại local thì có được flag
Việc đầu tiên là mình cần phải làm cách nào đó để set được thuộc tính admin là true trong bảng users
Tuy nhiên nếu set là admin=1 ở trong request là bị filter giá trị ngay.
Đầu tiên thì mình cũng không để ý, nhưng trong folder log có file development.log là log của quá trình author tạo dựng và thực hiện challenge (mình đoán vậy), tại đây mình có thể thấy rõ ràng kết quả được đưa vào insert trong db sqlite:
Mặc định không nhét giá trị admin vào thì admin sẽ được gán là 0 -> false
Kéo xuống tiếp 1 đoạn khá dài thì mình để ý thấy có request này khiến admin trở thành 1:
Thay vì truyền vào admin=1
thì mình sử dụng admin()=1
, đây là cách sử dụng multiparameter attributes trong ruby on rails, thường được sử dụng để lưu giá trị ngày tháng ref. Ví dụ như date(1i) -> phần tử thứ nhất có kiểu integer
Từ đó trong log mình cũng thấy author sử dụng admin(1i)=1
Mình đã có được cách bypass admin từ author ròi=)))
Giờ mình cần tìm cách trỏ đến route /flag
tại route /home
, người em thiện lành - teammate của mình l3mnt2010 phát hiện thằng này dính host header injection khi truyền vào X-Forwarded-Host thì nó sẽ redirect theo, mình thử chèn burp collaborator thì đúng thật:
Theo như mình hiểu là Rails sẽ lấy giá trị của Host để nối với đoạn users_latest_url
của users tạo thành _url
hoàn chỉnh rồi follow theo bằng GET request, đây gần như là tính năng của Rails khi các helpers của _url
dùng giá trị của header Host để build up url: Ref. Có thể khẳng định nó dính host header injection.
Ngon, vậy mình đến local luôn thôi:
=))) Có vẻ là nó không nhận cổng của mình mà chỉ lấy tên miền xong redirect theo, nên mình sẽ ngrok để host 1 web để redirect ra flag:
Khi request mình sẽ phải dấu # để request coi phần /users/latest là fragment chứ không phải url path, vì file kia của mình đang để index.php. Mình nghĩ cũng có thể nhét index.php vào thư mục /users/latest thì sẽ không cần xài dấu fragment nữa =))
Redirect thành công:
Gửi request tương tự trên server mình có flag
Flag: AKASEC{__W3lc0me_t0_HackerC0mmun1tyy__}
Đây là bài mình ngốn mấy ngày để ngồi làm nhưng mà không ra được kết quả ngay từ bước đầu tiên :((
Challenge bao gồm 2 service, web service code bằng rust và log service được thực hiện bằng js
Ban đầu website sẽ khai báo khởi tạo 3 giá trị, SECRET -> secret key token được gen 30 ký tự ngẫu nhiên, PASSWORD là mật khẩu của admin và API_KEY là api key cho service js:
Mình sẽ nói đến các helper/utils được sử trong các hàm xử lý req trước, trong đó có 2 hàm quan trọng mà mình muốn nói đến:
PASSWORD
trong input thành mật khẩu của admin:Tại route /register, khi người dùng đăng kí thì username sẽ được kiểm tra trước khi được lưu vào mutex:
user
-> khỏi bypass token admin luônChức năng đăng nhập tại route login
cũng khá tương tự, khi tìm thấy username đầu tiên trong mutex trùng khớp với username nhập tại form sẽ tiêp tục kiểm tra bằng verify hash để xem có trùng khớp hay không:
=> Mình có thể kết luận này không có cách nào leak trực tiếp được full mật khẩu admin ra màn hình với logic code này, cũng không có trò tạo admin thứ 2 (vì mình đã thử) bởi hàm find sẽ lấy kết quả nó tìm được đầu tiên và sẽ luôn lấy admin của nó vì admin gốc đứng đầu mảng =))
Route /log
sử dụng API_KEY ở trên đưa vào header rồi request đến service js port 3000, để sử dụng route này thì user_type phải là admin:
Tại đây mình thấy nó xài $
từ bun thì mình nghĩ ngay đến command injection, service chỉ phục vụ duy nhất 1 công dụng là chạy câu lệnh logger + body với body là nội dung của biến message theo kiểu JSON bằng 1 POST request:
Như vậy, mục tiêu của mình là bypass admin và sử dụng route /log
để RCE
Mình đã thử khá nhiều cách để bypass admin nhưng thất bại, vì cứ nghĩ vuln sẽ theo kiểu code logic nên mình đi tìm cách để leak luôn cả mật khẩu admin qua hàm subs kia, mình thử cả SSTI handlebars của Rust nữa nhưng cũng không mang lại kết quả gì.
Sau giải mình mới đc biết solution của 1 team là bcrypt truncation -> cái này mình không nghĩ đến.
Khi sử dụng bcrypt để hash thì chuỗi input chỉ có độ dài lớn nhất là 72 byte, nếu như input > 72 byte bcrypt sẽ tự động loại bỏ để tiến hành băm chuỗi đó. Đây là một vuln muôn thủa của bcrypt mà tại trang PHP khi sử dụng hash_password có nhắc đến. Nhưng mình không nghĩ ra vuln này khi đây là ngôn ngữ Rust :((((
Biết được vuln tại đấy, mình có thể leak từ từ từng kí tự của mật khẩu admin bằng việc:
/
, còn thất bại sẽ là /login
Mình viết script py dựa theo ý tưởng của teammate, thanks to Ngọc:
Tại local thì mình đã thấy ra mật khẩu thành công vì local nó hardcode là REDACTED
Ps: Tại challenge mật khẩu không phải chỉ có 10 kí tự nên mình đã tăng số lượng vòng lên 30 thì có mật khẩu đúng là M07H4F7H38167Hr33175JU57816M3
Coi như là mình đã bypass thành công admin, chuyển qua stage tiếp theo thôi:
Tại service js rõ ràng là nó đã đưa vào câu lệnh hệ thống bằng module bun, nhưng nếu như truyền vào thì sẽ không hoạt động, vì nếu truyền thẳng vào thì chuỗi sẽ bị escape.
Để command không bị escape, mình sẽ sử dụng đến thuộc tính raw: https://bun.sh/docs/runtime/shell#escape-escape-strings
Khi truyền vào raw thì string command sẽ không bị escape nữa, từ đó có thể thực thi được câu lệnh
Challenge không có curl hay wget, nên mình sẽ đi theo hướng reverse shell.
Mình xài luôn revese shell theo kiểu nodejs cho chắc cú:
Sử dụng bun run để chạy đoạn js này:
Base64 câu lệnh để đỡ phải escape mấy dấu nháy trong json:
Craft thành payload:
Reverse shell thành công
Flag: AKASEC{w311_17_41n7_7h47_2u57yyy_4f732_411}
Challenge này theo mình đánh giá là hay và khó nhất của giải, được code bằng Java và sử dụng một service code bằng python để hiện ra hackernickname random trong chuỗi có sẵn
Route home /
khi sẽ nhận POST request sẽ xử lý thông tin để trả về một jwt:
nicknameService.getNickName()
sẽ có nhiệm vụ lấy nickname bất kỳ trong array nicknames
getInfo()
:isAdmin()
, hàm sẽ trả về thuộc tính admin có trong thuộc tính role vì role là 1 instance của class UserRole
:-> Để có thể lên được admin thì mình thấy cần phải set được giá trị của hackerRole
là admin=true
thì hàm isAdmin()
mới trả về true
Sau khi POST thành công tại /
web server sẽ redirect mình sang /nickname
, tại đây server xử lý khá đơn giản là lấy giá trị của cookie jwt
tạo từ route trước để lấy giá trị của thuộc tính nickname
rồi hiện nó ra màn hình.
Thứ mình muốn đến là các route có điều kiện, đầu tiên là route của admin sẽ có prefix /admin/
, tại đây có chức năng update
cho phép thực thi lệnh curl mà mình có thể control giá trị url
http://
, host phải là nicknameservice
và port 5000 -> y như cái url update file nickname thì mới đưa vào được câu lệnh curlRoute mà mình nghĩ cần dùng để RCE được là tại /ExperimentalSerializer
, tuy nhiên nó chỉ được sử dụng ở localhost:
serialized
để thực thi hàm deserialize tự custom, nhưng để truy cập tới thì IP truy cập phải là localhost, kết hợp với route /admin/update
bên trên thì mình cần phải để server curl đến mới thỏa mãn truy cập vào đc route này.readValue
có chứa CVE-2017-17485Theo lý thuyết thì thuộc tính hackRole không thể truy cập được thông qua json, tuy nhiên teammate endy tìm được link doc và cách bypass rất hay (mình search không ra :cry:) https://blog.kuron3k0.vip/2021/04/10/vulns-of-misunderstanding-annotation/
Bằng cách truyền vào giá trị key là null: "", mình có thể ghi giá trị vào field kế tiếp.
Từ đó mình có thể truyền vào "":{"admin":true}
thì có thể set được giá trị của này vào role, bypass admin thành công:
Đã có jwt session valid, giờ mình cần tìm cách SSRF vào route /ExperimentalSerializer thông qua câu lệnh curl
tại route /admin/update
Vì được nhét vào câu lệnh curl, nên mình hoàn toàn có thể truyền 2 URL vào câu lệnh thông qua curl globbing để bypass đoạn check.
Có 2 cách để curl globbing, một là sử dụng []
để truyền vào một chuỗi tăng dần các ký tự, ví dụ [1-10]
hoặc [e-f]lag
, khi curl server sẽ curl đến 2 chỗ: elag và flag, còn hai là sử dụng {}
để truyền vào một tập các giá trị, cách nhau bằng dấu ,
ví dụ như {a,b,c}
thì khi curl sẽ thực hiện curl từng cái a,b,c một thành các request riêng biệt
Từ đó mình đã có payload như này:
Nhưng payload này không thành công
Mình đoán là do đang ko lấy được host đúng của URL, sau khi thử một hồi không được thì mình đã đi ngủ để mai dậy làm tiếp, sáng hôm sau mình thấy teammate chanze có cách bypass là sử dụng @
để truyền vào giá trị host:
@
là một native syntax để khai báo một URI, cấu trúc của một URI sẽ có như sau:
Bên trong phần authority sẽ chứa user, pass, host và port:
Một URI đầy đủ có thể được biểu diễn bằng hình dưới đây
Nên khi ta sử dụng dấu @
, webserver sẽ hiểu chuỗi đằng sau là hostname và bypass qua hàm check URL
Tại đoạn code xử lý deser, giá trị của biến serialized được đưa vào mảng dataList, sau dó tùy giá trị của thuộc tính type
mà sẽ xử lý khác nhau:
value
, cách nhau bằng dấu |
value
được coi là class để khởi tạo thông qua getConstructor
, giá trị thứ 2 đóng vai trò là tham số truyền vào class giá trị 1 thông qua instance mới được tạoresult.put
, tương tự khi truyền vào 3 giá trị thì giá trị thứ 2 và 3 sẽ là tham số truyền vào khi khởi tạo instance từ class giá trị 1.Sink của lỗ hổng deser nằm ở readValue
, mình đi xem xét các lib chương trình sử dụng để tìm kiếm các gadget chain được hiện trong file build.gradle
:
Tìm kiếm về nơi sử dụng thì mình thấy lib databind được dùng để tạo objectmapper để readvalue
Tìm kiếm thì lib này dính CVE-2017-17485, được sử dụng để convert json thành các object, cve này có cve rõ ràng tại: https://www.cnblogs.com/afanti/p/10203282.html
PoC này có đoạn code xử lý gần giống với challenge, nên mình nghĩ cũng không thể bê nguyên được payload xml của họ đi mà cần chỉnh sửa.
Gadgetchain dùng để gọi đến class FileSystemXmlApplicationContext
được sử dụng để tải file từ bên ngoài, ở đây là file pwn.xml chứa payload thực thi câu lệnh hệ thống thông qua các bean.
Chức năng readValue
dùng để deserialize json, sau đó lấy giá trị của bean thông qua getBean
và parse giá trị của câu lệnh như là một SpEL expression -> RCE
Giờ mình sẽ kết hợp nó vào chall để tạo thành payload của mình:
Đầu tiên, giá trị mình truyền vào là một List nên mình cần truyền vào kiểu mảng.
Giá trị của thuộc tính type
sẽ là object, payload sẽ nằm trong value
là class org.springframework.context.support.FileSystemXmlApplicationContext
và url đến file pwn.xml cách nhau bằng dấu |
. Giá trị name
không quan trọng.
Payload bị urldecode 1 lần khi đi qua việc gán vào URL, và 1 lần nữa khi đi qua curl, nên mình cần phải encode url đoạn payload gadget 2 lần.
PoC của link mình để bên trên sẽ không hoạt động vì nó không trigger SpEL injection như cách trên, nên mình sẽ chỉnh sửa để khi getBean lấy beans thì method start được kích hoạt thay vì phải .start()
bằng init-method
:
Mình sử dụng php server và ngrok để public file này ra ngoài
Gửi đi payload serialize:
Curl ra ngoài thành công, mình sẽ lầy flag bằng cách curl ra ngoài kèm câu lệnh /readflag
Processbuilder và runtime.exec đều sẽ đưa các phần tử vào mảng nếu chúng cách nhau bằng 1 dấu cách, nên nếu ghi thẳng vào payload curl sẽ rất dễ fail vì giá trị args sẽ bị đưa vào các mảng khác nhau, nên cách tốt nhất là đưa vào base64 để thực thi:
File pwn sẽ thêm một tham số nữa trong phần bean:
Exploit trên local thành công:
Thực thi trên server, mình có flag về request repo:
Flag: AKASEC{__I_gue55_y0u_do_l1k3_JAVA_aft3r_4LL__}