We are given the application source code and a challenge link. Also there is a bot.js for the admin bot. So it was some client side challenge. Looking at the application, its main functionality was to create pads (basically notes ) and view them. There was html and markdown allowed in the contents of the pad.
// open the link console.log(`Visiting URL: https://${padid}.${DOMAIN} `); await page.goto(`https://${padid}.${DOMAIN}`);
After looking at bot.js it’s clear that the flag is in the admins pad. So we have to somehow steal the contents of the admins pad using XSS or using some other client side attack. But unfortunately, the content inside the pad is sanitized using the HTML Sanitizer API . So there is no chance for direct XSS to steal the admins pad.
router.get('/', ensureAuthenticated, async (req, res) => { // get id from subdomain let id = req.subdomains[0] // show the index page if(!id){ let pads = awaitPad.find({username: req.session.username}) return res.render('index', { username: req.session.username, latest: req.cookies.latest, pads }) } if (!/^[a-f0-9]{48}$/.test(id)){ req.flash('danger', 'Invalid päääd id.') return res.redirect(`https://${process.env.DOMAIN}`) }
// find pad with id let pad = awaitPad.findOne({uniqueId: id})
here it is taking the id from req.subdomains[0] and fetching the pad from the database with that id . so anyone with that unique id can view the contents of the pad, since there are no checks.
Attack plan
So if we can manage to somehow get the admin pads unique_id , we can access his pad. So the idea is to somehow leak this unique subdomain. There is another feature of this application that I found interesting, that allows you to view the latest note created by a user.
Looking at the code for that functionality.
1 2 3 4 5 6 7 8 9 10 11 12 13
router.get('/p/latest', async (req, res) => { if(!req.cookies.latest){ req.flash('danger', 'No latest päääd.') return res.redirect('/') } let id = req.cookies.latest.uniqueId if (!/^[a-f0-9]{48}$/.test(id)){ req.flash('danger', 'Invalid päääd id.') return res.redirect(`https://${process.env.DOMAIN}`) } return res.redirect(`https://${id}.${process.env.DOMAIN}`) })
Basically, if we visit the endpoint /p/latest with the cookie latest, it will redirect to unique_id.paaad.space. So if we manage to somehow leak the subdomain from this redirection we can get the pad.
The initial plan is to use csp violations to leak the subdomain. So to do that we have to first redirect the bot to our attacker’s website. Since .setHTML() allows meta tags we can use a meta redirect to our attacker controlled website .
CSP violation leak
If we put https://xn--pd-viaaa.space/p/latest in an iframe and then add a csp with frame-src https://xn--pd-viaaa.space/p/latest it will trigger a csp violation , because https://xn--pd-viaaa.space/p/latest redirects to unique_id.xn--pd-viaaa.space .
So using this technique we can leak the unique_id .
CSRF to make the note public
After getting the unique id there is still one more problem to solve. The admins pad is not public, so we can’t access it directly due to this check.
1 2 3 4
if(!pad.isPublic && req.session.username != pad.username){ req.flash('danger', 'Not allowed to access this non-public päääd.') return res.redirect(`https://${process.env.DOMAIN}`) }
So we just have to make the admin send a get request using ?edit=isPublic to make the note public. But unfortunately, the session cookie is having sameSite: 'strict' . So doing a csrf to make the note public won’t work.
To overcome this we can run the bot twice, the first time to leak the unique_id and the next time with a pad that has a meta redirect to unique_id.xn--pd-viaaa.space?edit=isPublic to make the note public.
Final Payloads
First pad
1 2
<!-- redirect to attacker site --> <metahttp-equiv="refresh"content="1; url=https://attacker.com/attacker.html">