PATRlOT CTF 2024 (20/09/2024)

PATRlOT CTF 2024 (20/09/2024)

giraffe notes

Bài này khá dễ, logic cơ bản của source code như sau

<?php
$allowed_ip = ['localhost', '127.0.0.1'];

if (isset($_SERVER['HTTP_X_FORWARDED_FOR']) && in_array($_SERVER['HTTP_X_FORWARDED_FOR'], $allowed_ip)) {
    $allowed = true;
} else {
    $allowed = false;
}
?>
....

    <?php
    if (!$allowed) {
    ?>
        <div class="py-8 px-4 mx-auto max-w-screen-xl lg:py-16 lg:px-6">
            <div class="mx-auto text-center">
                <h1 class="mb-4 text-7xl tracking-tight font-extrabold lg:text-9xl text-primary-600 text-primary-500">
                🦒
                </h1>
                <p class="mb-4 text-3xl tracking-tight font-bold text-gray-900 md:text-4xl text-white">
                    Hah! Bet you cant access my notes on giraffes! They're super secure!
                </p>
            </div>
        </div>
    <?php
    } else {
    ?>
        <section class="bg-gray-900 antialiased">
        <h3 class="text-lg font-semibold text-gray-900 text-white">
        <span>CACI{placeholder}</span>

    <?php
    }
    ?>
</body>

</html>

Kiểm tra gói tin gửi đi xem biến $_SERVER['HTTP_X_FORWARDED_FOR'] có giá trị thuộc những ip cho phép hay không, ip cho phép gồm có localhost127.0.0.1, ta chỉ đơn giản bắt lại gói tin và thêm Header X-Forwarded-For có giá trị là localhost hoặc 127.0.0.1 là có thể đọc được flag

Impersonate

Trang chủ của websites như sau

Ta đăng nhập với tài khoản bất kỳ

Kiểm tra với BurpSuite

Khi đăng nhập, server sẽ tạo ra một web token, nó khá giống với JSON nhưng đây là web token của ứng dụng Flask và được lưu trong cookie với giá trị base64_encode, payload của token này như sau:

{"is_admin":false,"uid":"f2857d6c-461d-53a7-a032-cec668d48663","username":"aaaa"}

Ta mở source code ra để kiểm tra

#!/usr/bin/env python3
from flask import Flask, request, render_template, jsonify, abort, redirect, session
import uuid
import os
from datetime import datetime, timedelta
import hashlib
app = Flask(__name__)
server_start_time = datetime.now()
server_start_str = server_start_time.strftime('%Y%m%d%H%M%S')
secure_key = hashlib.sha256(f'secret_key_{server_start_str}'.encode()).hexdigest()
app.secret_key = secure_key
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(seconds=300)
flag = os.environ.get('FLAG', "flag{this_is_a_fake_flag}")
secret = uuid.UUID('31333337-1337-1337-1337-133713371337')
def is_safe_username(username):
    """Check if the username is alphanumeric and less than 20 characters."""
    return username.isalnum() and len(username) < 20
@app.route('/', methods=['GET', 'POST'])
def main():
    """Handle the main page where the user submits their username."""
    if request.method == 'GET':
        return render_template('index.html')
    elif request.method == 'POST':
        username = request.values['username']
        password = request.values['password']
        if not is_safe_username(username):
            return render_template('index.html', error='Invalid username')
        if not password:
            return render_template('index.html', error='Invalid password')
        if username.lower().startswith('admin'):
            return render_template('index.html', error='Don\'t try to impersonate administrator!')
        if not username or not password:
            return render_template('index.html', error='Invalid username or password')
        uid = uuid.uuid5(secret, username)
        session['username'] = username
        session['uid'] = str(uid)
        return redirect(f'/user/{uid}')
@app.route('/user/<uid>')
def user_page(uid):
    """Display the user's session page based on their UUID."""
    try:
        uid = uuid.UUID(uid)    
    except ValueError:
        abort(404)
    session['is_admin'] = False
    return 'Welcome Guest! Sadly, you are not admin and cannot view the flag.'
@app.route('/admin')
def admin_page():
    """Display the admin page if the user is an admin."""
    if session.get('is_admin') and uuid.uuid5(secret, 'administrator') and session.get('username') == 'administrator':
        return flag
    else:
        abort(401)
@app.route('/status')
def status():
    current_time = datetime.now()
    uptime = current_time - server_start_time
    formatted_uptime = str(uptime).split('.')[0]
    formatted_current_time = current_time.strftime('%Y-%m-%d %H:%M:%S')
    status_content = f"""Server uptime: {formatted_uptime}<br>
    Server time: {formatted_current_time}
    """
    return status_content
if __name__ == '__main__':
    app.run("0.0.0.0", port=9999)

Khi truy cập endpoint /admin, server sẽ kiểm tra token được đính kèm, nếu

thỏa mãn cả 3 điều kiện sau thì trả về flag

if session.get('is_admin') and uuid.uuid5(secret, 'administrator') and session.get('username') == 'administrator':
        return flag
    else:
        abort(401)

Vậy có cách nào để làm giả token này không ?

Muốn làm giả được token ta bắt buộc phải biết được secret_key tạo ra token này

Ở đây ta thấy cấu tạo của token này như sau

Khi đăng nhập thì mặc định is_admin = False, username = <username người dùng nhập vào>, uid = uuid.uuid5(secret, username) với secret cố định là secret = uuid.UUID('31333337-1337-1337-1337-133713371337') và token này được tạo với secret_key như sau

server_start_str = server_start_time.strftime('%Y%m%d%H%M%S')
secure_key = hashlib.sha256(f'secret_key_{server_start_str}'.encode()).hexdigest()
app.secret_key = secure_key

secret_key này chính là thời gian khởi động của server được băm bởi thuật toán SHA-256 rồi trả về giá trị băm dưới dạng chuỗi hex

Vậy ta giả sử thời gian khởi động của server nằm trong khoảng trước thời gian diễn ra giải Patriot 2 ngày (20/9/2024) đến thời gian hiện tại (22/9/2024) , ta sẽ tạo ra tất cả những secret_key có thể tồn tại trong thời gian này, sau đó dùng secret_key để tạo token với payload là

{'is_admin': True, 'uid': '02ec19dc-bb01-5942-a640-7099cda78081', 'username': 'administrator'}

để mạo danh administrator và đọc được flag

Đó là ý tưởng =)), còn thực hành có gặp một chút khó khăn vì mình không biết cái token này nó được tạo ra như thế nào vì đây là lần đầu mình gặp nó, sau khi search trên google được một lúc thì mình mình tìm được công cụ Flask-unsign

https://github.com/Paradoxis/Flask-Unsign

Đây là công cụ giúp decode, tạo session cookie và brute force để tìm secret_key trong các ứng dụng Flask, đúng thứ ta đang cần

Đầu tiên ta đăng nhập để lấy một token hợp lệ

Thử dùng Flask-unsign để decode token này

Tools chạy khá mượt. Giờ ta sẽ dùng python để tạo ra tất cả secret_key có thể có rồi lưu vào file txt để tiến hành Brute force

from datetime import datetime, timedelta
import hashlib

# Hàm để tạo ra secret_key dựa trên thời điểm cụ thể
def generate_secret_key(server_start_time):
    server_start_str = server_start_time.strftime('%Y%m%d%H%M%S')
    secure_key = hashlib.sha256(f'secret_key_{server_start_str}'.encode()).hexdigest()
    return secure_key

# Lấy thời gian hiện tại và ngày hôm kia
current_time = datetime.now()
two_days_ago = current_time - timedelta(days=2)

# Mở file để ghi các secret_key
with open('D:/PatriotCTF/secret_key.txt', 'w') as file:
    # Lặp qua từng giây từ ngày hôm kia đến thời điểm hiện tại
    current_time_iterator = two_days_ago
    while current_time_iterator <= current_time:
        secret_key = generate_secret_key(current_time_iterator)
        file.write(secret_key + '\n')  # Ghi mỗi secret_key vào file
        current_time_iterator += timedelta(seconds=1)

print("Secret keys have been generated and saved to 'secret_key.txt'.")

Và rất may mắn mình đã tìm được secet key, nếu không tìm ra, mình định sẽ mở rộng khoảng thời gian để tìm, ok vậy giờ ta tạo token của administrator thôi

Sau đó ta dùng token này để truy cập endpoint /admin

Và BÙMMMMMMMMMMMMMMMMMMMMMMMMMM

Chúng ta đã thất bại =))), sau khi mất 5p ngồi khóc thì mình đã hiểu tại sao token này không hợp lệ, vì server đã giới hạn thời gian tồn tại của token là trong vòng 300s = 5p

app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(seconds=300)

Nên lần này ta vẫn sẽ làm như vậy và làm thật nhanh =)))

Bạn để ý chúng ta đã tìm được một secret_key mới, và rõ ràng cứ 5p server sẽ đổi secret_key một lần

Và yess, chúng ta đã làm được

Flag: PCTF{Imp3rs0n4t10n_Iz_Sup3r_Ezz}