Tomcat CVE漏洞CVE-2017-12615和12616分析

简介

平时研究PHP相关的漏洞比较多,Java已经很长时间没有看了。最近出了Tomcat的远程代码执行漏洞和敏感信息泄漏漏洞,分别为CVE-2017-12615和CVE-2017-12616,趁此机会分析一波,顺便补一下相关的知识。

说明

本实验的实验环境:IDEA、Tomcat7.0.79。网上有很多的资料来讲这个漏洞的原理,

本篇文章主要偏向于如何使用IDEA动态调试Tomcat的代码,理解参数的流程。动态调试Tomcat的代码需要Tomcat的运行环境和Tomcat的源代码。运行环境下载地址源码下载地址

CVE-2017-12615

漏洞原理

这个漏洞的原因很简单,总结了一下主要是三个原因造成的。

  1. 当readonly为false,能够使用PUT/DELETE的方式操作文件
  2. 在Tomcat遇到当后缀是jspjspx时会使用JspServlet处理,其他情况下使用DefaultServlet处理。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <servlet-mapping>
    <servlet-name>default</servlet-name>
    <url-pattern>/</url-pattern>
    </servlet-mapping>

    <!-- The mappings for the JSP servlet -->
    <servlet-mapping>
    <servlet-name>jsp</servlet-name>
    <url-pattern>*.jsp</url-pattern>
    <url-pattern>*.jspx</url-pattern>
    </servlet-mapping>
  3. 在Windows环境下可以可以通过filename.jsp(后面存在空格)、filename.jsp::$DATA(ADS流)的方式绕过一些检测,创建文件。

漏洞调试

如果需要动态调试Tomcat的代码,要以Debug的方式时运行代码,还要导入Tomcat源代码。
当使用PUT方式创建文件时,

1
2
3
4
5
6
7
8
9
10
PUT /test.jsp%20 HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:55.0) Gecko/20100101 Firefox/55.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Connection: close
Upgrade-Insecure-Requests: 1
Content-Length: 26

<%out.print("test");%>

正式进入过程。如下:

进入到了DefaultServlet::doPut()方法中,同时由于readonly为false,能够执行后续的代码。

判断资源是否存在,如果不存在,则执行resource.bind()方法,跟踪进入到bind()中。

bind()位于ProxyDirContent.java:bind()中,代码如上所示。其中的关键代码是,dirContext.bind(parseName(name), obj);
跟踪进入到parseName()方法中:

1
2
3
protected String parseName(String name) throws NamingException {
return name;
}

并没有对Name进行处理。
进入到dirContexn.bind()方法中,如下所示:

发现又调用了一个bind()方法,跟踪进去看看:

进入到了FileDirContent.java中的bind()方法,其中的关键代码为File file = new File(base, name)rebind(name, obj, attrs)
进入到new File中查看代码,这个File类是JDK中的类,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public File(File parent, String child) {
if (child == null) {
throw new NullPointerException();
}
if (parent != null) {
if (parent.path.equals("")) {
this.path = fs.resolve(fs.getDefaultParent(),
fs.normalize(child));
} else {
this.path = fs.resolve(parent.path,
fs.normalize(child));
}
} else {
this.path = fs.normalize(child);
}
this.prefixLength = fs.prefixLength(this.path);
}

就是一个普通的文件类。
此时以test.jsp[空格]创建了一个File类,接下来就是写入文件内容了。跟踪进入到rebind()方法中。跟踪进入发现是在FileDirContext.java:rebind()中,源代码如下:

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
public void rebind(String name, Object obj, Attributes attrs)
throws NamingException {

// Note: No custom attributes allowed
// Check obj type

File file = new File(base, name);

InputStream is = null;
if (obj instanceof Resource) {
try {
is = ((Resource) obj).streamContent();
} catch (IOException e) {
// Ignore
}
} else if (obj instanceof InputStream) {
is = (InputStream) obj;
} else if (obj instanceof DirContext) {
if (file.exists()) {
if (!file.delete())
throw new NamingException
(sm.getString("resources.bindFailed", name));
}
if (!file.mkdir())
throw new NamingException
(sm.getString("resources.bindFailed", name));
}
if (is == null)
throw new NamingException
(sm.getString("resources.bindFailed", name));

// Open os

FileOutputStream os = null;
byte buffer[] = new byte[BUFFER_SIZE];
int len = -1;
try {
os = new FileOutputStream(file);
while (true) {
len = is.read(buffer);
if (len == -1)
break;
os.write(buffer, 0, len);
}
} catch (IOException e) {
NamingException ne = new NamingException
(sm.getString("resources.bindFailed", e));
ne.initCause(e);
throw ne;
} finally {
if (os != null) {
try {
os.close();
} catch (IOException e) {
}
}
try {
is.close();
} catch (IOException e) {
}
}

}

这样就成功地写入了文件,但是由于在windows中不支持文件名后面存在空格,所以最终会创建一个test.jsp,这样就可以任意地创建文件,包括webshell文件。
除了使用空格的方式之外,还可以使用windows中的ADS流的方式创建文件。
正两种方法的最终测试结果如下:
使用%20的方式绕过,如下:

使用ADS流的方式绕过,如下:

除了这两种方式之外,还存在一种方式使用类似于test.jsp/,当创建一个这样的文件时候,同样是由DefaultServlet处理,但是问题出在File file = new File(base, name);的地方,我们通过调试进入到这段代码里面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public File(File parent, String child) {
if (child == null) {
throw new NullPointerException();
}
if (parent != null) {
if (parent.path.equals("")) {
this.path = fs.resolve(fs.getDefaultParent(),
fs.normalize(child));
} else {
this.path = fs.resolve(parent.path,
fs.normalize(child));
}
} else {
this.path = fs.normalize(child);
}
this.prefixLength = fs.prefixLength(this.path);
}

