Trong giải HTB Business này, mình tham gia vào làm challenge Omniwatch và Magicom cùng với các teammates trong câu lạc bộ. Chúng mình đã solve được challenge Omniwatch, còn Magicom thì gần như đã làm được, chỉ thiếu một bước nữa nhưng chúng mình đã đi sai hướng và không tìm ra cách giải kịp giờ nên không kịp solve.
Mình muốn viết là để chia sẻ lại quá trình giải challenge của mình và các teammates, và cũng như là hướng giải đúng đắn để solve challenge. Mình đã tham khảo official solution và nhận ra anh em đã đi đúng gần hết các bước, chỉ có bước đầu là chưa ra, nên bước đầu của mình sẽ đi theo con đường của official write up. Mình sẽ tiến hành vào khai thác tại local vì mình viết write up này hơi muộn nên không deploy trên server kịp=))
Challenge xuất hiện dưới dạng một website có chức năng xem sản phẩm và thêm sản phẩm, người dùng có thể thêm sản phẩm và một số các thông tin của sản phẩm, trong đó là phần ảnh minh họa:
Đã được add product tùy theo ý mình mà còn unauthen, mình ban đầu cũng nghĩ upload php để RCE:
Ngay sau khi mình mở source của challenge mình đã thấy đây chắc chắn không phải là một challenge upload file PHP thông thường. Vì challenge cung cấp cả phpinfo tại /info, và file php.ini, mình nhìn sơ qua qua thì cũng chưa có gì bất thường, nhưng chắc chắn là mình cần phải dùng đến chúng.
Challenge đã quy định những route có thể truy cập tại website trong index.php:
Mình ngay lập tức đi tìm kiếm những đoạn code xử lý upload file:
[key] => value
tmp_name
bin2hex(random_bytes(8))
, move vào thư mục uploads và trả về thông báo và lỗi nếu có.Riêng đoạn code check valid tại class ImageModel đã làm cho mình không tin tưởng vào việc có thể bypass upload file PHP nữa, mặc dù đoạn lấy extension ở controller khá ngon nhưng hàm valid lại quá chặt nên mình đi đọc những file khác vì còn rất nhiều thứ chưa được sử dụng.
Đáng chú ý nhất chắc chắn là file cli/cli.php, nó vốn được dùng để import file sql chứa các sản phẩm mặc định vào database bằng command line, sau đó file sql sẽ bị xóa:
Vì pha import này quá cồng kềnh, chả có lí do gì phải làm hẳn 1 file php chỉ để import 1 file sql vào, nên mình chắc chắn sẽ khai thác từ file này ra:
Ngay từ đầu file đã đánh phủ đầu bằng việc check xem file có được thực thi thông qua dòng lệnh hay không:
argv
và argc
là 2 biến siêu toàn cục, trong đó argv
sẽ là một mảng lưu giá trị của các tham số truyền được truyền vào file php dưới dạng mảng [số thứ tự] => value
. Còn argc
sẽ chứa số các tham số được truyền vào.
Ví dụ như với câu lệnh chạy file cli.php như ở trên, thì giá trị của argv
sẽ có dạng như sau:
Còn argc
sẽ mang giá trị là 6, ứng với số tham số truyền vào
File có thể gồm 3 tham số truyền vào:
Tham số -c
khá quan trọng, giá trị này sẽ được check xem có phải là thư mục hay không, nếu có sẽ chèn thêm config.xml
trước rồi return đệ quy để tiếp tục kiểm tra, tiếp đó kiểm tra xem file có tồn tại, và file đó thêm .xml
có tồn tại hay không, nếu 1 trong 2 tồn tại thì return về giá trị đó.
Sau khi đã xác định file config có tồn tại, file mới được đưa vào xử lý:
<config>
-><db>
, lấy attribute name
lưu vào biến $vars và lấy attribute value
của name tương ứng.Hàm generateFilename
dùng để tạo ra tên file ngẫu nhiên khi sử dụng method backup:
=> Kết quả cho ra file sql backup với filename sử dụng kết hợp ngày tháng và 8 ký tự random
=> Method backup và import có khả năng command injection nếu như control được giá trị trong file config. Còn với option -f thì không chắc vì nó cũng bị kiểm tra bằng file_exists
nên không thể command injection sau filename được.
Mình mất một lúc để nhận ra có thể truy cập đến cli.php thông qua website /cli/cli.php. Nên mình đang nghĩ đến command injection vào các method, nhưng làm thế nào để bypass sử dụng trên command line?
Đang tìm cách thì teammates của mình phát hiện ra tại php.ini giá trị register_argc_argv
đã bị comment: Giá trị này được mặc định là off, nếu như config này được kích hoạt thì mình hoàn toàn có thể truyền vào giá trị của 2 biến này thông qua dấu +
thay vì dùng dấu &
để ngăn cách.
Vậy thì mình có thể lợi dụng việc này để truyền giá trị vào các tham số -m và -c, thực thi file cli.php như đang ở command line, mình truy cập thử đến mode healthcheck thì thấy file hoàn toàn có thể thực thi được:
Mình quyết định sẽ lấy mode backup làm sink để RCE, với việc sử dụng DOMXPath query lấy dự liệu từ file xml, mình tạm thời bỏ qua việc upload file lên như nào mà craft một file xml để command injection vào username:
Để tránh câu lệnh bị thực thi khi echo vào thì mình base64 trước rồi truyền vào a.xml
Thử truyền vào method backup và tên file config là: /tmp/a.xml
Và mình thấy kết quả trả về requestrepo, đây là sink đúng để có thể RCE:
Oke vậy là đã có chỗ để RCE, vấn đề còn lại là upload file xml này lên như nào với cái rule whitelist kia thôi.
Như mình đã nói ở trên, có 2 thứ mà mình thấy mình chưa sử dụng được trong challenge này, thứ nhất là trang phpinfo, và thứ 2 là cái print_r hiện thông tin của file. Khi PHP script nhận được một file request, file đó sẽ được lưu tạm thời trong thư mục /tmp và có thể tùy chỉnh trong php.ini. Trong trường hợp này sẽ là /tmp/php+6 ký tự [a-zA-Z0-9]. File trong thư mục tmp sẽ biến mất sau khi có sự xuất hiện của hàm move_uploaded_file
hoặc khi PHP script đó kết thúc. Nghe ná ná giống với case LFI2RCE với phpinfo nên mình và teammate triển luôn theo hướng này.
Bọn mình dự định sẽ upload file sau đó vào phpinfo để xem đường dẫn file tmp, rồi sử dụng mode backup để command injection. Nhưng sau khi thử rất nhiều lần không được, mình đã đi tìm hiểu kĩ hơn và nhận ra mình đã sai và chưa hiểu bản chất: Lỗi LFI2RCE với phpinfo xảy ra khi mình có thể upload file tại trang phpinfo luôn, tức là có thể sử dụng POST request. PHP sẽ lưu các file được upload lên thư mục temp đối với cả các PHP script không hề hỗ trợ xử lý file trong nó, còn ở đây mình chỉ có thể GET /info, nên cách này coi như tiêu tùng. Ref: http://dann.com.br/php-winning-the-race-condition-vs-temporary-file-upload-alternative-way-to-easy_php-n1ctf2018/
Không từ bỏ việc race, mình quyết định đánh vào khả năng print_r mảng về thông tin file khi upload file tại /addProduct
, nhưng việc này cũng bất khả thi vì file đã được xử xong xuôi hết r mới có response trả về cho mình, mình đã tốn 2 ngày để thử theo phương pháp này và không thu được kết quả gì, vậy là challenge đã không thể solve kịp trước khi cuộc thi kết thúc :((
Mình cứ mải đi race mà không nghĩ ra DOMDocument hỗ trợ cả file phar, tương tự như trong case XXE to Phar Deserialize khi DOMDocument->loadXML có thể truyền vào phar protocol thì ở đây, DOMDocument->load cũng có thể truyền vào phar protocol -> +1 kiến thức.
Thiên thời địa lợi nhân hòa, config phar.readonly cũng được tắt => có thể deser file phar:
Nếu như đã hỗ trợ deser phar file, thì mình chỉ cần để file xml của mình vào file phar và nhét vào 1 file ảnh valid là được. Về cách gen file phar như nào thì mình sẽ làm tương tự như chall upload phar file trong root me. Ref: https://thanhlocpanda.wordpress.com/2023/08/07/php-phar-jpeg-polyglot-javascript-jpeg-polyglot-root-me-part-ii/ (pass: thanhlocpanda)
Đường đi nước bước đã đủ, mình tổng hợp lại attack chain như sau:
Mình gen ảnh 1 pixel cho nó bé bằng đoạn code:
Mình lấy hex bằng cyberchef và được chuỗi:
Đưa chuỗi vào scrip gen file phar, mình nhét file a.xml với payload đọc flag bằng /readflag
vào trong file phar, rồi đổi tên file thành phar.png:
Upload file phar lên server thành công:
Vào list product để lấy tên ảnh, của mình là /uploads/3e10700de9433bdb.png
Request đến cli.php để lụm flag thôi: