Untitled

문제 파일을 내려받고, 사이트로 들어가보자.

Untitled

간단한 로그인 창이 보인다.

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로 접속해보도록 하자.

Untitled

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 접속 상태일 때 패킷을 확인해 보자.

Untitled

Cookie 부분을 보면, auth가 있는 것을 확인할 수 있다. 이 auth를 아래 사이트에서 복호화해보자.

JWT.IO

Untitled

암호화 알고리즘으로 hs256 암호화를 쓴다는 것을 알 수 있다.

<aside> 💡 hs256 암호화 알고리즘: 대칭 키 암호화 방식을 사용하며 키 한 개만으로 암호화와 복호화를 모두 수행하는 알고리즘이다. HMAC에 SHA256 알고리즘을 섞은 것으로, jwt 토큰에 많이 쓰인다.

</aside>

key 값을 알아도 암/복호화 모두 수행 가능하다는 의미

대충 auth값을 조작해서, admin으로 접속해서 flag값을 얻어내는 것으로 보인다.

const isDevelopmentEnv = true;