mirror of
https://github.com/p0dalirius/LDAPmonitor
synced 2026-08-09 13:01:33 +00:00
Fix kerberos and AES auth, use sectools (#14)
Co-authored-by: lefayjey <lefayjey@lefayjey.fr>
This commit is contained in:
+22
-227
@@ -10,10 +10,8 @@ import os
|
||||
import sys
|
||||
import ssl
|
||||
import random
|
||||
import ldap
|
||||
import ldap3
|
||||
from impacket.smbconnection import SMBConnection, SMB2_DIALECT_002, SMB2_DIALECT_21, SMB_DIALECT, SessionError
|
||||
from impacket.spnego import SPNEGO_NegTokenInit, TypesMech
|
||||
from sectools.windows.ldap import raw_ldap_query, init_ldap_session
|
||||
from sectools.windows.crypto import nt_hash, parse_lm_nt_hashes
|
||||
from ldap3.protocol.formatters.formatters import format_sid
|
||||
import time
|
||||
import datetime
|
||||
@@ -158,222 +156,6 @@ class LDAPConsole(object):
|
||||
return results
|
||||
|
||||
|
||||
def get_machine_name(args, domain):
|
||||
if args.dc_ip is not None:
|
||||
s = SMBConnection(args.dc_ip, args.dc_ip)
|
||||
else:
|
||||
s = SMBConnection(domain, domain)
|
||||
try:
|
||||
s.login('', '')
|
||||
except Exception:
|
||||
if s.getServerName() == '':
|
||||
raise Exception('Error while anonymous logging into %s' % domain)
|
||||
else:
|
||||
s.logoff()
|
||||
return s.getServerName()
|
||||
|
||||
|
||||
def init_ldap_connection(target, tls_version, args, domain, username, password, lmhash, nthash):
|
||||
user = '%s\\%s' % (domain, username)
|
||||
if tls_version is not None:
|
||||
use_ssl = True
|
||||
port = 636
|
||||
tls = ldap3.Tls(validate=ssl.CERT_NONE, version=tls_version)
|
||||
else:
|
||||
use_ssl = False
|
||||
port = 389
|
||||
tls = None
|
||||
ldap_server = ldap3.Server(target, get_info=ldap3.ALL, port=port, use_ssl=use_ssl, tls=tls)
|
||||
|
||||
if args.use_kerberos:
|
||||
ldap_session = ldap3.Connection(ldap_server)
|
||||
ldap_session.bind()
|
||||
ldap3_kerberos_login(ldap_session, target, username, password, domain, lmhash, nthash, args.auth_key, kdcHost=args.dc_ip)
|
||||
elif args.auth_hashes is not None:
|
||||
if lmhash == "":
|
||||
lmhash = "aad3b435b51404eeaad3b435b51404ee"
|
||||
ldap_session = ldap3.Connection(ldap_server, user=user, password=lmhash + ":" + nthash, authentication=ldap3.NTLM, auto_bind=True)
|
||||
else:
|
||||
ldap_session = ldap3.Connection(ldap_server, user=user, password=password, authentication=ldap3.NTLM, auto_bind=True)
|
||||
|
||||
return ldap_server, ldap_session
|
||||
|
||||
|
||||
def init_ldap_session(logger, args, domain, username, password, lmhash, nthash):
|
||||
if args.use_kerberos:
|
||||
target = get_machine_name(args, domain)
|
||||
else:
|
||||
if args.dc_ip is not None:
|
||||
target = args.dc_ip
|
||||
else:
|
||||
target = domain
|
||||
logger.print("[>] Trying to connect to %s ..." % target)
|
||||
if args.use_ldaps is True:
|
||||
try:
|
||||
return init_ldap_connection(target, ssl.PROTOCOL_TLSv1_2, args, domain, username, password, lmhash, nthash)
|
||||
except ldap3.core.exceptions.LDAPSocketOpenError:
|
||||
return init_ldap_connection(target, ssl.PROTOCOL_TLSv1, args, domain, username, password, lmhash, nthash)
|
||||
else:
|
||||
return init_ldap_connection(target, None, args, domain, username, password, lmhash, nthash)
|
||||
|
||||
|
||||
def ldap3_kerberos_login(connection, target, user, password, domain='', lmhash='', nthash='', aesKey='', kdcHost=None, TGT=None, TGS=None, useCache=True):
|
||||
from pyasn1.codec.ber import encoder, decoder
|
||||
from pyasn1.type.univ import noValue
|
||||
"""
|
||||
logins into the target system explicitly using Kerberos. Hashes are used if RC4_HMAC is supported.
|
||||
:param string user: username
|
||||
:param string password: password for the user
|
||||
:param string domain: domain where the account is valid for (required)
|
||||
:param string lmhash: LMHASH used to authenticate using hashes (password is not used)
|
||||
:param string nthash: NTHASH used to authenticate using hashes (password is not used)
|
||||
:param string aesKey: aes256-cts-hmac-sha1-96 or aes128-cts-hmac-sha1-96 used for Kerberos authentication
|
||||
:param string kdcHost: hostname or IP Address for the KDC. If None, the domain will be used (it needs to resolve tho)
|
||||
:param struct TGT: If there's a TGT available, send the structure here and it will be used
|
||||
:param struct TGS: same for TGS. See smb3.py for the format
|
||||
:param bool useCache: whether or not we should use the ccache for credentials lookup. If TGT or TGS are specified this is False
|
||||
:return: True, raises an Exception if error.
|
||||
"""
|
||||
|
||||
if lmhash != '' or nthash != '':
|
||||
if len(lmhash) % 2:
|
||||
lmhash = '0' + lmhash
|
||||
if len(nthash) % 2:
|
||||
nthash = '0' + nthash
|
||||
try: # just in case they were converted already
|
||||
lmhash = unhexlify(lmhash)
|
||||
nthash = unhexlify(nthash)
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
# Importing down here so pyasn1 is not required if kerberos is not used.
|
||||
from impacket.krb5.ccache import CCache
|
||||
from impacket.krb5.asn1 import AP_REQ, Authenticator, TGS_REP, seq_set
|
||||
from impacket.krb5.kerberosv5 import getKerberosTGT, getKerberosTGS
|
||||
from impacket.krb5 import constants
|
||||
from impacket.krb5.types import Principal, KerberosTime, Ticket
|
||||
import datetime
|
||||
|
||||
if TGT is not None or TGS is not None:
|
||||
useCache = False
|
||||
|
||||
if useCache:
|
||||
try:
|
||||
ccache = CCache.loadFile(os.getenv('KRB5CCNAME'))
|
||||
except Exception as e:
|
||||
# No cache present
|
||||
print(e)
|
||||
pass
|
||||
else:
|
||||
# retrieve domain information from CCache file if needed
|
||||
if domain == '':
|
||||
domain = ccache.principal.realm['data'].decode('utf-8')
|
||||
print('[debug] Domain retrieved from CCache: %s' % domain)
|
||||
|
||||
print('[debug] Using Kerberos Cache: %s' % os.getenv('KRB5CCNAME'))
|
||||
principal = 'ldap/%s@%s' % (target.upper(), domain.upper())
|
||||
|
||||
creds = ccache.getCredential(principal)
|
||||
if creds is None:
|
||||
# Let's try for the TGT and go from there
|
||||
principal = 'krbtgt/%s@%s' % (domain.upper(), domain.upper())
|
||||
creds = ccache.getCredential(principal)
|
||||
if creds is not None:
|
||||
TGT = creds.toTGT()
|
||||
print('[debug] Using TGT from cache')
|
||||
else:
|
||||
print('[debug] No valid credentials found in cache')
|
||||
else:
|
||||
TGS = creds.toTGS(principal)
|
||||
print('[debug] Using TGS from cache')
|
||||
|
||||
# retrieve user information from CCache file if needed
|
||||
if user == '' and creds is not None:
|
||||
user = creds['client'].prettyPrint().split(b'@')[0].decode('utf-8')
|
||||
print('[debug] Username retrieved from CCache: %s' % user)
|
||||
elif user == '' and len(ccache.principal.components) > 0:
|
||||
user = ccache.principal.components[0]['data'].decode('utf-8')
|
||||
print('[debug] Username retrieved from CCache: %s' % user)
|
||||
|
||||
# First of all, we need to get a TGT for the user
|
||||
userName = Principal(user, type=constants.PrincipalNameType.NT_PRINCIPAL.value)
|
||||
if TGT is None:
|
||||
if TGS is None:
|
||||
tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, password, domain, lmhash, nthash, aesKey, kdcHost)
|
||||
else:
|
||||
tgt = TGT['KDC_REP']
|
||||
cipher = TGT['cipher']
|
||||
sessionKey = TGT['sessionKey']
|
||||
|
||||
if TGS is None:
|
||||
serverName = Principal('ldap/%s' % target, type=constants.PrincipalNameType.NT_SRV_INST.value)
|
||||
tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS(serverName, domain, kdcHost, tgt, cipher, sessionKey)
|
||||
else:
|
||||
tgs = TGS['KDC_REP']
|
||||
cipher = TGS['cipher']
|
||||
sessionKey = TGS['sessionKey']
|
||||
|
||||
# Let's build a NegTokenInit with a Kerberos REQ_AP
|
||||
|
||||
blob = SPNEGO_NegTokenInit()
|
||||
|
||||
# Kerberos
|
||||
blob['MechTypes'] = [TypesMech['MS KRB5 - Microsoft Kerberos 5']]
|
||||
|
||||
# Let's extract the ticket from the TGS
|
||||
tgs = decoder.decode(tgs, asn1Spec=TGS_REP())[0]
|
||||
ticket = Ticket()
|
||||
ticket.from_asn1(tgs['ticket'])
|
||||
|
||||
# Now let's build the AP_REQ
|
||||
apReq = AP_REQ()
|
||||
apReq['pvno'] = 5
|
||||
apReq['msg-type'] = int(constants.ApplicationTagNumbers.AP_REQ.value)
|
||||
|
||||
opts = []
|
||||
apReq['ap-options'] = constants.encodeFlags(opts)
|
||||
seq_set(apReq, 'ticket', ticket.to_asn1)
|
||||
|
||||
authenticator = Authenticator()
|
||||
authenticator['authenticator-vno'] = 5
|
||||
authenticator['crealm'] = domain
|
||||
seq_set(authenticator, 'cname', userName.components_to_asn1)
|
||||
now = datetime.datetime.utcnow()
|
||||
|
||||
authenticator['cusec'] = now.microsecond
|
||||
authenticator['ctime'] = KerberosTime.to_asn1(now)
|
||||
|
||||
encodedAuthenticator = encoder.encode(authenticator)
|
||||
|
||||
# Key Usage 11
|
||||
# AP-REQ Authenticator (includes application authenticator
|
||||
# subkey), encrypted with the application session key
|
||||
# (Section 5.5.1)
|
||||
encryptedEncodedAuthenticator = cipher.encrypt(sessionKey, 11, encodedAuthenticator, None)
|
||||
|
||||
apReq['authenticator'] = noValue
|
||||
apReq['authenticator']['etype'] = cipher.enctype
|
||||
apReq['authenticator']['cipher'] = encryptedEncodedAuthenticator
|
||||
|
||||
blob['MechToken'] = encoder.encode(apReq)
|
||||
|
||||
request = ldap3.operation.bind.bind_operation(connection.version, ldap3.SASL, user, None, 'GSS-SPNEGO',
|
||||
blob.getData())
|
||||
|
||||
# Done with the Kerberos saga, now let's get into LDAP
|
||||
if connection.closed: # try to open connection if closed
|
||||
connection.open(read_server_info=False)
|
||||
|
||||
connection.sasl_in_progress = True
|
||||
response = connection.post_send_single_response(connection.send('bindRequest', request, None))
|
||||
connection.sasl_in_progress = False
|
||||
if response[0]['result'] != 0:
|
||||
raise Exception(response)
|
||||
|
||||
connection.bound = True
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def diff(last1_query_results, last2_query_results, logger, ignore_user_logon=False):
|
||||
ignored_keys = ["dnsRecord", "replUpToDateVector", "repsFrom"]
|
||||
@@ -448,6 +230,7 @@ def parse_args():
|
||||
|
||||
authconn = parser.add_argument_group('authentication & connection')
|
||||
authconn.add_argument('--dc-ip', dest="dc_ip", action='store', metavar="ip address", help='IP Address of the domain controller or KDC (Key Distribution Center) for Kerberos. If omitted it will use the domain part (FQDN) specified in the identity parameter')
|
||||
authconn.add_argument('--kdcHost', dest="kdcHost", action='store', metavar="FQDN KDC", help='FQDN of KDC for Kerberos.')
|
||||
authconn.add_argument("-d", "--domain", dest="auth_domain", metavar="DOMAIN", action="store", help="(FQDN) domain to authenticate to")
|
||||
authconn.add_argument("-u", "--user", dest="auth_username", metavar="USER", action="store", help="user to authenticate with")
|
||||
|
||||
@@ -499,15 +282,27 @@ if __name__ == '__main__':
|
||||
auth_nt_hash = args.auth_hashes.split(":")[1]
|
||||
else:
|
||||
auth_nt_hash = args.auth_hashes
|
||||
|
||||
if args.auth_key is not None:
|
||||
args.use_kerberos = True
|
||||
|
||||
if args.use_kerberos is True and args.kdcHost is None:
|
||||
print("[!] Specify KDC's Hostname of FQDN using the argument --kdcHost")
|
||||
exit()
|
||||
|
||||
try:
|
||||
logger.print("[>] Trying to connect to %s ..." % args.dc_ip)
|
||||
ldap_server, ldap_session = init_ldap_session(
|
||||
logger=logger,
|
||||
args=args,
|
||||
domain=args.auth_domain,
|
||||
username=args.auth_username,
|
||||
password=args.auth_password,
|
||||
lmhash=auth_lm_hash,
|
||||
nthash=auth_nt_hash
|
||||
auth_domain=args.auth_domain,
|
||||
auth_dc_ip=args.dc_ip,
|
||||
auth_username=args.auth_username,
|
||||
auth_password=args.auth_password,
|
||||
auth_lm_hash=auth_lm_hash,
|
||||
auth_nt_hash=auth_nt_hash,
|
||||
auth_key=args.auth_key,
|
||||
use_kerberos=args.use_kerberos,
|
||||
kdcHost=args.kdcHost,
|
||||
use_ldaps=args.use_ldaps
|
||||
)
|
||||
|
||||
logger.debug("Authentication successful!")
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
python-ldap
|
||||
ldap3
|
||||
impacket
|
||||
sectools
|
||||
Reference in New Issue
Block a user