Skip to content

Commit

Permalink
Added example script samedit.py
Browse files Browse the repository at this point in the history
  • Loading branch information
Iorpim committed Jun 11, 2024
1 parent 15eff88 commit ce927ae
Show file tree
Hide file tree
Showing 3 changed files with 337 additions and 2 deletions.
121 changes: 121 additions & 0 deletions examples/samedit.py
Original file line number Diff line number Diff line change
@@ -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()
174 changes: 173 additions & 1 deletion impacket/examples/secretsdump.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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("<L",rid) + constant )
rc4 = ARC4.new(rc4Key)
encryptedHash = rc4.encrypt(key)
else:
encryptedHash = self.__cryptoCommon.encryptAES(self.__hashedBootKey[:0x10], key, salt)

return encryptedHash

def __replaceValue(self, obj, offset, value):
obj = bytearray(obj)
for i, v in enumerate(value):
obj[offset + i] = v
return bytes(obj)

def dump(self):
NTPASSWORD = b"NTPASSWORD\0"
Expand Down Expand Up @@ -1473,6 +1524,127 @@ def dump(self):
ntHash = ntlm.NTOWFv1('','')

answer = "%s:%d:%s:%s:::" % (userName, rid, hexlify(lmHash).decode('utf-8'), hexlify(ntHash).decode('utf-8'))

self.__itemsFound[rid] = answer
self.__perSecretCallback(answer)

def edit(self, user, newNTHash, newLMHash=b''):
NTPASSWORD = b"NTPASSWORD\0"
LMPASSWORD = b"LMPASSWORD\0"

if self.__samFile is None:
# No SAM file provided
return

LOG.info('Editing local SAM hash for user "%s"' % user)
self.getHBootKey()

usersKey = 'SAM\\Domains\\Account\\Users'

# Enumerate all the RIDs
rids = self.enumKey(usersKey)
# Remove the Names item
try:
rids.remove('Names')
except:
pass

# Iterate through RIDs
for rid in rids:
userAccount = USER_ACCOUNT_V(self.getValue(ntpath.join(usersKey,rid,'V'))[1])
_rid = rid
rid = int(rid,16)

V = userAccount['Data']

userName = V[userAccount['NameOffset']:userAccount['NameOffset']+userAccount['NameLength']].decode('utf-16le')

# Check for requested user
if(userName.casefold() == user.casefold()):
LOG.debug('Located rid for "%s": %d' % (user, rid))
else:
continue

# User has no hash data
if userAccount['NTHashLength'] == 0:
logging.error('SAM hashes change for user %s failed. The account doesn\'t have hash information.' % userName)
return

# Retrieve old hashes to parse hash parameters and display values before the change
encNTHash = b''
if V[userAccount['NTHashOffset']:][2:3] == b'\x01':
# Old Style hashes
newStyle = False
if userAccount['LMHashLength'] == 20:
encLMHash = SAM_HASH(V[userAccount['LMHashOffset']:][:userAccount['LMHashLength']])
if userAccount['NTHashLength'] == 20:
encNTHash = SAM_HASH(V[userAccount['NTHashOffset']:][:userAccount['NTHashLength']])
else:
# New Style hashes
newStyle = True
if userAccount['LMHashLength'] == 24:
encLMHash = SAM_HASH_AES(V[userAccount['LMHashOffset']:][:userAccount['LMHashLength']])
encNTHash = SAM_HASH_AES(V[userAccount['NTHashOffset']:][:userAccount['NTHashLength']])

LOG.debug('NewStyle hashes is: %s' % newStyle)
LOG.debug('LMHashLength: %d - NTHashLength: %d' % (userAccount['LMHashLength'], userAccount['NTHashLength']))
if userAccount['LMHashLength'] >= 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)

Expand Down Expand Up @@ -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:
Expand Down
Loading

0 comments on commit ce927ae

Please sign in to comment.