聲明
出品|先知社區(qū)(ID:RoboTerh)
以下內(nèi)容,來自先知社區(qū)的RoboTerh作者原創(chuàng),由于傳播,利用此文所提供的信息而造成的任何直接或間接的后果和損失,均由使用者本人負責,長白山攻防實驗室以及文章作者不承擔任何責任。
漏洞分析
原理POC
String url = "jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor&user=yso_CommonsCollections4_calc";
關(guān)鍵屬性
queryInterceptors:一個逗號分割的Class列表(實現(xiàn)了com.mysql.cj.interceptors.QueryInterceptor接口的Class),在Query”之間”進行執(zhí)行來影響結(jié)果。(效果上來看是在Query執(zhí)行前后各插入一次操作)
statementInterceptors:和上面的攔截器作用一致,實現(xiàn)了com.mysql.jdbc.StatementInterceptor接口的Class
到底應該使用哪一個屬性,我們可以在對應版本的com.mysql.jdbc.ConnectionPropertiesImpl類中搜索,如果存在,就是存在的那個屬性
autoDeserialize:自動檢測與反序列化存在BLOB字段中的對象。
getObject方法的尋找
我們可以關(guān)注到mysql-connnector-java-xxx.jar包中存在有ResultSetImpl.getObject()方法
當然,同樣的,在不同的版本下的位置不同,我這里使用的5.1.48版本,他的位置
在com.mysql.jdbc.ResultSetImpl類中

首先他會判斷類型,如果是BIT類型,就會調(diào)用getObjectDeserializingIfNeeded方法,跟進

