Attack JMX Service的打开方式
有次漏洞挖掘项目中碰到了未授权JMX的情况,在复盘时发现对于整套攻击JMX服务的方式不太了解。趁着最近有时间 对JMX相关知识来次补充,遂写了此文。主要对JMX服务未鉴权时的利用方式、JMX各账户权限可对应执行的操作、Oracle官方对于漏洞的修复、鉴权后的攻击利用方式做了分析演示。在梳理完此文后基本上对攻击JMX服务有底了,也成功利用这些特性PWN掉了***产品,中间的过程也比较有趣,等有机会再做分享…
1、基础知识
JMX是JAVA1.5引入的新特性,全称为Java Management Extension,即Java管理扩展,是管理/监控应用程序、设备、系统对象的工具。这些被管理的对象都可以抽象为MBean进行表示,客户端连接到服务端来管理MBean,如查询MBean属性、调用MBean方法等操作。而MBean的代码定义是有要求的,需要实现一个接口,所有需要对外公开的方法都需要在该接口中声明。另外此接口要求在MBean类名后加上MBean后缀,这里例子中的MBean类是Hello,接口为HelloMBean
1 |
|
HelloMBean接口:
1 |
|
对于管理器而言,这些MBean中公开的方法,最终会被JMX转化为属性( Attribute )、调用( Invoke )、监听( Listener )等概念。默认情况下每个Java进程都运行着MBean管理服务,使用ManagementFactory.getPlatformMBeanServer()
获取到MBeanServer后可对MBean进行操作。下面的例子模拟管理器注册MBean并显示当前java进程中的所有MBean
如果想让我们的MBean在管理器上可调用,那么需要指定一个ObjectName
对象,关于对象名称的详细语法可参考:https://www.oracle.com/java/technologies/javase/management-extensions-best-practices.html。
Object Name 在注册MBean时用于指定名称,在查询的时候可以指定正则用于查询,去匹配名称符合正则条件的MBean。每个Object Name都需要包含一个type关键属性
1 |
|
使用jconsole连接本地的org.example.MBeanExample类起的9052进程后,可以设置MBean的属性/调用方法,如我们这里的sayHello()方法
调用sayHello()方法
也可以开启远程服务,将Hello、HelloMBean、MBeanExample打包为jmxserver.jar包后,用如下命令开启调试功能、JMX监听端口并设置非认证。这里为演示命令执行的效果,将groovy-2.3.9.jar添加到classpath中,方便我们后续利用Groovy Gadget
1 |
|
使用nmap扫描其端口可以看到,2222端口MBean管理服务实际上是基于RMI Registry的,对象名称为jmxrmi,stub端口为56139
2、攻击JMX
我们按照整体调用流程、过程中存在的利用点、官方对漏洞的修复措施及后续利用进行分析,也为了方便理解,个人将攻击JMX服务分为5种利用方式:
1、jmx-url连接地址可控的JNDI注入利用方式
2、攻击RMI Registry的利用方式
3、RMI层”自定义”方法newClient利用方式
4、JMX层MBean方法getLoggerLevel/gcClassHistogram利用方式
5、MLET动态加载Evil MBean的利用方式
第一种是针对JMX客户端的利用方式,也捎带看一下。后面几种都是针对JMX服务,其中234都需要目标ClassPath存在可用的Gadget链,而第5个MLET动态加载是载入执行攻击者创建的恶意MBean类方法,所以没有Gadget的限制。从整体看,JMX客户端与服务端交互的流程及利用点如下:
1、客户端使用javax.naming.InitialContext#lookup获取到名称为”jmxrmi”的 Stub代理对象,当JMX url可控时,会造成JNDI注入的问题。这是第一个利用点
2、客户端调用javax.management.remote.rmi.RMIServer#newClient去获取RMIConnectionImpl Stub代理对象。当JMX服务需要验证时,会使用JAAS-based authenticator进行权限校验:根据服务的启动参数及jmxremote.password、jmxremote.access配置文件去匹配,当校验通过后返回代理对象。
这部分会涉及两个利用点:
a、JMX底层是依据RMI进行通信,当JDK版本在低版本时,可以使用攻击RMI Registry的exp进行攻击,可利用bind/lookup方法传输恶意序列数据/UnicastRef链利用。这是第二个利用点
b、newClient方法符合我们在攻击RMI中提到的应用层反序列化问题情况,参数为Object类型,可以塞入我们的恶意Padyload数据。这是第三个利用点
3、客户端invoke调用RMIConnectionImpl Stub代理对象的方法去操作MBean/获取MBean信息
这部分会涉及两个攻击点:
a、JMX层在还原MBean方法参数时也是采用反序列化方式进行还原的,所以可将恶意数据塞入默认MBean的有参方法。这是第四个利用点
b、在客户端连接成功创建MBean时,可调用MLET动态加载的方式去加载攻击者构建的Evil MBean完成利用。这是第五个利用点
下面是详细分析及密码验证后的绕过方式
2.1 jmx-url连接地址可控的JNDI注入利用方式
使用Java代码JMXConnectorFactory#connect连接JMX服务端时,会调用到InitialContext#lookup去获取名称为”jmxrmi”的远端对象。当JMX url可控时,会造成JNDI注入的问题,调用栈及演示如下
1 |
|
起个恶意LDAP服务,javaReferenceAddress放置我们的Groovy1链的Padyload,可以看到JMX客户端在JDK高版本的情况下成功触发命令执行。绕过原理见先前文章:当我们谈论JNDI注入时,我们在谈论什么
JMX JNDI注入调用栈:
2.2 攻击RMI Registry的利用方式
因为JMX底层也是依据RMI进行通信,所以当JDK版本在低版本时,也可以使用攻击RMI Registry的exp进行攻击。且这种攻击方式的触发点是在RMI层,还未执行到JMX权限校验部分,所以不受JMX权限的限制
JEP290前的JDK8u112版本,使用默认ysoserial中的ysoserial.exploit.RMIRegistryExploit测试成功
JEP290后的JDK131版本使用 UnicastRef 链绕过成功、141可使用改造后的lookup()+UnicastRef 链进行绕过
测试JDK191版本下,在sun.management.jmxremote.SingleEntryRegistry#singleRegistryFilter触发检查,导致反序列化失败。
JMX服务端调试情况,在singleRegistryFilter触发白名单检查报错
攻击端:
2.3 RMI层”自定义”方法newClient利用方式-CVE-2016-3427
当客户端使用JMXConnectorFactory.connect
去连接服务端时,最终调用到javax.management.remote.rmi.RMIServerImpl_Stub#newClient
发起连接。其实该方法符合我们在攻击RMI Registry中提到的“应用层反序列化问题”情况:newClient方法参数为Object类型,可以塞入我们的恶意Padyload(利用JMXConnector.CREDENTIALS配置添加),exp如下
1 |
|
成功在JDK8u77-JMX服务上执行成功:
该漏洞在JDK8u91时被修复,新增了javax.management.remote.rmi.RMIJRMPServerImpl.ExportedWrapper类继承自DeserializationChecker接口,该类实现了check、checkProxyClass方法检查参数类型,限制只能为[Ljava.lang.String;、java.lang.String类型。
在该版本的环境下,RMI层使用sun.rmi.server.UnicastServerRef#unmarshalParameters方法还原”自定义方法”参数时,由于jmx服务注册Target的weakImpl#referent为ExportedWrapper,所以在还原操作时会调用到ExportedWrapper#check检查序列化是否在白名单中,很显然Groovy1外部包装类AnnotationInvocationHandler不在白名单中,反序列化操作报错
javax.management.remote.rmi.RMIJRMPServerImpl.ExportedWrapper实现DeserializationChecker接口
JMX服务端调用sun.rmi.server.UnicastServerRef#unmarshalParameters还原newClient的参数:由于实现了DeserializationChecker接口,所以会走checked流程。普通RMI服务的自定义方法会走unchecked流程
服务端执行反序列化操作还原参数检查白名单:javax.management.remote.rmi.RMIJRMPServerImpl.ExportedWrapper#check
此漏洞被分配编号CVE-2016-3427,在JDK8u91时被修复
2.4 JMX层MBean方法getLoggerLevel/gcClassHistogram利用方式
当JMX客户端调用createMBean/getObjectInstance/invoke等方法时,服务端处理时会先经过sun.rmi.server.UnicastServerRef#dispatch进行分发,当不执行RMI内置的bind/lookup/dirty方法时,会进入”调用自定义方法”的逻辑
客户端调用createMBean/getAttribute等内置方法时,服务端到达javax.management.remote.rmi.RMIConnectionImpl#invoke中:
1、调用java.rmi.MarshalledObject#get还原参数值
2、调用到javax.management.remote.rmi.RMIConnectionImpl#doOperation根据客户端调用具体方法进行分发处理,包括createMBean、getAttribute、getObjectInstance、getObjectInstance等方法
java.rmi.MarshalledObject#get
javax.management.remote.rmi.RMIConnectionImpl#doOperation
在使用java.rmi.MarshalledObject#get还原调用方法的参数值时,会直接调用readObject进行反序列化操作。我们只需要找到MBean中带参数的方法,将我们的恶意数据填充即可,对应exp为ysoserial中的ysoserial.exploit.JMXInvokeMBean,该exp通过调用对象名称为”java.util.logging:type=Logging”的MBean的getLoggerLevel方法触发
1 |
|
jconsole查看java.util.logging:type=Logging为默认的MBean,另外还有很多MBean的方法也可以用:java.lang:type=Threading#getThreadCpuTime、java.lang:type=Threading#getThreadInfo、com.sun.management:type=DiagnosticCommand#gcClassHistogram等等
com.sun.management:type=DiagnosticCommand#gcClassHistogram利用
2.5 MLET动态加载Evil MBean利用方式
除了利用本身存在的MBean,我们也可以自行添加MBean进行利用,可以使用javax.management.loading.MLet
MBean 并调用其getMBeansFromURL操作指示JMX服务端从远端加载注册构建的恶意MBean,这样就可以调用我们创建的的MBean操作而不需要服务端ClassPath存在Gadget。这种从外部加载MBean的方式在官方也有说明 https://docs.oracle.com/javase/7/docs/technotes/guides/management/agent.html
分析下getMBeansFromURL方法是如何操作的,javax.management.loading.MLet#getMBeansFromURL(java.lang.String)中分为2步:
1、加载mlet文件解析标签
2、当创建MBean指定的class在本地ClassPath中找不到时,则使用MLET classloader去外部地址(第一步得到的codebase+archive属性值)进行加载
javax.management.loading.MLetParser#parse解析<mlet>
标签的内容并放入attributes
接着返回到getMBeansFromURL方法调用com.sun.jmx.mbeanserver.JmxMBeanServer#createMBean创建MBean
调用到com.sun.jmx.interceptor.DefaultMBeanServerInterceptor#createMBean创建MBean时,会检查是否有instantiate、registerMBean权限。如果未开启SecurityManager,则会跳过检查
最终在 com.sun.jmx.mbeanserver.MBeanInstantiator#loadClass中使用MLET classloader去加载org.example.Evil类(codesource就是mlet文件中codebase+archive属性值)。这里使用Class.forName(className, false, loader);
初始化的选项为false,不会执行静态代码块中的代码。所以我们需要选择invoke调用MBean的恶意方法进行利用
针对前文在2222端口开启的JMX服务,复现下MLET这种利用方法:
1、创建Evil类及EvilMBean接口(恶意操作为runCommand),并将其打包为JmxEvilBean.jar
1 |
|
2、创建MLET文件
1 |
|
将JmxEvilBean.jar、mlet文件放在web服务下:python -m SimpleHTTPServer 3333
3、EXP利用:连接服务、创建MLet MBean、invoke调用getMBeansFromURL加载外部Evil MBean、invoke调用Evil MBean的runCommand操作返回执行结果
1 |
|
exp运行后,可以在jconsole中看到创建的test.Mbean:type=MLet,id=1、MLetCompromise:name=evil,id=10 MBean
EXP也回显了ipconfig命令执行的结果
需要注意的是:当服务未重启的情况下,后续利用直接invoke调用runCommand就可以了,不需要再次创建MBean
3、密码验证
第二章节分析的是JMX服务未开启权限验证及SSL验证时的漏洞利用情况,另外分析下当开启权限验证时漏洞利用的情况有什么变化。默认启动JMX管理服务时(不指定com.sun.management.jmxremote.authenticate配置),远程客户端连接时就需要通过验证。而验证所需的密码-权限是以明文存储在服务端/jre/lib/management/目录的jmxremote.password、jmxremote.access文件中的,且需要设置这两个文件的权限为:除文件所有者具有控制权,其它用户无任何权限。否则启动时会报错:sun.management.AgentConfigurationError。如下是启动命令
1 |
|
启动截图及文件权限如下:
使用客户端jconsole 以只读账户guest password1进行连接
也可以使用JAVA代码连接:
1 |
|
文件权限设置参考:https://docs.oracle.com/javase/7/docs/technotes/guides/management/security-windows.html
分析下当加入权限验证后,JMX服务端的检测逻辑是怎样的?当我们获取了一个低权限/只读权限账户时可以做哪些事情?我们从连接服务端到执行MBean操作的整体流程来看,分为几步:
1、客户端使用javax.naming.InitialContext#lookup获取到名称为”jmxrmi”的 Stub代理对象,即下图的变量server;
2、客户端调用javax.management.remote.rmi.RMIServer#newClient去获取RMIConnectionImpl Stub代理对象,服务端javax.management.remote.rmi.RMIJRMPServerImpl.ExportedWrapper#newClient执行JAAS-based authenticator进行权限校验:根据服务的启动参数及jmxremote.password、jmxremote.access配置文件去匹配,当校验通过后返回代理对象,即下图的变量c;
3、客户端invoke调用RMIConnectionImpl Stub代理对象的方法去操作MBean/获取MBean信息。在另一边的JMX服务端会根据objid确认处理客户端此次请求逻辑的Target,[0:0:0,0]、[0:0:0,2] 这是在之前攻击RMI中分析过的RegistryImpl_Stub、DGCImpl_Stub,而涉及JMX是另外几个Target,在本实例中的objID为:[613d5ccd:18470553d20:-7fff, -4868886411976892153]、[613d5ccd:18470553d20:-7ffa, -8893277592355947578],如下图是客户端拿到两次请求的返回对象调试情况
当调用MBean的具体操作方法时,如javax.management.remote.rmi.RMIConnection#getConnectionId,在服务端会调用到javax.management.remote.rmi.RMIConnectionImpl#getConnectionId进行处理
添加鉴权前后,由于服务端在启动时添加的Target不同,在添加鉴权后 mbeanServer的值从DefaultMBeanServerInterceptor变为MBeanServerAccessController,对于每个操作具体需要的权限都在MBeanServerAccessController中进行判断(objid与上面演示的不同,因为是后面的补图)
无鉴权时:
1 |
|
有鉴权时:
1 |
|
翻了下源码统计下jmx配置文件中的权限可对应调用MBean的哪些操作。这些操作可以辅助我们对JMX服务进行进一步的测试:
read权限可执行的操作
1 |
|
Write权限可执行的操作
1 |
|
Unregister权限可执行的操作
1 |
|
write权限、非MLet#addURL/getMBeansFromURL方法可执行的操作
1 |
|
read权限可以执行查询操作,如列出全部MBean的信息
我们以低权限账户guest登录后再次测试如上几种利用方式
3.1 影响地址可控的JNDI注入 & RMI Registry利用方式 & CVE-2016-3427
地址可控的JNDI注入利用在密码鉴权流程之前,与是否鉴权无关,只与JDK版本有关
RMI方式的利用在密码鉴权流程之前,与是否鉴权无关,只与JDK版本有关
CVE-2016-3427是在RMI层-还原自定义方法的参数时触发的,与是否鉴权无关,只与JDK版本有关
3.2 影响JMX层MBean方法的利用方式
由于通过JMX层MBean方法getLoggerLevel/gcClassHistogram利用方式是在javax.management.remote.rmi.RMIConnectionImpl#invoke 还原参数时触发的漏洞,并未执行到判断权限的位置。所以使用只读权限的guest账户即可继续进行利用
3.3 影响MLET加载Evil MBean利用方式
当未授权情况下JMX服务下MLET方式利用的步骤
1、客户端调用JMXConnectorFactory.connect
连接到JMX服务端
2、调用createMBean创建javax.management.loading.MLet MBean
3、invoke调用MLet#getMBeansFromURL操作从外部获取Evil MBean
4、invoke调用Evil MBean的runCommand操作执行命令
当以低权限账户登录后,在第2步com.sun.jmx.remote.security.MBeanServerAccessController#createMBean创建bean的起点使用checkCreate(className)
检查权限,执行到com.sun.jmx.remote.security.MBeanServerFileAccessController#checkAccess代码逻辑:1、获取当前登录用户的权限;2、判断权限是否包括create权限;3、如果无create权限或未登录用户则报错:Access denied! Invalid access level for requested MBeanServer operation
而我们登录使用的guest用户只有read权限,没法执行createBean操作,所以在执行EXP的客户端报错:
另外如果登录用户有create权限还是不能invoke调用MLet#getMBeansFromURL操作的,因为在invoke前会检查write、MLetMethods权限,write权限与上面判断read权限的流程一致,而MLetMethods权限是在com.sun.jmx.remote.security.MBeanServerAccessController#checkMLetMethods中判断的,如果调用javax.management.loading.MLet的addURL/getMBeansFromURL都会报错退出
4、参考
http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/853f699a5273
https://mogwailabs.de/en/blog/2019/04/attacking-rmi-based-jmx-services/
https://www.cnblogs.com/afanti/p/12468693.html
https://pwnull.github.io/2022/jndi-injection-history/
https://pwnull.github.io/2022/Exploring-JAVA-RMI's-offensive-and-defensive-history/