前段时间有篇文章[^1]披露了开源项目 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,如果一切正常,则会得到以下结果:
利用方法
以 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 方法中,使用自定义的 Gatewayeva luationContext 类替换了原来的 Standardeva luationContext 类。查看 Gatewayeva luationContext 类的实现可知,其是对 Simpleeva luationContext 类的简单封装。
通过查询文档可知,Standardeva luationContext 和 Simpleeva luationContext 都类是执行 Spring 的 SpEL 表达式的接口,区别在于前者支持 SpEL 表达式的全部特性,后者相当于一个沙盒,限制了很多功能,如对 Java 类的引用等[^5][^6]。因此通过将 Standardeva luationContext 类替换为 Gatewayeva luationContext 类,可以限制执行注入的 SpEL 表达式。
禁用 actuator gateway
通过前面的漏洞利用过程可以看到,首先需要通过 /actuator/gateway/routes/{id} API 创建一条路由。因此将此 API 禁止,也可实现漏洞的修复。根据 Actuator 的 API 文档[^7]可知,启用 actuator gateway 需要设置以下两个配置的值:
management.endpoint.gateway.enabled=true # default valuemanagement.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 方法中使用了 Standardeva luationContext 类来执行 SpEL 表达式。
第一个区别在于,2.x 版本在刷新路由后需要额外一次请求才能触发 SpEL 表达式的执行。而 3.x 版本在刷新路由后会立即执行。
第二个区别在于对此方法的调用链。通过查找源代码可知,只有 ConfigurationService 类的内部类 ConfigurableBuilder 的 normalizeProperties 方法(重写了父类中的方法)中调用了 normalize 方法。而 ConfigurableBuilder 类继承自内部抽象类 AbstractBuilder。AbstractBuilder 类中有一公有方法 bind 调用了 normalizeProperties 方法。
继续跟进对 bind 方法的引用,可知有三处:
-
AbstractRateLimiter 类的 onApplicationEvent 方法。
RouteDefinitionRouteLocator 类的 loadGatewayFilters 方法和 lookup 方法。
然后继续回溯可以知道所有有可能触发 SpEL 表达式执行的地方。