Disapproved
Omar Mohamed
Thanks for sharing!
بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيمِ

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.
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.
We can use that id to further approve ourselves.

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!

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 fromres.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 :)
Tags: