Party Cat0%

Disapproved

Omar Mohamed
Thanks for sharing!

بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيمِ

Disapproved
Hello nerrrrrds! Today we got an enjoyable challenge. A cool CSP Bypass idea! Before we start, you can download the source code from here

Source Code

First thing you notice is the CSP header:
This means we can only load scripts from cdn.jsdelivr.net and can only make requests to the same origin.

We have got 3 main routes: Auth, user and admin.

The auth routes are pretty standard. As for user routes:
Dashboard shows your user details, if you check the dashboard.ejs, you can see that it gives you the flag if your status is approved.
The user schema looks like this (Admin user is pre-created):
And whenever a new user is created, it's status is set to pending by default.

Next endpoint is /update-message, which updates the message field in the user schema.

/report endpoint lets you report a user by their username.
The bot:
You can see it appends the username directly to the URL and visits it.

Finally, the admin routes:
In /admin/dashboard, the admin can see all users and can approve them with their userId.
In /admin/approve/:userId, the admin can approve a user, which sets their status to approved.

/admin/message/:username shows the message of a user.
Here we notice that the message is rendered with <%- %>, which means it is not escaped. This is a potential XSS vector.

The last endpoint is /admin/user/:username (which the bot uses), which shows the details of a user.

Exploitation

Now.. we need to get our user approved, and since we have user role, we can't access admin routes. That means we need to get the admin to approve us via XSS.
We noticed that the only view that shows un-escaped user input is message.ejs, which is only accessible by the admin. So let's fire up docker, login as admin and try to get our xss to work.

Your admin password will be printed in the console when you start the server.
In CSP we can only load scripts from cdn.jsdelivr.net. If we can host our payload there, we can load it. So how can we do that?
In cdn.jsdelivr.net, you can load any file from any public github repo like this:
So I created a repo with a simple alert payload and loaded it with:
Now visit /admin/message/admin (since we are logged in as admin) and you should see the alert pop up.
xss

Very well! Now we can execute XSS to make the admin approve us. But.. we need to know our userId to do that. The user have no way to see their userId so we should leak it first from the admin dashboard.
You can make the admin fetch the dashboard and get the userId in lots of ways, one of them is:
This logs the userId of mushroom to the console.
xss2
We can use that id to further approve ourselves.
xss3
And we did it! All we need to do now is report that to the bot

We have another issue though. The bot only visits /admin/user/:username, which does not show the message un-escaped. So we need to make the bot visit /admin/message/:username instead.
In server.js, we can notice this normalization:
This means we can use path traversal to break out of the /user/ route and go to /message/ instead.
We will report ../message/mushroom to the bot, which will make it visit /admin/user/../message/mushroom -> /admin/message/mushroom.

And here we got it!
flag
IEEE{U_F0rc3d_Th3_4dm1n_2_4ppr0v3_Y0u}
Note: you will need to logout and login again to see the flag, since the view gets the user from res.locals.user and the approving does not update that.

For the record, another way to leak the userId (more complexity for no reason, but keep the technique in mind you might use it in another chal) is to get the html of the dashboard, CSRF login with your user and then add the html to your message.

That's all for today, hope you enjoyed the writeup! See you next time :)

You might also like