LDAP 中继扫描

2022-01-21 14:16:27 浏览数 (1)

检查有关 NTLM 身份验证中继的 LDAP 保护

概括

尝试在域控制器上中继 NTLM 身份验证 LDAP 时,有几个服务器端保护。此工具尝试枚举的 LDAP 保护包括:

  • LDAPS -通道绑定
  • LDAP -服务器签名要求

可以从未经身份验证的角度确定通过 SSL/TLS 对 LDAP 执行通道绑定。这是因为在 LDAP 绑定过程中验证凭据之前,将发生与缺少正确执行通道绑定能力的 LDAP 客户端相关的错误。

但是,要确定是否强制执行标准 LDAP 的服务器端保护(服务器签名完整性要求),必须首先在 LDAP 绑定期间验证客户端凭据。识别执行此保护的潜在错误是从经过身份验证的角度识别的。

TL;DR - 可以未经身份验证检查 LDAPS,但检查 LDAP 需要身份验证。

用法

注意:DNS 需要正确解析。如果您正在通过 SOCKS 路由或在未加入域的主机上运行,​​请确保它正常工作。

该工具有两种方法,LDAPS(默认)和BOTH。LDAPS 只需要域控制器 IP 地址,因为此检查可以在未经身份验证的情况下执行。BOTH 方法将需要用户名和密码或 NT 哈希。Active Directory 域不是必需的,它将通过匿名 LDAP 绑定来确定。

例子

注意:在客户端使用 python3.9 测试,针对未修补的 Windows Server 2016 和最新的 Windows Server 2022

代码语言:javascript复制
python3.9 LdapRelayScan.py -method LDAPS -dc-ip 10.0.0.20
python3.9 LdapRelayScan.py -method BOTH -dc-ip 10.0.0.20 -u domainuser1 
python3.9 LdapRelayScan.py -method BOTH -dc-ip 10.0.0.20 -u domainuser1 -p badpassword2
python3.9 LdapRelayScan.py -method BOTH -dc-ip 10.0.0.20 -u domainuser1 -nthash e6ee750a1feb2c7ee50d46819a6e4d25

基于错误的枚举规范

[LDAPS] 通道绑定令牌要求

在自CVE-2017-8563以来已修补的域控制器上,已经存在强制执行 LDAPS 通道绑定的功能。特定策略被调用Domain Controller: LDAP server channel binding token requirements,可以设置为NeverWhen supportedAlways。默认情况下也不需要这样做(在撰写本文时)。

在域控制器上通过 SSL/TLS 流量解密和监视 LDAP 允许在强制执行通道绑定与未强制执行通道绑定时识别绑定尝试期间的错误差异。当尝试使用无效凭据通过 SSL/TLS 绑定到 LDAP 时,您将收到预期的resultCode 49,并且您将在错误消息内容中看到data 52e。但是,当强制执行通道绑定并且 LDAP 客户端未计算并包含通道绑定令牌 (CBT) 时,resultCode 仍将为 49,但错误消息内容将包含data 80090346含义SEC_E_BAD_BINDINGS或客户端的 Supplied Support Provider Interface (SSPI)通道绑定不正确。

data 8009034注意: LDAP over SSL/TLS 绑定期间的错误提及[1] [2] [3] [4] [5]

[LDAP] 服务器签名要求

在域控制器上,调用的策略Domain Controller: LDAP server signing requirements设置为None, Require signing,或者只是未定义。如果未定义,则默认为不需要签名(在撰写本文时)。当sicily NTLM或简单绑定尝试以8 的 resultCode响应时,识别此保护所需的错误,表示strongerAuthRequired. 仅当验证 LDAP 绑定期间的凭据时才会发生这种情况。

代码语言:javascript复制
import dns.resolver
import ldap3
import argparse
import sys
import ssl
import socket
import getpass


class CheckLdaps:
    def __init__(self, nameserver, username, cmdLineOptions):
        self.options = cmdLineOptions
        self.__nameserver = nameserver
        self.__username = username

#Conduct a bind to LDAPS and determine if channel
#binding is enforced based on the contents of potential
#errors returned. This can be determined unauthenticated,
#because the error indicating channel binding enforcement
#will be returned regardless of a successful LDAPS bind.
def run_ldaps(inputUser, inputPassword, dcTarget):
    try:
        tls = ldap3.Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLSv1_2)
        ldapServer = ldap3.Server(
            dcTarget, use_ssl=True, port=636, get_info=ldap3.ALL, tls=tls)
        ldapConn = ldap3.Connection(
            ldapServer, user=inputUser, password=inputPassword, authentication=ldap3.NTLM)
        if not ldapConn.bind():
            if "data 80090346" in str(ldapConn.result):
                return True #channel binding IS enforced
            elif "data 52e" in str(ldapConn.result):
                return False #channel binding not enforced
            else:
                print("UNEXPECTED ERROR: "   str(ldapConn.result))
        else:
            #LDAPS bind successful
            return False #because channel binding is not enforced
            exit()
    except Exception as e:
        print("n   [!] "  dcTarget " -", str(e))
        print("        * Ensure DNS is resolving properly, and that you can reach LDAPS on this host")


#DNS query of an SRV record that should return
#a list of domain controllers.
def ResolveDCs(nameserverIp, fqdn):
    dcList = []
    DnsResolver = dns.resolver.Resolver()
    DnsResolver.nameservers = [nameserverIp]
    dcQuery = DnsResolver.resolve(
        "_ldap._tcp.dc._msdcs." fqdn, 'SRV', tcp=True)
    testout = str(dcQuery.response).split("n")
    for line in testout:
        if "IN A" in line:
            dcList.append(line.split(" ")[0].rstrip(line.split(" ")[0][-1]))
    return dcList

#Conduct an anonymous bind to the provided "nameserver"
#arg during execution. This should work even if LDAP
#server integrity checks are enforced. The FQDN of the
#internal domain will be parsed from the basic server
#info gathered from that anonymous bind.
def InternalDomainFromAnonymousLdap(nameserverIp):
    tls = ldap3.Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLSv1_2)
    #ldapServer = ldap3.Server(dcTarget, use_ssl=True, port=636, get_info=ldap3.ALL, tls=tls)
    ldapServer = ldap3.Server(
        nameserverIp, use_ssl=False, port=389, get_info=ldap3.ALL)
    ldapConn = ldap3.Connection(ldapServer, authentication=ldap3.ANONYMOUS)
    ldapConn.bind()
    parsedServerInfo = str(ldapServer.info).split("n")
    fqdn = ""
    for line in parsedServerInfo:
        if "$" in line:
            fqdn = line.strip().split(":")[0]
    return fqdn


#Domain Controllers do not have a certificate setup for
#LDAPS on port 636 by default. If this has not been setup,
#the TLS handshake will hang and you will not be able to 
#interact with LDAPS. The condition for the certificate
#existing as it should is either an error regarding 
#the fact that the certificate is self-signed, or
#no error at all. Any other "successful" edge cases
#not yet accounted for.
def DoesLdapsCompleteHandshake(dcIp):
  s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  s.settimeout(5)
  ssl_sock = ssl.wrap_socket(s,
                            cert_reqs=ssl.CERT_OPTIONAL,
                            suppress_ragged_eofs=False,
                            do_handshake_on_connect=False)
  ssl_sock.connect((dcIp, 636))
  try:
    ssl_sock.do_handshake()
    ssl_sock.close()
    return True
  except Exception as e:
    if "CERTIFICATE_VERIFY_FAILED" in str(e):
        ssl_sock.close()
        return True
    if "handshake operation timed out" in str(e):
        ssl_sock.close()
        return False
    else:
      print("Unexpected error during LDAPS handshake: "   e)
    ssl_sock.close()


