async def open(self): if not self.dce: await rr(self.connect()) await rr(self.dce.bind(drsuapi.MSRPC_UUID_DRSUAPI)) request = drsuapi.DRSBind() request['puuidClientDsa'] = drsuapi.NTDSAPI_CLIENT_GUID drs = drsuapi.DRS_EXTENSIONS_INT() drs['cb'] = len(drs) #- 4 drs['dwFlags'] = drsuapi.DRS_EXT_GETCHGREQ_V6 | drsuapi.DRS_EXT_GETCHGREPLY_V6 | drsuapi.DRS_EXT_GETCHGREQ_V8 | \ drsuapi.DRS_EXT_STRONG_ENCRYPTION drs['SiteObjGuid'] = drsuapi.NULLGUID drs['Pid'] = 0 drs['dwReplEpoch'] = 0 drs['dwFlagsExt'] = 0 drs['ConfigObjGUID'] = drsuapi.NULLGUID # I'm uber potential (c) Ben drs['dwExtCaps'] = 0xffffffff request['pextClient']['cb'] = len(drs) request['pextClient']['rgb'] = list(drs.getData()) resp, _ = await rr(self.dce.request(request)) # Let's dig into the answer to check the dwReplEpoch. This field should match the one we send as part of # DRSBind's DRS_EXTENSIONS_INT(). If not, it will fail later when trying to sync data. drsExtensionsInt = drsuapi.DRS_EXTENSIONS_INT() # If dwExtCaps is not included in the answer, let's just add it so we can unpack DRS_EXTENSIONS_INT right. ppextServer = b''.join(resp['ppextServer']['rgb']) + b'\x00' * ( len(drsuapi.DRS_EXTENSIONS_INT()) - resp['ppextServer']['cb']) drsExtensionsInt.fromString(ppextServer) if drsExtensionsInt['dwReplEpoch'] != 0: # Different epoch, we have to call DRSBind again if logger.level == logging.DEBUG: logger.debug( "DC's dwReplEpoch != 0, setting it to %d and calling DRSBind again" % drsExtensionsInt['dwReplEpoch']) drs['dwReplEpoch'] = drsExtensionsInt['dwReplEpoch'] request['pextClient']['cb'] = len(drs) request['pextClient']['rgb'] = list(drs.getData()) resp, _ = await rr(self.dce.request(request)) self.handle = resp['phDrs'] # Now let's get the NtdsDsaObjectGuid UUID to use when querying NCChanges resp, _ = await rr( drsuapi.hDRSDomainControllerInfo(self.dce, self.handle, self.domainname, 2)) if logger.level == logging.DEBUG: logger.debug('DRSDomainControllerInfo() answer %s' % resp.dump()) if resp['pmsgOut']['V2']['cItems'] > 0: self.__NtdsDsaObjectGuid = resp['pmsgOut']['V2']['rItems'][0][ 'NtdsDsaObjectGuid'] else: logger.error("Couldn't get DC info for domain %s" % self.domainname) raise Exception('Fatal, aborting!') return True, None
async def __handle_smb_in(self): """ Waits from SMB messages from the NetBIOSTransport in_queue, and fills the connection table. This function started automatically when calling connect. """ try: while True: msg, err = await self.netbios_transport.in_queue.get() self.activity_at = datetime.datetime.utcnow() if err is not None: logger.error( '__handle_smb_in got error from transport layer %s' % err) #setting all outstanding events to finished for mid in self.OutstandingResponsesEvent: self.OutstandingResponses[mid] = None self.OutstandingResponsesEvent[mid].set() await self.terminate() return logger.log( 1, '__handle_smb_in got new message with Id %s' % msg.header.MessageId) if isinstance(msg, SMB2Transform): #message is encrypted #this point we should decrypt it and only store the decrypted part in the OutstandingResponses table #but for now we just thropw exception bc encryption is not implemented raise Exception( 'Encrypted SMBv2 message recieved, but encryption is not yet supported!' ) if msg.header.Status == NTStatus.PENDING: self.pending_table[msg.header.MessageId] = SMBPendingMsg( msg.header.MessageId, self.OutstandingResponses, self.OutstandingResponsesEvent) await self.pending_table[msg.header.MessageId].run() continue if msg.header.MessageId in self.pending_table: await self.pending_table[msg.header.MessageId].stop() del self.pending_table[msg.header.MessageId] self.OutstandingResponses[msg.header.MessageId] = msg if msg.header.MessageId in self.OutstandingResponsesEvent: self.OutstandingResponsesEvent[msg.header.MessageId].set() else: #here we are loosing messages, the functionality for "PENDING" and "SHARING_VIOLATION" should be implemented continue except asyncio.CancelledError: #the SMB connection is terminating return except: logger.exception('__handle_smb_in')
async def keepalive(self): """ Sends an echo message every X seconds to the server to keep the channel open """ try: sleep_time = 10 if self.target.timeout < 0: return elif self.target.timeout > 0: sleep_time = max(self.target.timeout - 1, sleep_time) while True: await asyncio.sleep(sleep_time) if (datetime.datetime.utcnow() - self.activity_at).seconds > sleep_time: await self.echo() except asyncio.CancelledError: return except Exception as e: logger.error('Keepalive failed! Server probably disconnected!') await self.disconnect()
async def negotiate(self): """ Initiates protocol negotiation. First we send an SMB_COM_NEGOTIATE_REQ with our supported dialects """ #let's construct an SMBv1 SMB_COM_NEGOTIATE_REQ packet header = SMBHeader() header.Command = SMBCommand.SMB_COM_NEGOTIATE header.Status = NTStatus.SUCCESS header.Flags = 0 header.Flags2 = SMBHeaderFlags2Enum.SMB_FLAGS2_UNICODE command = SMB_COM_NEGOTIATE_REQ() command.Dialects = ['SMB 2.???'] msg = SMBMessage(header, command) message_id = await self.sendSMB(msg) #recieveing reply, should be version2, because currently we dont support v1 :( rply = await self.recvSMB(message_id) #negotiate MessageId should be 1 if rply.header.Status == NTStatus.SUCCESS: if isinstance(rply, SMB2Message): if rply.command.DialectRevision == NegotiateDialects.WILDCARD: command = NEGOTIATE_REQ() command.SecurityMode = NegotiateSecurityMode.SMB2_NEGOTIATE_SIGNING_ENABLED | NegotiateSecurityMode.SMB2_NEGOTIATE_SIGNING_REQUIRED command.Capabilities = 0 command.ClientGuid = self.ClientGUID command.Dialects = self.dialects header = SMB2Header_SYNC() header.Command = SMB2Command.NEGOTIATE header.CreditReq = 0 msg = SMB2Message(header, command) message_id = await self.sendSMB(msg) rply = await self.recvSMB( message_id) #negotiate MessageId should be 1 if rply.header.Status != NTStatus.SUCCESS: print('session got reply!') print(rply) raise Exception( 'session_setup_1 (authentication probably failed) reply: %s' % rply.header.Status) if rply.command.DialectRevision not in self.supported_dialects: raise SMBUnsupportedDialectSelected() self.selected_dialect = rply.command.DialectRevision self.signing_required = NegotiateSecurityMode.SMB2_NEGOTIATE_SIGNING_ENABLED in rply.command.SecurityMode logger.log( 1, 'Server selected dialect: %s' % self.selected_dialect) self.MaxTransactSize = min(0x100000, rply.command.MaxTransactSize) self.MaxReadSize = min(0x100000, rply.command.MaxReadSize) self.MaxWriteSize = min(0x100000, rply.command.MaxWriteSize) self.ServerGuid = rply.command.ServerGuid self.SupportsMultiChannel = NegotiateCapabilities.MULTI_CHANNEL in rply.command.Capabilities else: logger.error( 'Server choose SMB v1 which is not supported currently') raise SMBUnsupportedSMBVersion() else: print('session got reply!') print(rply) raise Exception( 'session_setup_1 (authentication probably failed) reply: %s' % rply.header.Status) self.status = SMBConnectionStatus.SESSIONSETUP
async def get_user_secrets(self, username): ra = { 'userPrincipalName': '1.2.840.113556.1.4.656', 'sAMAccountName': '1.2.840.113556.1.4.221', 'unicodePwd': '1.2.840.113556.1.4.90', 'dBCSPwd': '1.2.840.113556.1.4.55', 'ntPwdHistory': '1.2.840.113556.1.4.94', 'lmPwdHistory': '1.2.840.113556.1.4.160', 'supplementalCredentials': '1.2.840.113556.1.4.125', 'objectSid': '1.2.840.113556.1.4.146', 'pwdLastSet': '1.2.840.113556.1.4.96', 'userAccountControl': '1.2.840.113556.1.4.8' } formatOffered = drsuapi.DS_NT4_ACCOUNT_NAME_SANS_DOMAIN crackedName, _ = await rr( self.DRSCrackNames(formatOffered, drsuapi.DS_NAME_FORMAT.DS_UNIQUE_ID_NAME, name=username)) ###### TODO: CHECKS HERE #guid = GUID.from_string(crackedName['pmsgOut']['V1']['pResult']['rItems'][0]['pName'][:-1][1:-1]) guid = crackedName['pmsgOut']['V1']['pResult']['rItems'][0][ 'pName'][:-1][1:-1] userRecord, _ = await rr(self.DRSGetNCChanges(guid, ra)) replyVersion = 'V%d' % userRecord['pdwOutVersion'] if userRecord['pmsgOut'][replyVersion]['cNumObjects'] == 0: raise Exception('DRSGetNCChanges didn\'t return any object!') #print(userRecord.dump()) #print(userRecord['pmsgOut'][replyVersion]['PrefixTableSrc']['pPrefixEntry']) record = userRecord prefixTable = userRecord['pmsgOut'][replyVersion]['PrefixTableSrc'][ 'pPrefixEntry'] ##### decryption! logger.debug('Decrypting hash for user: %s' % record['pmsgOut'][replyVersion]['pNC']['StringName'][:-1]) us = SMBUserSecrets() user_properties = None rid = int.from_bytes(record['pmsgOut'][replyVersion]['pObjects'] ['Entinf']['pName']['Sid'][-4:], 'little', signed=False) for attr in record['pmsgOut'][replyVersion]['pObjects']['Entinf'][ 'AttrBlock']['pAttr']: try: attId = drsuapi.OidFromAttid(prefixTable, attr['attrTyp']) LOOKUP_TABLE = self.ATTRTYP_TO_ATTID except Exception as e: logger.error( 'Failed to execute OidFromAttid with error %s, fallbacking to fixed table' % e) logger.error('Exception', exc_info=True) # Fallbacking to fixed table and hope for the best attId = attr['attrTyp'] LOOKUP_TABLE = self.NAME_TO_ATTRTYP if attId == LOOKUP_TABLE['dBCSPwd']: if attr['AttrVal']['valCount'] > 0: encrypteddBCSPwd = b''.join( attr['AttrVal']['pAVal'][0]['pVal']) encryptedLMHash = drsuapi.DecryptAttributeValue( self.dce.get_session_key(), encrypteddBCSPwd) us.lm_hash = drsuapi.removeDESLayer(encryptedLMHash, rid) else: us.lm_hash = bytes.fromhex( 'aad3b435b51404eeaad3b435b51404ee') elif attId == LOOKUP_TABLE['unicodePwd']: if attr['AttrVal']['valCount'] > 0: encryptedUnicodePwd = b''.join( attr['AttrVal']['pAVal'][0]['pVal']) encryptedNTHash = drsuapi.DecryptAttributeValue( self.dce.get_session_key(), encryptedUnicodePwd) us.nt_hash = drsuapi.removeDESLayer(encryptedNTHash, rid) else: us.nt_hash = bytes.fromhex( '31d6cfe0d16ae931b73c59d7e0c089c0') elif attId == LOOKUP_TABLE['userPrincipalName']: if attr['AttrVal']['valCount'] > 0: try: us.domain = b''.join( attr['AttrVal']['pAVal'][0]['pVal']).decode( 'utf-16le').split('@')[-1] except: us.domain = None else: us.domain = None elif attId == LOOKUP_TABLE['sAMAccountName']: if attr['AttrVal']['valCount'] > 0: try: us.username = b''.join(attr['AttrVal']['pAVal'][0] ['pVal']).decode('utf-16le') except Exception as e: logger.error('Cannot get sAMAccountName for %s' % record['pmsgOut'][replyVersion]['pNC'] ['StringName'][:-1]) us.username = '******' else: logger.error('Cannot get sAMAccountName for %s' % record['pmsgOut'][replyVersion]['pNC'] ['StringName'][:-1]) us.username = '******' elif attId == LOOKUP_TABLE['objectSid']: if attr['AttrVal']['valCount'] > 0: us.object_sid = SID.from_bytes(b''.join( attr['AttrVal']['pAVal'][0]['pVal'])) else: logger.error('Cannot get objectSid for %s' % record['pmsgOut'][replyVersion]['pNC'] ['StringName'][:-1]) us.object_sid = rid elif attId == LOOKUP_TABLE['pwdLastSet']: if attr['AttrVal']['valCount'] > 0: try: us.pwd_last_set = FILETIME.from_bytes( b''.join(attr['AttrVal']['pAVal'][0] ['pVal'])).datetime.isoformat() except Exception as e: logger.error('Cannot get pwdLastSet for %s' % record['pmsgOut'][replyVersion]['pNC'] ['StringName'][:-1]) us.pwd_last_set = None elif attId == LOOKUP_TABLE['userAccountControl']: if attr['AttrVal']['valCount'] > 0: us.user_account_status = int.from_bytes(b''.join( attr['AttrVal']['pAVal'][0]['pVal']), 'little', signed=False) else: us.user_account_status = None if attId == LOOKUP_TABLE['lmPwdHistory']: if attr['AttrVal']['valCount'] > 0: encryptedLMHistory = b''.join( attr['AttrVal']['pAVal'][0]['pVal']) tmpLMHistory = drsuapi.DecryptAttributeValue( self.dce.get_session_key(), encryptedLMHistory) for i in range(0, len(tmpLMHistory) // 16): LMHashHistory = drsuapi.removeDESLayer( tmpLMHistory[i * 16:(i + 1) * 16], rid) us.lm_history.append(LMHashHistory) else: logger.debug('No lmPwdHistory for user %s' % record['pmsgOut'][replyVersion]['pNC'] ['StringName'][:-1]) elif attId == LOOKUP_TABLE['ntPwdHistory']: if attr['AttrVal']['valCount'] > 0: encryptedNTHistory = b''.join( attr['AttrVal']['pAVal'][0]['pVal']) tmpNTHistory = drsuapi.DecryptAttributeValue( self.dce.get_session_key(), encryptedNTHistory) for i in range(0, len(tmpNTHistory) // 16): NTHashHistory = drsuapi.removeDESLayer( tmpNTHistory[i * 16:(i + 1) * 16], rid) us.nt_history.append(NTHashHistory) else: logger.debug('No ntPwdHistory for user %s' % record['pmsgOut'][replyVersion]['pNC'] ['StringName'][:-1]) elif attId == LOOKUP_TABLE['supplementalCredentials']: if attr['AttrVal']['valCount'] > 0: blob = b''.join(attr['AttrVal']['pAVal'][0]['pVal']) supplementalCredentials = drsuapi.DecryptAttributeValue( self.dce.get_session_key(), blob) if len(supplementalCredentials) < 24: supplementalCredentials = None else: try: user_properties = samr.USER_PROPERTIES( supplementalCredentials) except Exception as e: # On some old w2k3 there might be user properties that don't # match [MS-SAMR] structure, discarding them pass if user_properties is not None: propertiesData = user_properties['UserProperties'] for propertyCount in range(user_properties['PropertyCount']): userProperty = samr.USER_PROPERTY(propertiesData) propertiesData = propertiesData[len(userProperty):] # For now, we will only process Newer Kerberos Keys and CLEARTEXT if userProperty['PropertyName'].decode( 'utf-16le') == 'Primary:Kerberos-Newer-Keys': propertyValueBuffer = bytes.fromhex( userProperty['PropertyValue'].decode()) kerbStoredCredentialNew = samr.KERB_STORED_CREDENTIAL_NEW( propertyValueBuffer) data = kerbStoredCredentialNew['Buffer'] for credential in range( kerbStoredCredentialNew['CredentialCount']): keyDataNew = samr.KERB_KEY_DATA_NEW(data) data = data[len(keyDataNew):] keyValue = propertyValueBuffer[ keyDataNew['KeyOffset']:][:keyDataNew['KeyLength']] if keyDataNew['KeyType'] in self.KERBEROS_TYPE: answer = ( self.KERBEROS_TYPE[keyDataNew['KeyType']], keyValue) else: answer = (hex(keyDataNew['KeyType']), keyValue) # We're just storing the keys, not printing them, to make the output more readable # This is kind of ugly... but it's what I came up with tonight to get an ordered # set :P. Better ideas welcomed ;) us.kerberos_keys.append(answer) elif userProperty['PropertyName'].decode( 'utf-16le') == 'Primary:CLEARTEXT': # [MS-SAMR] 3.1.1.8.11.5 Primary:CLEARTEXT Property # This credential type is the cleartext password. The value format is the UTF-16 encoded cleartext password. # SkelSec: well, almost. actually the property is the hex-encoded bytes of an UTF-16LE encoded plaintext string encoded_pw = bytes.fromhex( userProperty['PropertyValue'].decode('ascii')) try: answer = encoded_pw.decode('utf-16le') except UnicodeDecodeError: # This could be because we're decoding a machine password. Printing it hex answer = encoded_pw.decode('utf-8') us.cleartext_pwds.append(answer) return us, None