Files
2025-06-24 15:34:21 +02:00

204 lines
8.6 KiB
Python

import ssl
from binascii import unhexlify
from typing import Any
import ldap3
from R2Log import logger
from impacket.smbconnection import SMBConnection
from impacket.spnego import SPNEGO_NegTokenInit, TypesMech
def get_machine_name(dc_ip: str, domain: str):
if dc_ip is not None:
s = SMBConnection(dc_ip, 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: Any, tls_version: Any, kerberos: bool, domain: str, username: str, password: str, lm_hash: str, nt_hash: str, aes_key: str, dc_ip: str):
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 kerberos:
ldap_session = ldap3.Connection(ldap_server)
ldap_session.bind()
ldap3_kerberos_login(connection=ldap_session, target=target, username=username, password=password, domain=domain, lm_hash=lm_hash, nt_hash=nt_hash, aes_key=aes_key, kdc_host=dc_ip)
elif not (lm_hash == '' and nt_hash == ''):
ldap_session = ldap3.Connection(ldap_server, user=user, password=lm_hash + ":" + nt_hash, 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 ldap3_kerberos_login(connection: ldap3.Connection, target: str, username: str, password: str, domain='', lm_hash='', nt_hash='', aes_key='', kdc_host=None, tgt=None, st=None, use_cache=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 lm_hash != '' or nt_hash != '':
if len(lm_hash) % 2:
lm_hash = '0' + lm_hash
if len(nt_hash) % 2:
nt_hash = '0' + nt_hash
try: # just in case they were converted already
lm_hash = unhexlify(lm_hash)
nt_hash = unhexlify(nt_hash)
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 st is not None:
use_cache = False
target = 'ldap/%s' % target
if use_cache:
domain, username, tgt, st = CCache.parseFile(domain, username, target)
# First of all, we need to get a TGT for the user
userName = Principal(username, type=constants.PrincipalNameType.NT_PRINCIPAL.value)
if tgt is None:
if st is None:
tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, password, domain, lm_hash, nt_hash, aes_key, kdc_host)
else:
tgt = tgt['KDC_REP']
cipher = tgt['cipher']
sessionKey = tgt['sessionKey']
if st is None:
serverName = Principal(target, type=constants.PrincipalNameType.NT_SRV_INST.value)
tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS(serverName, domain, kdc_host, tgt, cipher, sessionKey)
else:
tgs = st['KDC_REP']
cipher = st['cipher']
sessionKey = st['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, username, 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
class LdapClient:
server = None
session = None
def __init__(self, domain: str, username: str, password: str, lm_hash: str, nt_hash: str, aes_key: str, kerberos: bool, dc_ip: str, use_ldaps: bool):
if kerberos:
target = get_machine_name(dc_ip=dc_ip, domain=domain)
else:
if dc_ip is not None:
target = dc_ip
else:
target = domain
if use_ldaps is True:
try:
self.server, self.session = init_ldap_connection(target=target, tls_version=ssl.PROTOCOL_TLSv1_2, domain=domain, username=username, password=password, lm_hash=lm_hash, nt_hash=nt_hash, aes_key=aes_key, dc_ip=dc_ip, kerberos=kerberos)
except ldap3.core.exceptions.LDAPSocketOpenError:
self.server, self.session = init_ldap_connection(target=target, tls_version=ssl.PROTOCOL_TLSv1, domain=domain, username=username, password=password, lm_hash=lm_hash, nt_hash=nt_hash, aes_key=aes_key, dc_ip=dc_ip, kerberos=kerberos)
else:
self.server, self.session = init_ldap_connection(target=target, tls_version=None, domain=domain, username=username, password=password, lm_hash=lm_hash, nt_hash=nt_hash, aes_key=aes_key, dc_ip=dc_ip, kerberos=kerberos)
def check_ldap_result(self):
if self.session.result['result'] == 0:
return True
else:
if self.session.result['result'] == 50:
logger.error('Could not modify object, the server reports insufficient rights: %s', self.session.result['message'])
elif self.session.result['result'] == 19:
logger.error('Could not modify object, the server reports a constrained violation: %s', self.session.result['message'])
else:
logger.error('The server returned an error: %s', self.session.result['message'])
return False