漏洞概述
当在Windows上运行Apache Tomcat 7.0.0到7.0.79并启用HTTP put时(例如,通过将Default的只读初始化参数设置为false),可以通过一个特殊的请求将JSP文件上传到服务器。 然后可以请求这个JSP,它包含的任何代码都将由服务器执行。
利用条件
漏洞利用需要在 Windows 环境,且需要将 readonly 初始化参数由默认值设置为 false,经过实际测试,Tomcat 7.x 版本内 web.xml 配置文件内默认配置无 readonly 参数,需要手工添加,默认配置条件下不受此漏洞影响。
漏洞复现
该漏洞vulfocus上有现成的环境,当前直接用vulfoucs靶场进行复现。
首先启动环境,浏览器访问:
通过BP抓包,更改请求方式为PUT,并且在body中添加JSP一句话木马:
可以看到响应码为201,HTTP响应码中201代表一个新的资源依据请求的需要建立。
接下来直接通过浏览器去访问1.jsp就可以了:
代码分析
漏洞复现完了,接下来来分析一下代码。
首先来看一下web.xml,web.xml是配置服务器启动时加载的一些参数和初始化servlet的信息,通过查看web.xml文件可以使我们对项目有一个基本的了结。
首先定义了两个servlet,分别为default:
<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>
<load-on-startup>1</load-on-startup>
</servlet>
以及JSP:
<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>
接下来可以看到定义了每个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主要处理后缀为JSP和JSPX的页面,其他页面都由default来处理,回忆我们刚开始PUT的路径为1.jsp/ 很明显漏洞点在default这个servlet。
接下来我们根据web.xml文件中的default的定义,找到对应的类文件,详细路径是:org.apache.catalina.servlets.DefaultServlet
找到文件后,因为我们使用的是PUT方法,这代表漏洞点在doPut方法,直接打开doPut方法查看代码:
@Override
protected void doPut(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
if (readOnly) {
sendNotAllowed(req, resp);
return;
}
String path = getRelativePath(req);
boolean exists = true;
try {
resources.lookup(path);
} catch (NamingException e) {
exists = false;
}
boolean result = true;
Range range = parseContentRange(req, resp);
InputStream resourceInputStream = null;
try {
// Append data specified in ranges to existing content for this
// resource - create a temp. file on the local filesystem to
// perform this operation
// Assume just one range is specified for now
if (range != null) {
File contentFile = executePartialPut(req, range, path);
resourceInputStream = new FileInputStream(contentFile);
} else {
resourceInputStream = req.getInputStream();
}
Resource newResource = new Resource(resourceInputStream);
// FIXME: Add attributes
if (exists) {
resources.rebind(path, newResource);
} else {
resources.bind(path, newResource);
}
} catch(NamingException e) {
result = false;
}
if (result) {
if (exists) {
resp.setStatus(HttpServletResponse.SC_NO_CONTENT);
} else {
resp.setStatus(HttpServletResponse.SC_CREATED);
}
} else {
resp.sendError(HttpServletResponse.SC_CONFLICT);
}
}
可以看到,进入方法内部第一件事就是检查readonly这个变量,这也是为什么在前面漏洞概述和利用条件中强调了要手动更改这个变量的值,因为这个变量默认情况下为True:
继续往下,真正写入文件的是:
resource在init方法中被赋值,是一个ProxyDirContext对象:
最后真正写入文件在 FileDirContext.java 的 rebind 函数里。
详细调用过程就不跟了,能力有限,简而言之:由于java.io.File特性会过滤掉末尾的斜杠。
其他几种利用方式,比如后缀为空格的,是利用windowsAPI创建文件时会去掉末尾的空格等特殊字符。
参考: