문제 파일을 내려받고, 사이트로 들어가보자.
간단한 로그인 창이 보인다.
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);
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,
};
customcrypto.js를 이용해서 jwt 토큰을 만들고, 이를 이용해 인증하는 방식인듯 하다. 일단, index.js에서 나와있는 대로 guest로 접속해보도록 하자.
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" });
}
}
});
이 부분을 보면 알겠지만, 만약 admin으로 접속할 경우, flag가 뜨는 것으로 보인다.
버프 슈트를 켜서, guest 접속 상태일 때 패킷을 확인해 보자.
Cookie 부분을 보면, auth가 있는 것을 확인할 수 있다. 이 auth를 아래 사이트에서 복호화해보자.
암호화 알고리즘으로 hs256 암호화를 쓴다는 것을 알 수 있다.
<aside> 💡 hs256 암호화 알고리즘: 대칭 키 암호화 방식을 사용하며 키 한 개만으로 암호화와 복호화를 모두 수행하는 알고리즘이다. HMAC에 SHA256 알고리즘을 섞은 것으로, jwt 토큰에 많이 쓰인다.
</aside>
key 값을 알아도 암/복호화 모두 수행 가능하다는 의미
대충 auth값을 조작해서, admin으로 접속해서 flag값을 얻어내는 것으로 보인다.
const isDevelopmentEnv = true;