#Conduct and LDAP bind and determine if server signing
#requirements are enforced based on potential errors
#during the bind attempt. 
def run_ldap(inputUser, inputPassword, dcTarget):
    try:
        ldapServer = ldap3.Server(
            dcTarget, use_ssl=False, port=389, get_info=ldap3.ALL)
        ldapConn = ldap3.Connection(
            ldapServer, user=inputUser, password=inputPassword, authentication=ldap3.NTLM)
        if not ldapConn.bind():
            if "stronger" in str(ldapConn.result):
                return True #because LDAP server signing requirements ARE enforced
            elif "data 52e" or "data 532" in str(ldapConn.result):
                print("[!!!] invalid credentials - aborting to prevent unnecessary authentication")
                exit()
            else:
                print("UNEXPECTED ERROR: "   str(ldapConn.result))
        else:
            #LDAPS bind successful
            return False #because LDAP server signing requirements are not enforced
            exit()
    except Exception as e:
        print("n   [!] "  dcTarget " -", str(e))
        print("        * Ensure DNS is resolving properly, and that you can reach LDAPS on this host")


if __name__ == '__main__':
    parser = argparse.ArgumentParser(
        add_help=True, description="Checks Domain Controllers for LDAP authentication protection."
                                      " You can check for only LDAPS protections (channel binding), this is done unauthenticated. "
                                      "Alternatively you can check for both LDAPS and LDAP (server signing) protections. This requires a successful LDAP bind.")
    parser.add_argument('-method', choices=['LDAPS','BOTH'], default='LDAPS', metavar="method", action='store',
                        help="LDAPS or BOTH - LDAPS checks for channel binding, BOTH checks for LDAP signing and LDAP channel binding [authentication required]")
    parser.add_argument('-dc-ip', required=True, action='store',
                        help='DNS Nameserver on network. Any DC's IPv4 address should work.')
    parser.add_argument('-u', default='guest', metavar='username',action='store',
                        help='Domain username value.')
    parser.add_argument('-p', default='defaultpass', metavar='password',action='store',
                        help='Domain username value.')
    parser.add_argument('-nthash', metavar='nthash',action='store',
                        help='NT hash of password')
    options = parser.parse_args()
    domainUser = options.u

    password = options.p

    if len(sys.argv) == 1:
        parser.print_help()
        sys.exit(1)
    
    if options.dc_ip == None:
        print("-dc-ip is required")
        exit()
    if options.method == 'BOTH':
        if domainUser == 'guest':
            print("[i] Using BOTH method requires a username parameter")
            exit()
    if options.method == 'BOTH' and options.u != 'guest' and (options.p != 'defaultpass' or options.nthash != None):
        if options.p == 'defaultpass' and options.nthash != None:
            password = "aad3b435b51404eeaad3b435b51404ee:"   options.nthash
        elif options.p != 'defaultpass' and options.nthash == None:
            password = options.p
        else:
            print("Something incorrect while providing credential material options")

    if options.method =='BOTH' and options.p == 'defaultpass' and options.nthash == None:   
        password = getpass.getpass(prompt="Password: ")
    fqdn = InternalDomainFromAnonymousLdap(options.dc_ip)


    dcList = ResolveDCs(options.dc_ip, fqdn)
    print("n~Domain Controllers identifed~")
    for dc in dcList:
        print("   "   dc)

    print("n~Checking DCs for LDAP NTLM relay protections~")
    username = fqdn   "\"   domainUser
    #print("VALUES AUTHING WITH:nUser: " domainUser "nPass: "  password   "nDomain:  " fqdn)
    for dc in dcList:
        print("   "   dc)
        if options.method == "BOTH":
            ldapIsProtected = run_ldap(username, password, dc)
            if ldapIsProtected == False:
                print("      [ ] (LDAP) SERVER SIGNING REQUIREMENTS NOT ENFORCED! ")
            elif ldapIsProtected == True:
                print("      [-] (LDAP) Server enforcing signing requirements")
            else:
                print("Something bad happened during LDAP bind")
        if DoesLdapsCompleteHandshake(dc) == True:
            ldapsIsProtected = run_ldaps(username, password, dc)
            if ldapsIsProtected == False:
                print("      [ ] (LDAPS) CHANNEL BINDING NOT REQUIRED! PARTY TIME!")
            elif ldapsIsProtected == True:
                print("      [-] (LDAPS) channel binding required, no fun allowed")
            else:
                print("nSomething went wrong...")
                exit()
        elif DoesLdapsCompleteHandshake(dc) == False:
            print("      [!] " dc  " - cannot complete TLS handshake, cert likely not configured")
    print()

0 人点赞