CVE-2022-22947 Spring Cloud Gateway RCE
漏洞信息
漏洞编号:CVE-2022-22947
影响版本:3.1.1+
和 3.0.7+
之前的 Spring Cloud Gateway
版本
漏洞描述:在 3.1.1+
和 3.0.7+
之前的 Spring Cloud Gateway
版本中,当启用、暴露和不安全的 Gateway Actuator
端点时,应用程序容易受到代码注入攻击。远程攻击者可以发出恶意制作的请求,允许在远程主机上进行任意远程执行。
漏洞补丁:Commit
漏洞分析
Spring Cloud Gateway 的 SSRF
本节是对panda
师傅文章的学习与总结
官方文档中提到,通过Spring Cloud Gateway
执行器(actuator
)提供的管理功能就可以对路由进行添加、删除等操作。
官方示例如下
根据官方示例,师傅提供了添加路由的poc
1 | POST /actuator/gateway/routes/new_route |
然后在执行 refresh
操作后
1 | POST /actuator/gateway/refresh |
最后直接访问/new_route/index.php
,就可以成功执行了一个SSRF
请求,即访问到了https://www.xxx.net
payload为什么这么写?
与上文提到的官方示例进行对比,发现区别就在filter
这里。
而纵观整个payload
,实际上可以发现,其就是一个动态路由的配置过程。
在Spring Cloud Gateway
中,路由的配置分为静态配置和动态配置,对于静态配置而言,一旦要添加、修改或者删除内存中的路由配置和规则,就必须重启才可以。但在现实生产环境中,使用 Spring Cloud Gateway
都是作为所有流量的入口,为了保证系统的高可用性,需要尽量避免系统的重启,因而一般情况下,Spring Cloud Gateway
使用的都是动态路由。Spring Cloud Gateway
配置动态路由的方式有两种,第一种就是比较常见的,通过重写代码,实现一套动态路由方法,如这里就有一个动态路由的配置过程。第二种就是上文中SSRF
这种方式,但是这种方式是基于jvm
内存实现,一旦服务重启,新增的路由配置信息就是完全消失了。
所以其实payload
就是比较固定的格式,首先定义一个谓词(predicates
),用来匹配来自用户的请求,然后再增加一个内置或自定义的过滤器(filters
),用于执行额外的功能逻辑。
payload
的filter
中用的是重写路径过滤器(RewritePath
),类似的还有设置路径过滤器(SetPath
)、去掉URL前缀过滤器(StripPrefix
)等,具体可以参考gateway
内置的filter
这张图:
以及gateway
内置的Global Filter
图:
至此,我们就能搞懂payload
这么写的原因了
整个请求流程
还是如上例所演示的,当在浏览器中向127.0.0.1:8080
地址发起根路径为/new_route
的请求时,会被 Spring Cloud Gateway
转发请求到https://www.xxx.net/
的根路径下
比如,我们向127.0.0.1:8080
地址发起为/new_route/index.php
的请求,那么实际上会被 Spring Cloud Gateway
转发请求到https://www.xxx.net/index.php
的路径下,官方在其官方文档(Spring Cloud GateWay
工作流程)简单说明了流程:
关于请求流程更详细的分析可以去看panda
师傅文章
CVE-2022-22947 分析
ssrf
部分的分析是为了对这个洞更加熟悉一点,和这个洞没有太强的相关性
通过查看diff,看到使用GatewayEvaluationContext
替换StandardEvaluationContext
可以发现属于SpEL
造成的RCE
这样我们直接下载源码,然后进行回溯
1 | git clone https://github.com/spring-cloud/spring-cloud-gateway.git |
然后在源码中对StandardEvaluationContext
进行全局搜索或是根据diff
查看代码位置。在org.springframework.cloud.gateway.support.ShortcutConfigurable#getValue
方法中,可以看到spel
的使用。panda
师傅也说可以根据这个diff
得出结论:在动态添加路由的过程中,某个filter
可以对传入进来的值进行SpEL
表达式解析,从而造成了远程代码执行漏洞。
打一下断点,看一下谁用了getValue
方法
首先先创建路由,filter
中填充spel
表达式,然后refresh
执行。
1 | POST /actuator/gateway/routes/test HTTP/1.1 |
注意,由于gateway actuator endpoint
的内容类型是JSON
,所以包含了反斜杠。
另外这里args
中键名要填充replacement
属性,不然会报空指针
调用栈如下:
从org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator#convertToRoute
开始,getFilters(routeDefinition)
方法获取了我们第一步的配置信息
接着将路由的id
和filters
字段中的信息传入loadGatewayFilters
方法
然后进行bind
bind
方法中调用了normalizeProperties
方法
在normalizeProperties()
方法里,会将filter
的配置属性传入normalize
中
继而走到getValue
方法中
最后进行SpEL
处理
下面这个poc
也可以
1 | POST /actuator/gateway/routes/new_route HTTP/1.1 |
1 | POST /actuator/gateway/refresh HTTP/1.1 |
最后直接访问/new_route
即可
另外,c0ny1
师傅也提供了一个高可用的payload
- 解决BCEL/js引擎兼容性问题
- 解决base64在不同版本jdk的兼容问题
- 可多次运行同类名字节码
- 解决可能导致的ClassNotFound问题
1 | #{T(org.springframework.cglib.core.ReflectUtils).defineClass('Memshell',T(org.springframework.util.Base64Utils).decodeFromString('yv66vgAAA....'),new javax.management.loading.MLet(new java.net.URL[0],T(java.lang.Thread).currentThread().getContextClassLoader())).doInject()} |
回显
这里复现一下Y4er
师傅的分析
根据上面的分析,可知通过getValue()
函数可以将args的value
执行spel
表达式,并且保存为properties
,那么properties
在哪里可以返回给我们的http response
呢?
在org.springframework.cloud.gateway.filter.factory.AddResponseHeaderGatewayFilterFactory#apply
中,将config
的键值对添加到header
中Y4er
师傅的poc
我本地测试失败,这里用的是网上的poc
如下:
1 | POST /actuator/gateway/routes/hacktest |
这里注意几个问题
首先一定要传uri
和order
,否则爆空指针异常。因为在org.springframework.cloud.gateway.route.Route#async(org.springframework.cloud.gateway.route.RouteDefinition)
函数中对routeDefinition
参数进行了处理,所以必须要有uri
和order
。
第二个问题是value
必须是一个String
类型,否则在bind
的时候会报类型不匹配异常。因为AddResponseHeaderGatewayFilterFactory
采用的配置是NameValueConfig
实例,而value
是string
类型。
refresh
之后访问即可回显
cmd
为whoami
时的回显
打完之后可以删除路由,refresh
后生效
1 | DELETE /actuator/gateway/routes/hacktest |
panda
师傅提醒,在实际环境中,如果由于某种原因删除不起作用,有可能会导致刷新请求失败,那么就会有可能会导致站点出现问题,所以在实际测试的过程中,建议别乱搞,不然就要重启站点了。
这里有一个陈师傅提供的一个实例https://github.com/API-Security/APISandbox/tree/main/OASystem
漏洞修复
在 Commit 中进行了修复
由于是SpEL
表达式注入漏洞,而引起这个漏洞的原因一般是使用了 StandardEvaluationContext
方法去解析表达式,解析表达式的方法有两个:
SimpleEvaluationContext
- 针对不需要SpEL
语言语法的全部范围并且应该受到有意限制的表达式类别,公开SpEL
语言特性和配置选项的子集。StandardEvaluationContext
- 公开全套SpEL
语言功能和配置选项。您可以使用它来指定默认的根对象并配置每个可用的评估相关策略。
SimpleEvaluationContext
旨在仅支持SpEL
语言语法的一个子集,不包括 Java
类型引用、构造函数和bean
引用。而StandardEvaluationContext
支持全部SpEL
语法。所以根据功能描述,将StandardEvaluationContext
方法用 SimpleEvaluationContext
方法替换即可。
官方的修复方法是利用 BeanFactoryResolver
的方式去引用Bean
,然后将其传入官方自己写的一个解析的方法GatewayEvaluationContext
中
官方还建议如果不需要Gateway actuator
的endpoint
功能,就关了它吧,如果需要,那么就利用 Spring Security
对其进行保护,具体的保护方式可以参考:https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html#actuator.endpoints.security
CodeQL
漏洞发现者在他的文章中介绍,在发现漏洞后,写了一个CodeQL
,但是使用默认的CodeQL
查询(CWE-094
)追踪不到,作者介绍是因为java/ql/lib/semmle/code/java/security/SpelInjectionQuery.qll
没有将Mono
库作为source
。
然后作者在SpelInjectionQuery.qll
的基础上将SpringManagedResource也作为source
,增加额外的来源,这主要是为了检查注解的@RequestBody
和@RequestParam
方法
但是,我这么做直接报错Could not resolve type SpringManagedResource
,没看懂这个类,太菜了。找个时间好好看看CodeQL
语法
作者介绍如果限制属性访问器(restrictive-property-accessor
)被禁用时,DOS
仍有效(虽然但是还没复现和研究)
内存马
https://gv7.me/articles/2022/the-spring-cloud-gateway-inject-memshell-through-spel-expressions/
https://xz.aliyun.com/t/11331
Reference
https://www.cnpanda.net/sec/1159.html
https://y4er.com/post/cve-2022-22947-springcloud-gateway-spel-rce-echo-response/
https://wya.pl/2022/02/26/cve-2022-22947-spel-casting-and-evil-beans/