Rating: 5.0

Going to the homepage link in the description, we can see a page with a login/register form.

After registering, we can see a textbox with a string and we can see that a cookie is set with a JWT ([Json Web Token](https://jwt.io/)).

We can get the content of the token by going on `https://jwt.io/#debugger-io`. Here we can see the that it contains the User-ID of the account that's logged in

Now, looking at the source of the challenge, we can see that the objective is to visit the endpoint `/vault` while being "unrestricted"
```js
app.get("/vault", (req, res) => {
if (!res.locals.user) {
res.status(401).send("Log in first");
return;
}
const user = users.get(res.locals.user.uid);
res.type("text/plain").send(user.restricted ? user.vault : flag);
});
```
Going back on the site while being logged in, the content of `user.vault` is none other than the content of the text area in the home:


As we have seen before, after login or registration, the cookie will be set with the UID of the user and signed with a random key.
Since we already know the format of the secret key (`0.[0-9]+`), I've started to try to break the jwt token by using [jwtcrack](https://github.com/brendan-rius/c-jwt-cracker), to try to forge a valid token.
This tool uses a brute-force attack (so it tries every possible combination of the specified charset) to retrieve the secret used to sign the token.
It can be lauched with:
`docker run -it --rm jwtcrack [token] [charset] [maxlen] [algorithm]`
So, in our case:
`docker run -it --rm jwtcrack eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiIwLjM0Nzc5Mjc3MTY2MDg4MjIiLCJpYXQiOjE2NTE1NzcwNDh9.asR5mHIczZ-s1tZHCoCNoLKtPsPFu8S46adyRwOYa-U 0.123456789 20 sha256`
After a day without anything (and thanks to a nudge from VCT on Discord), I've turned back to the source code.
In express, every app has an `app.use` function that's used as middleware and is called on every request.
```js
app.use((req, res, next) => {
try {
res.locals.user = jwt.verify(req.cookies.token, jwtKey, {
algorithms: ["HS256"],
});
} catch (err) {
if (req.cookies.token) {
res.clearCookie("token");
}
}
next();
});
```
We can notice that the only condition set the user variable, is to use a valid jwt as a cookie.
Also, the login function (as opposed to the register function) never verifies that the fields of the request are actually defined.
```js
app.post("/login", (req, res) => {
const user = users.get(users.lookup(req.body.username));
if (user && user.password === req.body.password) {
res.cookie(
"token",
jwt.sign({ uid: user.uid }, jwtKey, { algorithm: "HS256" })
);
res.redirect("/");
} else {
res.redirect("/?e=" + encodeURIComponent("Invalid username/password"));
}
});
```
We can use this to our advantage: in theory by making a login request without having a body, it will generate a valid token
Let's try our hypothesis: we can use a tool called "Burp" to intercept our login request and modify its content.


Here we can send the request to the Repeater page, where we can edit and replay the request without problems.


Now we have a valid token, but that's not related to any user!

We can see why this worked by carefully reading the functions called in the login method:
```js
const user = users.get(users.lookup(req.body.username));
//~~~~
get(uid) {
return this.users[uid] ?? {};
}
lookup(username) {
return this.usernames[username];
}
```
In javascript, some objects when used in a boolean statement are always evaluated to `true` and others to `false`. These objects are called [Truthy](https://developer.mozilla.org/en-US/docs/Glossary/Truthy) and [Falsy](https://developer.mozilla.org/en-US/docs/Glossary/Falsy) respectively.
Here we can see two examples of these kinds of objects:
In the `lookup` function, if we pass an undefined value, it will return `undefined`.
As for the `get(uid)`, il will try to evaluate `this.users[undefined]`, but since `undefined` is a [Falsy](https://developer.mozilla.org/en-US/docs/Glossary/Falsy), it will return an empty object.
So, now we have
```js
if (user && user.password === req.body.password)
```
where `user` is an empty object so a [Truthy](https://developer.mozilla.org/en-US/docs/Glossary/Truthy).
The same goes for the password check: if we don't send a password in the request and since the empty object won't have a password attribute, both will be evaluated to `undefined` so `undefined === undefined` which is true and will be created a correct cookie.
Now, when accessing the `vault` endpoint, `user.restricted` will be undefined too, and since `undefined` is a [Falsy](https://developer.mozilla.org/en-US/docs/Glossary/Falsy), instead of the vault content, we'll get the flag!
So everything we need to do is to set the cookie with the value obtained by making the empty `/login` request, and then we can read the flag successfully.


---
Now, this isn't the supposed way to solve the CTF: there's a "[Use after free](https://cwe.mitre.org/data/definitions/416.html)" vulnerability present (as indicated in the flag text) that enables you to use the cookie of a deleted user
In this case, the exploit works mostly the same, it's also based on the possibility to use a valid signed token in the request, but instead of using an empty one, is of a deleted user.
This causes the script to retrieve an undefined `user` object and therefore the checks will fail in a similar way