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协议的:
AjpNioProtocolAjpAprProtocol
ProtocolHandler有三个非常重要的组件:Endpoint,Processor和Adapter。
Endpoint: 用于处理底层Socket的网络协议。 Endpoint的抽象实现类AbstractEndpoint中定义了Acceptor,AsyncTimeout两个内部类和一个接口HandlerAcceptor: 用于监听请求。AsyncTimeout: 用于异步检查request超时。Handler: 用于处理接收到的Socket,在内部调用了Processor
Processor: 用于将Endpoint接收到的Socket封装成RequestAdapter: 用于将封装好的Request交给Containner进行具体处理。
Connector处理请求过程如下图:
(图片引用自参考1)
Tomcat启动时会初始化server.xml中定义的所有Connector,默认存在两个Connector,分别是HTTP和AJP,server.xml中的相关配置如下:
1 | <Service name="Catalina"> |
- 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 | //org.apache.catalina.startup.Catalina::createStartDigester |
1 | //org.apache.catalina.startup.ConnectorCreateRule::begin |
可见Connector的创建流程最终执行到了org.apache.catalina.Connector的构造函数中,下面贴出Connector中的几个关键方法:
- 在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();
} - 在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
21public 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);
} - 初始化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已经初始化完成,启动过程不再详述。在开始调用栈分析前先总结一下上文要点:
- Tomcat中的Connector用于Server和Container之间的消息传递;
- Connector会对不同的协议分配ProtocolHandler,用于该协议生命周期的管理。
先看调用栈
- 任意文件读取调用栈: compile:593, JspCompilationContext (org.apache.jasper)
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
29serveResource: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)
```
- 远程文件包含调用栈:
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)//org.apache.tomcat.util.net.NioEndpoint.Acceptor1
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)
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) {......} } ...... } ......看一下Processor中的关键代码,携带Poller提交给Processor的channel转交给processSocket进行处理: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
29setSocketOptions将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;
}我们看下此时的attachment,其中存在上文提到的Timeout、keepAlive等属性,并且封装了socket,在socket中可以看到所在的Channel以及本地服务端口8009和Server的端口51584。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)
......
}
}
在processSocket方法中,将请求转交给了SocketProcessorBase,并调用SocketProcessorBase的run方法:
1 | //org.apache.tomcat.util.net.AbstractEndpoint.processSocket |
1 | //org.apache.tomcat.util.net.SocketProcessorBase |
在org.apache.tomcat.util.net.NioEndpoint.SocketProcessor重写了SocketProcessor,并交给Handler中的process,这里的Handler就是AjpConnectionHandler,并在process中调用Processer处理请求。
1 | //org.apache.tomcat.util.net.NioEndpoint.SocketProcessor |
1 | //org.apache.tomcat.util.net.AbstractProtocol.ConnectionHandler.process |
上面代码中的processor.process()会设置processor状态为open,并对socket内容进行解析,包括protocol、requestURI、remoteAddr、remoteHost、Headers以及本漏洞的重点attributes。
解析attributes的代码如下,通过循环取出attributes中的所有attribute,并解析其name和value:
1 | // org.apache.coyote.ajp.AjpProcessor.prepareRequest |
经过Processer解析后的request数据如下,包含了通过AJP提交的所有数据:
在org.apache.coyote.ajp.AjpProcessor.prepareRequest.SocketState中将request数据提交给adapter。
1 | //org.apache.coyote.ajp.AjpProcessor.prepareRequest.SocketState |
在adapter中的service方法将获取Container的Pipeline对request进行处理。
1 | public void service(org.apache.coyote.Request req, org.apache.coyote.Response res) throws Exception { |
//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 | 接`org.apache.catalina.DefaultServlet.serveResource`的代码,在获取到文件后,设置response头部并将序列化的文件内容装入response。之后经过Context -> Host -> Engine -> Connector将文件返回给Server。至此完成任意文件读取。 |
读取ROOT/WEB-INF/web.xml:
JSP文件包含漏洞与上面类似,若AJP请求中的URI包含jsp,则单独使用jspServletorg.apache.jasper.servlet.JspServlet来处理。从下面的代码可以看到JspServlet同样会获取attributes,并在serviceJspFile方法对jsp文件进行编译:
1 | //org.apache.jasper.servlet.JspServlet |
1 | //org.apache.jasper.servlet.JspServlet |
后续的编译过程和response的代码不再详述,至此完成jsp文件包含。
包含/ROOT/目录下的test_jsp.tt,打印”hello world”,若tomcat存在文件上传,能够利用此漏洞执行远程命令。