[备份]浅谈Spring框架CVE-2022-22950

前言:水文,没有什么技术,错误之处欢迎大佬们指出

今天Spring核心框架爆出拒绝服务漏洞,由于SpringJava中的地位超然,该漏洞会影响到几乎所有的Spring系列组件,例如常见的SpringBootSpringCloud等。但也不用担心,因为利用门槛很高,需要可以执行SPEL

值得注意的是:安全的SimpleEvaluationContext同样存在拒绝服务漏洞

于是写了一篇水文

 

0x00 漏洞介绍

前段时间在研究三梦师傅的文章:一种普遍存在于java系统的缺陷 - Memory DoS

写了一篇 跟着三梦学Java安全 文章,内容是如何半自动检测三梦师傅提到的几种漏洞

 

经过一段时间的学习与实践,发现了不少的Memory DoS漏洞(称之为缺陷更合适一些)

虽然很多组件或平台给拒绝服务(DENY OF SERVICE)漏洞至少中危,甚至高危,但该漏洞大部分情况下是鸡肋洞,没有太多的实际利用价值,称之为垃圾洞也不为过。不过如果TomcatSpring这种大范围使用的框架存在低门槛的拒绝服务漏洞,也会有比较严重的后果。这次Spring核心框架的拒绝服务漏洞有较高的门槛,并不能通杀所有的Spring应用

一位大佬曾经说过:赚钱的业务,不怕信息泄露,也不怕你RCE,只怕你把它打挂了

 

0x01 如何发现

三月初爆出了Spring Cloud GatewayRCE漏洞,简单分析发现依旧是Spring Expression模块的问题

参考去年底Apache Log4j2出现著名的RCE漏洞后,爆出两个拒绝服务漏洞,所以我想从SPEL本身来分析是否存在DOS

官方的修复代码: Spring Cloud Gateway 修复

当使用StandardEvaluationContextSPEL允许执行恶意代码例如T(java.lang.Runtime).getRuntime()

context的方法都是基于delegate对象的,注意到这是SimpleEvaluationContext

使用SimpleEvaluationContextSpEL无法调用Java类对象或引用bean

在历史上,一些曾经出现过的SpEL漏洞大部分采用了该context做修复,修复后确实无法RCE

但使用了SimpleEvaluationContext后是否让SpEL达到了绝对的安全

于是我翻阅SpEL官方文档,查询是否有其他漏洞的可能性,首先发现了正则匹配,下面是文档介绍

尝试修改为ReDoSPayload测试

发现并不会产生拒绝服务,报错Pattern access threshold exceeded

分析Spring代码后发现已对此问题做了处理

继续阅读文档,发现支持数组创建

其实看过三梦师傅文章的话,稍微分析代码后,即可发现漏洞

以上代码将导致OOM(堆内存耗尽)并耗尽CPU以实现拒绝服务

无论使用危险的StandardEvaluationContext或者安全的SimpleEvaluationContext再或者使用修复后的GatewayEvaluationContext这三种情况,只要SPeL可控那么就存在拒绝服务漏洞

注意:

漏洞基本原理简单,但我为什么我要用new int[1024*1024*1024][2]这样的Payload

而不是new int[0x7fffffff](0x7fffffff是int型最大值)

我会在0x03 深入原理中分析,涉及到JVM的一些C++代码

 

0x02 直接利用

遗憾,这个Spring的拒绝服务漏洞有一定的门槛,需要可控SPEL能够执行

该漏洞的发现源于Spring Cloud Gateway所以就先拿这个测试

