tomat/CVE-2020-1938深度分析

本文涉及的Tomcat环境为Tomcat/8.5.19

Tomcat初始化过程

本章节将详细介绍Tomcat处理AJP请求的流程,以及用于处理AJP请求的关键部分Connector的初始化过程,阅读本章节有助于理解Tomcat处理AJP请求的过程,如只需了解漏洞的利用原理也可直接跳到第二章节。

Connector的初始化流程

Connector作为Server(Apache)和Container(Servlet)之间的消息传递者,通过指定的协议和接口来监听Server的请求,在对请求进行必要的处理和解析后将请求的内容传递给对应的Containe,经Containe一层层的处理后,生成最终的响应信息,返回给Server。

Connector使用ProtocolHandler来处理请求,并根据携带请求的协议类型对应到各自的ProtocolHandler,如对应AJP协议的:

  • AjpNioProtocol
  • AjpAprProtocol

ProtocolHandler有三个非常重要的组件:Endpoint,Processor和Adapter。

  • Endpoint: 用于处理底层Socket的网络协议。 Endpoint的抽象实现类AbstractEndpoint中定义了Acceptor,AsyncTimeout两个内部类和一个接口Handler
    • Acceptor: 用于监听请求。
    • AsyncTimeout: 用于异步检查request超时。
    • Handler: 用于处理接收到的Socket,在内部调用了Processor
  • Processor: 用于将Endpoint接收到的Socket封装成Request
  • Adapter: 用于将封装好的Request交给Containner进行具体处理。

Connector处理请求过程如下图:
(图片引用自参考1)
image

Tomcat启动时会初始化server.xml中定义的所有Connector,默认存在两个Connector,分别是HTTP和AJP,server.xml中的相关配置如下:

1
2
3
4
<Service name="Catalina">
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
</Service>
  • Http Connector, 基于 HTTP 协议,负责建立 HTTP 连接。它又分为 BIO Http Connector 与 NIO Http Connector 两种,后者提供非阻塞 IO 与长连接 Comet 支持。
  • AJP Connector, 基于 AJP 协议,AJP 是专门设计用来为 tomcat 与 http 服务器之间通信专门定制的协议,能提供较高的通信速度和效率。如与 Apache 服务器集成时,采用这个协议。

Connector的初始化是在org.apache.catalina.startup.Catalina的load方法中,执行createStartDigester()解析server.xml配置文件的配置信息所创建的。

1
2
//org.apache.catalina.startup.Catalina::createStartDigester
digester.addRule("Server/Service/Connector", new ConnectorCreateRule());
1
2
//org.apache.catalina.startup.ConnectorCreateRule::begin
Connector con = new Connector(attributes.getValue("protocol"));

