|
| 1 | +#!/usr/bin/env python3 |
| 2 | +#Impacket - Collection of Python classes for working with network protocols. |
| 3 | +# |
| 4 | +# SECUREAUTH LABS. Copyright (C) 2022 SecureAuth Corporation. All rights reserved. |
| 5 | + |
| 6 | +# Description: |
| 7 | +# This module will try to get the Machine Account Quota from the domain attribute ms-DS-MachineAccountQuota. |
| 8 | +# If the value is superior to 0, it tries to list any computer object created by a user and returns the machine |
| 9 | +# name and its creator sAMAccountName and SID. |
| 10 | +# |
| 11 | +# Author: |
| 12 | +# TahiTi |
| 13 | +# |
| 14 | + |
| 15 | +import argparse |
| 16 | +import logging |
| 17 | +import sys |
| 18 | +import ldap3 |
| 19 | +import ssl |
| 20 | +import traceback |
| 21 | +from binascii import unhexlify |
| 22 | + |
| 23 | +from ldap3.protocol.formatters.formatters import format_sid |
| 24 | +import ldapdomaindump |
| 25 | +from impacket import version |
| 26 | +from impacket.examples import logger, utils |
| 27 | +from impacket.examples.utils import parse_credentials |
| 28 | +from impacket.ldap import ldap, ldapasn1 |
| 29 | +from impacket.smbconnection import SMBConnection |
| 30 | +from impacket.spnego import SPNEGO_NegTokenInit, TypesMech |
| 31 | + |
| 32 | +class GetMachineAccountQuota: |
| 33 | + def __init__(self, ldap_server, ldap_session, args): |
| 34 | + self.ldap_server = ldap_server |
| 35 | + self.ldap_session = ldap_session |
| 36 | + |
| 37 | + logging.debug('Initializing domainDumper()') |
| 38 | + cnf = ldapdomaindump.domainDumpConfig() |
| 39 | + cnf.basepath = None |
| 40 | + self.domain_dumper = ldapdomaindump.domainDumper(self.ldap_server, self.ldap_session, cnf) |
| 41 | + |
| 42 | + def machineAccountQuota(self, maq): |
| 43 | + try: |
| 44 | + self.ldap_session.search(self.domain_dumper.root, '(objectClass=*)', attributes=['mS-DS-MachineAccountQuota']) |
| 45 | + maq = self.ldap_session.entries[0]['mS-DS-MachineAccountQuota'].values[0] |
| 46 | + logging.info('MachineAccountQuota: %s' % maq) |
| 47 | + return maq |
| 48 | + except ldap.LDAPSearchError: |
| 49 | + raise |
| 50 | + |
| 51 | + def maqUsers(self): |
| 52 | + self.ldap_session.search(self.domain_dumper.root, '(&(objectCategory=computer)(mS-DS-CreatorSID=*))', attributes=['mS-DS-CreatorSID']) |
| 53 | + logging.info("Retrieving non privileged domain users that added a machine account...") |
| 54 | + users_sid = [] |
| 55 | + if len(self.ldap_session.entries) != 0: |
| 56 | + for entry in self.ldap_session.entries: |
| 57 | + user_sid = format_sid(entry['mS-DS-CreatorSID'].values[0]) |
| 58 | + self.ldap_session.search(self.domain_dumper.root, '(objectSID=%s)' % user_sid, attributes=['objectSID', 'sAMAccountName']) |
| 59 | + if user_sid in users_sid: |
| 60 | + continue |
| 61 | + else: |
| 62 | + users_sid.append(user_sid) |
| 63 | + logging.info('sAMAccountName : %s' % self.ldap_session.entries[0]['sAMAccountName'].values[0]) |
| 64 | + logging.info('User SID : %s ' % user_sid) |
| 65 | + else: |
| 66 | + logging.info("No non-privileged user added a computer to the domain.") |
| 67 | + |
| 68 | +def parse_args(): |
| 69 | + parser = argparse.ArgumentParser(add_help=True, description='Retrieve the machine account quota value from the domain.') |
| 70 | + |
| 71 | + parser.add_argument('identity', action='store', help='domain/username[:password]') |
| 72 | + parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') |
| 73 | + parser.add_argument('-use-ldaps', action='store_true', help='Use LDAPS instead of LDAP') |
| 74 | + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') |
| 75 | + |
| 76 | + group = parser.add_argument_group('authentication') |
| 77 | + |
| 78 | + group.add_argument('-hashes', action='store', metavar='LMHASH:NTHASH', help='NTLM hashes, format is LMHASH:NTHASH') |
| 79 | + group.add_argument('-no-pass', action='store_true', help='don\'t ask for password (useful for -k)') |
| 80 | + group.add_argument('-k', action='store_true', |
| 81 | + help='Use Kerberos authentication. Grabs credentials from ccache file ' |
| 82 | + '(KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use the ' |
| 83 | + 'ones specified in the command line') |
| 84 | + group.add_argument('-aesKey', action='store', metavar='hex key', help='AES key to use for Kerberos Authentication ' |
| 85 | + '(128 or 256 bits)') |
| 86 | + group.add_argument('-dc-ip', action='store', metavar='ip address', help='IP Address of the domain controller. If ' |
| 87 | + 'omitted it use the domain part (FQDN) specified in the target parameter') |
| 88 | + |
| 89 | + if len(sys.argv) == 1: |
| 90 | + parser.print_help() |
| 91 | + sys.exit(1) |
| 92 | + |
| 93 | + return parser.parse_args() |
| 94 | + |
| 95 | +def parse_identity(args): |
| 96 | + domain, username, password = utils.parse_credentials(args.identity) |
| 97 | + |
| 98 | + if domain == '': |
| 99 | + logging.critical('Domain should be specified!') |
| 100 | + sys.exit(1) |
| 101 | + |
| 102 | + if password == '' and username != '' and args.hashes is None and args.no_pass is False and args.aesKey is None: |
| 103 | + from getpass import getpass |
| 104 | + logging.info("No credentials supplied, supply password") |
| 105 | + password = getpass("Password:") |
| 106 | + |
| 107 | + if args.aesKey is not None: |
| 108 | + args.k = True |
| 109 | + |
| 110 | + if args.hashes is not None: |
| 111 | + lmhash, nthash = args.hashes.split(':') |
| 112 | + else: |
| 113 | + lmhash = '' |
| 114 | + nthash = '' |
| 115 | + |
| 116 | + return domain, username, password, lmhash, nthash |
| 117 | + |
| 118 | +def init_logger(args): |
| 119 | + #Init the example's logger theme and debug level |
| 120 | + logger.init(args.ts) |
| 121 | + if args.debug is True: |
| 122 | + logging.getLogger().setLevel(logging.DEBUG) |
| 123 | + # Print the Library's installation path |
| 124 | + logging.debug(version.getInstallationPath()) |
| 125 | + else: |
| 126 | + logging.getLogger().setLevel(logging.INFO) |
| 127 | + logging.getLogger('impacket.smbserver').setLevel(logging.ERROR) |
| 128 | + |
| 129 | +def get_machine_name(args, domain): |
| 130 | + if args.dc_ip is not None: |
| 131 | + s = SMBConnection(args.dc_ip, args.dc_ip) |
| 132 | + else: |
| 133 | + s = SMBConnection(domain, domain) |
| 134 | + try: |
| 135 | + s.login('', '') |
| 136 | + except Exception: |
| 137 | + if s.getServerName() == '': |
| 138 | + raise Exception('Error while anonymous logging into %s' % domain) |
| 139 | + else: |
| 140 | + s.logoff() |
| 141 | + return s.getServerName() |
| 142 | + |
| 143 | +def ldap3_kerberos_login(connection, target, user, password, domain='', lmhash='', nthash='', aesKey='', kdcHost=None, |
| 144 | + TGT=None, TGS=None, useCache=True): |
| 145 | + from pyasn1.codec.ber import encoder, decoder |
| 146 | + from pyasn1.type.univ import noValue |
| 147 | + """ |
| 148 | + logins into the target system explicitly using Kerberos. Hashes are used if RC4_HMAC is supported. |
| 149 | + :param string user: username |
| 150 | + :param string password: password for the user |
| 151 | + :param string domain: domain where the account is valid for (required) |
| 152 | + :param string lmhash: LMHASH used to authenticate using hashes (password is not used) |
| 153 | + :param string nthash: NTHASH used to authenticate using hashes (password is not used) |
| 154 | + :param string aesKey: aes256-cts-hmac-sha1-96 or aes128-cts-hmac-sha1-96 used for Kerberos authentication |
| 155 | + :param string kdcHost: hostname or IP Address for the KDC. If None, the domain will be used (it needs to resolve tho) |
| 156 | + :param struct TGT: If there's a TGT available, send the structure here and it will be used |
| 157 | + :param struct TGS: same for TGS. See smb3.py for the format |
| 158 | + :param bool useCache: whether or not we should use the ccache for credentials lookup. If TGT or TGS are specified this is False |
| 159 | + :return: True, raises an Exception if error. |
| 160 | + """ |
| 161 | + |
| 162 | + if lmhash != '' or nthash != '': |
| 163 | + if len(lmhash) % 2: |
| 164 | + lmhash = '0' + lmhash |
| 165 | + if len(nthash) % 2: |
| 166 | + nthash = '0' + nthash |
| 167 | + try: # just in case they were converted already |
| 168 | + lmhash = unhexlify(lmhash) |
| 169 | + nthash = unhexlify(nthash) |
| 170 | + except TypeError: |
| 171 | + pass |
| 172 | + |
| 173 | + # Importing down here so pyasn1 is not required if kerberos is not used. |
| 174 | + from impacket.krb5.ccache import CCache |
| 175 | + from impacket.krb5.asn1 import AP_REQ, Authenticator, TGS_REP, seq_set |
| 176 | + from impacket.krb5.kerberosv5 import getKerberosTGT, getKerberosTGS |
| 177 | + from impacket.krb5 import constants |
| 178 | + from impacket.krb5.types import Principal, KerberosTime, Ticket |
| 179 | + import datetime |
| 180 | + |
| 181 | + if TGT is not None or TGS is not None: |
| 182 | + useCache = False |
| 183 | + |
| 184 | + target = 'ldap/%s' % target |
| 185 | + if useCache: |
| 186 | + logging.info('dans la co kerberos la target est : %s' % target) |
| 187 | + domain, user, TGT, TGS = CCache.parseFile(domain, user, target) |
| 188 | + |
| 189 | + # First of all, we need to get a TGT for the user |
| 190 | + userName = Principal(user, type=constants.PrincipalNameType.NT_PRINCIPAL.value) |
| 191 | + if TGT is None: |
| 192 | + if TGS is None: |
| 193 | + tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, password, domain, lmhash, nthash, |
| 194 | + aesKey, kdcHost) |
| 195 | + else: |
| 196 | + tgt = TGT['KDC_REP'] |
| 197 | + cipher = TGT['cipher'] |
| 198 | + sessionKey = TGT['sessionKey'] |
| 199 | + |
| 200 | + if TGS is None: |
| 201 | + serverName = Principal(target, type=constants.PrincipalNameType.NT_SRV_INST.value) |
| 202 | + tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS(serverName, domain, kdcHost, tgt, cipher, |
| 203 | + sessionKey) |
| 204 | + else: |
| 205 | + tgs = TGS['KDC_REP'] |
| 206 | + cipher = TGS['cipher'] |
| 207 | + sessionKey = TGS['sessionKey'] |
| 208 | + |
| 209 | + # Let's build a NegTokenInit with a Kerberos REQ_AP |
| 210 | + |
| 211 | + blob = SPNEGO_NegTokenInit() |
| 212 | + |
| 213 | + # Kerberos |
| 214 | + blob['MechTypes'] = [TypesMech['MS KRB5 - Microsoft Kerberos 5']] |
| 215 | + |
| 216 | + # Let's extract the ticket from the TGS |
| 217 | + tgs = decoder.decode(tgs, asn1Spec=TGS_REP())[0] |
| 218 | + ticket = Ticket() |
| 219 | + ticket.from_asn1(tgs['ticket']) |
| 220 | + |
| 221 | + # Now let's build the AP_REQ |
| 222 | + apReq = AP_REQ() |
| 223 | + apReq['pvno'] = 5 |
| 224 | + apReq['msg-type'] = int(constants.ApplicationTagNumbers.AP_REQ.value) |
| 225 | + |
| 226 | + opts = [] |
| 227 | + apReq['ap-options'] = constants.encodeFlags(opts) |
| 228 | + seq_set(apReq, 'ticket', ticket.to_asn1) |
| 229 | + |
| 230 | + authenticator = Authenticator() |
| 231 | + authenticator['authenticator-vno'] = 5 |
| 232 | + authenticator['crealm'] = domain |
| 233 | + seq_set(authenticator, 'cname', userName.components_to_asn1) |
| 234 | + now = datetime.datetime.utcnow() |
| 235 | + |
| 236 | + authenticator['cusec'] = now.microsecond |
| 237 | + authenticator['ctime'] = KerberosTime.to_asn1(now) |
| 238 | + |
| 239 | + encodedAuthenticator = encoder.encode(authenticator) |
| 240 | + |
| 241 | + # Key Usage 11 |
| 242 | + # AP-REQ Authenticator (includes application authenticator |
| 243 | + # subkey), encrypted with the application session key |
| 244 | + # (Section 5.5.1) |
| 245 | + encryptedEncodedAuthenticator = cipher.encrypt(sessionKey, 11, encodedAuthenticator, None) |
| 246 | + |
| 247 | + apReq['authenticator'] = noValue |
| 248 | + apReq['authenticator']['etype'] = cipher.enctype |
| 249 | + apReq['authenticator']['cipher'] = encryptedEncodedAuthenticator |
| 250 | + |
| 251 | + blob['MechToken'] = encoder.encode(apReq) |
| 252 | + |
| 253 | + request = ldap3.operation.bind.bind_operation(connection.version, ldap3.SASL, user, None, 'GSS-SPNEGO', |
| 254 | + blob.getData()) |
| 255 | + |
| 256 | + # Done with the Kerberos saga, now let's get into LDAP |
| 257 | + if connection.closed: # try to open connection if closed |
| 258 | + connection.open(read_server_info=False) |
| 259 | + |
| 260 | + connection.sasl_in_progress = True |
| 261 | + response = connection.post_send_single_response(connection.send('bindRequest', request, None)) |
| 262 | + connection.sasl_in_progress = False |
| 263 | + if response[0]['result'] != 0: |
| 264 | + raise Exception(response) |
| 265 | + |
| 266 | + connection.bound = True |
| 267 | + |
| 268 | + return True |
| 269 | + |
| 270 | +def init_ldap_connection(target, tls_version, args, domain, username, password, lmhash, nthash): |
| 271 | + user = '%s\\%s' % (domain, username) |
| 272 | + connect_to = target |
| 273 | + if args.dc_ip is not None: |
| 274 | + connect_to = args.dc_ip |
| 275 | + if tls_version is not None: |
| 276 | + use_ssl = True |
| 277 | + port = 636 |
| 278 | + tls = ldap3.Tls(validate=ssl.CERT_NONE, version=tls_version) |
| 279 | + else: |
| 280 | + use_ssl = False |
| 281 | + port = 389 |
| 282 | + tls = None |
| 283 | + ldap_server = ldap3.Server(connect_to, get_info=ldap3.ALL, port=port, use_ssl=use_ssl, tls=tls) |
| 284 | + if args.k: |
| 285 | + ldap_session = ldap3.Connection(ldap_server) |
| 286 | + ldap_session.bind() |
| 287 | + ldap3_kerberos_login(ldap_session, target, username, password, domain, lmhash, nthash, args.aesKey, kdcHost=args.dc_ip) |
| 288 | + elif args.hashes is not None: |
| 289 | + ldap_session = ldap3.Connection(ldap_server, user=user, password=lmhash + ":" + nthash, authentication=ldap3.NTLM, auto_bind=True) |
| 290 | + else: |
| 291 | + ldap_session = ldap3.Connection(ldap_server, user=user, password=password, authentication=ldap3.NTLM, auto_bind=True) |
| 292 | + |
| 293 | + return ldap_server, ldap_session |
| 294 | + |
| 295 | +def init_ldap_session(args, domain, username, password, lmhash, nthash): |
| 296 | + if args.k: |
| 297 | + target = get_machine_name(args, domain) |
| 298 | + else: |
| 299 | + if args.dc_ip is not None: |
| 300 | + target = args.dc_ip |
| 301 | + else: |
| 302 | + target = domain |
| 303 | + |
| 304 | + if args.use_ldaps is True: |
| 305 | + try: |
| 306 | + return init_ldap_connection(target, ssl.PROTOCOL_TLSv1_2, args, domain, username, password, lmhash, nthash) |
| 307 | + except ldap3.core.exceptions.LDAPSocketOpenError: |
| 308 | + return init_ldap_connection(target, ssl.PROTOCOL_TLSv1, args, domain, username, password, lmhash, nthash) |
| 309 | + else: |
| 310 | + return init_ldap_connection(target, None, args, domain, username, password, lmhash, nthash) |
| 311 | + |
| 312 | +def main(): |
| 313 | + print(version.BANNER) |
| 314 | + args = parse_args() |
| 315 | + init_logger(args) |
| 316 | + |
| 317 | + if args.debug is True: |
| 318 | + logging.getLogger().setLevel(logging.DEBUG) |
| 319 | + # Print the Library's installation path |
| 320 | + logging.debug(version.getInstallationPath()) |
| 321 | + else: |
| 322 | + logging.getLogger().setLevel(logging.INFO) |
| 323 | + |
| 324 | + domain, username, password, lmhash, nthash = parse_identity(args) |
| 325 | + machine_account_quota = 0 |
| 326 | + |
| 327 | + try: |
| 328 | + ldap_server, ldap_session = init_ldap_session(args, domain, username, password, lmhash, nthash) |
| 329 | + execute = GetMachineAccountQuota(ldap_server, ldap_session, args) |
| 330 | + |
| 331 | + if execute.machineAccountQuota(machine_account_quota) != 0: |
| 332 | + execute.maqUsers() |
| 333 | + |
| 334 | + except Exception as e: |
| 335 | + if logging.getLogger().level == logging.DEBUG: |
| 336 | + traceback.print_exc() |
| 337 | + logging.error(str(e)) |
| 338 | + |
| 339 | +if __name__ == '__main__': |
| 340 | + main() |
0 commit comments