Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Baseline Windows Enumerations #154

Draft
wants to merge 10 commits into
base: release-v0.5.0
Choose a base branch
from
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,19 @@ and simply didn't have the time to go back and retroactively create one.
### Fixed
- Pinned container base image to alpine 3.13.5 and installed to virtualenv ([#134](https://github.com/calebstewart/pwncat/issues/134))
- Fixed syntax for f-strings in escalation command
- Re-added `readline` import for windows platform after being accidentally removed
### Changed
- Changed session tracking so session IDs aren't reused
- Changed zsh prompt to match CWD of other shell prompts
- Changed LinuxWriter close routine again to account for needed EOF signals ([#140](https://github.com/calebstewart/pwncat/issues/140))
### Added
- Added better file io test cases
- Added `ssl-bind` and `ssl-connect` channel protocols for encrypted shells
- Added `ncat`-style ssl arguments to entrypoint and `connect` command
- Added query-string arguments to connection strings for both the entrypoint
and the `connect` command.


## [0.4.2] - 2021-06-15
Quick patch release due to corrected bug in `ChannelFile` which caused command
output to be empty in some situations.
Expand Down
11 changes: 0 additions & 11 deletions pwncat/commands/upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,6 @@ def run(self, manager: "pwncat.manager.Manager", args):

if not args.destination:
args.destination = f"./{os.path.basename(args.source)}"
# else:
# access = pwncat.victim.access(args.destination)
# if Access.DIRECTORY in access:
# args.destination = os.path.join(
# args.destination, os.path.basename(args.source)
# )
# elif Access.PARENT_EXIST not in access:
# console.log(
# f"[cyan]{args.destination}[/cyan]: no such file or directory"
# )
# return

try:
length = os.path.getsize(args.source)
Expand Down
37 changes: 36 additions & 1 deletion pwncat/facts/windows.py
Original file line number Diff line number Diff line change
@@ -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


Expand Down Expand Up @@ -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)
71 changes: 71 additions & 0 deletions pwncat/modules/windows/enumerate/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Empty file.
53 changes: 53 additions & 0 deletions pwncat/modules/windows/enumerate/creds/cachedcount.py
Original file line number Diff line number Diff line change
@@ -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
53 changes: 53 additions & 0 deletions pwncat/modules/windows/enumerate/creds/remotedesktopmanager.py
Original file line number Diff line number Diff line change
@@ -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
Empty file.
54 changes: 54 additions & 0 deletions pwncat/modules/windows/enumerate/powershell/history.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/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


"""
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"])

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)
68 changes: 68 additions & 0 deletions pwncat/modules/windows/enumerate/powershell/modulelogging.py
Original file line number Diff line number Diff line change
@@ -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)
Loading