#!/usr/bin/env python3 # -*- coding: utf-8 -*- import ldap3 from impacket.dcerpc.v5 import samr, epm, transport class NXCModule: ''' Module by CyberCelt: @Cyb3rC3lt Initial module: https://github.com/Cyb3rC3lt/CrackMapExec-Modules Thanks to the guys at impacket for the original code ''' name = 'add-computer' description = 'Adds or deletes a domain computer' supported_protocols = ['smb'] opsec_safe = True multiple_hosts = False def options(self, context, module_options): ''' add-computer: Specify add-computer to call the module using smb NAME: Specify the NAME option to name the Computer to be added PASSWORD: Specify the PASSWORD option to supply a password for the Computer to be added DELETE: Specify DELETE to remove a Computer CHANGEPW: Specify CHANGEPW to modify a Computer password Usage: nxc smb $DC-IP -u Username -p Password -M add-computer -o NAME="BADPC" PASSWORD="Password1" nxc smb $DC-IP -u Username -p Password -M add-computer -o NAME="BADPC" DELETE=True nxc smb $DC-IP -u Username -p Password -M add-computer -o NAME="BADPC" PASSWORD="Password2" CHANGEPW=True ''' self.__baseDN = None self.__computerGroup = None self.__method = "SAMR" self.__noAdd = False self.__delete = False self.noLDAPRequired = False if 'DELETE' in module_options: self.__delete = True if 'CHANGEPW' in module_options and ('NAME' not in module_options or 'PASSWORD' not in module_options): context.log.error('NAME and PASSWORD options are required!') elif 'CHANGEPW' in module_options: self.__noAdd = True if 'NAME' in module_options: self.__computerName = module_options['NAME'] if self.__computerName[-1] != '$': self.__computerName += '$' else: context.log.error('NAME option is required!') exit(1) if 'PASSWORD' in module_options: self.__computerPassword = module_options['PASSWORD'] elif 'PASSWORD' not in module_options and not self.__delete: context.log.error('PASSWORD option is required!') exit(1) def on_login(self, context, connection): #Set some variables self.__domain = connection.domain self.__domainNetbios = connection.domain self.__kdcHost = connection.hostname + "." + connection.domain self.__target = self.__kdcHost self.__username = connection.username self.__password = connection.password self.__targetIp = connection.host self.__port = context.smb_server_port self.__aesKey = context.aesKey self.__hashes = context.hash self.__doKerberos = connection.kerberos self.__nthash = "" self.__lmhash = "" if context.hash and ":" in context.hash[0]: hashList = context.hash[0].split(":") self.__nthash = hashList[-1] self.__lmhash = hashList[0] elif context.hash and ":" not in context.hash[0]: self.__nthash = context.hash[0] self.__lmhash = "00000000000000000000000000000000" # First try to add via SAMR over SMB self.doSAMRAdd(context) # If SAMR fails now try over LDAPS if not self.noLDAPRequired: self.doLDAPSAdd(connection,context) else: exit(1) def doSAMRAdd(self,context): if self.__targetIp is not None: stringBinding = epm.hept_map(self.__targetIp, samr.MSRPC_UUID_SAMR, protocol = 'ncacn_np') else: stringBinding = epm.hept_map(self.__target, samr.MSRPC_UUID_SAMR, protocol = 'ncacn_np') rpctransport = transport.DCERPCTransportFactory(stringBinding) rpctransport.set_dport(self.__port) if self.__targetIp is not None: rpctransport.setRemoteHost(self.__targetIp) rpctransport.setRemoteName(self.__target) if hasattr(rpctransport, 'set_credentials'): # This method exists only for selected protocol sequences. rpctransport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, self.__aesKey) rpctransport.set_kerberos(self.__doKerberos, self.__kdcHost) dce = rpctransport.get_dce_rpc() servHandle = None domainHandle = None userHandle = None try: dce.connect() dce.bind(samr.MSRPC_UUID_SAMR) samrConnectResponse = samr.hSamrConnect5(dce, '\\\\%s\x00' % self.__target, samr.SAM_SERVER_ENUMERATE_DOMAINS | samr.SAM_SERVER_LOOKUP_DOMAIN ) servHandle = samrConnectResponse['ServerHandle'] samrEnumResponse = samr.hSamrEnumerateDomainsInSamServer(dce, servHandle) domains = samrEnumResponse['Buffer']['Buffer'] domainsWithoutBuiltin = list(filter(lambda x : x['Name'].lower() != 'builtin', domains)) if len(domainsWithoutBuiltin) > 1: domain = list(filter(lambda x : x['Name'].lower() == self.__domainNetbios, domains)) if len(domain) != 1: context.log.highlight(u'{}'.format( 'This domain does not exist: "' + self.__domainNetbios + '"')) logging.critical("Available domain(s):") for domain in domains: logging.error(" * %s" % domain['Name']) raise Exception() else: selectedDomain = domain[0]['Name'] else: selectedDomain = domainsWithoutBuiltin[0]['Name'] samrLookupDomainResponse = samr.hSamrLookupDomainInSamServer(dce, servHandle, selectedDomain) domainSID = samrLookupDomainResponse['DomainId'] if logging.getLogger().level == logging.DEBUG: logging.info("Opening domain %s..." % selectedDomain) samrOpenDomainResponse = samr.hSamrOpenDomain(dce, servHandle, samr.DOMAIN_LOOKUP | samr.DOMAIN_CREATE_USER , domainSID) domainHandle = samrOpenDomainResponse['DomainHandle'] if self.__noAdd or self.__delete: try: checkForUser = samr.hSamrLookupNamesInDomain(dce, domainHandle, [self.__computerName]) except samr.DCERPCSessionError as e: if e.error_code == 0xc0000073: context.log.highlight(u'{}'.format( self.__computerName + ' not found in domain ' + selectedDomain)) self.noLDAPRequired = True raise Exception() else: raise userRID = checkForUser['RelativeIds']['Element'][0] if self.__delete: access = samr.DELETE message = "delete" else: access = samr.USER_FORCE_PASSWORD_CHANGE message = "set the password for" try: openUser = samr.hSamrOpenUser(dce, domainHandle, access, userRID) userHandle = openUser['UserHandle'] except samr.DCERPCSessionError as e: if e.error_code == 0xc0000022: context.log.highlight(u'{}'.format( self.__username + ' does not have the right to ' + message + " " + self.__computerName)) self.noLDAPRequired = True raise Exception() else: raise else: if self.__computerName is not None: try: checkForUser = samr.hSamrLookupNamesInDomain(dce, domainHandle, [self.__computerName]) self.noLDAPRequired = True context.log.highlight(u'{}'.format( 'Computer account already exists with the name: "' + self.__computerName + '"')) raise Exception() except samr.DCERPCSessionError as e: if e.error_code != 0xc0000073: raise else: foundUnused = False while not foundUnused: self.__computerName = self.generateComputerName() try: checkForUser = samr.hSamrLookupNamesInDomain(dce, domainHandle, [self.__computerName]) except samr.DCERPCSessionError as e: if e.error_code == 0xc0000073: foundUnused = True else: raise try: createUser = samr.hSamrCreateUser2InDomain(dce, domainHandle, self.__computerName, samr.USER_WORKSTATION_TRUST_ACCOUNT, samr.USER_FORCE_PASSWORD_CHANGE,) self.noLDAPRequired = True context.log.highlight('Successfully added the machine account: "' + self.__computerName + '" with Password: "' + self.__computerPassword + '"') except samr.DCERPCSessionError as e: if e.error_code == 0xc0000022: context.log.highlight(u'{}'.format( 'The following user does not have the right to create a computer account: "' + self.__username + '"')) raise Exception() elif e.error_code == 0xc00002e7: context.log.highlight(u'{}'.format( 'The following user exceeded their machine account quota: "' + self.__username + '"')) raise Exception() else: raise userHandle = createUser['UserHandle'] if self.__delete: samr.hSamrDeleteUser(dce, userHandle) context.log.highlight(u'{}'.format('Successfully deleted the "' + self.__computerName + '" Computer account')) self.noLDAPRequired=True userHandle = None else: samr.hSamrSetPasswordInternal4New(dce, userHandle, self.__computerPassword) if self.__noAdd: context.log.highlight(u'{}'.format( 'Successfully set the password of machine "' + self.__computerName + '" with password "' + self.__computerPassword + '"')) self.noLDAPRequired=True else: checkForUser = samr.hSamrLookupNamesInDomain(dce, domainHandle, [self.__computerName]) userRID = checkForUser['RelativeIds']['Element'][0] openUser = samr.hSamrOpenUser(dce, domainHandle, samr.MAXIMUM_ALLOWED, userRID) userHandle = openUser['UserHandle'] req = samr.SAMPR_USER_INFO_BUFFER() req['tag'] = samr.USER_INFORMATION_CLASS.UserControlInformation req['Control']['UserAccountControl'] = samr.USER_WORKSTATION_TRUST_ACCOUNT samr.hSamrSetInformationUser2(dce, userHandle, req) if not self.noLDAPRequired: context.log.highlight(u'{}'.format( 'Successfully added the machine account "' + self.__computerName + '" with Password: "' + self.__computerPassword + '"')) self.noLDAPRequired = True except Exception as e: if logging.getLogger().level == logging.DEBUG: import traceback traceback.print_exc() finally: if userHandle is not None: samr.hSamrCloseHandle(dce, userHandle) if domainHandle is not None: samr.hSamrCloseHandle(dce, domainHandle) if servHandle is not None: samr.hSamrCloseHandle(dce, servHandle) dce.disconnect() def doLDAPSAdd(self, connection, context): ldap_domain = connection.domain.replace(".", ",dc=") spns = [ 'HOST/%s' % self.__computerName, 'HOST/%s.%s' % (self.__computerName, connection.domain), 'RestrictedKrbHost/%s' % self.__computerName, 'RestrictedKrbHost/%s.%s' % (self.__computerName, connection.domain), ] ucd = { 'dnsHostName': '%s.%s' % (self.__computerName, connection.domain), 'userAccountControl': 0x1000, 'servicePrincipalName': spns, 'sAMAccountName': self.__computerName, 'unicodePwd': ('"%s"' % self.__computerPassword).encode('utf-16-le') } tls = ldap3.Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLSv1_2, ciphers='ALL:@SECLEVEL=0') ldapServer = ldap3.Server(connection.host, use_ssl=True, port=636, get_info=ldap3.ALL, tls=tls) c = Connection(ldapServer, connection.username + '@' + connection.domain, connection.password) c.bind() if (self.__delete): result = c.delete("cn=" + self.__computerName + ",cn=Computers,dc=" + ldap_domain) if result: context.log.highlight(u'{}'.format('Successfully deleted the "' + self.__computerName + '" Computer account')) elif result == False and c.last_error == "noSuchObject": context.log.highlight(u'{}'.format('Computer named "' + self.__computerName + '" was not found')) elif result == False and c.last_error == "insufficientAccessRights": context.log.highlight( u'{}'.format('Insufficient Access Rights to delete the Computer "' + self.__computerName + '"')) else: context.log.highlight(u'{}'.format( 'Unable to delete the "' + self.__computerName + '" Computer account. The error was: ' + c.last_error)) else: result = c.add("cn=" + self.__computerName + ",cn=Computers,dc=" + ldap_domain, ['top', 'person', 'organizationalPerson', 'user', 'computer'], ucd) if result: context.log.highlight('Successfully added the machine account: "' + self.__computerName + '" with Password: "' + self.__computerPassword + '"') context.log.highlight(u'{}'.format('You can try to verify this with the nxc command:')) context.log.highlight(u'{}'.format( 'nxc ldap ' + connection.host + ' -u ' + connection.username + ' -p ' + connection.password + ' -M group-mem -o GROUP="Domain Computers"')) elif result == False and c.last_error == "entryAlreadyExists": context.log.highlight(u'{}'.format('The Computer account "' + self.__computerName + '" already exists')) elif not result: context.log.highlight(u'{}'.format( 'Unable to add the "' + self.__computerName + '" Computer account. The error was: ' + c.last_error)) c.unbind()