Java Freemarker SSTI
最新版本也能注入,具体看有没有通过freemarker.core.Configurable#setNewBuiltinClassResolver
方法进行安全设置
代码测试环境(位于HelloController):https://github.com/godownio/SSTIVuln
pom:
1 | <dependency> |
Freemarker的使用
准备一个ftl文件,放在resources/templates目录下
1 | Hello, ${name}! |
写Java代码加载并渲染模板
1 | import freemarker.template.*; |
可以看到Alice代替了${name}
被输出
上面这个代码是脱离spring环境去使用的Freemarker
springBoot 集成Freemarker就更简单了:
application.yml配置,指定用freemarker去找templates目录下的ftl文件解析:
1 | spring: |
Controller直接返回.ftl
,注意如果你的环境即有Thymeleaf又有freemarker,springBoot默认优先用Thymeleaf去解析模板,得把Thymeleaf依赖注释掉
1 |
|
FTL语法
简单的提一下几个漏洞触发会用到的语法,其他的感兴趣可以官网见
功能 | 示例 |
---|---|
变量 | ${name} |
条件判断 | <#if age gt 18>Adult<#else>Minor</#if> |
循环 | <#list users as user>${user.name}</#list> |
调用方法 | ${user.getName()} |
引入其他模板 | <#include "footer.ftl"> |
#
和@
搭配使用,#
定义,@
调用,效果等同函数,定义一个可复用的片段:
1 | <#macro sayHello name> |
assign
就是 在模板中声明变量、做赋值、准备数据的指令,必须是 <#assign>
,不能是 <#Assign>
,大小写敏感。
比如以下三种定义的变量的使用,分别是定义变量、定义Map、定义list和在assign中进行简单运算并赋值
1 | <#assign name = "Tom"> |
还有很多内置函数,这里说两个漏洞利用中会用到的
内建函数 new
new
是一个内建函数,用来在模板中创建 “对象实例”。就是你可以在模板里,像在 Java 里 new 一个对象
一样。但注意:FreeMarker 里 new
出来的不是任意 Java 类,只能是 FreeMarker 支持的特定类型。
语法为:<要处理的对象>?new
比如new一个WordWrapperDirective赋值给word_wrapp:<#assign word_wrapp = "com.acmee.freemarker.WordWrapperDirective"?new()>
注意new_list、new_map和new不一样,new_list和new_map是单独的内置函数,虽然使用上是一样的
比如new一个空list赋值给animals:<#assign animals = []?new_list>
或者new一个Map赋给person:<#assign person = {}?new_map>
该内建函数可以创建任意的 Java 对象,只要类实现了 TemplateModel 接口即可创建,进而使用这些对象, 并且可以触发没有实现 TemplateModel 接口的类的静态初始化块。美妙的链子警告
api
api 内建函数于 FreeMarker 2.3.22 版本出现,之前版本不存在
?api
是什么?
在 FreeMarker 里,?api
内建函数的作用是:
把一个普通的 Java 对象,暴露成一个可以直接调用 Java 方法和字段的对象。
正常情况下,FreeMarker模板是安全沙箱的 —— 它不允许你直接在模板里调用任意 Java 类或者方法,以避免安全问题。
而 ?api
就是一个突破限制的“开关”。
举个例子:
假设有个Person类:
1 | public class Person { |
你在模板里拿到一个 person
对象,正常只能通过 ${person.name}
来取属性。
但是如果用了 ?api
,就可以直接调用方法:
1 | ${person?api.getName()} |
在 2.3.22 之前,FreeMarker对 Java 对象访问控制得很严格(只通过 getter / 公开字段),但大家在实际开发中有很多需要更灵活操作 Java 对象的需求,所以从 2.3.22 起,官方引入了 ?api
,允许开发者自愿打开这个“低层访问”的功能。当然,默认是关着的,需要在配置里允许(通过 ObjectWrapper
设置)。
Freemarker模板注入漏洞分析
把hello.ftl修改为如下:
1 | <#assign value="freemarker.template.utility.Execute"?new()>${value("calc")} |
在对该模板进行渲染的时候就会触发漏洞
1 | public class nospringToFTL { |
那么按照一般的场景,如下:
1 | Hello, ${name}! |
向name传参payload能顺利执行吗?答案是不可以。直接渲染用户输入${}
占位符的内容会进行转码,所以payload会失效。想要顺利的进行命令执行必须直接控制模板内容,看起来非常鸡肋,也确实鸡肋。。一般的利用场景为上传或直接能修改模板内容,而不是直接渲染用户输入去触发。所以,严格说来根本不算注入型的漏洞
那寻找那万分之一的可能,如果真有注入模板的场景,那代码长什么样?
如下代码:
后续我们分析均是用如下代码作为demo
1 |
|
freemarker.template.Configuration为配置器,在 FreeMarker 中,StringTemplateLoader
的 putTemplate(String name, String templateContent)
方法用于将模板添加到加载器中。
这种方式会直接把整个用户输入作为模板,而不是嵌入值。我们之后测试就用这个代码作为环境,这种通过java代码设置Configuration的方式,可以省略application.yml。因为getTemplate(“payload”)是指定渲染模板名为payload的模板
所以payload必须key名与getTemplate参数一致:payload=%3C%23assign+value%3D%22freemarker.template.utility.Execute%22%3Fnew%28%29%3E%24%7Bvalue%28%22calc%22%29%7D
如果你多次调用 putTemplate
方法,并使用相同的模板名称(name
),那么后一次的调用会覆盖前一次的模板内容。
payload
以下payload均是直接修改ftl才能触发,而不是向${}
传参
我突然想起python SSTI也是render(payload字符串)去触发,而不是向占位符传参。
new触发:
1 | <#assign value="freemarker.template.utility.Execute"?new()>${value("calc")} |
1 | <#assign value="freemarker.template.utility.ObjectConstructor"?new()>${value("java.lang.ProcessBuilder","Calc").start()} |
下面这个需要目标有完整的Jython依赖
1 | <#assign value="freemarker.template.utility.JythonRuntime"?new()><@value>import os;os.system("calc")</@value> |
api触发:
1 | <#assign classLoader=object?api.class.protectionDomain.classLoader> |
读文件
1 | <#assign is=object?api.class.getResourceAsStream("/Test.class")> |
调试分析
用的payload:<#assign value="freemarker.template.utility.Execute"?new()>${value("calc")}
,yakit记得url编码
字符串分割
freemarkerConfig.getTemplate作为字符串分割的入口
经过如下栈后,在freemarker.core.FMParser#MixedContentElement对字符串进行分割
因为开头的<#
,如下一堆case都是以<#
开头的,调用FreemarkerDirective,而且我们的<#assign
也属于这里面
跟进后FreemarkerDirective函数,发现调用Assign
Assgin就是根据标签分离<#assign
片段
下面的具体操作我们就不看了,知道整个分割的过程发生在freemarker.core.FMParser#MixedContentElement即可。最后得到的结果是一个<#assign>
片段和一个${value}
片段
模板解析
将断点打到freemarker.template.Template#process处
经过如下调用栈,来到freemarker.core.Environment#visit()方法
先是一个对当前element的压栈,然后调用了element.accept。accept得到的结果就是上一节分离得到的两个片段,然后调用visit(el),这是一个递归调用
不要被压栈退栈搞晕了,简单说来,就是进行递归访问模板元素,处理子元素
解析<#assign>
首先是处理<#assign>
元素,看到调用element为<#assign value="freemarker.template.utility.Execute"?new()>
片段的accept方法
进到Assignment#accept方法内,处理赋值操作符为等于号(=)的情况,调用 valueExp.eval(env) 计算表达式的值并赋给变量 value
跟进eval,持续跟进到freemarker.core.MethodCall#_eval
函数,这是Freemarker解析模板标签最关键的函数
我挨个解释一下:
- 首先对目标表达式(target)进行求值,得到一个TemplateModel对象;
- 如果该对象是TemplateMethodModel类型,则:
若为扩展方法模型(TemplateMethodModelEx),获取参数的模型列表;
否则获取参数的值列表;
执行方法并返回结果包装后的内容; - 如果目标是宏(Macro),则调用宏并返回结果;
首先target是"freemarker.template.utility.Execute"?new
,eval后得到封装好Execute
类的一个构造器类
然后跟进exec内,实例化了Execute
解析${value("calc")}
跟进到DollerVariable#accept,看这个类名也知道,是用来解析${}
的类。accept方法内,可以看到当前是处理${value("calc")}
。跟进该方法的calculateInterpolatedStringOrMarkup方法
调用了escapedExpression.eval
跟进到MethodCall._eval,这里target.eval(env)是从环境变量中取出键为value
的值,执行的结果就是Execute类,然后调用到了Execute.exec执行了代码。我们唯一需要关心的是,因为这个if的原因,取到的类必须是TemplatesMethodModel的子类,所以选择的Execute。这个类集成了TemplatesMethodModel
除此以外,另外的一个payload用到的freemarker.template.utility.ObjectConstructor也满足继承了TemplateMethodModel的条件,且能调用任意构造函数
target.eval就是根据不同的target去取值,我这里不想调了,感兴趣的可以跟一下,应该很简单,写上来会很乱
调试分析2
而JythonRuntime用到的就是截然不同的一条路径了
1 | <#assign value="freemarker.template.utility.JythonRuntime"?new()><@value>import os;os.system("calc")</@value> |
@value
表示将 value
这个变量当作一个用户自定义指令(directive)来调用。FreeMarker 允许这种用法,但仅当这个对象实现了 TemplateDirectiveModel
接口。而某些特殊类如 JythonRuntime
(在一些漏洞版本中)被恶意使用时,可以达到执行任意代码的效果。
不过这个调用方式需要目标有Jython的完整依赖:
1 | <dependency> |
调用 <@value>import os; os.system("calc")</@value>
会触发以下逻辑:
- step 1:FreeMarker 调用
getWriter(...)
- step 2:模板内容写入匿名 Writer
内容 import os; os.system("calc")
被写入匿名 Writer
的 buf
缓冲区。
- step 3:模板引擎关闭 Writer → 调用
close()
→interpretBuffer()
,以python形式执行exec
我们打个断点看看,怎么调用到getWriter的
因为是调用自定义函数的原因,<@
的解析来到了UnifiedCall.accept,先是照常,解析value,发现对应的是前面定义的JythonRuntime,然后JythonRuntime匹配各种if,最后来到了env.visitAndTransform
然后一进来就调用了getWriter
但是这种利用方式就是看个乐子
API的利用方式这里就不调了
从 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
这三个类的解析。
官网的修复:https://freemarker.apache.org/docs/versions_2_3_17.html
当然,我们遇到的freemarker很有可能没开这个安全配置,爽爽注入了