SSTI-Freemarker模板注入漏洞
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 添加依赖
1 |
|
spring-boot-starter-freemarker-2.7.13.pom
1 |
|
application.yml 添加配置
1 |
|
新建 index.ftl
1 |
|
新建 UserController
1 |
|
运行 Spring boot 项目,访问 index 成功显示
2、Freemarker 模板
见官方文档(http://freemarker.foofun.cn/dgui_template.html)
(1)、总体结构
实际上用程序语言编写的程序就是模板。 FTL (代表FreeMarker模板语言)。 这是为编写模板设计的非常简单的编程语言。
模板(FTL编程)是由如下部分混合而成的:
- 文本:文本会照着原样来输出。
- 插值:这部分的输出会被计算的值来替换。插值由 ${ and } 所分隔(或者 #{ and },这种风格已经不建议再使用了;点击查看更多)。
- FTL 标签:FTL标签和HTML标签很相似,但是它们却是给FreeMarker的指示, 而且不会打印在输出内容中。
- 注释:注释和HTML的注释也很相似,但它们是由 <#– 和 –>来分隔的。注释会被FreeMarker直接忽略, 更不会在输出内容中显示。
我们来看一个具体的模板。其中的内容已经用颜色来标记了: 文本, 插值, FTL 标签, 注释。为了看到可见的 换行符, 这里使用了 [BR]。s
注:
- FTL是区分大小写的。 list 是指令的名称而 List 就不是。类似地 ${name} 和 ${Name} 或 ${NAME} 也是不同的。
- 插值 仅仅可以在 文本 中使用。 (也可以是字符串表达式;请参考 后续内容)
- 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_来决定。
事实上,指令有两种类型: 预定义指令 和 用户自定义指令。 对于用户自定义的指令使用 @ 来代替 #,比如,<@mydirective _parameters_>…</@mydirective>。 更深的区别在于如果指令没有嵌套内容,那么必须这么使用 <@mydirective _parameters_ />,这和XML语法很相似 (例如 )。
assign 指令:主要是用于为该模板页面创建或替换一个顶层变量。
1 |
|
(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})。
插值是用来给 表达式 插入具体值然后转换为文本(字符串)。插值仅仅可以在两种位置使用:在 文本区 (比如 Hello ${name}!) 和 字符串表达式 (比如 <#include “/footer/${company}.html”>)中。
表达式的结果必须是字符串,数字或者日期/时间/日期-时间值, 因为(默认是这样)仅仅这些值可以被插值自动转换为字符串。其它类型的值 (比如布尔值,序列)必须 “手动地” 转换成字符串(后续会有一些建议), 否则就会发生错误,中止模板执行。
3、Freemarker 内建函数
Freemarker 提供了很多内置函数供开发者使用,具体见官方文档,存在风险的函数为 new 和 api,详情如下:
(1)、new
用来创建一个确定的 TemplateModel 实现变量的内建函数。
在 ? 的左边你可以指定一个字符串, 值为 TemplateModel 实现类的完全限定名。 结果是调用构造方法生成一个方法变量,然后将新变量返回。
例:
1 |
|
该内建函数可以创建任意的 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() 函数
1 |
|
1 |
|
1 |
|
1 |
|
(2)、api() 函数
1 |
|
1 |
|
四、漏洞修复
从 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。