Tomcat 架构与Context分析

#Tomcat 架构原理

Tomcat需要实现的2个核心功能:

  • 处理socket连接,负责网络字节流与Request和Response对象的转化
  • 加载并管理Servlet,以及处理具体的Request请求

为此Tomcat设计了两个核心组件: 连接器(Connector)和容器(Container),连接器负责对外交流,容器负责内部处理;同时Tomcat为了实现支持多种IO模型和应用层协议,多个连接器对接一个容器。

  • Server 对应一个Tomcat实例;
  • Service 默认只有一个,一个Tomcat实例默认一个Service;
  • Connector 一个Service可能多个连接器,接收不同的连接协议;
  • Container 多个连接器对应一个容器,顶层容器其实就是Engine;

#Connecter(连接器)

连接器对Servlet容器屏蔽了网络协议和IO模型的区别,无论是HTTP还是AJP协议,在容器中获取到的都是一个标准的ServletRequest对象。

其中Tomcat设计了三个组件,其负责功能如下:

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

Adapter组件

由于协议的不同,Tomcat定义了自己的Request类来存放请求信息,但是这个不是标准的ServletRequest。于是需要使用Adapter将Tomcat Request对象转成ServletRequest对象,然后就能调用容器的service方法。

简而言之,Endpoint接收到Socket连接后,生成一个SocketProcessor任务提交到线程池进行处理,SocketProcessor的run方法将调用Processor组件进行应用层协议的解析,Processor解析后生成Tomcat Request对象,然后会调用Adapter的Service方法,方法内部通过如下代码将Request请求传递到容器中。

connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);

#Container(容器)

Connector连接器负责外部交流,Container容器负责内部处理。也就是说连接器处理Socket通信和应用层协议的解析,得到ServletRequest,而容器则负责处理ServletRequest 。

Tomcat设计了4种容器: Engine、Host、Context、Wrapper ,这四种容器是父子关系。

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

每一个Context都有唯一的path。这里的path不是指servlet绑定的WebServlet地址,而是指独立的一个Web应用地址。就好比Tomat默认的/地址和/manager地址就是两个不同的web应用,所以对应两个不同的Context。要添加Context需要在server.xml中配置docbase。

如下图所示, 在一个web应用中创建了2个servlet服务,WebServlet地址分别是/Demo1/Demo2。 因为它们属于同一个Web应用所以Context一样,但访问地址不一样所以Wrapper不一样。/manager访问的Web应用是Tomcat默认的管理页面,是另外一个独立的web应用, 所以Context与前两个不一样。

#从HTTP请求到Servlet

Tomcat使用Mapper组件来将用户请求的URL定位到某个Servlet。Mapper组件里保存了WEB应用的配置信息,也就是容器组件与访问路径的映射关系。比如Host容器里配置的域名、Context容器里的WEB应用路径以及Wrapper容器里Servlet映射的路径。

Mapper组件通过解析请求URL里的域名和路径,再到自己保存的Map里去找,就能定位到一个Servlet。 最终一个请求URL只会定位到一个Wrapper容器,也就是一个Servlet 。

Pipeline-Valve

最先拿到请求的是Engine容器,Engine容器对请求做一些处理后,把请求传给子容器Host继续处理,以此类推,最终这个请求会传给Wrapper容器,Wrapper容器会调用Servlet来处理。 整个调用过程是通过Pipeline-Valve管道进行的。

每层容器都有一个Pipeline对象,只要触发了这个Pipeline的第一个Valve,这个容器里的Pipeline中的Valve都会被调用到。Pipeline中的getBasic方法获取的Valve处于Valve链的末端,负责调用下层容器的Pipeline里的第一个Valve 。

Pipeline中的addValve方法维护Valve链表,Valve可以通过该方法插入到Pipeline中,来对请求做某些处理。Valve通过自己的invoke方法处理请求后,调用getNext().invoke()来触发下一个Valve调用。

Wrapper容器的最后一个Valve会创建一个Filter链,并调用doFileter方法。

#Tomcat 类加载机制

由于Tomcat中有多个WebApp同时要确保之间相互隔离,所以Tomcat的类加载机制也不是传统的双亲委派机制

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

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

#Tomcat Context

#三种Context的联系与区别

Tomcat中有如下三种Context: ServletContext、StandardContext、ApplicationContext。下面这张图很好的展现了这三个Context的结构。

  • ServerletContext(是一个接口)

    通过request.getServletContext() 获取到的是ApplicationContextFacade对象,它是对ServerletContext接口的实现类,该类提供了Web应用所有Servlet的视图,可以对某个Web应用的各种资源和功能进行访问。

    WEB容器在启动时,它会为每个Web应用程序都创建一个对应的ServletContext。它代表当前Web应用,并且它被所有客户端共享

  • ApplicationContext

    ApplicationContext也是对ServerletContext接口的实现类,由上图可知该类被包装在ApplicationContextFacade类中。

  • StandardContext

    org.apache.catalina.Context接口的默认实现为StandardContext,而Context在Tomcat中代表一个web应用。ApplicationContext所实现的方法其实都是调用的StandardContext中的方法,StandardContext是Tomcat中真正起作用的Context。

#获取StandardContext

向各种中间件和框架注入内存马的基础,就是要获得context,context实际上就是拥有当前中间件或框架处理请求、保存和控制servlet对象、保存和控制filter对象等功能的对象。

  1. 在已有request对象的情况,可以直接获取。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    ServletContext servletContext = request.getSession().getServletContext();
    Field appctx = servletContext.getClass().getDeclaredField("context");
    appctx.setAccessible(true);
    //ApplicationContext为ServletContext的实现类
    ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
    
    Field stdctx = applicationContext.getClass().getDeclaredField("context");
    stdctx.setAccessible(true);
    StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
    
  2. 适用于Tomcat 6、7、8、9的StandardContext获取方法

    每个Tomcat版本下都会开一个Http-xio-端口-Acceptor的线程,Acceptor是用来接收请求的,这些请求会交给后面的Engine->Host->Context->servlet。

    通过遍历当前所有线程,从Acceptor线程中获取到请求的requset对象,这里的requset是org.apache.coyote.Reuqest类,它没有servletContext属性,所以不能直接通过它获取Context。但是可以通过它获取到请求路径和请求域名。同样在该线程中可以获得StandardContext。下图把StandardContext路径展示的非常明白。可以根据前面获取的请求路径和请求域名,来找到对应的StandardContext。

    实现代码

      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
     79
     80
     81
     82
     83
     84
     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    
    package com.example.demo;
    
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.io.PrintWriter;
    import java.lang.reflect.Field;
    import java.util.HashMap;
    import java.util.Iterator;
    
    import org.apache.catalina.core.StandardContext;
    import org.apache.catalina.core.StandardEngine;
    import org.apache.catalina.core.StandardHost;
    
    public class goodServlet extends HttpServlet {
        public void init() {
        }
    
        public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
    
            try {
                PrintWriter out = response.getWriter();
                Tomcat6789 a = new Tomcat6789();
                out.println(a.getSTC());
    
            } catch (Exception e) {
                e.printStackTrace();
            }
    
        }
    
        public void destroy() {
        }
    
        class Tomcat6789 {
            String uri;
            String serverName;
            StandardContext standardContext;
    
            public Object getField(Object object, String fieldName) {
                Field declaredField;
                Class clazz = object.getClass();
                while (clazz != Object.class) {
                    try {
    
                        declaredField = clazz.getDeclaredField(fieldName);
                        declaredField.setAccessible(true);
                        return declaredField.get(object);
                    } catch (NoSuchFieldException e) {
                    } catch (IllegalAccessException e) {
                    }
                    clazz = clazz.getSuperclass();
                }
                return null;
            }
    
            public Object GetAcceptorThread() {
                //获取当前所有线程
                Thread[] threads = (Thread[]) this.getField(Thread.currentThread().getThreadGroup(), "threads");
                //从线程组中找到Acceptor所在的线程 在tomcat6中的格式为:Http-端口-Acceptor
                for (Thread thread : threads) {
                    if (thread == null || thread.getName().contains("exec")) {
                        continue;
                    }
                    if ((thread.getName().contains("Acceptor")) && (thread.getName().contains("http"))) {
                        Object target = this.getField(thread, "target");
                        if (!(target instanceof Runnable)) {
                            try {
                                Object target2 = this.getField(thread, "this$0");
                                target = thread;
                            } catch (Exception e) {
                                continue;
                            }
                        }
                        Object jioEndPoint = getField(target, "this$0");
                        if (jioEndPoint == null) {
                            try {
                                jioEndPoint = getField(target, "endpoint");
                            } catch (Exception e) {
                                continue;
                            }
                        }
                        return jioEndPoint;
                    }
                }
                return null;
            }
    
            public Tomcat6789() {
                Object jioEndPoint = this.GetAcceptorThread();
                if (jioEndPoint == null) {
                    return;
                }
                Object object = getField(getField(jioEndPoint, "handler"), "global");
                //从找到的Acceptor线程中获取请求域名、请求的路径
                java.util.ArrayList processors = (java.util.ArrayList) getField(object, "processors");
                Iterator iterator = processors.iterator();
                while (iterator.hasNext()) {
                    Object next = iterator.next();
                    Object req = getField(next, "req");
                    Object serverPort = getField(req, "serverPort");
                    if (serverPort.equals(-1)) {
                        continue;
                    }
                    org.apache.tomcat.util.buf.MessageBytes serverNameMB = (org.apache.tomcat.util.buf.MessageBytes) getField(req, "serverNameMB");
                    this.serverName = (String) getField(serverNameMB, "strValue");
                    if (this.serverName == null) {
                        this.serverName = serverNameMB.toString();
                    }
                    org.apache.tomcat.util.buf.MessageBytes uriMB = (org.apache.tomcat.util.buf.MessageBytes) getField(req, "uriMB");
                    this.uri = (String) getField(uriMB, "strValue");
                    if (this.uri == null) {
                        this.uri = uriMB.toString();
                    }
                    //根据获取到的服务器名、路径信息获取StandardContext
                    this.getStandardContext();
                    return;
                }
            }
    
            public void getStandardContext() {
                Object jioEndPoint = this.GetAcceptorThread();
                if (jioEndPoint == null) {
                    return;
                }
                Object service = getField(getField(getField(getField(getField(jioEndPoint, "handler"), "proto"), "adapter"), "connector"), "service");
                StandardEngine engine = (StandardEngine) getField(service, "container");
                if (engine == null) {
                    engine = (StandardEngine) getField(service, "engine");
                }
    
                HashMap children = (HashMap) getField(engine, "children");
                StandardHost standardHost = (StandardHost) children.get(this.serverName);
                if(standardHost==null){
                    standardHost = (StandardHost) children.get("localhost");
                }
                children = (HashMap) getField(standardHost, "children");
                Iterator iterator = children.keySet().iterator();
                while (iterator.hasNext()) {
                    String contextKey = (String) iterator.next();
                    if (contextKey.isEmpty()||!(this.uri.startsWith(contextKey))) {
                        continue;
                    }
                    StandardContext standardContext = (StandardContext) children.get(contextKey);
                    this.standardContext = standardContext;
                    return;
                }
            }
            public StandardContext getSTC() {
                return this.standardContext;
            }
        }
    }
    

    另外,在Tomcat下还有一个Poller线程,它和Acceptor一起负责接收请求,在Poller线程中同样可用找到StandardContext,流程和Acceptor是一样的。(在ContainerBackgroundProcessor线程下也可以找到StandardContext)

  3. 从ContextClassLoader获取(只可用于Tomcat 8 9)

    由于Tomcat处理请求的线程中,存在ContextLoader对象,而这个对象又保存了StandardContext对象,所以很方便就获取了。

    1
    2
    
    org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =(org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
    StandardContext standardContext = (StandardContext)webappClassLoaderBase.getResources().getContext();
    
  4. 从ThreadLocal获取request

    这种方法可以兼容tomcat 789,但在Tomcat 6下无法使用。

    基于tomcat的内存 Webshell 无文件攻击技术

  5. 从MBean中获取(兼容Tomcat789)

    Tomcat 使用 JMX MBean 来实现自身的性能管理。而我们可以从jmxMBeanServer对象,在其field中一步一步找到StandardContext对象。但有个很大的局限性在于,必须猜中项目名和host,才能获取到对应的standardContext对象。

    中间件内存马注入&冰蝎连接(附更改部分代码)

#参考

  1. JSP Webshell那些事 -- 攻击篇(下)
  2. Tomcat架构原理
  3. Java内存马:一种Tomcat全版本获取StandardContext的新方法
加载评论