1224: Fireplace Generator
Writeup for the Intigriti December 2024 challenge π₯
Last updated
Writeup for the Intigriti December 2024 challenge π₯
Last updated
Find the FLAG and win Intigriti swag! π
Find the special ENDCI--->
cache format delimiter to remove the start of the response until inside the id=
attribute. This gets you out of the attribute
Bypass the xss_clean()
function using a Mutation XSS with the allowed xmp
tag: <xmp><p id='</xmp><style/onload=alert(origin)>'>
Visiting the challenge URL, we find a simple form with a single input to "generate a fireplace". The bottom right also shows a button to download the source code of the application.
We can try to input something and press "Ignite!", which takes us to a /view
page with a ?title=
parameter. The HTML source shows that our input ended up in two places, the main <h1>
header and an id=
attribute.
The source code allows us to run it locally making it easier to debug and understand the backend logic. After unzipping, we can start it with the following command:
Then, it should be accessible on http://localhost:8000. The source code leaves traces of "CodeIgniter", specifically version 3: https://github.com/bcit-ci/CodeIgniter The history of the source code was tracked by Git, so we can find what files were added or changed.
This reveals a small config change:
Along with with source code of application/controllers/View.php
:
This renders a template named view
, found in application/views/view.php
:
From this, we can gather that we have one input, the title=
query parameter. The xss_clean()
function transforms our input, and then an ID is made of the string using the custom str2id()
function. Finally, the $title
variable is displayed safely using htmlspecialchars()
while the $id
variable is not escaped. However, we cannot use a quote ("
) character to break out of the id=
attribute because of the strstr($str, '"')
check.
We can quickly check what happens if our input contains the special <
and >
characters:
http://localhost:8000/index.php/view?title=%3Cu%3Etest
Clearly, the $title
variable was HTML-encoded, but the $id
variable is output without encoding and still shows the raw <>
characters. We cannot yet escape the attribute due to "
being blocked, but it is something to keep in mind.
You may also notice the line $this->output->cache(1);
. Combined with the change to the caching configuration this may be interesting. This code is documented here and implemented in system/core/Output.php
. The _display_cache()
function defined in there is called for every request. The code can be summarized as follows:
First, it calculates a cache path from the current URI, which in our case is the path and query parameters. If this path doesn't exist, the request isn't cached and it generates a new response like normal. If the file does exist, it is read and then uses a RegEx for /^(.*)ENDCI--->/
to find a separator between serialized cache info and the response data itself.
We can find these files locally by entering the Docker container after having generated a /view
response:
So there is a special delimiter as ENDCI--->
that separates the serialized cache info from the HTML body. This HTML body contains our input, and so does this cache file, in plain text. What if we put the string "ENDCI--->" in our input, will it get confused and use our delimiter?
http://localhost:8000/index.php/view?title=ENDCI---%3E
For some reason, it is double-encoded now, while previously the <>
characters worked fine. After some investigating, we can find the root cause is in the xss_clean()
function. It contains an array of $_never_allowed_str
here including a mapping from -->
to -->
. Our input includes this so it is replaced. The function _do_never_allowed()
is called right before returning and ensures that these strings are always replaced before returning the "safe" content.
In our case, this is not the final string being output, because $id
is put through the str2id()
function after xss_clean()
. Conveniently, the str2id()
function replaces all spaces with dashes (-
), and the input we want to smuggle through contains dashes. This means we can replace the dashes in ENDCI--->
with spaces like ENDCI >
, which the XSS filter won't recognize anymore put the replacement will transform it into ENDCI--->
again:
http://localhost:8000/index.php/view?title=ENDCI%20%20%20%3E
This seems to have worked. The file is generated and its cache entry now looks like this:
Now there are two ENDCI--->
delimiters, so which will the preg_match()
match? We can quickly find out by force-reloading the page:
We certainly seem to have broken the page. Checking the source code shows not much is left of our payload:
So what happened? Regular Expressions are "greedy" by default meaning they try to match as long of a string as possible. The .*
will then look past the first ENDCI--->
to find a second in our payload and use that because it generates a longer match. Note that .
matches all characters, except newlines because the PCRE_DOTALL
(s) flag was not given. In our case, the views are minimized to not contain newlines so we can inject our payload into the first line.
The PHP unserialize()
function also received some garbage after its serialized object, but luckily the }
end marker stops its parsing and it won't care about the HTML body after it.
This means we can now throw out the whole start of the HTML body, including opening the attribute. We are now in HTML context and can directly write HTML:
http://localhost:8000/index.php/view?title=ENDCI%20%20%20%3E%3Cu%3Etest
If we try to write an XSS payload now, however, we can see that it is still sanitized by xss_clean()
:
http://localhost:8000/index.php/view?title=ENDCI%20%20%20%3E%3Cimg%20src%20onerror=alert(origin)%3E
Now that we can directly write HTML, we should take a look at the xss_clean()
function and see how it blocks malicious inputs. Reading the code, we can see that it performs a few steps:
Remove control characters
URL-decode and HTML-decode recursively
Remove 'never allowed' strings from $_never_allowed_str
and $_never_allowed_regex
HTML-encode <?
and ?>
tags
Remove spaces between certain words like "javascript" or "alert"
Remove javascript:
protocol from a
and img
tags, and remove "script" and "xss" strings
Parse string as HTML with tags and attributes with RegEx. _sanitize_naughty_html()
handles removing dangerous tags and attributes.
HTML-encode function calls like alert()
to alert()
Remove 'never allowed' strings again
These are quite some layers to get through, but the main hurdle is step 7 where dangerous tags and attributes are removed. The list seems quite comprehensive so we cannot just come up with a unique XSS payload.
One thing to notice is that the string is parsed into HTML tags and attributes using a Regular Expression. There is a funny answer on StackOverflow explaining that it is impossible to parse HTML with RegEx. In this case, the RegEx is a complicated combination of a few parts:
It reads an opening <
, then a tag name, followed by attributes, and finally >
. This assumes the whole string is in HTML context, but if you have some experience with Mutation XSS, you may know that there are different contexts inside specific tags. The <title>
tag, for example, contains not HTML but Text. That means HTML like the following will close the title tag in what looks to us like an attribute, and open the <img>
tag:
The RegEx that xss_clean()
uses would see this as just a <title>
tag and a <p>
tag with an attribute, it would not find the <img>
tag. Unfortunately, the title
tag specifically is part of the $naughty_tags
list that are removed. But more tags get parsed as text, such as:
style
, script
, xmp
, iframe
, noembed
, noframes
, plaintext
, noscript
, title
and textarea
Comparing this with the list of denied tags, we can find a few that are not blocked:
We can use any of these like <xmp>
in our payload to confuse the parser. By writing a tag-looking string inside of the xmp content we can hide a closing </xmp>
tag inside its attribute and then immediately start an arbitrary tag for XSS.
Note that we still cannot use double quote ("
) characters, so we should use a single quote ('
) to open the attribute. Then we also can't use spaces in the eventual XSS payload because str2id()
replaces them, so we can use /
as an alternative attribute separator. Combined with the ENDCI--->
prefix we had to be able to write HTML, our final payload becomes:
http://localhost:8000/index.php/view?title=ENDCI%20%20%20%3E%3Cxmp%3E%3Cp%20id=%27%3C/xmp%3E%3Cstyle/onload=alert(origin)%3E%27%3E
The response to this request after the 2nd reload is:
Even though the alert()
call is HTML-encoded, it is put into an attribute that makes the browser decode it for us! This triggers the XSS:
To consistently deliver this payload to a victim, we need to cache the URL server-side before visiting it, but make sure it is not cached client-side because then another request wouldn't be sent. We can achieve this through a simple cross-origin fetch()
to the URL before navigating the full page to it: