背景
上周 BlackHat Europe 2019 的议题《New Exploit Technique In Java Deserialization Attack》中提到了一个通过注入 JDBC URL 实现反序列化攻击的场景, 简单分析一下.
分析
首先, 当 java 应用使用 MySQL Connector/J(官方的 JDBC 驱动, 本文基于其 8.0 + 版本) 连接 MySQL 时, JDBC URL 的格式如下: protocol//[hosts]/[database]?properties, 具体可看 MySQL 官方文档, 示例: jdbc:MySQL://localhost:3306/test?useSSL=true
其中, protocol,host,database 都比较好理解, URL 中的 properties 可以设定 MySQL Connector/J 连接 MySQL 服务器的具体方式, 关于 properties 的官方文档地址, 其中和本文相关的连接属性有两个, 分别是 autoDeserialize 和 queryInterceptors, 前者是设定 MySQL Connector/J 是否反序列化 BLOB 类型的数据, 后者是拦截器, 在查询执行时触发, 由 com.MySQL.cj.protocol.a.NativeProtocol#sendQueryPacket 方法源码可知, 会在执行查询语句前后分别调用拦截器的 preProcess 和 postProcess 方法.
接下来定位下反序列化的触发点, 在 MySQL-connector-java 组件下全局搜索关键字 ".readObject()", 定位到 com.MySQL.cj.jdbc.result.ResultSetImpl 类中的 getObject(int columnIndex) 方法, 部分核心代码如下:
- public Object getObject(int columnIndex) throws SQLException {
- ......
- case BLOB:
- byte[] data = getBytes(columnIndex);
- if (this.connection.getPropertySet().getBooleanProperty(PropertyDefinitions.PNAME_autoDeserialize).getValue()) {
- Object obj = data;
- // Serialized object?
- try {
- ByteArrayInputStream bytesIn = new ByteArrayInputStream(data);
- ObjectInputStream objIn = new ObjectInputStream(bytesIn);
- obj = objIn.readObject();
- }
- }
- }
变量 data 即为 MySQL 返回结果集, 当 JDBC URL 中设定属性 autoDeserialize 为 true 时, 会对类型为 bit,binary 以及 blob 的数据进行反序列化, 如何触发 getObject(int columnIndex) 方法的调用呢? 议题中给出的调用链如下:
- > com.MySQL.cj.jdbc.interceptors.ServerStatusDiffInterceptor#preProcess/postProcess
- > com.MySQL.cj.jdbc.interceptors.ServerStatusDiffInterceptor#populateMapWithSessionStatusValues
- > com.MySQL.cj.jdbc.util.ResultSetUtil#resultSetToMap
- > com.MySQL.cj.jdbc.result.ResultSetImpl#getObject
ServerStatusDiffInterceptor 即为此前提到过的拦截器, 在 JDBC URL 中设定属性 queryInterceptors 为 ServerStatusDiffInterceptor 时, 执行查询语句会调用拦截器的 preProcess 和 postProcess 方法, 进而通过上述调用链最终调用 getObject(int columnIndex) 方法.
实际利用还有一个问题, 最终调用 getObject 方法的对象是数据库返回的结果集, 由 populateMapWithSessionStatusValues 方法可知:
- try {
- toPopulate.clear();
- stmt = this.connection.createStatement();
- rs = stmt.executeQuery("SHOW SESSION STATUS");
- ResultSetUtil.resultSetToMap(toPopulate, rs);
- }
这个结果集是执行 SQL 语句 "SHOW SESSION STATUS" 后数据库返回的值, SQL 语句 "SHOW SESSION STATUS" 返回当前数据库连接的状态值, 实际是读取系统表 INFORMATION_SCHEMA.SESSION_VARIABLES 的值, 也可能是 PERFORMANCE_SCHEMA.SESSION_VARIABLES(MySQL 版本差异导致). 但是 MySQL 中 INFORMATION_SCHEMA 和 PERFORMANCE_SCHEMA 都是不允许被修改的, 所以需要想办法操纵返回的数据.
利用条件
1. 本质上还是 Java 原生的反序列化利用, 所以需要环境中有可用的 Gadget;
2. 需要能伪造相关系统表的数据, 将 "SHOW SESSION STATUS" 的执行结果设置为我们精心构造的反序列化数据, 或者基于 MySQL 连接协议, 自定义返回数据, 后面有时间的时候会写写这块儿.
3. 可控的 JDBC URL
来源: https://www.cnblogs.com/Welk1n/p/12056097.html