diff --git a/examples/samedit.py b/examples/samedit.py new file mode 100644 index 0000000000..7043ca8475 --- /dev/null +++ b/examples/samedit.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# Copyright (C) 2024 Fortra. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# Simple implementation for replacing a local user's password through +# editing of a copy of the SAM and SYSTEM hives. +# +# It still needs some improvement to handle some scenarios and expanded +# to allow user creation/password setting as it currently only allows +# for the replacing of an existing password for an existing user. +# +# Author: +# Otavio Brito (@Iorpim) +# +# References: +# The code is largely based on previous impacket work, namely +# the secretsdump and winregistry packages. (both by @agsolino) +# + +import sys +import codecs +import argparse +import logging +import binascii + +from impacket import version, ntlm +from impacket.examples import logger + +from impacket.examples.secretsdump import LocalOperations, SAMHashes + +try: + input = raw_input +except NameError: + pass + + +if __name__ == '__main__': + if sys.stdout.encoding is None: + sys.stdout = codecs.getWriter('utf8')(sys.stdout) + + print(version.BANNER) + + parser = argparse.ArgumentParser(add_help = True, description = "In-place edits a local user's password in a SAM hive file") + + parser.add_argument('user', action='store', help='Name of the user account to replace the password') + parser.add_argument('sam', action='store', help='SAM hive file to edit') + + parser.add_argument('-password', action='store', help='New password to be set') + parser.add_argument('-hashes', action='store', help='Replace NTLM hash directly (LM hash is optional)') + + parser.add_argument('-system', action='store', help='SYSTEM hive file containing the bootkey for password encryption') + parser.add_argument('-bootkey', action='store', help='Bootkey used to encrypt and decrypt SAM passwords') + + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') + + + if len(sys.argv) < 4: + parser.print_help() + sys.exit(1) + + options = parser.parse_args() + + logger.init(options.ts) + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + + if options.system is None and options.bootkey is None: + logging.critical('A SYSTEM hive or bootkey value is required for password changing') + sys.exit(1) + + if options.system is not None and options.bootkey is not None: + logging.critical('Only a SYSTEM hive or bootkey value can be supplied') + sys.exit(1) + + if options.password is None and options.hash is None: + logging.critical('A password or hash argument is required') + sys.exit(1) + + if options.password is not None and options.hash is not None: + logging.critical('Only a password or hash argument can be supplied') + sys.exit(1) + + if options.bootkey: + bootkey = binascii.unhexlify(options.bootkey) + else: + localOperations = LocalOperations(options.system) + bootkey = localOperations.getBootKey() + + hive = SAMHashes(options.sam, bootkey, False) + + if options.hash: + if ':' not in options.hash: + LMHash = b'' + NTHash = binascii.unhexlify(options.hash) + else: + LMHash, NTHash = [binascii.unhexlify(hash) for hash in options.hash.split(":")] + + if options.password: + LMHash = b'' + NTHash = ntlm.NTOWFv1(options.password) + + try: + hive.edit(options.user, NTHash, LMHash) + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + logging.error(e) + + hive.finish() \ No newline at end of file diff --git a/impacket/examples/secretsdump.py b/impacket/examples/secretsdump.py index a3f2af0609..bad83e78fc 100644 --- a/impacket/examples/secretsdump.py +++ b/impacket/examples/secretsdump.py @@ -1296,6 +1296,24 @@ def decryptAES(key, value, iv=b'\x00'*16): plainText += aes256.decrypt(cipherBuffer) return plainText + + @staticmethod + def encryptAES(key, value, iv=b'\x00'*16): + cipherText = b'' + if iv != b'\x00'*16: + aes256 = AES.new(key,AES.MODE_CBC, iv) + + # Pad input to 16 bytes using PKCS7 + pad = 16 - (len(value) % 16) + value += bytes([pad]*pad) + + for index in range(0, len(value), 16): + if iv == b'\x00'*16: + aes256 = AES.new(key,AES.MODE_CBC, iv) + plainBuffer = value[index:index+16] + cipherText += aes256.encrypt(plainBuffer) + + return cipherText class OfflineRegistry: @@ -1331,6 +1349,14 @@ def getValue(self, keyValue): return return value + + def setValue(self, keyValue, dataValue): + value = self.__registryHive.setValue(keyValue, dataValue) + + if value is None: + return + + return value def getClass(self, className): value = self.__registryHive.getClass(className) @@ -1407,6 +1433,31 @@ def __decryptHash(self, rid, cryptedHash, constant, newStyle = False): decryptedHash = Crypt1.decrypt(key[:8]) + Crypt2.decrypt(key[8:]) return decryptedHash + + def __encryptHash(self, rid, plaintextHash, salt, constant, newStyle = False): + # Section 2.2.11.1.1 Encrypting an NT or LM Hash Value with a Specified Key + # plus hashedBootKey stuff (as well) + Key1,Key2 = self.__cryptoCommon.deriveKey(rid) + + Crypt1 = DES.new(Key1, DES.MODE_ECB) + Crypt2 = DES.new(Key2, DES.MODE_ECB) + + key = Crypt1.encrypt(plaintextHash[:8]) + Crypt2.encrypt(plaintextHash[8:]) + + if newStyle is False: + rc4Key = self.MD5( self.__hashedBootKey[:0x10] + pack("= 20: + lmHash = self.__decryptHash(rid, encLMHash, LMPASSWORD, newStyle) + else: + lmHash = b'' + newLMHash = b'' + + if encNTHash != b'': + ntHash = self.__decryptHash(rid, encNTHash, NTPASSWORD, newStyle) + else: + ntHash = b'' + newNTHash = b'' + + userChanged = False + if newLMHash != b'': + encLMHash['Hash'] = self.__encryptHash(rid, newLMHash, encLMHash['Salt'], LMPASSWORD, newStyle) + if userAccount['LMHashLength'] != len(encLMHash.getData()): + LOG.error('Mistaching LM lengths received.') + LOG.info('User probably has an empty password. Unable to set new LM hash.') + LOG.debug('Received: %d - Expected: %d' % (userAccount['LMHashLength'], len(encLMHash.getData()))) + newLMHash = b'' + # Missing LM data is unlikely to be a failure scenario, keep going + else: + userAccount['Data'] = self.__replaceValue(V, userAccount['LMHashOffset'], encLMHash.getData()) + userChanged = True + + if newNTHash != b'': + encNTHash['Hash'] = self.__encryptHash(rid, newNTHash, encNTHash['Salt'], NTPASSWORD, newStyle) + if userAccount['NTHashLength'] != len(encNTHash.getData()): + LOG.error("Mistaching NT lengths received!") + LOG.info("User probably has an empty password. Unable to set new NT hash.") + LOG.debug(f"Received: {userAccount['NTHashLength']} - Expected: {len(encNTHash.getData())}") + # Missing NT data *is* a failure scenario, return + return + userAccount['Data'] = self.__replaceValue(V, userAccount['NTHashOffset'], encNTHash.getData()) + userChanged = True + + if lmHash == b'': + lmHash = ntlm.LMOWFv1('','') + if ntHash == b'': + ntHash = ntlm.NTOWFv1('','') + + LOG.info("Previous user hash: %s:%d:%s:%s:::" % (userName, rid, hexlify(lmHash).decode('utf-8'), hexlify(ntHash).decode('utf-8'))) + + if userChanged: + if self.setValue(ntpath.join(usersKey,_rid,'V'), userAccount.getData()) is None: + LOG.error('Failed to write new user hash to SAM hive.') + return + else: + LOG.info("Unable to change user hash, please ensure the target user already has a password set.") + + if newLMHash == b'': + newLMHash = ntlm.LMOWFv1('', '') + if newNTHash == b'': + newNTHash = ntlm.NTOWFv1('', '') + + answer = "%s:%s:%s:%s:::" % (userName, rid, hexlify(newLMHash).decode('utf-8'), hexlify(newNTHash).decode('utf-8')) + self.__itemsFound[rid] = answer self.__perSecretCallback(answer) @@ -2755,7 +2927,7 @@ def dump(self): else: LOG.warning('DRSCrackNames returned %d items for user %s, skipping' % ( crackedName['pmsgOut']['V1']['pResult']['cItems'], user) -) + ) #userRecord.dump() replyVersion = 'V%d' % userRecord['pdwOutVersion'] if userRecord['pmsgOut'][replyVersion]['cNumObjects'] == 0: diff --git a/impacket/winregistry.py b/impacket/winregistry.py index d45cbb387a..6a7b44f14d 100644 --- a/impacket/winregistry.py +++ b/impacket/winregistry.py @@ -165,7 +165,7 @@ def __init__(self, hive, isRemote = False): self.fd = self.__hive self.__hive.open() else: - self.fd = open(hive,'rb') + self.fd = open(hive,'r+b') data = self.fd.read(4096) self.__regf = REG_REGF(data) self.indent = '' @@ -233,6 +233,10 @@ def __getValueBlocks(self, offset, count): def __getData(self, offset, count): self.fd.seek(4096+offset, 0) return self.fd.read(count)[4:] + + def __setData(self, offset, value): + self.fd.seek(4096+offset+4, 0) + return self.fd.write(value) def __processDataBlocks(self,data): res = [] @@ -264,6 +268,23 @@ def __getValueData(self, rec): return rec['OffsetData'] else: return self.__getData(rec['OffsetData'], rec['DataLen']+4) + + def __setValueData(self, rec, value): + if len(value) != rec['DataLen']: + # The case of data stored in the Offset field itself still needs more + # work as it's necessary to identify the offset in the file to overwrite it. + # Leaving unimplemented for now as there's no clear use case yet. + # if rec['DataLen'] < 0: + # if len(value) <= 4: + # rec['OffsetData'] = int.from_bytes(value) + LOG.debug("Invalid value length received by __setValueData. Expected: %d - Got: %d" % (rec['DataLen'], len(value))) + # This is a much more relevant scenario that should be revisited and properly implemented. + raise NotImplementedError("Setting key values with differing lengths is not implemented.") + if rec['DataLen'] == 0: + LOG.debug("Received 0 length input for __setValueData.") + return 0 + else: + return self.__setData(rec['OffsetData'], value) def __getLhHash(self, key): res = 0 @@ -470,6 +491,27 @@ def getValue(self, keyValue): return value['ValueType'], self.__getValueData(value) return None + + def setValue(self, keyValue, valueData): + # Returns a tuple with (ValueType, BytesWritten) for the request keyValue + regKey = ntpath.dirname(keyValue) + regValue = ntpath.basename(keyValue) + + key = self.findKey(regKey) + + if key is None: + return None + + if key['NumValues'] > 0: + valueList = self.__getValueBlocks(key['OffsetValueList'], key['NumValues']+1) + + for value in valueList: + if value['Name'] == b(regValue): + return value['ValueType'], self.__setValueData(value, valueData) + elif regValue == 'default' and value['Flag'] <=0: + return value['ValueType'], self.__setValueData(value, valueData) + + return None def getClass(self, className):