roaris / ctf-log

0 stars 0 forks source link

HackTheBox: baby todo or not todo (Web) #29

Open roaris opened 4 months ago

roaris commented 4 months ago

https://app.hackthebox.com/challenges/baby%2520todo%2520or%2520not%2520todo

roaris commented 4 months ago

schema.sqlを見ると、todosテーブルの最初のレコードがフラグになっている 定期的にGET /api/list/user5773E6Ba/?secret=C76e8A41F907Eeeというようなリクエストが送信されるので、user5773E6Baのところをallに書き換えて、リクエストを送信すると、フラグが得られた

image

roaris commented 4 months ago

そこそこコード量があるけど、ほぼ読まずに解けてしまったので少し読む

まず、routes.py コメントからGET /list/allが怪しいと分かる

from flask import Blueprint, request, jsonify, session, render_template, g
from application.util import verify_integrity
from application.models import todo

main  = Blueprint('main', __name__)
api   = Blueprint('api', __name__)

@main.route('/')
def index():
    context = {
        'list_access': g.user,
        'secret': todo.get_secret_from(g.user)
    }
    return render_template('index.html', **context)

@api.before_request
@verify_integrity
def and_then(): pass

# TODO: There are not view arguments involved, I hope this doesn't break
# the authentication control on the verify_integrity() decorator
@api.route('/list/all/')
def list_all():
    return jsonify(todo.get_all())

@api.route('/list/<assignee>/')
def list_tasks(assignee):
    return jsonify(todo.get_by_user(assignee))

@api.route('/add/', methods=['POST'])
def add():
    todo.add(g.name, g.user)
    return {'success': f'Successfuly added {g.name} by user {g.user}'}

@api.route('/rename/<int:todo_id>/<new_name>/')
def rename_task(todo_id, new_name):
    g.selected.rename(new_name)
    return {'success': f'Successfuly edited {todo_id} to {new_name}'}

@api.route('/delete/<int:todo_id>/', methods=['DELETE'])
def delete(todo_id):
    g.selected.delete()
    return {'success': f'Successfuly deleted {todo_id}'}

@api.route('/complete/<int:todo_id>/')
def complete(todo_id):
    g.selected.complete()
    return {'success': f'Successfuly completed {todo_id}'}

@api.route('/assign/<int:todo_id>/<new_assignee>/')
def assign(todo_id, new_assignee):
    g.selected.reassign(new_assignee)
    return {'success': f'Successfuly reassigned {todo_id} to {new_assignee}'}

次にutil.pyのverify_integrity

def verify_integrity(func):
    def check_secret(secret, name):
        if secret != todo.get_secret_from(name):
            return abort(403)

    @functools.wraps(func)
    def check_integrity(*args, **kwargs):
        g.secret = request.args.get('secret', '') or request.form.get('secret', '')

        if request.view_args:
            list_access = request.view_args.get('assignee', '')

            if list_access and list_access != g.user:
                return abort(403)

            todo_id = request.view_args.get('todo_id', '')
            if todo_id:
                g.selected = todo.get_by_id(todo_id)

                if g.selected: 
                    if dict(g.selected).get('assignee') == g.user:
                        check_secret(g.secret, g.user)
                        return func(*args, **kwargs)

                    return abort(403)

                return abort(404)

        if request.is_json:
            g.task = request.get_json()
            g.name = g.task.get('name', '')

            if g.name and len(g.name) <= 100 and not re.search('script|meta|link|src|on[a-z]', g.name, re.IGNORECASE):
                g.name = g.name.replace('<', '&lt;').replace('>', '&gt;')
                check_secret(g.task.get('secret', ''), g.user)
                return func(*args, **kwargs)

            return abort(400)

        check_secret(g.secret, g.user)

        return func(*args, **kwargs)
    return check_integrity

GET /list/\<assignee>/の場合は、セッションで決まるユーザIDとパスパラメータ中のユーザIDが一致しないと403エラーとなる g.userというのは、app.pyのis_authenticatedで代入されている セッションで決まるユーザIDとパスパラメータ中のユーザIDが一致していても、リクエスト中で指定したsecretとユーザIDに紐づいているsecretが一致しなければ403エラーとなる

@app.before_request
def is_authenticated():
    g.user = session.get('authentication')
    if not g.user:
        username = f'user{generate(8)}'
        todo.create_user(username, generate(15))
        g.user = session['authentication'] = username

GET /rename///, GET /delete//, GET /complete//, GET /assign///の場合は、todo_idに紐づくtodoの所有者とセッションで決まるユーザIDが一致しなければ403エラーとなる

POST /api/addを呼び出すと、if request.is_jsonに引っかかり、scriptタグなどが含まれていると400エラーとなる また<と>がエスケープされている(出力時にエスケープするのが一般的な気がするが)