First of all, I want to thank @J0R1AN and @intigriti for this challenge. I have been studying to focus on bug bounty, and it has been a very fun way to learn XSS.
I have tried to show not just the result but also my thought process to get there. As I’ve heard NahamSec (whom I take this opportunity to thank for his educational work) mention in an interview, the important thing is not the payload or the solution itself but the steps and reasoning to arrive at it, and I totally agree.
Even so, if you’re in a hurry or just want the quick recipe, you can see a summary here.
Challenge description
Pop an alert and win Intigriti swag! 🏆
Link to the challenge: Intigriti #1224 Challenge by Jorian
The Full Story Solution
At first, I thought it was just an XSS challenge. Since I didn’t know much about the topic beyond the basic concepts, I went through the PortSwigger Academy material 1 and read HackTricks 2 to find a way to solve it, but there was no way.
Then I saw that they provided the source code.🤦🏼♂️
It was a framework called CodeIgniter, version 3. The framework had a function called xss_clean
that was responsible for sanitizing the form.
//view.php
...
$title = $this->input->get('title') ?: 'Christmas Fireplace';
$title = xss_clean($title);
$id = str2id($title);
$this->load->view('view', array(
"id" => $id,
"title" => $title
));
...
I pulled out the code for the xss_clean
function and wrote a custom PHP file to test text strings quickly. I tested so much, kept reading and trying, but nothing worked. :`(
At some point, one of the things I did was download the original framework and compare it with the one from the challenge to see if anything had been modified to introduce a vulnerability.
And the truth is, except for the application/views
folder and some configuration to cache not just pages but also parameters, the rest was exactly the same as the original framework.
So, the solution had to be somewhere in this code:
//View.php
<?php
defined('BASEPATH') OR exit('No direct script access allowed');
function str2id($str)
{
if (strstr($str, '"')) {
die('Error: No quotes allowed in attribute');
}
// Lowercase everything except first letters
$str = preg_replace_callback('/(^)?[A-Z]+/', function($match) {
return isset($match[1]) ? $match[0] : strtolower($match[0]);
}, $str);
// Replace whitespace with dash
return preg_replace('/[\s]/', '-', $str);
}
class View extends CI_Controller
{
public function index()
{
$this->load->helper('string');
$this->load->helper('security');
$this->output->cache(1);
$title = $this->input->get('title') ?: 'Christmas Fireplace';
$title = xss_clean($title);
$id = str2id($title);
$this->load->view('view', array(
"id" => $id,
"title" => $title
));
}
}
//view.php
...
<h1><?= htmlspecialchars($title) ?></h1>
...
<div class="fireplace" id="<?= $id ?>">
Just as I was starting to think I didn’t have the necessary knowledge yet, the first hint came along.
I had seen the cache, but I just thought it was there for optimization purposes, so I didn’t pay much attention to it. (Personal note: check everything, even briefly, next time.) However, given the hint, I started looking into it and also tried to understand the framework’s flow.
I’m not an expert in PHP, but I’m good at reading and understanding code, so it didn’t take me long to notice something in the cache.
The cache was stored as files in the system, with metadata and the cached page separated by the string ENDCI--->
. Similarly, it was split the same way when retrieving the cache.
//Output.php
$output = $cache_info.'ENDCI--->'.$output;
....
// Look for embedded serialized file info.
if ( ! preg_match('/^(.*)ENDCI--->/', $cache, $match))
{
return FALSE;
}
Now the strange str2id
function started to make sense: the first word in uppercase, spaces replaced with -
, and the >
left unsanitized… and voilà!
ENDCI >
was the key to break the cache.
It must be said that setting up the system in Docker also helped me understand what was happening. The Dockerfile was included in the source to make it easier.
So there we were, with the DOM broken; everything seemed to be solved, but it wasn’t. Although I had escaped the ID quotes and could inject tags, the truly useful ones were sanitized by our old friend xss_clean
. Back to square one.
//Security.php
protected function _sanitize_naughty_html($matches)
{
static $naughty_tags = array(
'alert', 'area', 'prompt', 'confirm', 'applet', 'audio', 'basefont', 'base', 'behavior', 'bgsound',
'blink', 'body', 'embed', 'expression', 'form', 'frameset', 'frame', 'head', 'html', 'ilayer',
'iframe', 'input', 'button', 'select', 'isindex', 'layer', 'link', 'meta', 'keygen', 'object',
'plaintext', 'style', 'script', 'textarea', 'title', 'math', 'video', 'svg', 'xml', 'xss'
);
...
By this point, I already had Docker set up, and after a few hours of testing without coming up with anything new, I simply injected lists of around 8,000 XSS payloads both in the original form and after the ENDCI >
to see how it reacted. (Spoiler: fail)
It didn’t help much; I read dozens of results just to confirm that this wasn’t the right approach. There had to be something else.
There was something else I had been thinking about, which was something CryptoCat mentioned in the Intigriti Discord…
A mutant elf has been causing chaos in the toy factory, making it a slow mess! Surely adding a little cache would make it faster?
I was clear about the cache part, but what about the other hint? And bam, Mutation XSS. A spark in some neuron gave me the clue. By that point, I had been Googling about XSS for a few hours, and while I hadn’t read directly about Mutation XSS, it had popped up now and then in search results or in the indexes of the websites I was reading. At some point, the dots simply connected, and I realized that it was very likely to be the hint. I read the first article3, and it all became clear.
But now, what the heck is Mutation XSS, and what do I do with it? Back to reading again. 😅
I read several more articles until I found a very interesting video about a Mutation XSS found on Google by Masato Kinugawa, which is pure gold. It explains how and why it happens and, most importantly, provides key insights about certain tags that the browser renders differently depending on whether JavaScript is present, such as <noscript>
.
You can watch it here : XSS on Google Search - Sanitizing HTML in The Client? — 100% recommended to watch this video.
And at this point, with everything I had learned along the way, it was relatively “easy” to find the answer.
ENDCI ><data title='<noscript><image/src/onerror=alert(document.domain)>'></noscript>
https://challenge-1224.intigriti.io/index.php/view?title=ENDCI%20%20%20%3E%3Cdata%20title=%27%3Cnoscript%3E%3Cimage%2Fsrc%2Fonerror%3Dalert(document.domain)%3E%27%3E%3C/noscript%3E
And that’s it! It wasn’t easy, but it was definitely both challenging and educational. I hope you’ve been able to take away something useful from reading this, and if you have any questions or suggestions, feel free to reach out to me. 😊
Quick Solution
- Corrupt the cache: Inject
ENDCI >
in the title parameter. - Exploit Mutation XSS: Inject
ENDCI ><data title='<noscript><image/src/onerror=alert(document.domain)>'></noscript>
in the title parameter. - Profit: Pop an alert and win Intigriti swag! 🏆
- Bonus: Watch the video XSS on Google Search - Sanitizing HTML in The Client? to understand Mutation XSS better.