点击上方蓝字·关注我们
由于传播、利用本公众号菜狗安全所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,公众号菜狗安全及作者不为此承担任何责任,一旦造成后果请自行承担!如有侵权烦请告知,会立即删除并致歉。
介绍
环境搭建
漏洞复现
深入分析
危险内置类
Execute
ObjectConstructor
JythonRuntime
漏洞修复以及高版本特性
最后
FreeMarker 是一款模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具。 它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入他们所开发产品的组件。
模板编写为FreeMarker Template Language (FTL)。它是简单的,专用的语言,不是像PHP那样成熟的编程语言。 那就意味着要准备数据在真实编程语言中来显示,比如数据库查询和业务运算, 之后模板显示已经准备好的数据。在模板中,你可以专注于如何展现数据, 而在模板之外可以专注于要展示什么数据。
这种方式通常被称为MVC (模型 视图 控制器) 模式,对于动态网页来说,是一种特别流行的模式。 它帮助从开发人员(Java 程序员)中分离出网页设计师(HTML设计师)。设计师无需面对模板中的复杂逻辑, 在没有程序员来修改或重新编译代码时,也可以修改页面的样式。
而FreeMarker最初的设计,是被用来在MVC模式的Web开发框架中生成HTML页面的,它没有被绑定到 Servlet或HTML或任意Web相关的东西上。它也可以用于非Web应用环境中。
创建一个javaweb项目
项目加载完成后,在pom.xml文件中添加FreeMarker依赖
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.31</version>
</dependency>
然后编写一个.ftl模板文件
<html>
<head>
<metacharset="utf-8">
<title>这是一个Freemarker测试页面</title>
</head>
<body>
<#--我只是一个注释,我不会有任何输出 -->
${name} 你好
</body>
</html>
然后开始编写调用FreeMarker通过模板文件动态生成我们的页面
publicclassTestDemo {
public static void main(String[] args) throws IOException, TemplateException {
Configuration config = new Configuration(Configuration.getVersion());
//创建一个Configuration对象,并传入当前FreeMarker版本号
config.setDirectoryForTemplateLoading(new File("D:/JAVA开发/FreeMarkerDemo/src/main/webapp"));
//设置FreeMarker模板文件的加载目录
config.setDefaultEncoding("UTF-8");
//设置FreeMarker模板文件的默认编码为UTF-8
Template template = config.getTemplate("test.ftl");
//从配置中加载名为test.ftl的模板文件
HashMap Map = new HashMap();
//创建一个HashMap对象,用于存储传递给模板的数据
Map.put("name","菜狗");
//向HashMap中添加一个键值对,键为"name",值为"菜狗"
FileWriter fileWriter = new FileWriter(new File("D:/JAVA开发/FreeMarkerDemo/src/main/webapp/test.html"));
//创建一个FileWriter对象,用于将生成的HTML内容写入到对应文件中
template.process(Map,fileWriter);
//使用模板和数据生成HTML内容,并将其写入到FileWriter对象中
fileWriter.close();
//关闭FileWriter对象,确保所有内容都被正确写入文件并释放资源
}
}
运行看下效果
可以看到多了个html文件
我们尝试在test.ftl文件中插入我们的poc,然后再次生成文件
poc:
<
可以看到计算器弹出,漏洞存在
然后我们来分析漏洞成因,在分析前我们先了解下FreeMarker模板文件的组成
FreeMarker模板文件主要由如下4个部分组成:
- (1)文本:直接输出的部分
- (2)注释:使用<#-- ... -->格式做注释,里面内容不会输出
- (3)插值:即${...}或#{...}格式的部分,类似于占位符,将使用数据模型中的部分替代输出
- (4)FTL指令:即FreeMarker指令,全称是:FreeMarker Template Language,和HTML标记类似,但名字前加#予以区分,不会输出。FreeMarker采用FreeMarker Template Language(FTL),它是简单的,专用的语言。但是FTL不是像PHP那样成熟的编程语言,这意味着需要其他真实变成语言中进行数据准备,比如数据库查询和业务运算,之后模板显示已经准备好的数据。在模板中,你可以专注于如何展现数据, 而在模板之外可以专注于要展示什么数据。
下面是一个FreeMarker模板的例子,包含了以上所说的4个部分:
<html>
<head>
<title>Welcome to FreeMarker 中文官网</title><br>
</head>
<body>
<#-- 注释部分 -->
<#-- 下面使用插值 -->
<h1>Welcome ${user} !</h1><br>
<p>We have these animals:<br>
<u1>
<#-- 使用FTL指令 -->
<#list animals as being><br>
<li>${being.name} for ${being.price} Euros<br>
<#list>
<u1>
</body>
</html>
看完后,我们再把我们的poc拿出来分析下
<
1、可以看到这个poc是FTL指令,用的是<#assign
标签,使用该指令你可以创建一个新的变量, 或者替换一个已经存在的变量
2、value的值对应的是"freemarker.template.utility.Execute"这个类是freemarker内置的一个工具类,用于执行系统命令
3、?new() 是FreeMarker中的操作符,用于实例化一个对象
4、${value("calc.exe")} 是FreeMarker中的插值表达式,用于输出变量的值
我们看下freemarker.template.utility.Execute.class
在执行处和参数下断点,调用堆栈如下,我们一步步跟下流程
exec:84, Execute (freemarker.template.utility)
_eval:62, MethodCall (freemarker.core)
eval:101, Expression (freemarker.core)
calculateInterpolatedStringOrMarkup:100, DollarVariable (freemarker.core)
accept:63, DollarVariable (freemarker.core)
visit:347, Environment (freemarker.core)
visit:353, Environment (freemarker.core)
process:326, Environment (freemarker.core)
process:383, Template (freemarker.template)
main:28, TestDemo (org.example.freemarkerdemo)
从template.process()方法开始
进入process,执行createProcessingEnvironment(),process()方法
我们继续跟进
前面部分是获取当前线程中的环境变量然后设置,关键点在visit方法,它是用于访问模板的根节点并进行处理的,getTemplate()获取当前的模板对象,getRootTreeNode()从模板对象中获取根节点,我们跟进
element就是我们获取到的模板内容,然后通过.accept变量模板文件里面的内容,传入templateElementsToVisit
接着进入for循环,把templateElementsToVisit中的模板内容一条条取出,然后进入二次调用visit(),这里跳过前面的看下关于我们传入POC的处理流程
把我们的POC赋值给栈数组instructionStack的最后一个位置
接着触发accept,这里条件判断对走第三条,触发env.getCurrentNamespace()
然后接着往下走,来到一个if语句,看样子是判断语句类型,接着触发valueExp.eval,env是我们的模板对象
来到eval方法,这里constantValue = null,符合条件,执行_eval(env),继续跟
这里会执行一条Object result = targetMethod.exec(argumentStrings);
我们可以看到 targetMethod 目前就是我们在 ftl 语句当中构造的那个能够进行命令执行的类
也就是说这里的语句执行等同于Object result = freemarker.template.utility.Execute.exec(argumentStrings);
接着来到exec,这里通过会newInstance() 的方式进行初始化,命令执行的参数,会被拿出来,在下一次的同样流程中作为命令被执行
到这里触发流程就跟完了,那么FreeMarker的注入问题的成因是ftl中存在某些具有高风险操作的elements tag,这些elements tag的解析类通过继承实现了对应的eval接口,并且在实现类中引入了高风险的操作
危险内置类
Execute
我们上面的poc中使用到的是freemarker.template.utility.Execute这个内置类的exec方法,那么还有哪些freemarker的内置类的exec方法能够造成高危行为呢?
ObjectConstructor
我们看下这个类的exec方法
反射调用我们传入的对象,我们尝试构造poc
<
通过它这里的反射调用JDK自带的命令执行类ProcessBuilder,执行calc
JythonRuntime
这个类我没见过,查资料后,说是可以使得 Python 脚本可以在 Java 虚拟机上运行
网上关于这个类的poc如下
<
运行后报错,计算器未弹出,查资料发现要执行这个类环境要有jython-standalone依赖,所以不是很推荐使用,不如前两个
在pom.xml中添加对应依赖
<dependency>
<groupId>org.python</groupId>
<artifactId>jython-standalone</artifactId>
<version>2.7.0</version>
</dependency>
修复方式以及高版本特性
从漏洞复现和深入分析我们了解实际上照成安全问题的原因是调用了能够照成危害的freemarker内置类,那么我们只要把有危害的内置类禁用掉不就行了,官方也是这样想的
后续freemarker更新了个.setNewBuiltinClassResolver()方法来限制内建函数对类的访问
该配置有以下三种参数
- UNRESTRICTED_RESOLVER:可以通过ClassUtil.forName(String)获得任何类
- SAFER_RESOLVER:禁止加载ObjectConstructor,Execute和freemarker.template.utility.JythonRuntime这三个类
- ALLOWS_NOTHING_RESOLVER:禁止解析任何类
我们可以尝试配置下
再尝试运行poc
可以看到执行不了,这个是要手动设置的,非默认所以高版本系统也有可能出现安全问题
除了类禁用还有一个问题,在freemark的2.3.22版本之后api_builtin_enabled默认为false,同时FreeMarker为了防御通过其他方式调用恶意方法,在FreeMarker中内置了一份危险方法名单unsafeMethods.properties,例如:getClassLoader、newInstance等危险方法都被禁用了
最新版本2.3.33禁用的api如下
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()
可以看到非常多,要想调用api函数则必须将该值设置为true
使用.setSetting("api_builtin_enabled", "true");
可以设置,如果是spring boot的话可以在application.properties文件中启用配置
spring.freemarker.settings.api_builtin_enabled=true
像网上公开的部分setNewBuiltinClassResolver()绕过和文件读取poc有版本限制也是这个原因
目前交流群人数超了只能邀请,需要进群交流的加作者微信备注交流群
性感群主不定期水群在线解答问题!
原文始发于微信公众号(菜狗安全):JAVA安全-模板注入-FreeMarker
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论