背景
抽空在h1上看到了一个和JDBC Attack有关的案例,于是就简单看了下。
https://hackerone.com/reports/1547877
这个案例中提到的Aiven是一家提供Saas化数据管理服务的云厂商,包括Apache Kafka、Postgres、MySQL、Redis,所以自然就存在JDBC连接的场景。
由于漏洞已经修复,无法从控制台上看到相关的功能。不过可以根据漏洞作者的描述了解整个漏洞的利用过程:
-
Aiven的Apache Kafka Connect Connector支持包括JDBC Sink Connector和HTTP Sink Connector等多种Sink Connector。
-
通过控制台对服务日志的查看,作者发现支持Jolokia,并且监听在
localhost:6725
-
HTTP Sink Connector允许发送HTTP请求到
localhost
并且Jolokia监听在本地的localhost:6725
-
Jolokia暴露了MBean
com.sun.management:type=DiagnosticCommand
, 而该Mbean存在一个operationjvmtiAgentLoad
-
由于HTTP Sink Connector没有校验目标是不是本地,所以可以通过
jvmtiAgentLoad
这个operation可以加载一个本地的jar文件。 -
至于如何上传,作者通过JDBC Sink Connector的功能,利用SQLite JDBC driver在JDBC连接时创建一个数据库文件,再把恶意jar内容写入数据库的表里,SQLite的数据文件都是存在本地,可以通过JDBC连接的URL指定数据库文件的具体路径。
-
最后通过
jvmtiAgentLoad
加载指定的恶意jar,既数据库文件。
JVMTI和Instrument
上文在介绍漏洞利用过程时,提到Jolokia暴露的MBean com.sun.management:type=DiagnosticCommand
和它的operation jvmtiAgentLoad
,这里涉及到另一个概念,叫JVMTI
。
还是先了解下基本概念:JVMTI(JVM Tool Interface)
是 Java 虚拟机所提供的 native 编程接口。JVMTI只是一套接口,我们要开发JVM工具就需要写一个Agent程序来使用这些接口。Agent程序其实就是一个C/C++语言编写的动态链接库。
所以加载恶意.so文件的方法可以实现RCE,不过Java在JDK 5开始引入了Instrument机制。利用Instrument接口,可以通过Java代码调用libinstrument的动态库与JVMTI接口进行交互,从而不需要再开发native的动态连结库文件,更加方便。
说到Instrument机制,它包含两种方式的整合形式,一种是main方法启动前执行,另一种是main方法内部通过attach来进行加载。
- premain(Agent模式): 目标应用main方法启动前
java -javaagent:/path/to/javaagent.jar -jar application.jar
其中,-javaagent
需要在-jar的前面,如果在后面,不生效。
public static void premain(String agentArgs, Instrumentation inst);
public static void premain(String agentArgs);
premain方式相对简单,就是有一个javaagent的jar包,然后,在启动命令上把这个jar加上去之后,就会在启动main方法之前先运行这个premain方法。需要注意的是,要想使这个jar包知道启动哪一个premain方法,我们还需要在manifest文件里面进行定义。定义menifast的方法也有两种,一种是直接编写menifast文件,还有一种使用maven的插件进行编写。
- agentmain(Attach模式): 目标应用之外,用一个attach应用将javaagent.jar注入到目标应用中
public static void agentmain(String agentArgs, Instrumentation inst);
public static void agentmain(String agentArgs);
attach方式相对要麻烦一些,需要单独起一个应用(或者使用一个另外的线程),通过VirturalMachine.list()
找到所有运行的VirtualMachineDescriptor
,匹配到目标应用之后,再把javaagent.jar注入到目标应用里面去。
有了以上这些知识就不难理解,我们可以直接通过com.sun.management:type=DiagnosticCommand
的jvmtiAgentLoad
operation,使用attach的方式把我们的恶意agent注入进去,从而达到不重启应用也可以实现RCE。
实现恶意Java Agent
其实非常的简单,因为JDK里提供了premain
和agentmain
两个静态方法,直接使用就好。这里使用的agentmain
直接attach
如果是手工创建MANIFEST.MF,需要指定Agent-Class,最后打成jar包
这里需要注意的是如果随便load一个文件,会存在以下报错
"Agent_OnAttach is not available in /tmp/ext.so "
原因是JVMTI底层是通过Agent_OnAttach
作为入口函数,然后执行以下流程,最终加载Java agent
- 获取JNIEnv,保证已经成功attach到Java进程
- 创建并初始化JPLISAgent、设置VMInit监听(不会触发了),逻辑与OnLoad相同
- 读取Agent-Class并加载
- 读取META-INFO相关配置,设置mRetransformEnvironment ClassFileLoadHook监听,逻辑与OnLoad相同
- 创建InstrumentationImpl实例
- 设置mNormaltransformEnvironment ClassFileLoadHook监听
- 执行AgentMain方法
所以最终一个简单的恶意Java agent的代码如下:
public class JavaAgent {
private static final String RCE_COMMAND = "open -a calculator";
public static void cmd() {
try {
Runtime.getRuntime().exec(RCE_COMMAND);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void agentmain(String args, Instrumentation inst) {
System.out.println("In JavaAgent Agentmain");
cmd();
}
}
Jar文件的特性
这个漏洞利用了Jar文件的特性,说到特性就得先了解Jar的定义。以下是Oracle官方的定义
两点有帮助的信息:
-
Jar是基于Zip文件的一种文件格式
-
Jar对文件名的要求不严格
所以我们可以像对Zip文件一样,把jar文件嵌入在其他文件里,而不影响其正常使用。
演示测试效果
因为官方早已经修复了该漏洞,我在本地搭建了一个环境,演示以上两点的效果:
-
把恶意的Java agent写入SQLite数据库
-
在JDBC连接时,如果之前数据库文件不存在,会通过DriverManager的getConnection方法会自动创建一个SQLite数据库的文件 xxx.jar(文件后缀名对数据库的读取没有任何影响),并通过相应的SQL语句创建表。
-
把恶意的Java agent以Blob数据类型写入数据库,演示中是把打好的恶意agent.jar写入SQLite数据库文件test.jar
- 加载恶意Java agent
http://127.0.0.1:8099/actuator/jolokia/exec/com.sun.management:type=DiagnosticCommand/jvmtiAgentLoad/!/tmp!/test.jar
成功attach进去了恶意的Java agent,完成了RCE