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

Well well.. another challenge from Mr Mushroom. This one has plenty of stuff you are gonna chain together to eventually get an RCE!
This writeup might me long so.. let's start right away. Download challenge from here
Solver from here
Source code
Let's start by our entry point:
index.js
. The noticeable thing is the csp headers:Which in short means:
- Only allow resources from the same origin (self) and cdnjs.cloudflare.com
- Allow eval (unsafe-eval)
This might limit us a bit if there is an XSS. We will see.
Next our routes:
Auth routes are pretty standard, the only thing to mention is that after login you get an extra
csrf_token
cookie.For the paste routes where you can create pastes (notes) there is a heavy blacklist middleware:
After some analysis, you can tell it only allows a very limited set of HTML tags including
<script>
tag..?? but not allowing me to write inbetween <script>
and </script>
. I can use <script src=...>
though, I can use any attributes in the allowed tags.This is interesting. So will I need to host my payload somewhere and load it? but what about the CSP? We will see shortly.
Report routes are pretty standard as well, you just report a paste by ID for the admin to review.
Upload routes are only limited to admin with some filters and logic inside, so we will keep it after we deal with this XSS first.
In
utils/bot.js
the bot logs in as admin, deletes csrf_token
and visits the reported paste:The cookie is httpOnly
In the codebase root there is
nginx.conf
file:We can see the
/report
route is only allowed from localhost. This is a crucial endpoint for us to deliver the XSS payload to the admin, so we need to find a way to access it.First Bug: Nginx Misconfiguration
If you do a little bit of search about nginx misconfigurations, you can find out the issue is in the
/api
location. It proxies to /auth/
. The issue is with the trailing slash, it must have been /api/
to proxy correctly.Quick Explanation: When a request is made to
/api/something
, nginx will strip the /api
part and forward the request to http://app:3000/auth//something
(notice the double slash). If you pass /apisomething
nginx will strip the /api
part and forward to http://app:3000/auth/something
. Got it? We can traverse back to the root by using /api../
, it will resolve to /auth/../
which is /
. And like that we can access any route we want including /report
.
Check out this article for more info.
Second Bug: XSS & CSP Bypass
Now we can access the
/report
route, but how do we deliver the XSS payload to the admin? We need to create a paste with our payload and report it.We know that we can use
<script src=...>
to load an external script, but the CSP only allows self
and cdnjs.cloudflare.com
. We can't host our payload on either of those. But wait, what if we can upload our payload to cdnjs.cloudflare.com?After some searching you will find out you can indeed host your scrpts on some CDNs like jsDelivr, but you can't on CloudFlare CDN. Let's check the blacklist one more time.
<div>
tag is allowed, and it allows any attributes. So is there any way we can execute js from a <div>
attribute? There are frameworks that allow this, like AngularJS and AlpineJS. What about if we can load one of those frameworks from cdnjs.cloudflare.com and use it to execute our payload?We can use AlpineJS for this, it is available on cdnjs.cloudflare.com. We can use
x-init
attribute to execute our payload. Let's create a simple payload to test this:
Amazing! We have got our XSS...
Now what?
Side Trick (not related to the challenge)
If the cookie was not httpOnly we could have stolen it. You are wondering How would you do that with this strict csp?
If you have a non-httpOnly cookie with a strict csp, you can make the admin add a note of his cookies using your cookies, and you simply see the note on your profile. Example payload:
Now back to the challenge, since we can't steal the cookie, we can try to make the admin do something for us on the app (remember we only can interact with the app, we can't make him do any externals because of the csp), we still have this upload functionality that only admin can access. Let's check it out.
Third Bug: File Upload & Path Traversal
upload.controller.js
:Let's analyze this filters: first it removes any
../
from the filename, then it checks if the filename ends with .jpg
, .jpeg
or .png
(case insensitive). Finally it limits the filename to 20 characters.We have a path traversal protection, but we notice
.normalize("NFKC")
is used. This function normalizes unicode characters, so we can use some unicode tricks to bypass the filters.using
/
(U+ff0f) nstead of /
we can bypass the path traversal since the normalization is happening after the removal of ../
. You can access different encoding like this from here (Download it and open it with a browser).Cool! Now we can upload a file to any path we want. But what about the extension check? We noticed it truncates the filename to 20 characters, so we can build our file name to be exactly 20 characters then add
.jpg
at the end so it gets truncated. Example:Now we can upload a file to any path we want with the name we want!
Let's try that! Login as admin on your local instance and try to upload a test image, inspect the request in burp:


We successfully uploaded a file to another path! What can we do with this?
In
views/
there are some ejs templates, we can overwrite one of those templates and get an SSTI > RCE!
Overwritten
index.ejs
successfully! Notice I used ///
in the payload to reach this 20 chacracters limit.
Now for our SSTI payload, I used:

And here we have it! RCE achieved!
It is not done yet, we implemented that as an admin, but we need the admin to do it for us.
We will utilize the XSS to make the admin upload a file for us.
But.. it is csrf protected:
CSRF Middleware:
It is a simple md5 hash of
username:SALT
. We know the username is admin
but the SALT is REDACTED. Without overcomplicating things, if you tried to bruteforce it you will find out it is mushrooms
.Just register a user (call it
test
), login and get your csrf token from the cookie, then brute force the salt until you get the same token. At the end the salt is mushrooms
.Now forge the csrf token for admin and make the XSS payload to upload our malicious ejs file:
Add this to a paste, report it using the
/api../report
nginx misconfigiration and wait for the admin to visit it.
And here you have it, you can get the flag from
config/constants.js
file. Download the solver script from here.This challenge had a bit of everything. The main goal of the challenge is to learn how to chain multiple vulns together in one attack. I know you can simply exploit each one separated, but to chain them is the tricky part! I hope you enjoyed it as much as I did while writing it. Thank you for reading!
I am open for any challenges creation requests, you can reach me on X/Twitter @MushroomWasp or email me at MushroomWasp@gmail.com!
See you in the next one!