可见Connector的创建流程最终执行到了org.apache.catalina.Connector的构造函数中,下面贴出Connector中的几个关键方法:

  1. 在Connector的构造函数中,将server.xml中Connector的protocol “HTTP/1.1”,”AJP/1.3” 转换为ProtocolHandler:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    //org.apache.catalina.Connector
    public Connector(String protocol) {
    // 将server.xml中配置的protocol “HTTP/1.1”,“AJP/1.3” 转换为上文中的6种protocol;
    setProtocol(protocol);
    Class<?> clazz = Class.forName(protocolHandlerClassName);

    //调用具体protocol的无参构造函数,实例化。
    this.protocolHandler = (ProtocolHandler) clazz.newInstance();
    }
  2. 在setPortocal中指定ProtocolHandler类型,如protocol为AJP且没有定义aprConnector,将使用org.apache.coyote.ajp.AjpNioProtocol
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public void setProtocol(String protocol) {
    boolean aprConnector = AprLifecycleListener.isAprAvailable() &&
    AprLifecycleListener.getUseAprConnector();

    if ("HTTP/1.1".equals(protocol) || protocol == null) {
    if (aprConnector) {
    setProtocolHandlerClassName("org.apache.coyote.http11.Http11AprProtocol");
    } else {
    setProtocolHandlerClassName("org.apache.coyote.http11.Http11NioProtocol");
    }
    //创建AJP的ProtocolHandler
    } else if ("AJP/1.3".equals(protocol)) {
    if (aprConnector) {
    setProtocolHandlerClassName("org.apache.coyote.ajp.AjpAprProtocol");
    } else {
    setProtocolHandlerClassName("org.apache.coyote.ajp.AjpNioProtocol");
    }
    } else {
    setProtocolHandlerClassName(protocol);
    }
    }
    org.apache.coyote.ajp.AjpNioProtocol中,设置了NioEndpoint类型的endpoint,以及AjpProtocol的Handler,关键代码如下:
    1
    2
    3
    4
    // org.apache.coyote.ajp.AjpNioProtocol
    public AjpNioProtocol() {
    super(new NioEndpoint());
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // org.apache.coyote.ajp.AbstractAjpProtocol
    public AbstractAjpProtocol(AbstractEndpoint<S> endpoint) {
    super(endpoint);
    setConnectionTimeout(Constants.DEFAULT_CONNECTION_TIMEOUT);
    // AJP does not use Send File
    getEndpoint().setUseSendfile(false);
    ConnectionHandler<S> cHandler = new ConnectionHandler<>(this);
    setHandler(cHandler);
    getEndpoint().setHandler(cHandler);
    }
    1
    2
    3
    4
    5
    6
    // org.apache.coyote.AbstractProtocol
    public AbstractProtocol(AbstractEndpoint<S> endpoint) {
    this.endpoint = endpoint;
    setSoLinger(Constants.DEFAULT_CONNECTION_LINGER);
    setTcpNoDelay(Constants.DEFAULT_TCP_NO_DELAY);
    }
  3. 初始化Connector和protocolHandler
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    //org.apache.catalina.Connector
    protected void initInternal() throws LifecycleException {

    super.initInternal();

    // 初始化adapter并将adapter设置到protocolHandler中
    adapter = new CoyoteAdapter(this);
    protocolHandler.setAdapter(adapter);
    ......
    try {
    // 初始化protocolHandler
    protocolHandler.init();
    } catch (Exception e) {
    throw new LifecycleException(
    sm.getString("coyoteConnector.protocolHandlerInitializationFailed"), e);
    }
    }

DEBUG CVE-2020-1938

到这里为止Connector已经初始化完成,启动过程不再详述。在开始调用栈分析前先总结一下上文要点:

  1. Tomcat中的Connector用于Server和Container之间的消息传递;
  2. Connector会对不同的协议分配ProtocolHandler,用于该协议生命周期的管理。

先看调用栈

  • 任意文件读取调用栈:
    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
    serveResource:780, DefaultServlet (org.apache.catalina.servlets)
    doGet:461, DefaultServlet (org.apache.catalina.servlets)
    service:635, HttpServlet (javax.servlet.http)
    service:441, DefaultServlet (org.apache.catalina.servlets)
    service:742, HttpServlet (javax.servlet.http)
    internalDoFilter:231, ApplicationFilterChain (org.apache.catalina.core)
    doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
    doFilter:52, WsFilter (org.apache.tomcat.websocket.server)
    internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
    doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
    invoke:199, StandardWrapperValve (org.apache.catalina.core)
    invoke:96, StandardContextValve (org.apache.catalina.core)
    invoke:140, StandardHostValve (org.apache.catalina.core)
    invoke:80, ErrorReportValve (org.apache.catalina.valves)
    invoke:639, AbstractAccessLogValve (org.apache.catalina.valves)
    invoke:87, StandardEngineValve (org.apache.catalina.core)
    service:342, CoyoteAdapter (org.apache.catalina.connector)
    service:486, AjpProcessor (org.apache.coyote.ajp)
    process:66, AbstractProcessorLight (org.apache.coyote)
    process:868, AbstractProtocol$ConnectionHandler (org.apache.coyote)
    doRun:1455, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
    run:49, SocketProcessorBase (org.apache.tomcat.util.net)
    runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
    run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
    run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
    run:748, Thread (java.lang)
    ```

    - 远程文件包含调用栈:
    compile:593, JspCompilationContext (org.apache.jasper)
    service:368, JspServletWrapper (org.apache.jasper.servlet)
    serviceJspFile:385, JspServlet (org.apache.jasper.servlet)
    service:329, JspServlet (org.apache.jasper.servlet)
    service:742, HttpServlet (javax.servlet.http)
    internalDoFilter:231, ApplicationFilterChain (org.apache.catalina.core)
    doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
    doFilter:52, WsFilter (org.apache.tomcat.websocket.server)
    internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
    doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
    invoke:199, StandardWrapperValve (org.apache.catalina.core)
    invoke:96, StandardContextValve (org.apache.catalina.core)
    invoke:140, StandardHostValve (org.apache.catalina.core)
    invoke:80, ErrorReportValve (org.apache.catalina.valves)
    invoke:639, AbstractAccessLogValve (org.apache.catalina.valves)
    invoke:87, StandardEngineValve (org.apache.catalina.core)
    service:342, CoyoteAdapter (org.apache.catalina.connector)
    service:486, AjpProcessor (org.apache.coyote.ajp)
    process:66, AbstractProcessorLight (org.apache.coyote)
    process:868, AbstractProtocol$ConnectionHandler (org.apache.coyote)
    doRun:1455, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
    run:49, SocketProcessorBase (org.apache.tomcat.util.net)
    runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
    run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
    run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
    run:748, Thread (java.lang)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    可以看到两者的调用栈在`service:742, HttpServlet (javax.servlet.http)`这里以及之前都是相同的,Tomcat对于jsp文件会单独使用JspServlet进行处理并编译。

    下文将对两个调用栈进行完整的分析。

    首先Tomcat从`doRun:1455, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)`这里开始监听Server的request并进行后续处理。下文继续详述Tomcat处理request的过程以及对AJP协议数据的处理。
    ### Tomcat对AJP协议请求的处理过程以及漏洞成因

    从上文可以了解到EndPoint用于处理Socket,其中的Acceptor用于接收来自Server的请求。

    Acceptor不断监听Server端的请求,并将请求交给Processor(用于将Endpoint接收到的Socket封装成Request)
    //org.apache.tomcat.util.net.NioEndpoint.Acceptor
    protected class Acceptor extends AbstractEndpoint.Acceptor {
    @Override
    public void run() {
        int errorDelay = 0;
        while (running) {
            ......
            try {
                SocketChannel socket = null;
                try {
                    // 接受来自Server的下一个请求,封装成socket
                    socket = serverSock.accept();
                } catch (IOException ioe) {......}
                }
                ......
                if (running && !paused) {
                    // 通过setSocketOptions()将socket交给Processor
                    if (!setSocketOptions(socket)) {
                        closeSocket(socket);
                    }
                } else {
                    closeSocket(socket);
                }
            } catch (Throwable t) {......}
        }
        ......
    }
    ......
    }
    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
    setSocketOptions将socket注册到Poller线程进行轮询并交给Processor:  

    ```java
    //org.apache.tomcat.util.net.NioEndpoint.Acceptor
    protected boolean setSocketOptions(SocketChannel socket) {
    try {
    //disable blocking
    socket.configureBlocking(false);
    Socket sock = socket.socket();
    // setProperties具体实现是在org.apache.tomcat.util.net.socketProperties,该方法设置了socket的一些属性如BufferSize、KeepAlive、Timeout等
    socketProperties.setProperties(sock);

    // 根据socket中的BufSize创建channel
    NioChannel channel = nioChannels.pop();
    if (channel == null) {
    SocketBufferHandler bufhandler = new SocketBufferHandler(
    socketProperties.getAppReadBufSize(),
    socketProperties.getAppWriteBufSize(),
    socketProperties.getDirectBuffer());
    if (isSSLEnabled()) {......}
    else {
    channel = new NioChannel(socket, bufhandler);
    }
    } else {......}
    // 将chanel注册到Poller线程,进行轮询,在org.apache.tomcat.util.net.NioEndpoint.Poller中转交给Processor
    getPoller0().register(channel);
    } catch (Throwable t) {......}
    return true;
    }
    看一下Processor中的关键代码,携带Poller提交给Processor的channel转交给processSocket进行处理:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    //org.apache.tomcat.util.net.NioEndpoint.Poller
    public class Poller implements Runnable {
    ......
    public void run() {
    // attachment等同于承载了socket的channel
    ......
    NioSocketWrapper attachment = (NioSocketWrapper)sk.attachment();
    processKey(sk, attachment)
    ......
    }
    protected void processKey(SelectionKey sk, NioSocketWrapper attachment){
    ......
    // 调用processSocket处理socket
    processSocket(attachment, SocketEvent.OPEN_WRITE, true)
    ......
    }

    }
    我们看下此时的attachment,其中存在上文提到的Timeout、keepAlive等属性,并且封装了socket,在socket中可以看到所在的Channel以及本地服务端口8009和Server的端口51584。
    image

在processSocket方法中,将请求转交给了SocketProcessorBase,并调用SocketProcessorBase的run方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
//org.apache.tomcat.util.net.AbstractEndpoint.processSocket
public boolean processSocket(SocketWrapperBase<S> socketWrapper, SocketEvent event, boolean dispatch) {
try {
......
SocketProcessorBase<S> sc = processorCache.pop();
if (sc == null) {
sc = createSocketProcessor(socketWrapper, event);
} else {......}
......
sc.run();
} catch (RejectedExecutionException ree) {......}
return true;
}
1
2
3
4
5
6
//org.apache.tomcat.util.net.SocketProcessorBase
public abstract class SocketProcessorBase<S> implements Runnable {
public final void run() {
doRun();
}
}

org.apache.tomcat.util.net.NioEndpoint.SocketProcessor重写了SocketProcessor,并交给Handler中的process,这里的Handler就是AjpConnectionHandler,并在process中调用Processer处理请求。

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
//org.apache.tomcat.util.net.NioEndpoint.SocketProcessor
protected class SocketProcessor extends SocketProcessorBase<NioChannel> {
public SocketProcessor(SocketWrapperBase<NioChannel> socketWrapper, SocketEvent event) {
super(socketWrapper, event);
}

@Override
protected void doRun() {
// socketWrapper等同于attachment,从socketWrapper中获取socket
NioChannel socket = socketWrapper.getSocket();
// socket中获取channel的key
SelectionKey key = socket.getIOChannel().keyFor(socket.getPoller().getSelector());
try {
int handshake = -1;
try {
if (key != null) {
if (socket.isHandshakeComplete()) {
// 在这里将socket交给Handler
handshake = 0;
}
......
}
}
} catch (CancelledKeyException ckx) {
handshake = -1;
}
if (handshake == 0) {
SocketState state = SocketState.OPEN;
if (event == null) {
......
} else {
// 调用Handler中的process方法
state = getHandler().process(socketWrapper, event);
}
}
}
......
}
}
}
1
2
3
4
5
6
7
8
9
//org.apache.tomcat.util.net.AbstractProtocol.ConnectionHandler.process
public SocketState process(SocketWrapperBase<S> wrapper, SocketEvent status) {
S socket = wrapper.getSocket();
// 实例化processor,此时processor为null
Processor processor = connections.get(socket);
// 从队列中获取processor
processor = recycledProcessors.pop();
// 更新processor的状态(状态默认为CLOSED)
state = processor.process(wrapper, status);

上面代码中的processor.process()会设置processor状态为open,并对socket内容进行解析,包括protocol、requestURI、remoteAddr、remoteHost、Headers以及本漏洞的重点attributes。
解析attributes的代码如下,通过循环取出attributes中的所有attribute,并解析其name和value:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// org.apache.coyote.ajp.AjpProcessor.prepareRequest
private void prepareRequest() {
......
while ((attributeCode = requestHeaderMessage.getByte()) != Constants.SC_A_ARE_DONE) {
// 判断是否包含attributes,并对attributeCode内容进行解析
switch (attributeCode) {
case Constants.SC_A_REQ_ATTRIBUTE :
// 解析attributes中的name
requestHeaderMessage.getBytes(tmpMB);
String n = tmpMB.toString();
// 解析attributes中的value
requestHeaderMessage.getBytes(tmpMB);
String v = tmpMB.toString();
......
// 将attribute添加到request中
request.setAttribute(n, v );
}
}
......
}

经过Processer解析后的request数据如下,包含了通过AJP提交的所有数据:
image
org.apache.coyote.ajp.AjpProcessor.prepareRequest.SocketState中将request数据提交给adapter。

1
2
3
4
5
6
//org.apache.coyote.ajp.AjpProcessor.prepareRequest.SocketState
public SocketState service(SocketWrapperBase<?> socket) throws IOException {
......
getAdapter().service(request, response);
......
}

在adapter中的service方法将获取Container的Pipeline对request进行处理。

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
public void service(org.apache.coyote.Request req, org.apache.coyote.Response res) throws Exception {
......
// 设置scheme,serverName,port,sessionId等
postParseSuccess = postParseRequest(req, request, res, response);
if (postParseSuccess) {
......
// call container来处理请求
connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);
}
......
}
```
参考2中描述了Container的结构:
![image](https://note.youdao.com/yws/res/2786/0736BDFB5550430DBCA4C3E8FC061321)

可见Container包含Engine、Host、Context和Wrapper四个容器,分别对应各自的Pipeline,对应的实现类在`org.apache.catalina.core`中:
- Engine:用来管理host,寻找host,并将request交给host;
- Host:Host为Container中的虚拟主机,负责设置context;
- Context:Context为Host中的应用,对应WEB-INF目录,负责寻找wrapper;
- Wrapper:Servlet封装在Weapper中。

此处列举代码执行流程:
Engine寻找Host并将request提交给Host,这里本机环境的Host为localhost:
```java
//org.apache.catalina.core.StandardEngineValue
public final void invoke(Request request, Response response){
......
// 查找并获取Host
Host host = request.getHost();
// 请求host处理这个request
host.getPipeline().getFirst().invoke(request, response);
......
}
```
Host寻找Context并将request提交给Context:
```java
//org.apache.catalina.core.StandardHostValue
public final void invoke(Request request, Response response) throws IOException, ServletException {
......
// 查找并获取Host
Context context = request.getContext();
// 请求Context处理这个request
context.getPipeline().getFirst().invoke(request, response);
......
}
```
Context中将request交给Wrapper:
```java
//org.apache.catalina.core.StandardContextValue
public final void invoke(Request request, Response response) throws IOException, ServletException {
......
// 获取request中的path,这里path必须在WEB-INF或META-INF目录下
MessageBytes requestPathMB = request.getRequestPathMB()
// 禁止直接访问WEB-INF或META-INF下的资源
if ((requestPathMB.startsWithIgnoreCase("/META-INF/", 0))
|| (requestPathMB.equalsIgnoreCase("/META-INF"))
|| (requestPathMB.startsWithIgnoreCase("/WEB-INF/", 0))
|| (requestPathMB.equalsIgnoreCase("/WEB-INF"))) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
// 获取Wrapper并将Request交给Wrapper
Wrapper wrapper = request.getWrapper();
wrapper.getPipeline().getFirst().invoke(request, response);
......
}
```
Wrapper中为reqeust创建了一个过滤器,主要用于检查一些字符是否安全:
```java
//org.apache.catalina.core.StandardWrapperValue
public final void invoke(Request request, Response response) throws IOException, ServletException {
......
// 创建wrapper实例
StandardWrapper wrapper = (StandardWrapper) getContainer();
// 获取reqeust中的请求路径,并设置到属性中
MessageBytes requestPathMB = request.getRequestPathMB();
request.setAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR, requestPathMB);
// 为这个request创建一个filterChain,在filterChain中设置了servlet
ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
filterChain.doFilter(request.getRequest(), response.getResponse());
......
}
```
在doFilter方法中,调用`javax.servlet.http`中的service方法对request进行处理,这里根据URI中的文件路径进行判断,若为JSP则走到`org.apache.jasper.servlet.JspServlet`造成jsp文件包含,否则走到`org.apache.catalina.DefaultServlet`造成任意文件读取:
```java
//org.apache.catalina.core.ApplicationFilterChain
private void internalDoFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
if ((request instanceof HttpServletRequest) && (response instanceof HttpServletResponse) && Globals.IS_SECURITY_ENABLED ) {
......
} else {
servlet.service(request, response);
}
}
}
```
先看下任意文件读取漏洞:

经过`org.apache.catalina.DefaultServlet.service`,获取了request中的所有数据包括Method、URI、attributes等。
代码之后走到了`org.apache.catalina.DefaultServlet.serveResource`,并通过`String path = getRelativePath(request, true);`从attributes中获取servletPath和pathinfo,并返回`servletPath + pathInfo + /`的字符串:
```java
//org.apache.catalina.DefaultServlet.getRelativePath
protected String getRelativePath(HttpServletRequest request, boolean allowEmptyPath) {
String servletPath;
String pathInfo;
if (request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI) != null) {
// 在attribute中查找path_info并赋值
pathInfo = (String)
request.getAttribute(RequestDispatcher.INCLUDE_PATH_INFO);
// 在attribute中查找servlet_path并赋值
servletPath = (String) request.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH);
else{......}
StringBuilder result = new StringBuilder();
// 将servletPath和pathInfo进行拼接,格式为servletPath + pathInfo + /
if (servletPath.length() > 0) {
result.append(servletPath);
}
if (pathInfo != null) {
result.append(pathInfo);
}
if (result.length() == 0 && !allowEmptyPath) {
result.append('/');
}
return result.toString();
}
```
继续看`org.apache.catalina.DefaultServlet.serveResource`,在获取到完整path路径后,会通过getResource获取文件,并且获取文件的过程没有任何过滤措施。

```java
//org.apache.catalina.DefaultServlet.serveResource
protected void serveResource(HttpServletRequest request, HttpServletResponse response, boolean content, String inputEncoding) throws IOException, ServletException {
......
boolean serveContent = content;
//在这里获取servletPath和pathinfo,
String path = getRelativePath(request, true);
WebResource resource = resources.getResource(path);
......
}
```
`getResource`方法会对通过`org.apache.cataline.webresources.StandardRoot`对path进行过滤,若path不是以'/'开头将返回500错误,所以本漏洞只能够包含项目根目录下的所有文件。

//org.apache.cataline.webresources.StandardRoot
private String validate(String path) {
if (path == null || path.length() == 0 || !path.startsWith(“/“)) {
throw new IllegalArgumentException(
sm.getString(“standardRoot.invalidPath”, path));
}
……

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
接`org.apache.catalina.DefaultServlet.serveResource`的代码,在获取到文件后,设置response头部并将序列化的文件内容装入response。之后经过Context -> Host -> Engine -> Connector将文件返回给Server。至此完成任意文件读取。
```java
//org.apache.catalina.DefaultServlet.serveResource
protected void serveResource(HttpServletRequest request, HttpServletResponse response, boolean content, String inputEncoding) throws IOException, ServletException {
......
WebResource resource = resources.getResource(path);
String contentType = resource.getMimeType();
// 这里省略的代码用于准备response,对返回的文件类型或名称没有做过滤(设置response的头部
......
// 这里省略的代码用于序列化文件内容
......
ServletOutputStream ostream = null;
ostream = response.getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(ostream, charset);
......
......
}

读取ROOT/WEB-INF/web.xml:
image

JSP文件包含漏洞与上面类似,若AJP请求中的URI包含jsp,则单独使用jspServletorg.apache.jasper.servlet.JspServlet来处理。从下面的代码可以看到JspServlet同样会获取attributes,并在serviceJspFile方法对jsp文件进行编译:

1
2
3
4
5
6
7
8
9
10
11
//org.apache.jasper.servlet.JspServlet
public void service (HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 获取servlet path
jspUri = (String) request.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH);
// 获取path
String pathInfo = (String) request.getAttribute(RequestDispatcher.INCLUDE_PATH_INFO);
// 组装成完成的jsp路径
jspUri += pathInfo
serviceJspFile(request, response, jspUri, precompile);
......
}
1
2
3
4
5
6
7
8
//org.apache.jasper.servlet.JspServlet
private void serviceJspFile(HttpServletRequest request, HttpServletResponse response, String jspUri, boolean precompile) throws ServletException, IOException {
......
//对jsp文件进行编译
boolean precompile = preCompile(request);
serviceJspFile(request, response, jspUri, precompile);
......
}

后续的编译过程和response的代码不再详述,至此完成jsp文件包含。
包含/ROOT/目录下的test_jsp.tt,打印”hello world”,若tomcat存在文件上传,能够利用此漏洞执行远程命令。
image

参考

  1. https://blog.csdn.net/it_freshman/article/details/81710793
  2. https://www.infoq.cn/article/zh-tomcat-http-request-1/