0x00 前言
当传入DriverManager.getConnection(jdbcUrl)中的jdbcUrl用户任意可控时,通过构造恶意的jdbcUrl(恶意连接属性、恶意连接类型等),在JDBC Driver处理jdbcUrl进行连接的过程中进行恶意操作(这类JDBC Driver本身是有漏洞的),从而导致任意文件读取、RCE等危害。
0x01 常见防护方法
可以使用jdbc支持的数据库来构造连接需要使用的jdbc串,一般是如下的格式,主要是由数据库类型,连接地址还有额外的属性对组成:
jdbc:<type>://<hosts|host:port>/<db><properties>
一般情况下,常见的防护主要是对拼接到jdbcUrl中的相关字段进行格式校验。
- 对type字段,主要是数据源类型,如果可以调用任意类型的数据源,就可以执行任意风险数据源对应的攻击方式,比如jdbc:mysql可以实现任意文件读取、RCE、jdbc:postgresql可以实现任意文件写、RCE等。一般情况下会使用黑白名单限制数据源的连接。
可以看到JDBC支持相关数据库的有这些:https://www.oracle.com/java/technologies/industry-support.html
- 对于host和port以及db字段,一般会进行输入格式的限制,例如port限制只允许数字,host字段限制只允许大小写字母、数字、中文、点(.),短横线(-),斜杠(/) 冒号(:)等。
- 对于连接时设置的属性properties,一般情况下会对存在漏洞的数据源的黑名单属性校验,不同数据源的恶意连接属性(property)不同,需要不断维护对应的黑名单,避免可能的字段校验缺失导致可控jdbcUrl进而绕过。以mysql为例,例如类似的属性配置会导致对应的风险:
/**
* The sensitive param may lead the attack.
*/
private
static
final
Map
SENSITIVE_REPLACE_PARAM_MAP =
new
HashMap
(
)
{
{
put(
"autoDeserialize"
,
"false"
);
put(
"allowLoadLocalInfile"
,
"false"
);
put(
"allowUrlInLocalInfile"
,
"false"
);
}
};
private
static
final
Set
SENSITIVE_REMOVE_PARAM_MAP =
new
HashSet
(
)
{
{
add(
"allowLoadLocalInfileInPath"
);
}
};
0x02 绕过方式
在简单了解了常见的防护方法后,下面以MySQL 驱动里的一些绕过姿势为例进行讨论,对于其他类型的数据库源,同样存在类似的思路和手法。
2.1 使用特定的值替换
mysql JDBC 中包含⼀个危险的扩展参数: autoDeserialize。这个参数配置为true时,JDBC客户端将会⾃动反序列化服务端返回的BLOB类型字段。一般情况下,会获取并校验提交的配置参数以及相关的值,避免设置了不安全的参数。例如会检测autoDeserialize参数是否设置成了true。
实际上可以通过yes关键字进行绕过,在mysql-connector-java中,yes跟true实际上是等价的。这里以几个版本为例,查看具体的源码实现:
- mysql-connector-java-8.0.12
在com.mysql.cj.conf.BooleanPropertyDefinition的AllowableValue枚举类中可以看到,设置TRUE和设置YES效果是⼀样的:
- mysql-connector-java-6.0.5
com.mysql.cj.core.conf.BooleanPropertyDefinition#getAllowableValues方法中,同样可以看到设置TRUE和设置YES效果是⼀样的:
- mysql-connector-java-5.1.1
在BooleanConnectionProperty#getAllowableValues中,同样可以看到设置TRUE和设置YES效果是⼀样的:
综上,也就是说可以通过设置autoDeserialize=yes
来尝试绕过一些现有的属性值检查防护措施。
2.2 大小写绕过
从前面的枚举类可以看到,mysql-connector-java-8.0.12在解析时还会统一转换成大写:
其他版本在值判断时,通过equalsIgnoreCase()方法将字符串与指定的另一个字符串进行比较,不考虑大小写:
也就是说,对于autoDeserialize=true
,可以通过将对应恶意属性的value进行大小写转换来尝试绕过一些现有的属性值检查防护措施。
2.3 URL编码绕过
除了上面的方法外,还可以考虑使用URL编码进行绕过。以mysql-connector-java-8.0.12为例,查看8.0.x版本的解析过程。
主要是加载过程,在getConnection⽅法中,会遍历registeredDrivers变量的值,⾥⾯存放着注册过的驱动,然后通过DriverInfo对象获取对应驱动,并调⽤其connet⽅法进⾏连接:
在connect方法中,会对传入的jdbcUrl进行解析:
ConnectionUrl.acceptsUrl(url)⽅法判断url是否合法,调用com.mysql.cj.conf.ConnectionUrlParser#isConnectionStringSupported方法判断协议是否支持:
在isConnectionStringSupported方法中,主要通过正则捕获对应的协议,然后进行URL解码后进行匹配:
如果url合法,则调用ConnectionUrl.getConnectionUrlInstance⽅法,这里会调用ConnectionUrlParser#parseConnectionString方法对传入的url进行进一步的处理:
首先会再次判断url是否合法,然后再调用parseConnectionString方法:
在parseConnectionString方法中,主要通过正则捕获对应的内容,可以看到除了协议部分以外,path部分同样进行了URL解码操作:
private
static
final
Pattern CONNECTION_STRING_PTRN = Pattern.compile(
"(?[\w:%]+)\s*(?://(?[^/?#]*))?\s*(?:/(?!\s*/)(?[^?#]*))?(?:\?(?!\s*\?)(?[^#]*))?(?:\s*#(?.*))?"
);
解析完成后,会调⽤Util.getInstance⽅法,这里实例化了⼀个SingleConnettionUrl对象:
这里会调用对应对象的构造方法:
在其父类的构造方法中,会调⽤collectProperties⽅法,从字面上看应该是进行url中的参数收集:
最终会调用com.mysql.cj.conf.ConnectionUrlParser#processKeyValuePattern方法解析query为键值对,并通过HashMap对象进⾏封装,可以看到这里对参数的值以及参数都进行了url解码操作:
后续解析完传入的url并建立连接后,即可完成对应的sql交互。
通过上述分析,对8.0.x版本来说,可以看到在解析过程中,对协议、path以及请求的参数对都进行了URL解码操作。也就是说,对于autoDeserialize=true
,可以考虑进行URL编码%61%75%74%6f%44%65%73%65%72%69%61%6c%69%7a%65=%74%72%75%65
,来尝试绕过现有的属性值检查防护措施。
但是在低版本5.1.x版本,仅仅只对参数值进行了解码操作:
2.4 头尾空白符绕过
前面提到了,对于通过属性值检查的防护措施,可以通过大小写/URL编码的方式进行绕过,如果在实际防护时进行按照标准 URL 处理字符串进行自动解码并统一大小写,然后再过滤 yes 和 true 选项。那这样是不是就万无一失了呢?
实际上很多基于属性值检查的防护措施都是在获取到jdbcUrl后,进行URL解码以及统一大小写的操作后,提取到请求参数部分,然后通过&进行分隔,提取到对应的key-value后再通过黑名单的方式进行匹配。例如下面的代码:
private
static
boolean
check
(String jdbcUrl)
{
try
{
Map params =
new
HashMap<>();
String query = jdbcUrl.split(
"\?"
)[
1
];
if
(query !=
null
) {
String[] pairs = query.split(
"&"
);
for
(String pair : pairs) {
String[] keyValue = pair.split(
"="
);
String key = keyValue[
0
];
String value = keyValue.length >
1
? keyValue[
1
] :
""
;
params.put(key, value);
}
}
for
(Map.Entry p: params.entrySet()){
if
(p.getKey().equals(
"autoDeserialize"
)) {
if
(p.getValue().equals(
"true"
)||p.getValue().equals(
"yes"
)){
return
false
;
}
}
}
return
true
;
}
catch
(Exception e) {
e.printStackTrace();
return
false
;
}
}
上述代码通过split获取到参数部分,然后通过&以及=获取请求的key-value并封装在Map中,然后遍历获取到的Map进行黑名单的检测,如果发现请求参数存在autoDeserialize并且值为true/yes时认为存在风险,返回false。
实际上只需要通过autoDeserialize = true
结合头尾空白符的方法即可绕过上述的安全检查了,此时获取到的key和value多了额外的空格,并不在黑名单范围内。前提是mysql-connector-java在解析参数时对空白符号进行了处理。
结合前面mysql-connector-java-8.0.12的解析过程,在com.mysql.cj.conf.ConnectionUrlParser#processKeyValuePattern方法解析query为键值对时,除了对参数的值以及参数都进行了url解码操作以外,在此之前调用了StringUtils#safeTrim方法进行处理:
这里对通过trim()方法去除了字符串两端的空白字符。这些空白字符包括空格(Space)、制表符(Tab)、换行符(Line Feed)、回车符(Carriage Return)等:
也就是说前面通过autoDeserialize = true
结合头尾空白符绕过的猜想是可行的。(5.1.x版本并没有对空白符进行额外的处理)
Apache inlong也有过类似的绕过case,对应CVE编号CVE-2023-46227(Apache inlong JDBC URL反序列化漏洞),在受影响版本中,由于只对用户输入的 jdbc url 参数中的空格做了过滤,没有对其他空白字符过滤。具备 InLong Web 端登陆权限的攻击者可以使用t绕过对 jdbc url 中autoDeserialize、allowUrlInLocalInfile、allowLoadLocalInfileInPath参数的检测,进而在MySQL客户端造成任意代码执行、任意文件读取等危害。
简单看下对应的代码,防护方式主要是定义了对应的属性黑名单进行检查:
可以看到在受影响版本中,仅仅替换了jdbcUrl中的空格,而修复版本则对空白字符进行了彻底过滤,然后再进行key-value的获取以及安全检查:
InlongConstants.REGEX_WHITESPACE是定义的常量,\s 是正则表达式,含义是匹配全部空白字符:
此外,这里还有通过java.net.URI对jdbcUrl进行封装,然后获取query进行解析的:
URI uri =
new
URI(jdbcUrl.replace(
"jdbc:"
,
""
));
String query = uri.getQuery();
这样做的好处是在解析时会判断是否存在非法字符,能从一定程度避免无关字符的干扰,避免潜在的绕过风险:
2.5 使用注释符
除了通过属性值检查以外,某些修复方式可能会通过在jdbcUrl尾部强行添加 autoDeserialize=false通过覆盖变量进行处理,这样即使攻击者在连接url中设置了autoDeserialize参数也会被覆盖掉。.结合前面mysql-connector-java-8.0.12的解析过程,会调用ConnectionUrlParser#parseConnectionString方法对传入的url进行进一步的处理,而在parseConnectionString方法中,主要通过正则捕获对应的内容:
private
static
final
Pattern CONNECTION_STRING_PTRN = Pattern.compile(
"(?[\w:%]+)\s*(?://(?[^/?#]*))?\s*(?:/(?!\s*/)(?[^?#]*))?(?:\?(?!\s*\?)(?[^#]*))?(?:\s*#(?.*))?"
);
在匹配 URL 的查询参数部分时,使用 (?[^#]*)
匹配查询参数,这里#充当了注释符的作用,在获取query时会将#后面的注释部分去掉。
也就是说,8.0.x版本实际上是支持通过注释符#来注释掉后面的内容的。那么就可以通过#注释掉之后拼接的内容,从而覆盖后面想要赋值的变量。而5.1.x版本是不支持注释符的。
2.6 注入拼接
除了jdbcUrl直接可控的场景以外,用户可控还可能是host、用户名、密码、数据库名以及自定义的连接字符串。这一系列输入可能通过StringBuilder#append进行拼接,最终合并成一个完成的jdbcUrl进行连接:
类似上面的例子,jdbc连接参数可能已经做了严格的过滤了,但是类似user和password字段也是用户可控的,如果这两个字段没有过滤,且一系列用户输入可能通过StringBuilder#append进行拼接。那么就可以在用户名密码字段尝试注入恶意参数拼接到最终的jdbcUrl中,完成利用。
PS:user和password若以DriverManager.getConnection(url, user, password)的形式传入,则不受影响
0x03 其他
前面讨论了一些常见防护方法的绕过,简单总结下在实际修复的时候需要考虑的问题:
- 升级数据源组件至安全版本,例如通过将mysql-connector/j升级到8.0.21来解决该问题,在mysql-connector/j 8.0.21的
ServerStatusDiffInterceptor#populateMapWithSessionStatusValues
中不再使用getObject而是使用getString。 - 使用支持同一数据源但不存在漏洞的组件进行替换,比如使用mariadb-java-client替换mysql-connector-java,都支持mysql等的连接。
- 在进行属性值检查防护措施时,需要考虑归一化的问题,例如大小写、URL编码、额外空白符以及注释符#的问题。同时,不同数据源的恶意连接属性(property)不同,需要长期维护对应的黑名单内容。
- 类似userName、password字段内容不要包含类似等于(=)敏感字符,进行类似黑名单校验。同样的,对host等字段同样也需要进行检查,例如port应该限制输入,只允许数字。
- 限制支持的数据源,若对应的数据源都非恶意,则可以省略其他字段的校验,只需保证数据源type是符合预期的。
转载:
作者:
欢迎大家去关注作者
原文始发于微信公众号(星冥安全):Jdbc Attack防护绕过浅析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论