之后他首先會判斷field是否是Binary或者Blob
BLOB (binary large object),二進制大對象,是一個可以存儲二進制文件的容器。在計算機中,BLOB常常是數(shù)據(jù)庫中用來存儲二進制文件的字段類型
之后取出對應的字節(jié)數(shù),并且判斷是否開啟了autoDeserialize, 如果開啟了,將會進入if語句繼續(xù)判斷前兩個字節(jié)是否為-84?-19這是序列化字符串的標志,hex分別為AC ED, 如果滿足條件,就會調(diào)用對應的readObject方法進行反序列化
所以不難發(fā)現(xiàn),如果我們能夠控制需要反序列化的數(shù)據(jù),就能夠進行反序列化漏洞的利用
ServerStatusDiffInterceptor攔截器的妙用
我們可以關(guān)注到com.mysql.jdbc.interceptors.Server-StatusDiffInterceptor這個類,在其中的populateMap-WithSessionStatusValues方法中,會調(diào)用Util.resultSetToMap(toPopulate, rs);方法,進而調(diào)用了java.sql.ResultSet.getObject方法,形成利用鏈
rivate void populateMapWithSessionStatusValues(Connection connection, Map<String, String> toPopulate) throws SQLException {java.sql.Statement stmt = null;????java.sql.ResultSet?rs?=?null;try {????????toPopulate.clear();stmt = connection.createStatement();rs = stmt.executeQuery("SHOW SESSION STATUS");Util.resultSetToMap(toPopulate, rs); //調(diào)用getObject方法} finally {if (rs != null) {rs.close();????????}if (stmt != null) {stmt.close();}}}
//Util.resultSetToMappublic static void resultSetToMap(Map mappedValues, java.sql.ResultSet rs) throws SQLException {while (rs.next()) {mappedValues.put(rs.getObject(1), rs.getObject(2));}}
同樣通過idea的find Usages方法找到在postProcess方法中調(diào)用了populateMapWithSessionStatusValues
public ResultSetInternalMethods postProcess(String sql, Statement interceptedStatement, ResultSetInternalMethods originalResultSet, Connection connection)????????throws?SQLException?{if (connection.versionMeetsMinimum(5, 0, 2)) {//調(diào)用????????populateMapWithSessionStatusValues(connection,?this.postExecuteValues);connection.getLog().logInfo("Server status change for statement:\n" + Util.calculateDifferences(this.preExecuteValues, this.postExecuteValues));????}return null; // we don't actually modify a result set}
同樣在preProcess方法中也調(diào)用了
在調(diào)用鏈中也可以得到
populateMapWithSessionStatusValues:61, ServerStatusDiffInterceptor (com.mysql.jdbc.interceptors)preProcess:84, ServerStatusDiffInterceptor (com.mysql.jdbc.interceptors)preProcess:54, V1toV2StatementInterceptorAdapter (com.mysql.jdbc)preProcess:65, NoSubInterceptorWrapper (com.mysql.jdbc)invokeStatementInterceptorsPre:2824, MysqlIO (com.mysql.jdbc)sqlQueryDirect:2580, MysqlIO (com.mysql.jdbc)execSQL:2465, ConnectionImpl (com.mysql.jdbc)execSQL:2439, ConnectionImpl (com.mysql.jdbc)executeQuery:1365, StatementImpl (com.mysql.jdbc)loadServerVariables:3775, ConnectionImpl (com.mysql.jdbc)initializePropsFromServer:3196, ConnectionImpl (com.mysql.jdbc)connectOneTryOnly:2233, ConnectionImpl (com.mysql.jdbc)createNewIO:2015, ConnectionImpl (com.mysql.jdbc):768, ConnectionImpl (com.mysql.jdbc):47, JDBC4Connection (com.mysql.jdbc)newInstance0:-1, NativeConstructorAccessorImpl (sun.reflect)newInstance:62, NativeConstructorAccessorImpl (sun.reflect)newInstance:45, DelegatingConstructorAccessorImpl (sun.reflect)newInstance:423, Constructor (java.lang.reflect)handleNewInstance:425, Util (com.mysql.jdbc)getInstance:385, ConnectionImpl (com.mysql.jdbc)connect:323, NonRegisteringDriver (com.mysql.jdbc)getConnection:664, DriverManager (java.sql)getConnection:208, DriverManager (java.sql)main:15, Test (pers.xstream)com.mysql.jdbc.ConnectImpl#loadServerVariables方法存在需要執(zhí)行一段SHOW VARIABLES的sql語句
results = stmt.executeQuery(versionComment + "SHOW VARIABLES");因為在這個版本中的mysql-connector使用的是statementInterceptors作為在執(zhí)行SQL語句的攔截器類,所以在com.mysql.jdbc.MysqlIO#sqlQueryDirect方法中存在對這個屬性值是否存在的判斷,如果存在,就調(diào)用其中的攔截處理邏輯,不存在就直接放行

進而調(diào)用了對應Interceptor的preProcess方法,如果我們在JDBC連接串中使用的
是ServerStatusDiffInterceptor作為攔截器,那么就會調(diào)用他的preProcess方法,進而形成了利用鏈
注意:在populateMapWithSessionStatusValues方法中存在一個執(zhí)行SHOW SESSION STATUS獲取結(jié)果的邏輯
rs = stmt.executeQuery("SHOW SESSION STATUS");我們在惡意Mysql服務端進行處理的時候就可以通過進行SHOW SESSION STATUS或者其他版本的其他標志作為標志,返回我們構(gòu)造的惡意payload, 使得在后面調(diào)用了UtilresultSetToMap進行g(shù)etObject的調(diào)用
在ResultSetImpl#getObject方法中對mysql服務端返回的數(shù)據(jù)進行判斷,這里是Types.LONGVARBINARY類型(長二進制數(shù)據(jù)), 再然后就是前面提到了getObject方法尋找的部分了
detectCustomCollations的妙用
在這里我們將環(huán)境中的mysql-connector-java包改為5.1.29版本
來自chybeta佬的研究,我們可以關(guān)注到ConnectionImpl#buildCollationMapping中存在有Util.resultSetToMap的調(diào)用,能夠形成前面所描述的利用鏈

首先看一下調(diào)用棧
buildCollationMapping:1004, ConnectionImpl (com.mysql.jdbc)initializePropsFromServer:3617, ConnectionImpl (com.mysql.jdbc)connectOneTryOnly:2550, ConnectionImpl (com.mysql.jdbc)createNewIO:2320, ConnectionImpl (com.mysql.jdbc)<init>:834, ConnectionImpl (com.mysql.jdbc)<init>:46, JDBC4Connection (com.mysql.jdbc)newInstance0:-1, NativeConstructorAccessorImpl (sun.reflect)newInstance:62, NativeConstructorAccessorImpl (sun.reflect)newInstance:45, DelegatingConstructorAccessorImpl (sun.reflect)newInstance:423, Constructor (java.lang.reflect)handleNewInstance:411, Util (com.mysql.jdbc)getInstance:416, ConnectionImpl (com.mysql.jdbc)connect:347, NonRegisteringDriver (com.mysql.jdbc)getConnection:664, DriverManager (java.sql)getConnection:208, DriverManager (java.sql)main:16, Test (pers.xstream)從上面的截圖我們可以看到有幾個判斷條件
需要滿足服務端版本要大于4.1.0?, 而且detectCustomCollations需要為true
if (versionMeetsMinimum(4, 1, 0) && getDetectCustomCollations())2. 需要滿足大于5.0.0,在5.1.28不存在這個條件
同樣這里獲取了執(zhí)行SHOW COLLATION命令的結(jié)果集,同樣可以作為標志返回惡意payload
只要滿足上述條件,就只需要將結(jié)果集中的字段 2 或者 3 封裝我們的序列化數(shù)據(jù)就可以成功利用了
進行探索
過程
在大佬的研究中,提到了,detectCustomCollations觸發(fā)方式在5.1.40版本之后不能夠利用,因為沒有使用getObject的方式獲取SHOW COLLATION的結(jié)果
但是在我的跟蹤中,發(fā)現(xiàn)了,其實還是可以利用的,雖然這個發(fā)現(xiàn)沒有什么大用,在高版本不能使用ServerStatusDiffInterceptor的時候用用??
在idea中添加對應版本的包
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.41</version></dependency>
在版本對比中,的確在新版本中刪掉了Util.resultSetToMap的調(diào)用

但是卻在后面直接調(diào)用了SHOW COLLATION返回的結(jié)果,調(diào)用getObject方法,這里或許就可以達到我們的利用目的
為什么不能夠成功執(zhí)行
在發(fā)現(xiàn)了這個觸發(fā)位置之后,我使用工具進行漏洞利用的時候,發(fā)現(xiàn)并不能夠成功執(zhí)行payload, 為什么呢?
我們可以關(guān)注到在調(diào)用getObject的時候

這里是取的第3列的數(shù)據(jù),而在大佬的工具中有所描述
SHOW SESSION STATUS和SHOW COLLATION的公用列是第二列
同時在debug的過程中發(fā)現(xiàn)取出的并不是序列化數(shù)據(jù),所以我們需要修改工具,使得返回的結(jié)果集中第3列是惡意的序列化數(shù)據(jù),之后對利用工具進行了深入了解,和構(gòu)造分析,可以知道在server.py中的handle_server方法中需要我們對其更改

在圖片所指的位置,就是我們返回集的第1,2,3的數(shù)據(jù),可以直接改成content接收序列化數(shù)據(jù),相對的,如果使用的是config.json配置文件執(zhí)行命令的方式,就需要將上面某個的233改為yso_dict[username]
之后我們就可以成功利用了

只有直到在5.1.49版本中做出了更改,導致不能使用
6.x版本能夠利用嗎
當然可以,在6.x版本中,他就類似于5.1.41之前的調(diào)用Util.resultSetToMap, 在這里它使用的是ResultSetUtil.resultSetToMap,跟進一下看下邏輯


是不是和之前的差不多,利用:

版本區(qū)分
ServerStatusDiffInterceptor
5.1.11-6.0.6使用的是statementInterceptors屬性,而8.0以上使用queryInterceptors, 具體屬性可以在ConnectionPropertiesImpl類中搜索
5.1.11以下,不能通過這種方式利用,因為在5.1.10中Interceptors的初始化過程在漏洞利用過程之后,將會在利用中,因為找不到interceptor而不能夠觸發(fā)成功https://github.com/mysql/mysql-connector-j/compare/5.1.10...5.1.11#diff-4c9979a09004e7c2d55d663702c47c61e002e351856b83cda37414a15fb47dcaL779-L785
5.0.x沒有這個攔截器
detectCustomCollations
8.0.x不存在getObject方法的調(diào)用
6.x能夠利用,因為他在com.mysql.cj.jdbc.Connec-tionImpl中調(diào)用了ResultSetUtil.resultSetToMap和上面的功能類似,且沒有版本判斷

從5.1.29開始啟用detectCustomCollations屬性,但是直到5.1.49做出了更改導致不能使用https://github.com/mysql/mysql-connector-j/compare/5.1.48...5.1.49#diff-4c9979a09004e7c2d55d663702c47c61e002e351856b83cda37414a15fb47dcaL911-L914

在這里值得注意的是,在5.1.41做出了更改,不再調(diào)用Util.resultSetToMap方法,進而調(diào)用getObject方法,改為了直接調(diào)用getObject方法https://github.com/mysql/mysql-connector-j/compare/5.1.40...5.1.41#diff-4c9979a09004e7c2d55d663702c47c61e002e351856b83cda37414a15fb47dcaL944-R936
5.1.18以下沒有使用getObject方法的調(diào)用
在5.1.19-5.1.28過程中,不存在detectCustom-Collations屬性的判斷,但是仍然可以調(diào)用
https://github.com/mysql/mysql-connector-j/compare/5.1.28...5.1.29#diff-4c9979a09004e7c2d55d663702c47c61e002e351856b83cda37414a15fb47dcaL986-L1005
可用連接串
直接對fnmsd的研究稍作修改

將其中5.1.41不可用改成5.1.29以上只有5.1.49不可用,且6.x系列都可以使用
參考
https://github.com/fnmsd/MySQL_Fake_Server
https://www.anquanke.com/post/id/203086#h2-1
https://i.blackhat.com/eu-19/Thursday/eu-19-Zhang-New-Exploit-
Technique-In-Java-Deserialization-Attack.pdf