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/
