Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Samedit.py script addition - modify SAM remotely #73

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading