From d2a79360abc500f20e64c10cdfa4b4ebc4190cb6 Mon Sep 17 00:00:00 2001 From: John Hammond Date: Fri, 18 Jun 2021 19:50:37 -0400 Subject: [PATCH 1/9] Added system localtime module --- .../windows/enumerate/system/localtime.py | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 pwncat/modules/windows/enumerate/system/localtime.py diff --git a/pwncat/modules/windows/enumerate/system/localtime.py b/pwncat/modules/windows/enumerate/system/localtime.py new file mode 100644 index 00000000..3d9663a6 --- /dev/null +++ b/pwncat/modules/windows/enumerate/system/localtime.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 + +import datetime + +import rich.markup + +from pwncat.db import Fact +from pwncat.modules import ModuleFailed +from pwncat.platform.windows import Windows, PowershellError +from pwncat.modules.enumerate import EnumerateModule + + +class LocalTime(Fact): + def __init__(self, source, localtime_string: str): + super().__init__(source=source, types=["system.localtime"]) + + self.localtime_string: str = localtime_string + self.localtime: str = datetime.datetime.strptime( + localtime_string, "%A, %B %d, %Y %I:%M:%S %p" + ) + + def title(self, session): + return f"Local time is: {rich.markup.escape(self.localtime_string)}" + + +class Module(EnumerateModule): + """Enumerate the current Windows Defender settings on the target""" + + PROVIDES = ["system.localtime"] + PLATFORM = [Windows] + + def enumerate(self, session): + + try: + result = session.platform.powershell('Get-Date -Format "F"') + + if not result: + return + + if isinstance(result[0], list) and result: + date_time = result[0] + else: + date_time = result[0] + + except PowershellError as exc: + raise ModuleFailed("failed to retrieve local time") from exc + + yield LocalTime(self.name, date_time) From 91bfbcedbc6bfcd67b5bb49d08e3088ca366adc1 Mon Sep 17 00:00:00 2001 From: John Hammond Date: Fri, 18 Jun 2021 20:29:13 -0400 Subject: [PATCH 2/9] Added Audit Settings enumeration module --- .../windows/enumerate/system/auditsettings.py | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 pwncat/modules/windows/enumerate/system/auditsettings.py diff --git a/pwncat/modules/windows/enumerate/system/auditsettings.py b/pwncat/modules/windows/enumerate/system/auditsettings.py new file mode 100644 index 00000000..01c9933b --- /dev/null +++ b/pwncat/modules/windows/enumerate/system/auditsettings.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 + +import datetime +from typing import Any + +import rich.markup + +from pwncat.db import Fact +from pwncat.modules import ModuleFailed +from pwncat.platform.windows import Windows, PowershellError +from pwncat.modules.enumerate import EnumerateModule + + +class AuditSettings(Fact): + def __init__(self, source, setting: str, value: Any): + super().__init__(source=source, types=["system.auditsettings"]) + + self.setting = setting + self.value = value + + def title(self, session): + return f"[yellow]Audit [bold]{rich.markup.escape(self.setting)}[/bold][/yellow] = {rich.markup.escape(str(self.value))}" + + +class Module(EnumerateModule): + """Enumerate the current Windows Defender settings on the target""" + + PROVIDES = ["system.auditsettings"] + PLATFORM = [Windows] + + def enumerate(self, session): + + try: + result = session.platform.powershell( + "Get-ItemProperty HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System\\Audit" + ) + + if not result: + return + + if isinstance(result[0], list) and result: + settings = result[0] + else: + settings = result[0] + + for setting, value in settings.items(): + # Skip default/boilerplate values + if setting not in ( + "PSPath", + "PSParentPath", + "PSChildName", + "PSProvider", + "PSDrive", + ): + yield AuditSettings(self.name, setting, value) + + except PowershellError as exc: + raise ModuleFailed("failed to retrieve audit settings") from exc From dc48b5d264a16bc4be0ce8f104dc615335bd8e30 Mon Sep 17 00:00:00 2001 From: John Hammond Date: Fri, 18 Jun 2021 21:39:11 -0400 Subject: [PATCH 3/9] Added LAPS enumeration module --- .../windows/enumerate/protections/laps.py | 142 ++++++++++++++++++ .../windows/enumerate/protections/uac.py | 4 +- 2 files changed, 144 insertions(+), 2 deletions(-) create mode 100644 pwncat/modules/windows/enumerate/protections/laps.py diff --git a/pwncat/modules/windows/enumerate/protections/laps.py b/pwncat/modules/windows/enumerate/protections/laps.py new file mode 100644 index 00000000..63462754 --- /dev/null +++ b/pwncat/modules/windows/enumerate/protections/laps.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 + +from typing import Dict + +from pwncat.db import Fact +from pwncat.modules import ModuleFailed +from pwncat.platform.windows import Windows, PowershellError +from pwncat.modules.enumerate import EnumerateModule + + +class LAPSData(Fact): + def __init__(self, source, registry_values: Dict): + super().__init__(source=source, types=["protections.laps"]) + + self.registry_values: bool = registry_values + """ The current setting for LAPS""" + + def __getitem__(self, name): + + return self.registry_values[name] + + def title(self, session): + if not self.registry_values["AdmPwdEnabled"]: + return "AdmPwdEnabled (LAPS) is [bold green]inactive[/bold green]" + + return "AdmPwdEnabled (LAPS) is [bold red]active[/bold red]" + + def description(self, session): + + mappings = { + "AdmPwdEnabled": { + True: "[red]LAPS is [bold]active[/bold][/red]", + False: "[green]LAPS is [bold]inactive[/bold][/green]", + }, + "PDSList": { + True: "[red]configured to use PDS[/red]", + False: "[green][bold]not[/bold] configured to use PDS[/green]", + }, + "UseSharedSPN": { + True: "[red]PDS service runs under domain account[/red]", + False: "[green]PDS service does [bold]not[/bold] run under domain account[/green]", + }, + "ManualPasswordChangeProtectionEnabled": { + True: "[red]manual password change is [bold]not allowed[/bold][/red]", + False: "[green]manual password change [bold]is allowed[/bold][/green]", + }, + "PwdExpirationProtectionEnabled": { + True: "[red]password expiration policy is [bold]active[/bold][/red]", + False: "[green]password expiration policy is [bold]inactive[/bold][/green]", + }, + "PwdHistoryEnabled": { + True: "[red]password history is [bold]maintained[/bold][/red]", + False: "[green]password history is [bold]not maintained[/bold][/green]", + }, + "PwdEncryptionEnabled": { + True: "[red]password encryption is [bold]enabled[/bold][/red]", + False: "[green]password encryption is [bold]not enabled[/bold][/green]", + }, + # "EncryptionKey" : {}, + # "PublicKey" : {}, + # "AdministratorAccountName" : {}, + # "LogLevel" : {}, + # "MaxPasswordAge" : {}, + # "PasswordComplexity" : {}, + } + + output = [] + for registry_name in self.registry_values.keys(): + # Ingore the big LAPS property we have already displayed + if registry_name == "AdmPwdEnabled": + continue + registry_value = self.registry_values[registry_name] + if registry_name in mappings.keys(): + output.append( + f"[cyan]{registry_name}[/cyan] = {registry_value} : {mappings[registry_name][registry_value]}" + ) + else: + if not registry_value: + output.append( + f"[cyan]{registry_name}[/cyan] [green]not set[/green]" + ) + else: + output.append(f"[cyan]{registry_name}[/cyan] = {registry_value}") + + return "\n".join((" - " + line for line in output)) + + +class Module(EnumerateModule): + """Enumerate the current LAPS and password policy settings on the target""" + + PROVIDES = ["protections.laps"] + PLATFORM = [Windows] + + def enumerate(self, session): + + # Reference: + # https://getadmx.com/HKLM/Software/Policies/Microsoft%20Services/AdmPwd + + registry_key = "HKLM:\\Software\\Policies\\Microsoft Services\\AdmPwd\\" + + registry_values = { + "PDSList": bool, + "UseSharedSPN": bool, + "ManualPasswordChangeProtectionEnabled": bool, + "PwdExpirationProtectionEnabled": bool, + "PwdEncryptionEnabled": bool, + "EncryptionKey": str, + "PublicKey": str, + "AdminAccountName": str, + "LogLevel": int, + "MaxPasswordAge": int, + "PasswordComplexity": int, + "PasswordLength": int, + "PasswordAge": int, + "AdmPwdEnabled": bool, + "SupportedForests": str, + "PwdExpirationProtectionEnabled": bool, + "AdminAccountName": bool, + } + + for registry_value, registry_type in registry_values.items(): + try: + result = session.platform.powershell( + f"Get-ItemPropertyValue '{registry_key}' -Name '{registry_value}'" + ) + + if not result: + raise ModuleFailed( + f"failed to retrieve registry value {registry_value}" + ) + + registry_values[registry_value] = registry_type(result[0]) + + except PowershellError as exc: + if "does not exist" in exc.message: + registry_values[registry_value] = registry_type(0) + else: + raise ModuleFailed( + f"could not retrieve registry value {registry_value}: {exc}" + ) from exc + + yield LAPSData(self.name, registry_values) diff --git a/pwncat/modules/windows/enumerate/protections/uac.py b/pwncat/modules/windows/enumerate/protections/uac.py index bf2c3b8d..8e19b8da 100644 --- a/pwncat/modules/windows/enumerate/protections/uac.py +++ b/pwncat/modules/windows/enumerate/protections/uac.py @@ -102,7 +102,7 @@ def enumerate(self, session): for registry_value, registry_type in registry_values.items(): try: result = session.platform.powershell( - f"Get-ItemPropertyValue {registry_key} -Name {registry_value}" + f"Get-ItemPropertyValue '{registry_key}' -Name '{registry_value}'" ) if not result: @@ -120,4 +120,4 @@ def enumerate(self, session): f"could not retrieve registry value {registry_value}: {exc}" ) from exc - yield UACData(self.name, registry_values) + yield UACData(self.name, registry_values) From 1f8e4f5dec66df611af306c1e8360600d8017244 Mon Sep 17 00:00:00 2001 From: John Hammond Date: Fri, 18 Jun 2021 22:23:17 -0400 Subject: [PATCH 4/9] Added enumeration module for event log forwarding --- .../enumerate/system/forwardeventlogs.py | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 pwncat/modules/windows/enumerate/system/forwardeventlogs.py diff --git a/pwncat/modules/windows/enumerate/system/forwardeventlogs.py b/pwncat/modules/windows/enumerate/system/forwardeventlogs.py new file mode 100644 index 00000000..ac5e1f9b --- /dev/null +++ b/pwncat/modules/windows/enumerate/system/forwardeventlogs.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 + + +import rich.markup + +from pwncat.db import Fact +from pwncat.modules import ModuleFailed +from pwncat.platform.windows import Windows, PowershellError +from pwncat.modules.enumerate import EnumerateModule + + +class ForwardEventLogData(Fact): + def __init__(self, source, string: str): + super().__init__(source=source, types=["system.forwardeventlogs"]) + + self.string = string + self.configured = bool(string) # if it is configured, set it + if self.configured: + for setting in self.string.split(","): + variable, value = setting.split("=") + setattr(self, variable, value) + + def title(self, session): + output = "Event log forwarding is " + if self.configured: + output += "[bold red]enabled[/bold red]" + else: + output += "[bold green]not configured[/bold green]" + return output + + def description(self, session): + output = [] + if self.configured: + for setting in self.string.split(","): + variable, value = setting.split("=") + output.append( + f"'{rich.markup.escape(variable)}' = {rich.markup.escape(str(value))}" + ) + output = "\n".join((" - " + line for line in output)) + if not output: + return None + return output + + +class Module(EnumerateModule): + """Enumerate the current Windows Defender settings on the target""" + + PROVIDES = ["system.forwardeventlogs"] + PLATFORM = [Windows] + + def enumerate(self, session): + + try: + result = session.platform.powershell( + "Get-ItemProperty 'HKLM:\\Software\\Policies\\Microsoft\\Windows\\EventLog\\EventForwarding\\SubscriptionManager'" + ) + + if not result: + result = "" + else: + settings = result[0] + for setting, value in settings.items(): + # Skip default/boilerplate values + if setting not in ( + "PSPath", + "PSParentPath", + "PSChildName", + "PSProvider", + "PSDrive", + ): + yield ForwardEventLogData(self.name, value) + + except PowershellError as exc: + if "does not exist" in exc.message: + result = "" # registry path does not exist... not configured + else: + raise ModuleFailed( + "failed to retrieve SubscriptionManager registry key" + ) from exc + + yield ForwardEventLogData(self.name, result) From 6626a47516284b541d13410e303d0f243cde7c86 Mon Sep 17 00:00:00 2001 From: John Hammond Date: Fri, 18 Jun 2021 23:11:37 -0400 Subject: [PATCH 5/9] Added wdigest and cached credentials enumeration modules --- .../windows/enumerate/creds/__init__.py | 0 .../windows/enumerate/creds/cachedcount.py | 53 +++++++++++++++++++ .../windows/enumerate/system/wdigest.py | 53 +++++++++++++++++++ 3 files changed, 106 insertions(+) create mode 100644 pwncat/modules/windows/enumerate/creds/__init__.py create mode 100644 pwncat/modules/windows/enumerate/creds/cachedcount.py create mode 100644 pwncat/modules/windows/enumerate/system/wdigest.py diff --git a/pwncat/modules/windows/enumerate/creds/__init__.py b/pwncat/modules/windows/enumerate/creds/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pwncat/modules/windows/enumerate/creds/cachedcount.py b/pwncat/modules/windows/enumerate/creds/cachedcount.py new file mode 100644 index 00000000..b747e511 --- /dev/null +++ b/pwncat/modules/windows/enumerate/creds/cachedcount.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 + +from typing import Any + +import rich.markup + +from pwncat.db import Fact +from pwncat.modules import ModuleFailed +from pwncat.platform.windows import Windows, PowershellError +from pwncat.modules.enumerate import EnumerateModule + + +class CachedCredsCount(Fact): + def __init__(self, source, count: Any): + super().__init__(source=source, types=["creds.cachedcount"]) + + self.count = count + + def title(self, session): + if self.count: + return f"'CachedLogonsCount' = {rich.markup.escape(self.count)}, you need SYSTEM rights to extract them" + else: + return f"'CachedLogonsCount' = 0" + + +class Module(EnumerateModule): + """Enumerate the number of cached credentials on the target""" + + PROVIDES = ["creds.cachedcount"] + PLATFORM = [Windows] + + def enumerate(self, session): + + try: + result = session.platform.powershell( + "Get-ItemPropertyValue 'HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon' -Name 'CachedLogonsCount'" + ) + + if not result: + yield CachedCredsCount(self.name, count=0) + + if isinstance(result[0], list) and result: + count = result[0] + else: + count = result[0] + + yield CachedCredsCount(self.name, count) + + except PowershellError as exc: + if "does not exist" in exc.message: + yield CachedCredsCount(self.name, count=0) + else: + raise ModuleFailed("failed to retrieve wdigest settings") from exc diff --git a/pwncat/modules/windows/enumerate/system/wdigest.py b/pwncat/modules/windows/enumerate/system/wdigest.py new file mode 100644 index 00000000..38cf8c62 --- /dev/null +++ b/pwncat/modules/windows/enumerate/system/wdigest.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 + +from typing import Any + +import rich.markup + +from pwncat.db import Fact +from pwncat.modules import ModuleFailed +from pwncat.platform.windows import Windows, PowershellError +from pwncat.modules.enumerate import EnumerateModule + + +class WDigest(Fact): + def __init__(self, source, configured: Any): + super().__init__(source=source, types=["system.wdigest"]) + + self.configured = configured + + def title(self, session): + if self.configured: + return f"'UseLogonCredential' wdigest is 1, [bold green]there are plaintext credentials in memory![/bold green]" + else: + return f"'UseLogonCredential' wdigest is not configured" + + +class Module(EnumerateModule): + """Enumerate the current WDigest settings on the target""" + + PROVIDES = ["system.wdigest"] + PLATFORM = [Windows] + + def enumerate(self, session): + + try: + result = session.platform.powershell( + "Get-ItemPropertyValue 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\SecurityProviders\\WDigest\\' -Name 'UseLogonCredential'" + ) + + if not result: + yield WDigest(self.name, configured=False) + + if isinstance(result[0], list) and result: + configured = result[0] + else: + configured = result[0] + + yield WDigest(self.name, configured) + + except PowershellError as exc: + if "does not exist" in exc.message: + yield WDigest(self.name, configured=False) + else: + raise ModuleFailed("failed to retrieve wdigest settings") from exc From c6cc7a02d9d9ab998b9080c2be051e4d64e9f906 Mon Sep 17 00:00:00 2001 From: John Hammond Date: Sat, 19 Jun 2021 00:27:50 -0400 Subject: [PATCH 6/9] Added enumeration modules for PowerShell settings --- .../windows/enumerate/powershell/__init__.py | 0 .../windows/enumerate/powershell/history.py | 48 ++++++++ .../enumerate/powershell/modulelogging.py | 68 ++++++++++++ .../powershell/scriptblocklogging.py | 90 +++++++++++++++ .../enumerate/powershell/transcription.py | 103 ++++++++++++++++++ .../windows/enumerate/powershell/version.py | 53 +++++++++ 6 files changed, 362 insertions(+) create mode 100644 pwncat/modules/windows/enumerate/powershell/__init__.py create mode 100644 pwncat/modules/windows/enumerate/powershell/history.py create mode 100644 pwncat/modules/windows/enumerate/powershell/modulelogging.py create mode 100644 pwncat/modules/windows/enumerate/powershell/scriptblocklogging.py create mode 100644 pwncat/modules/windows/enumerate/powershell/transcription.py create mode 100644 pwncat/modules/windows/enumerate/powershell/version.py diff --git a/pwncat/modules/windows/enumerate/powershell/__init__.py b/pwncat/modules/windows/enumerate/powershell/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pwncat/modules/windows/enumerate/powershell/history.py b/pwncat/modules/windows/enumerate/powershell/history.py new file mode 100644 index 00000000..31bb15fb --- /dev/null +++ b/pwncat/modules/windows/enumerate/powershell/history.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 + +import rich.markup + +from pwncat.db import Fact +from pwncat.modules import ModuleFailed +from pwncat.platform.windows import Windows, PowershellError +from pwncat.modules.enumerate import EnumerateModule + + +class PowerShellHistory(Fact): + def __init__(self, source, path: str): + super().__init__(source=source, types=["powershell.history"]) + + self.path: str = path + + def title(self, session): + if self.path: + return f"PowerShell history file: '{rich.markup.escape(self.path)}'" + else: + return f"[yellow]PowerShell history file not found[/yellow]" + + +class Module(EnumerateModule): + """Enumerate the current Windows Defender settings on the target""" + + PROVIDES = ["powershell.history"] + PLATFORM = [Windows] + + def enumerate(self, session): + + try: + result = session.platform.powershell( + "(Get-PSReadLineOption | select -ExpandProperty HistorySavePath)" + ) + + if not result: + return PowerShellHistory(self.name, "") + + if isinstance(result[0], list) and result: + path = "\n".join(result[0]) + else: + path = result[0] + + except PowershellError as exc: + raise ModuleFailed("failed to retrieve powershell history file") from exc + + yield PowerShellHistory(self.name, path) diff --git a/pwncat/modules/windows/enumerate/powershell/modulelogging.py b/pwncat/modules/windows/enumerate/powershell/modulelogging.py new file mode 100644 index 00000000..492a4070 --- /dev/null +++ b/pwncat/modules/windows/enumerate/powershell/modulelogging.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 + +from typing import Dict + +import rich.markup + +from pwncat.db import Fact +from pwncat.modules import ModuleFailed +from pwncat.platform.windows import Windows, PowershellError +from pwncat.modules.enumerate import EnumerateModule + + +class PowerShellModuleLogging(Fact): + def __init__(self, source, registry_values: Dict): + super().__init__(source=source, types=["powershell.modulelogging"]) + + self.registry_values: bool = registry_values + """ The current setting for PowerShell transcription""" + + def __getitem__(self, name): + + return self.registry_values[name] + + def title(self, session): + if not self.registry_values["EnableModuleLogging"]: + return "[green]PowerShell Module Logging is [bold]disabled[/bold][/green]" + + return "[red]PowerShell Module Logging is [bold]enabled[/bold][/red]" + + +class Module(EnumerateModule): + """Enumerate the current PowerShell module logging settings on the target""" + + PROVIDES = ["powershell.modulelogging"] + PLATFORM = [Windows] + + def enumerate(self, session): + + registry_key = ( + "HKLM:\\SOFTWARE\\Policies\\Microsoft\\Windows\\PowerShell\\ModuleLogging" + ) + + registry_values = { + "EnableModuleLogging": bool, + } + + for registry_value, registry_type in registry_values.items(): + try: + result = session.platform.powershell( + f"Get-ItemPropertyValue '{registry_key}' -Name '{registry_value}'" + ) + + if not result: + raise ModuleFailed( + f"failed to retrieve registry value {registry_value}" + ) + + registry_values[registry_value] = registry_type(result[0]) + + except PowershellError as exc: + if "does not exist" in exc.message: + registry_values[registry_value] = registry_type(0) + else: + raise ModuleFailed( + f"could not retrieve registry value {registry_value}: {exc}" + ) from exc + + yield PowerShellModuleLogging(self.name, registry_values) diff --git a/pwncat/modules/windows/enumerate/powershell/scriptblocklogging.py b/pwncat/modules/windows/enumerate/powershell/scriptblocklogging.py new file mode 100644 index 00000000..1a8f872a --- /dev/null +++ b/pwncat/modules/windows/enumerate/powershell/scriptblocklogging.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 + +from typing import Dict + +import rich.markup + +from pwncat.db import Fact +from pwncat.modules import ModuleFailed +from pwncat.platform.windows import Windows, PowershellError +from pwncat.modules.enumerate import EnumerateModule + + +class PowerShellScriptBlockLogging(Fact): + def __init__(self, source, registry_values: Dict): + super().__init__(source=source, types=["powershell.scriptblocklogging"]) + + self.registry_values: bool = registry_values + """ The current setting for PowerShell transcription""" + + def __getitem__(self, name): + + return self.registry_values[name] + + def title(self, session): + if not self.registry_values["EnableScriptBlockLogging"]: + return "[green]PowerShell Script Block Logging is [bold]disabled[/bold][/green]" + + return "[red]PowerShell Script Block is [bold]enabled[/bold][/red]" + + def description(self, session): + if not self.registry_values["EnableScriptBlockLogging"]: + return None + + output = [] + for registry_name in self.registry_values.keys(): + registry_value = self.registry_values[registry_name] + # Ingore the big property we have already displayed + if registry_name == "EnableScriptBlockLogging": + continue + + if isinstance(registry_value, bool): + if registry_value == True: + output.append( + f"[cyan]{rich.markup.escape(registry_name)}[/cyan] is [bold red]enabled[/bold red]" + ) + else: + output.append( + f"[cyan]{rich.markup.escape(registry_name)}[/cyan] is [bold green]disabled[/bold green]" + ) + + return "\n".join((" - " + line for line in output)) + + +class Module(EnumerateModule): + """Enumerate the current PowerShell Script Block Logging settings on the target""" + + PROVIDES = ["powershell.scriptblocklogging"] + PLATFORM = [Windows] + + def enumerate(self, session): + + registry_key = "HKLM:\\SOFTWARE\\Policies\\Microsoft\\Windows\\PowerShell\\ScriptBlockLogging" + + registry_values = { + "EnableScriptBlockLogging": bool, + "EnableScriptBlockInvocationLogging": bool, + } + + for registry_value, registry_type in registry_values.items(): + try: + result = session.platform.powershell( + f"Get-ItemPropertyValue '{registry_key}' -Name '{registry_value}'" + ) + + if not result: + raise ModuleFailed( + f"failed to retrieve registry value {registry_value}" + ) + + registry_values[registry_value] = registry_type(result[0]) + + except PowershellError as exc: + if "does not exist" in exc.message: + registry_values[registry_value] = registry_type(0) + else: + raise ModuleFailed( + f"could not retrieve registry value {registry_value}: {exc}" + ) from exc + + yield PowerShellScriptBlockLogging(self.name, registry_values) diff --git a/pwncat/modules/windows/enumerate/powershell/transcription.py b/pwncat/modules/windows/enumerate/powershell/transcription.py new file mode 100644 index 00000000..e5e7e0ba --- /dev/null +++ b/pwncat/modules/windows/enumerate/powershell/transcription.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 + +from typing import Dict + +import rich.markup + +from pwncat.db import Fact +from pwncat.modules import ModuleFailed +from pwncat.platform.windows import Windows, PowershellError +from pwncat.modules.enumerate import EnumerateModule + + +class PowerShellTranscription(Fact): + def __init__(self, source, registry_values: Dict): + super().__init__(source=source, types=["powershell.transcription"]) + + self.registry_values: bool = registry_values + """ The current setting for PowerShell transcription""" + + def __getitem__(self, name): + + return self.registry_values[name] + + def title(self, session): + if not self.registry_values["EnableTranscripting"]: + return "[green]PowerShell Transcription Logging is [bold]disabled[/bold][/green]" + + return "[red]PowerShell Transcription Logging is [bold]enabled[/bold][/red]" + + def description(self, session): + if not self.registry_values["EnableTranscripting"]: + return None + + output = [] + for registry_name in self.registry_values.keys(): + registry_value = self.registry_values[registry_name] + # Ingore the big LAPS property we have already displayed + if registry_name == "EnableTranscripting": + continue + + if registry_name == "OutputDirectory": + if registry_value == "0": + output.append( + f"[cyan]{rich.markup.escape(registry_name)}[/cyan] not defined, likely $env:\\USERPROFILE" + ) + else: + output.append( + f"[cyan]{rich.markup.escape(registry_name)}[/cyan] = '{rich.markup.escape(registry_value)}'" + ) + + if isinstance(registry_value, bool): + if registry_value == True: + output.append( + f"[cyan]{rich.markup.escape(registry_name)}[/cyan] is [bold red]enabled[/bold red]" + ) + else: + output.append( + f"[cyan]{rich.markup.escape(registry_name)}[/cyan] is [bold green]disabled[/bold green]" + ) + + return "\n".join((" - " + line for line in output)) + + +class Module(EnumerateModule): + """Enumerate the current PowerShell transcription settings on the target""" + + PROVIDES = ["powershell.transcription"] + PLATFORM = [Windows] + + def enumerate(self, session): + + registry_key = ( + "HKLM:\\SOFTWARE\\Policies\\Microsoft\\Windows\\PowerShell\\Transcription" + ) + + registry_values = { + "EnableTranscripting": bool, + "OutputDirectory": str, + "EnableInvocationHeader": bool, + } + + for registry_value, registry_type in registry_values.items(): + try: + result = session.platform.powershell( + f"Get-ItemPropertyValue '{registry_key}' -Name '{registry_value}'" + ) + + if not result: + raise ModuleFailed( + f"failed to retrieve registry value {registry_value}" + ) + + registry_values[registry_value] = registry_type(result[0]) + + except PowershellError as exc: + if "does not exist" in exc.message: + registry_values[registry_value] = registry_type(0) + else: + raise ModuleFailed( + f"could not retrieve registry value {registry_value}: {exc}" + ) from exc + + yield PowerShellTranscription(self.name, registry_values) diff --git a/pwncat/modules/windows/enumerate/powershell/version.py b/pwncat/modules/windows/enumerate/powershell/version.py new file mode 100644 index 00000000..482026e8 --- /dev/null +++ b/pwncat/modules/windows/enumerate/powershell/version.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 + +import rich.markup + +from pwncat.db import Fact +from pwncat.modules import ModuleFailed +from pwncat.platform.windows import Windows, PowershellError +from pwncat.modules.enumerate import EnumerateModule + + +class PowerShellVersion(Fact): + def __init__(self, source, version_numbers: dict): + super().__init__(source=source, types=["powershell.version"]) + + self.version_numbers: dict = version_numbers + self.major = version_numbers["Major"] + self.minor = version_numbers["Minor"] + self.build = version_numbers["Build"] + self.revision = version_numbers["Revision"] + self.version: str = ".".join( + [ + rich.markup.escape(str(number)) + for number in [self.major, self.minor, self.build, self.revision] + ] + ) + + def title(self, session): + return f"Current PowerShell version: {self.version}" + + +class Module(EnumerateModule): + """Enumerate the current Windows Defender settings on the target""" + + PROVIDES = ["powershell.version"] + PLATFORM = [Windows] + + def enumerate(self, session): + + try: + result = session.platform.powershell("$PSVersionTable.PSVersion") + + if not result: + return + + if isinstance(result[0], list) and result: + version_numbers = "\n".join(result[0]) + else: + version_numbers = result[0] + + except PowershellError as exc: + raise ModuleFailed("failed to retrieve powershell version") from exc + + yield PowerShellVersion(self.name, version_numbers) From c6012903d84bcd823a94b58b661a512c2a50604b Mon Sep 17 00:00:00 2001 From: John Hammond Date: Sat, 19 Jun 2021 03:01:37 -0400 Subject: [PATCH 7/9] Added remote desktop manager enumeration module --- .../enumerate/creds/remotedesktopmanager.py | 53 +++++++++++++++++++ .../windows/enumerate/powershell/history.py | 6 +++ 2 files changed, 59 insertions(+) create mode 100644 pwncat/modules/windows/enumerate/creds/remotedesktopmanager.py diff --git a/pwncat/modules/windows/enumerate/creds/remotedesktopmanager.py b/pwncat/modules/windows/enumerate/creds/remotedesktopmanager.py new file mode 100644 index 00000000..ee8d0b3a --- /dev/null +++ b/pwncat/modules/windows/enumerate/creds/remotedesktopmanager.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 + +import rich.markup + +from pwncat.db import Fact +from pwncat.modules import ModuleFailed +from pwncat.platform.windows import Windows, PowershellError +from pwncat.modules.enumerate import EnumerateModule + + +class RemoteDesktopManagerCreds(Fact): + def __init__(self, source, path: str): + super().__init__(source=source, types=["creds.remotedesktopmanager"]) + + self.path: str = path + + def title(self, session): + if not self.path: + return "[red]Remote Desktop Manager credentials file not present[/red]" + else: + return f"[green]Remote Desktop Manager credentials file: {rich.markup.escape(self.path)}[/green]" + + +class Module(EnumerateModule): + """Enumerate the current Windows Defender settings on the target""" + + PROVIDES = ["creds.remotedesktopmanager"] + PLATFORM = [Windows] + + def enumerate(self, session): + + try: + result = session.platform.powershell( + '(Get-ChildItem "$env:APPDATA\\Local\\Microsoft\\Remote Desktop Connection Manager\\RDCMan.settings").FullName' + ) + + if not result: + yield RemoteDesktopManagerCreds(self.name, "") + + if isinstance(result[0], list) and result: + path = "\n".join(result[0]) + else: + path = result[0] + + yield RemoteDesktopManagerCreds(self.name, path) + + except PowershellError as exc: + if "does not exist" in exc.message: + yield RemoteDesktopManagerCreds(self.name, path="") + else: + raise ModuleFailed( + "failed to retrieve check for remote desktop creds" + ) from exc diff --git a/pwncat/modules/windows/enumerate/powershell/history.py b/pwncat/modules/windows/enumerate/powershell/history.py index 31bb15fb..9cd1f364 100644 --- a/pwncat/modules/windows/enumerate/powershell/history.py +++ b/pwncat/modules/windows/enumerate/powershell/history.py @@ -8,6 +8,12 @@ from pwncat.modules.enumerate import EnumerateModule +""" +TODO: This cooooould be improved by testing if the path actually exists, + and using Measure-Object to get the number of lines the history contains? +""" + + class PowerShellHistory(Fact): def __init__(self, source, path: str): super().__init__(source=source, types=["powershell.history"]) From 360c14300475f795915c38e7a3546a9f5c21dd29 Mon Sep 17 00:00:00 2001 From: Caleb Stewart Date: Fri, 9 Jul 2021 21:25:52 -0400 Subject: [PATCH 8/9] Added Windows Update Server enumeration (WSUS) --- .../modules/windows/enumerate/system/wsus.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 pwncat/modules/windows/enumerate/system/wsus.py diff --git a/pwncat/modules/windows/enumerate/system/wsus.py b/pwncat/modules/windows/enumerate/system/wsus.py new file mode 100644 index 00000000..862cdaa9 --- /dev/null +++ b/pwncat/modules/windows/enumerate/system/wsus.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 + +import rich.markup + +from pwncat.db import Fact +from pwncat.modules import ModuleFailed +from pwncat.modules.windows import Windows, PowershellError +from pwncat.modules.enumerate import Schedule, EnumerateModule + + +class WindowsUpdateServer(Fact): + def __init__(self, source: str, server: str): + super().__init__(source=source, types=["system.wsus.server"]) + + self.server = server + + def is_secure(self) -> bool: + """Check if the given server is secure""" + return self.server.startswith("https://") + + def title(self, session): + return rich.markup.escape(self.server) + + +class Module(EnumerateModule): + """Locate all WSUS update servers""" + + PROVIDES = ["system.wsus.server"] + PLATFORM = [Windows] + SCHEDULE = Schedule.ONCE + + def enumerate(self, session: "pwncat.manager.Session"): + + try: + result = session.platform.powershell( + "Get-ItemPropertyValue -Path 'HKLM:\\SOFTWARE\\Policies\\Microsoft\\Windows\\WindowsUpdate' -Name WUServer" + ) + if not result: + return + + yield WindowsUpdateServer(source=self.name, server=result[0]) + except PowershellError as exc: + pass From 502a6b790657555180b5fcea1101e3c352b6fc86 Mon Sep 17 00:00:00 2001 From: Caleb Stewart Date: Sat, 10 Jul 2021 23:27:53 -0400 Subject: [PATCH 9/9] Added generic Powershell enumeration and startups --- pwncat/facts/windows.py | 37 +++++++- pwncat/modules/windows/enumerate/__init__.py | 71 +++++++++++++++ .../windows/enumerate/system/startup.py | 22 +++++ .../modules/windows/enumerate/system/wsus.py | 87 +++++++++++-------- 4 files changed, 180 insertions(+), 37 deletions(-) create mode 100644 pwncat/modules/windows/enumerate/system/startup.py diff --git a/pwncat/facts/windows.py b/pwncat/facts/windows.py index 124ec80b..0041b5b8 100644 --- a/pwncat/facts/windows.py +++ b/pwncat/facts/windows.py @@ -1,9 +1,11 @@ """ Windows-specific facts which are used in multiple places throughout the framework. """ -from typing import List, Optional +import functools +from typing import Any, List, Callable, Optional from datetime import datetime +from pwncat.db import Fact from pwncat.facts import User, Group @@ -122,3 +124,36 @@ def __init__( self.group_description: str = description self.principal_source: str = principal_source self.domain: Optional[str] = domain + + +class PowershellFact(Fact): + """Powershell Object Wrapper Fact""" + + def __init__( + self, + source: str, + types: List[str], + obj: Any, + title: Callable, + description: Callable, + ): + super().__init__(source=source, types=types) + + self.obj = obj + + if description is not None: + self.description = functools.partial(description, self) + if title is not None: + self.title = functools.partial(title, self) + + def description(self, session): + return None + + def title(self, session): + return self.obj + + def __getattr__(self, key: str): + try: + return self.obj[key] + except KeyError: + return super().__getattr__(key) diff --git a/pwncat/modules/windows/enumerate/__init__.py b/pwncat/modules/windows/enumerate/__init__.py index e69de29b..2eb8cec8 100644 --- a/pwncat/modules/windows/enumerate/__init__.py +++ b/pwncat/modules/windows/enumerate/__init__.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +from typing import List, Callable + +from pwncat.facts.windows import PowershellFact +from pwncat.platform.windows import Windows, PowershellError +from pwncat.modules.enumerate import Schedule, EnumerateModule + + +def build_powershell_enumeration( + types: List[str], + schedule: Schedule, + command: str, + docstring: str, + title: Callable = None, + description: Callable = None, + single: bool = False, +): + """ + Build an enumeration module around a single powershell command. + This will construct and return an enumeration class which executes + the given powershell script and yields a fact with the given types + that exposes all properties of the returned powershell objects. This + is a helper to quickly develop basic powershell-based enumeration modules. + """ + + class Module(EnumerateModule): + + PROVIDES = types + PLATFORM = [Windows] + SCHEDULE = schedule + + def enumerate(self, session: "pwncat.manager.Session"): + + try: + result = session.platform.powershell(command) + + if not result: + return + + if isinstance(result[0], list): + results = result[0] + else: + results = [results[0]] + + if single: + yield PowershellFact( + source=self.name, + types=types, + data=results[0], + title=title, + description=description, + ) + else: + yield from [ + PowershellFact( + source=self.name, + types=types, + obj=obj, + title=title, + description=description, + ) + for obj in results + ] + + except PowershellError as exc: + pass + + # Set the docstring + Module.__doc__ = docstring + + return Module diff --git a/pwncat/modules/windows/enumerate/system/startup.py b/pwncat/modules/windows/enumerate/system/startup.py new file mode 100644 index 00000000..3924583d --- /dev/null +++ b/pwncat/modules/windows/enumerate/system/startup.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +from typing import Any, Dict + +import rich.markup + +from pwncat.modules.enumerate import Schedule +from pwncat.modules.windows.enumerate import build_powershell_enumeration + + +def startup_title(self, session: "pwncat.manager.Session"): + return f"[cyan]{rich.markup.escape(self.Caption)}[/cyan]: {repr(rich.markup.escape(self.Command))}" + + +Module = build_powershell_enumeration( + types=["system.startup.command"], + schedule=Schedule.ONCE, + command="Get-CimInstance -ClassName Win32_StartupCommand", + docstring="Locate all startup commands via WMI queries", + title=startup_title, + description=None, + single=False, +) diff --git a/pwncat/modules/windows/enumerate/system/wsus.py b/pwncat/modules/windows/enumerate/system/wsus.py index 862cdaa9..4f6f5c4a 100644 --- a/pwncat/modules/windows/enumerate/system/wsus.py +++ b/pwncat/modules/windows/enumerate/system/wsus.py @@ -4,40 +4,55 @@ from pwncat.db import Fact from pwncat.modules import ModuleFailed -from pwncat.modules.windows import Windows, PowershellError +from pwncat.platform.windows import Windows, PowershellError from pwncat.modules.enumerate import Schedule, EnumerateModule - - -class WindowsUpdateServer(Fact): - def __init__(self, source: str, server: str): - super().__init__(source=source, types=["system.wsus.server"]) - - self.server = server - - def is_secure(self) -> bool: - """Check if the given server is secure""" - return self.server.startswith("https://") - - def title(self, session): - return rich.markup.escape(self.server) - - -class Module(EnumerateModule): - """Locate all WSUS update servers""" - - PROVIDES = ["system.wsus.server"] - PLATFORM = [Windows] - SCHEDULE = Schedule.ONCE - - def enumerate(self, session: "pwncat.manager.Session"): - - try: - result = session.platform.powershell( - "Get-ItemPropertyValue -Path 'HKLM:\\SOFTWARE\\Policies\\Microsoft\\Windows\\WindowsUpdate' -Name WUServer" - ) - if not result: - return - - yield WindowsUpdateServer(source=self.name, server=result[0]) - except PowershellError as exc: - pass +from pwncat.modules.windows.enumerate import build_powershell_enumeration + + +def wsus_title(self, session: "pwncat.manager.Session"): + return rich.markup.escape(self.server) + + +Module = build_powershell_enumeration( + types=["system.wsus.server"], + schedule=Schedule.ONCE, + command="Get-ItemPropertyValue -Path 'HKLM:\\SOFTWARE\\Policies\\Microsoft\\Windows\\WindowsUpdate' -Name WUServer", + docstring="Locate all WSUS update server", + title=wsus_title, + description=None, + single=True, +) + +# class WindowsUpdateServer(Fact): +# def __init__(self, source: str, server: str): +# super().__init__(source=source, types=["system.wsus.server"]) +# +# self.server = server +# +# def is_secure(self) -> bool: +# """Check if the given server is secure""" +# return self.server.startswith("https://") +# +# def title(self, session): +# return rich.markup.escape(self.server) +# +# +# class Module(EnumerateModule): +# """Locate all WSUS update servers""" +# +# PROVIDES = ["system.wsus.server"] +# PLATFORM = [Windows] +# SCHEDULE = Schedule.ONCE +# +# def enumerate(self, session: "pwncat.manager.Session"): +# +# try: +# result = session.platform.powershell( +# "Get-ItemPropertyValue -Path 'HKLM:\\SOFTWARE\\Policies\\Microsoft\\Windows\\WindowsUpdate' -Name WUServer" +# ) +# if not result: +# return +# +# yield WindowsUpdateServer(source=self.name, server=result[0]) +# except PowershellError as exc: +# pass