当使用normalize()进行正规化时会去掉后面的/,导致最后写入的是test.jsp。动态调试的结果如下:

最终运行的结果如下:

以上就是整个CVE-2017-12615漏洞的调试过程。

CVE-2017-12616

漏洞原理

CVE-2017-12616(信息泄露):允许未经身份验证的远程攻击者查看敏感信息。如果tomcat开启VirtualDirContext有可能绕过安全限制访问服务器上的JSP文件源码。漏洞触发的先决条件是需要在conf/server.xml配置VirtualDirContex参数,默认情况下tomcat7并不会对该参数进行配置。

那么为什么要配置一个这样的虚拟目录呢?

通过VirtualDirContext,允许在单独的一个webapp应用下对外暴露出多个文件系统的目录。在实际开发中,为了避免拷贝静态资源文件如(images等)至webapp目录下,tomcat推荐的做法是在server.xml配置文件中建立虚拟子目录。

在开启了这个配置之后,可以通过windows目录下的文件的解析问题,从而暴露在在目录中的源码。

漏洞调试

在本地搭建一个系统环境,目录结果如下:
D:\testtomcat2的目录下面的文件结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
D:.
├─img
│ 1.jpg

├─src
│ HelloWorld.java

├─target
└─web
│ index.jsp

└─WEB-INF
web.xml

在tomcat中的conf/server.xml下的<host>标签下面增加如下的配置:

1
2
3
4
5
6
7
8
<Context path='/site' docBase="D:\testtomcat2\web" reloadable="true">
<!--配置项目的资源-->
<Resources className="org.apache.naming.resources.VirtualDirContext" extraResourcePaths="/WEB-INF/classes=F:/testtomcat2/target/classes,/images=D:/testtomcat2/img,/tmp=D:/testtomcat2/web" />
<!--配置项目的 classpath-->
<Loader className="org.apache.catalina.loader.VirtualWebappLoader" virtualClasspath="/WEB-INF/classes=F:/testtomcat2/target/classes" />
<!-- 扫描jar的内容-->
<JarScanner scanAllDirectories="true" />
</Context>

从配置可以发现,我创建了2个虚拟目录。分别为imagestmp,分别映射到本地的D:/testtomcat2/imgD:/testtomcat2/web。大家在进行测试的时候,可以根据自己的目录自行参照修改。

部署完毕之后,在浏览器中访问localhost:8080/site/images/1.jpg

顺利地出现了1.jpg,说明部署正确。

这个漏洞的触发,同样会使用到tomcat中因为文件后缀的解析的问题。和12615是一样的,只有后缀是jspjspxJSPservlet处理,其他都是由DefaultServlet处理。在12615中配合PUT方法,可以通过上传test.jsp%20test.jsp/test.jsp::$DATA的方式上传任意的问价,包括webshell。但是在本例中,只能通过test.jsp%20test.jsp::$DATA获得源代码,无法通过test.jsp/获取源代码。以下就是演示的结果:

而访问http://localhost:8080/site/tmp/index.jsp/会显示404,

整个漏洞的分析过程和12615是一样的,下面就为什么无法使用test.jsp/无法获取源代码进行说明。
当访问http://localhost:8080/site/tmp/index.jsp/时,是由Tomcat中的DefaultServelt::doGet来处理。

1
2
3
4
5
@Override
protected void doGet(HttpServletRequest request,HttpServletResponse response) throws IOException, ServletException {
// Serve the requested resource, including the data content
serveResource(request, response, true);
}

追踪进入到serveResource,其中的关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
protected void serveResource(HttpServletRequest request,HttpServletResponse response,boolean content) throws IOException, ServletException {
boolean serveContent = content;
// Identify the requested resource path
String path = getRelativePath(request, true);
CacheEntry cacheEntry = resources.lookupCache(path);
// If the resource is not a collection, and the resource path
// ends with "/" or "\", return NOT FOUND
if (cacheEntry.context == null) {
if (path.endsWith("/") || (path.endsWith("\\"))) {
// Check if we're included so we can return the appropriate
// missing resource name in the error
String requestUri = (String) request.getAttribute(
RequestDispatcher.INCLUDE_REQUEST_URI);
if (requestUri == null) {
requestUri = request.getRequestURI();
}
response.sendError(HttpServletResponse.SC_NOT_FOUND,
requestUri);
return;
}
}
}

所以当cacheEntry.context == null而且path.endsWith("/") || (path.endsWith("\\")),则直接向客户端返回404,所以采用test.jsp/方式并不能够成功触发获取服务端漏洞的JSP代码。

所以这也就是为什么通过test.jsp/获取源代码的原因了。

MISC

那么为什么这两个CVE都可以借助于test.jsp%20test.jsp::$DATA来达到攻击的目的呢?这个其实是由于JDK的解析以及windows的文件属性共同造成的。因为在windows中不允许文件名存在空格,所以JDK在创建的文件时候会去掉空格,当然这个只是猜测,具体还需要分析源代码才知道。其次是test.jsp::$DATA,这种文件的创建方式在windows中就是合法的,所以也没有什么问题。

总结

纸上得来终觉浅 绝知此事要躬行

漏洞还是要多调试

参考

云鼎实验室:Tomcat 远程代码执行漏洞分析(CVE-2017-12615)及补丁 Bypass

CVE-2017-12615 Tomcat远程代码执行漏洞复现

Tomcat漏洞CVE-2017-12615与CVE-2017-12616分析