利用某师傅 Github环境 并修改Spring Cloud Gateway到最新版来测试(3.1.1已修复RCE

 

使用Golang编写针对于该环境的Exp

 

效果:无法提供服务

在JVM监控中看到堆内存被拉满且CPU使用率直线上升

并且我的笔记本也被打满了

 

0x03 深入原理

当使用new int[0x7fffffff]时可以发现CPU和堆内存并没有明显的变化(相对于上图而言)

 

发现同样是OOM但实际上会有两种OOM所以下文主要分析这两种OOM的原理

 

分析Spring的源码可以跟到JDK的代码

java.lang.reflect.ArraynewInstance方法如果参数可控会造成OOM

 

发现底层是native方法

 

native方法对应的C++代码(来自openjdk-8HotSpot部分代码)

  1. 判断类型、维度、每个维度的长度是否合法
  2. 如果是基本数据类型数组(例如int)则直接构造对应的数组
  3. 如果不是基本类型,不关心
  4. 最后根据目标类型和维度,初始化数组,并分配内存

 

basic_type_mirror_to_arrayklass函数代码:根据基本类型获得TypeArrayKlass对象

 

klass.hpp中看到array_klass的虚函数,因此查找子类的array_klass_impl函数

 

不难看出上文是typeArrayKlass对象,这里面的实现比较复杂,省略了其中的代码

大致的逻辑是将TypeArrayKlass对象转为ObjArrayKlass对象(期间并未分配内存)

 

此时的klass对象是ObjArrayKlass对象,所以multi_allocate找到以下代码,写的比较巧妙

 

可以看到分配内存的函数为allocate

 

在分配堆内存之前,如果某维度数组长度大于某个值(有尝试跟过max_length函数发现比较麻烦)则会出现Requested array size exceeds VM limit报错,但实际上还没有分配内存所以不影响CPU和堆内存,谈不上真正的拒绝服务

 

跟踪array_allocate最终到common_mem_allocate_noinit函数,先分配了内存,然后才会产生Java heap spaceOOM

到这里疑问就解答了:为什么我不写成new int[0x7fffffff]

因为拒绝服务需要的是Java heap spaceOOM而不是Requested array size exceeds VM limit

 

new int[0x7fffffff]allocate函数中会走抛出异常的分支而不是分配内存,所以虽OOM但没有影响到内存

new int[1024*1024*1024][2]的写法,每一个维度都可以通过allocate方法的长度检测,成功分配内存

这个最大长度arrayOopDesc::max_array_length无法直观地看出,至少在0x7fffffff附近会超过,在1G左右没有问题

而一维[1024*1024*1024]会导致以下for循环的length为10亿,也就是说执行了10亿次某代码,以耗尽CPU

 

结论:

 

0x04 拓展原理

在文章 Java反序列化机制拒绝服务的利用与防御 中提到一种类似的拒绝服务

在JDK反序列化的流程中,同样使用到了Array.newInstance用来创建数组,而第二个int参数可控导致反序列化时候OOM达到拒绝服务的效果。上面这篇文章以及 反序列化炸弹 文章中提到一种巧妙的序列化数据构造方式,简短的序列化数据在反序列化的过程中会产生大量的数据。该攻击基于java.util.Set类实现

例如Spring-AMQPRCE修复方式是限制反序列化目标类必须为java.utiljava.lang开头,也许有操作空间。反序列化炸弹来源于《Effective Java》的第12章:Github链接

我向多个组件报告了类似的反序列化拒绝服务,并得到认可和致谢,坐等CVE中

当我查看Spring-AMQP官方通告 CVE-2021-22097 发现r00t4dm大佬已经想到了类似的方式,不过并不是利用java.util.Set炸弹,而是用java.util.Dictionary这个类,也许有更大的通用性

 

0x05 拓展利用

(1)Spring Cloud Function RCE

就近原则,第一想到的是上周五爆出的SPEL导致的RCE

借用网上师傅的图片,可以看出是从请求头中获取spring.cloud.function.routing-expression值并执行

不难推出POC

修复方案是SimpleEvaluationContext并不能防止拒绝服务

 

(2)CVE-2018-1273

Spring Data Commons中支持字段加[]的方式获取属性值,但需要fuzz确定controller中的方法名

官方的修复如下,该拒绝服务漏洞有效,不清楚最新版本如何,曾经的修复版本可用

 

(3)CVE-2018-1270 & CVE-2018-1275

Spring Websocket的一个RCE,自定义以下的前端代码,指定selector字段可以RCE

官方的修复同样是使用了SimpleEvaluationContext

 

(4)其他利用

曾经爆出来由SpEL导致的RCE漏洞并不止两三个,凡是采用了SimpleEvaluationContext修复的都存在拒绝服务漏洞

也有部分漏洞的修复方案对表达式内容做了限制:例如不允许数字和特殊符号(也许可以绕过?值得思考)

 

并不只是Spring系列框架使用了SpEL表达式,例如Apache Camel也使用到了,也许存在这样的问题

 

(5)注入

发现在SpEL中存在类似SQL注入的手段

情景:某个功能允许用户输入一个字符串,然后进行正则判断输入是否合法(常见的功能)

如果该功能使用了SpEL来做那么可以注入

使用'- [payload] - '即可注入拒绝服务的Payload

实际上语句是这样:'' - [payload] - '' matches '\\d'

其中的-是操作符号,将''[payload]进行运算,虽然最终会报错,但实际上会执行Payload内容

 

0x06 修复方式

官方的修复参考:https://github.com/spring-projects/spring-framework/commit/83ac65915871067c39a4fb255e0d484c785c0c11

修复方案很简单:限制长度,不过要区分一维数组和多维数组情况

用户和其他依赖框架的修复方案:

 

0x07 思考总结

通过这个漏洞,可以学到这样的思想