0x00 背景
本文针对基于java的三种常见的模版引擎(FreeMarker、Velocity、Thymeleaf)所引起的服务端模版注入漏洞SSTI进行分析、整理和总结。所谓模版引擎,简单来讲就是利用模版语言的特定语法处理模版中的特定参数,帮助动态渲染数据到view层或生成电子邮件、配置文件、HTML网页等输出文本。
0x01 FreeMarker
模板文件存放在Web服务器上,当访问指定模版文件时, FreeMarker会动态转换模板,用最新的数据内容替换模板中 ${...}的部分,然后返回渲染结果。
FreeMarker模版语言说明
文本:包括 HTML 标签与静态文本等静态内容,该部分内容会原样输出
插值:语法为 ${}, 这部分的输出会被模板引擎计算的值来替换。
指令标签:<# >或者 <@ >。如果指令为系统内建指令,如assign时,用<# >。如果指令为用户指令,则用<@ >。利用中最常见的指令标签为<#assign>,该指令可创建变量。
注释:由 <#--和-->表示,注释部分的内容会 FreeMarker 忽略
FreeMarker模版注入分析
这里介绍FreeMarker的两个内置函数—— new和api。
内置函数new
可创建任意实现了TemplateModel接口的Java对象,同时还可以触发没有实现 TemplateModel接口的类的静态初始化块。
以下两种常见的FreeMarker模版注入poc就是利用new函数,创建了继承TemplateModel接口的freemarker.template.utility.JythonRuntime和freemarker.template.utility.Execute。
<#assign value="freemarker.template.utility.JythonRuntime"?new()><@value>import os;os.system("calc")</@value>
<#assign value="freemarker.template.utility.Execute"?new()>${value("calc")}
防御措施:
从 2.3.17版本以后,官方版本提供了三种TemplateClassResolver对类进行解析:
1、UNRESTRICTED_RESOLVER:可以通过 ClassUtil.forName(className) 获取任何类。
2、SAFER_RESOLVER:不能加载 freemarker.template.utility.JythonRuntime、freemarker.template.utility.Execute、freemarker.template.utility.ObjectConstructor这三个类。
3、ALLOWS_NOTHING_RESOLVER:不能解析任何类。
可通过freemarker.core.Configurable#setNewBuiltinClassResolver方法设置TemplateClassResolver,从而限制通过new()函数对freemarker.template.utility.JythonRuntime、freemarker.template.utility.Execute、freemarker.template.utility.ObjectConstructor这三个类的解析。

api内置函数
value?api 提供对 value 的 API(通常是 Java API)的访问,例如 value?api.someJavaMethod() 或 value?api.someBeanProperty。可通过 getClassLoader获取类加载器从而加载恶意类,或者也可以通过 getResource来实现任意文件读取。
但是,当api_builtin_enabled为true时才可使用api函数,而该配置在2.3.22版本之后默认为false。
FreeMarker模版注入示例:
测试版本——2.3.23

模版注入执行任意命令
渲染内容:
<#assign value="freemarker.template.utility.Execute"?new()>${value("open /Applications/Calculator.app")}

若配置如下,执行命令失败。
cfg = new Configuration();
cfg.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);

总结
FreeMarker模版注入已有不错的安全防御措施,可通过配置来禁止解析常见的危险类,同时限制对api函数的使用。
0x02 Velocity
近年来,不少中间件服务器,如Solr、协同办公软件,如confluence、 Jria等被爆存在velocity模版注入漏洞(CVE-2019-17558、CVE-2019-3394、CVE-2021-43947等)。Velocity较FreeMarker而言更加常见。
基本语法
# 关键字
Velocity关键字都是使用#开头的,如#set、#if、#else、#end、#foreach等
$变量
Velocity变量都是使用\$开头的,如:$name、$msg
{}变量
Velocity对于需要明确表示的Velocity变量,可以使用{}将变量包含起来。
!变量
如果某个Velocity变量不存在,那么页面中就会显示$xxx的形式,为了避免这种形式,可以在变量名称前加上!。如页面中含有$msg,如果msg有值,将显示msg的值;如果不存在就会显示$msg。这是我们不希望看到的,为了把不存在的变量显示为空白,可以使用$!msg。
Velocity模版注入示例

Velocity 模版注入执行任意命令

有多种Velocity模版渲染进行命令执行的方式,可根据velocity模版的语法对poc进行变形利用,这里不进行详细阐述。
0x03 Thymeleaf
Thymeleaf是一款Spring官方支持的一款服务端模板引擎,一般用于Spring项目中渲染数据到View层。默认前缀:/templates/,默认后缀:.html。
Thymeleaf模版渲染示例
Controller代码:

根据return "hello"渲染 resources/templates/下的hello.html文件到view层。将参数name作为要渲染的内容放入Model中。
hello.html内容:

渲染结果:

模版文件中使用th:fragment、th:text属性包含的内容才可以被thymeleaf进行渲染处理。渲染过程中在${xx}中的内容可执行SPEL表达式。
Thymeleaf模版注入漏洞原理和场景
Thymeleaf模版注入漏洞分两种场景,按照经Servlet处理后得到的viewTemplateName包含"::"和不包含"::"两种情况。

1. 当不包含"::"时
直接处理viewTemplateName对应的模版文件。如果模版文件中包含th:text等形式的属性且内容可控,即可通过向th:text属性值中注入SPEL表达式,经渲染后可执行该表达式。如下图所示:

2. 当包含“::”时
触发绝大多数Thymeleaf模版注入漏洞的场景是这种情况。
如果符合__(.*?)__正则匹配,则取出(.*?)的数据当作表达式解析执行。

由此可见,获取的viewTemplateName值是触发漏洞的关键。如果viewTemplateName可控,则可设计viewTemplateName值使之成功被渲染从而执行SPEL表达式。
下面介绍2种用户可控viewTemplateName的场景:
1、return 语句中包含用户可控数据
比如 return语句中包含请求参数或者路径变量

2、没有return语句且路由可控
形如:

没有return语句的情况下,会通过org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator#getViewName方法从请求路径中获取ViewName。处理逻辑如下:
总结该逻辑,取路由中第一个/和最后一个.之间的部分,因此在路由可控的情况下,设计路由形如:
::__spel__.xx即可实现模版注入从而执行任意表达式。

防御措施
1、配置@ResponseBody或者@RestController
经以上注解后不会进行View解析而是直接返回。
2、在方法参数中加上 HttpServletResponse参数
此时spring会认为已经处理了response响应而不再进行视图解析。
3、在返回值前面加上 "redirect:"——经RedirectView处理。
0x04 总结
本文抛砖引玉,对Java常见的三种服务端模版引擎的渲染原理和模版注入场景进行简单说明和总结。其中每种引擎在渲染过程中,如何绕过安全沙箱,如何进行表达式的各种变形从而实现更多利用等仍然是很值得研究和探讨的问题。
跳跳糖