From a15577892d3397789a9fb4f834fa9b11a22ddcb2 Mon Sep 17 00:00:00 2001 From: Caleb Stewart Date: Tue, 25 May 2021 02:05:23 -0400 Subject: [PATCH] Added windows local user and group enumeration Also added markdown table generator/jinja filter for report generation. This is currentl the best I can do since commonmark (and therefore rich) doesn't support tables at the moment. :sob: --- pwncat/data/reports/generic.md | 15 ++-- pwncat/data/reports/windows.md | 8 ++ pwncat/facts/windows.py | 62 ++++++++++++++ pwncat/modules/agnostic/report.py | 36 ++++++++ pwncat/modules/windows/enumerate/__init__.py | 0 .../windows/enumerate/user/__init__.py | 43 ++++++++++ .../modules/windows/enumerate/user/group.py | 47 +++++++++++ .../modules/windows/enumerate/user/privs.py | 83 +++++++++++++++++++ .../windows/manage/powershell/import.py | 8 -- pwncat/platform/__init__.py | 9 +- pwncat/platform/windows.py | 2 +- test.py | 27 +++--- 12 files changed, 308 insertions(+), 32 deletions(-) create mode 100644 pwncat/facts/windows.py create mode 100644 pwncat/modules/windows/enumerate/__init__.py create mode 100644 pwncat/modules/windows/enumerate/user/__init__.py create mode 100644 pwncat/modules/windows/enumerate/user/group.py create mode 100644 pwncat/modules/windows/enumerate/user/privs.py diff --git a/pwncat/data/reports/generic.md b/pwncat/data/reports/generic.md index 1b00cbe..a43e12e 100644 --- a/pwncat/data/reports/generic.md +++ b/pwncat/data/reports/generic.md @@ -5,13 +5,14 @@ The report was generated on {{ datetime }}. ## Common System Information -| Platform | {{ platform.name }} | -|--------------|---------------------| -| Architecture | {{ session.run("enumerate", types=["system.arch"]) | first_or_none | title_or_unknown }} | -| Hostname | {{ session.run("enumerate", types=["system.hostname"]) | first_or_none | title_or_unknown }} | -| ASLR | {{ session.run("enumerate", types=["system.aslr"]) | first_or_none | title_or_unknown }} | -| Container | {{ session.run("enumerate", types=["system.container"]) | first_or_none | title_or_unknown }} | -| Distribution | {{ session.run("enumerate", types=["system.distro"]) | first_or_none | title_or_unknown }} | +{{ [ + ["**Platform**", platform.name], + ["**Architecture**", session.run("enumerate", types=["system.arch"]) | first_or_none | title_or_unknown ], + ["**Hostname**", session.run("enumerate", types=["system.hostname"]) | first_or_none | title_or_unknown], + ["**ASLR**", session.run("enumerate", types=["system.aslr"]) | first_or_none | title_or_unknown], + ["**Container**", session.run("enumerate", types=["system.container"]) | first_or_none | title_or_unknown], + ["**Distribution**", session.run("enumerate", types=["system.distro"]) | first_or_none | title_or_unknown], +] | table(headers=False) }} {% if session.run("enumerate", types=["implant.*"]) %} ## Installed Implants diff --git a/pwncat/data/reports/windows.md b/pwncat/data/reports/windows.md index e69de29..b34d11f 100644 --- a/pwncat/data/reports/windows.md +++ b/pwncat/data/reports/windows.md @@ -0,0 +1,8 @@ +{% extends "generic.md" %} + +{% block platform %} +## Windows Specific Info! + +{{ [["Hello", "World"], ["Goodbye", "World"]] | table(headers=True) }} + +{% endblock %} diff --git a/pwncat/facts/windows.py b/pwncat/facts/windows.py new file mode 100644 index 0000000..23c23aa --- /dev/null +++ b/pwncat/facts/windows.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +from typing import List, Optional +from datetime import datetime + +from pwncat.facts import User, Group + + +class WindowsUser(User): + """ Windows-specific user """ + + def __init__( + self, + source: str, + name: str, + uid: str, + account_expires: Optional[datetime], + description: str, + enabled: bool, + full_name: str, + password_changeable_date: Optional[datetime], + password_expires: Optional[datetime], + user_may_change_password: bool, + password_required: bool, + password_last_set: Optional[datetime], + last_logon: Optional[datetime], + principal_source: str, + password: Optional[str] = None, + hash: Optional[str] = None, + ): + super().__init__( + source=source, name=name, uid=uid, password=password, hash=hash + ) + + self.account_expires: Optional[datetime] = account_expires + self.user_description: str = description + self.enabled: bool = enabled + self.full_name: str = full_name + self.password_changeable_date: Optional[datetime] = password_changeable_date + self.password_expires: Optional[datetime] = password_expires + self.user_may_change_password: bool = user_may_change_password + self.password_required: bool = password_required + self.password_last_set: Optional[datetime] = password_last_set + self.last_logon: Optional[datetime] = last_logon + self.principal_source: str = principal_source + + +class WindowsGroup(Group): + """ Windows-specific group """ + + def __init__( + self, + source: str, + name: str, + gid: str, + description: str, + principal_source: str, + members: List[str], + ): + super().__init__(source=source, name=name, gid=gid, members=members) + + self.group_description: str = description + self.principal_source: str = principal_source diff --git a/pwncat/modules/agnostic/report.py b/pwncat/modules/agnostic/report.py index 2c59c84..25fd44c 100644 --- a/pwncat/modules/agnostic/report.py +++ b/pwncat/modules/agnostic/report.py @@ -1,8 +1,12 @@ #!/usr/bin/env python3 import os import datetime +import textwrap +from typing import List import jinja2 +import rich.box +from rich.table import Table from pwncat.util import console, strip_markup from rich.markdown import Markdown from pwncat.modules import Bool, Argument, BaseModule, ModuleFailed @@ -38,6 +42,37 @@ class Module(BaseModule): ), } + def generate_markdown_table(self, data: List[List], headers: bool = False): + """ Generate a markdown table from the given data and headers """ + + # Get column widths + widths = [ + max(len(data[r][c]) for r in range(len(data))) for c in range(len(data[0])) + ] + + rows = [] + for r in range(len(data)): + rows.append( + "|" + + "|".join( + [ + " " + data[r][c] + " " * (widths[c] - len(data[r][c]) + 1) + for c in range(len(data[r])) + ] + ) + + "|" + ) + + if headers: + rows.insert( + 1, + "|" + + "|".join([" " + "-" * widths[c] + " " for c in range(len(data[r]))]) + + "|", + ) + + return " \n".join(rows) + def run(self, session: "pwncat.manager.Session", output, template, fmt, custom): """ Perform enumeration and optionally write report """ @@ -74,6 +109,7 @@ class Module(BaseModule): else "unknown" ) env.filters["remove_rich"] = lambda thing: strip_markup(str(thing)) + env.filters["table"] = self.generate_markdown_table try: template = env.get_template(f"{template}.{fmt}") diff --git a/pwncat/modules/windows/enumerate/__init__.py b/pwncat/modules/windows/enumerate/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pwncat/modules/windows/enumerate/user/__init__.py b/pwncat/modules/windows/enumerate/user/__init__.py new file mode 100644 index 0000000..c1c33f7 --- /dev/null +++ b/pwncat/modules/windows/enumerate/user/__init__.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 + +from pwncat.modules import Status, ModuleFailed +from pwncat.facts.windows import WindowsUser +from pwncat.platform.windows import Windows, PowershellError +from pwncat.modules.enumerate import Schedule, EnumerateModule + + +class Module(EnumerateModule): + """ Enumerate users from a windows target """ + + PROVIDES = ["user"] + PLATFORM = [Windows] + SCHEDULE = Schedule.ONCE + + def enumerate(self, session: "pwncat.manager.Session"): + + try: + users = session.platform.powershell("Get-LocalUser") + if not users: + raise ModuleFailed("no users returned from Get-Localuser") + except PowershellError as exc: + raise ModuleFailed(str(exc)) from exc + + users = users[0] + + for user in users: + yield WindowsUser( + source=self.name, + name=user["Name"], + uid=user["SID"], + account_expires=None, + description=user["Description"], + enabled=user["Enabled"], + full_name=user["FullName"], + password_changeable_date=None, + password_expires=None, + user_may_change_password=user["UserMayChangePassword"], + password_required=user["PasswordRequired"], + password_last_set=None, + last_logon=None, + principal_source=user["PrincipalSource"], + ) diff --git a/pwncat/modules/windows/enumerate/user/group.py b/pwncat/modules/windows/enumerate/user/group.py new file mode 100644 index 0000000..d481fbd --- /dev/null +++ b/pwncat/modules/windows/enumerate/user/group.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 + +from pwncat.modules import Status, ModuleFailed +from pwncat.facts.windows import WindowsGroup +from pwncat.platform.windows import Windows, PowershellError +from pwncat.modules.enumerate import Schedule, EnumerateModule + + +class Module(EnumerateModule): + """Enumerate groups from a windows target""" + + PROVIDES = ["group"] + PLATFORM = [Windows] + SCHEDULE = Schedule.ONCE + + def enumerate(self, session: "pwncat.manager.Session"): + """ Yield WindowsGroup objects """ + + try: + groups = session.platform.powershell("Get-LocalGroup") + if not groups: + raise ModuleFailed("no groups returned from Get-LocalGroup") + except PowershellError as exc: + raise ModuleFailed(str(exc)) from exc + + for group in groups[0]: + try: + members = session.platform.powershell( + f"Get-LocalGroupMember {group['Name']}" + ) + if members: + members = ( + [m["SID"] for m in members[0]] + if isinstance(members[0], list) + else [members[0]["SID"]["Value"]] + ) + except PowershellError as exc: + members = [] + + yield WindowsGroup( + source=self.name, + name=group["Name"], + gid=group["SID"], + description=group["Description"], + principal_source=group["PrincipalSource"], + members=members, + ) diff --git a/pwncat/modules/windows/enumerate/user/privs.py b/pwncat/modules/windows/enumerate/user/privs.py new file mode 100644 index 0000000..7c16731 --- /dev/null +++ b/pwncat/modules/windows/enumerate/user/privs.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +import dataclasses +from enum import IntFlag + +from rich.table import Table +from pwncat.facts import Fact +from pwncat.platform.windows import Windows +from pwncat.modules.enumerate import Schedule, EnumerateModule + + +class LuidAttributes(IntFlag): + + DISABLED = 0x00 + SE_PRIVILEGE_ENABLED_BY_DEFAULT = 0x01 + SE_PRIVILEGE_ENABLED = 0x02 + SE_PRIVILEGE_REMOVE = 0x04 + SE_PRIVILEGE_USED_FOR_ACCESS = 0x80000000 + + +class TokenPrivilegeData(Fact): + def __init__( + self, + source: str, + privilege: str, + attributes: LuidAttributes, + token_handle: int, + pid: int, + ): + super().__init__(source=source, types=["user.privs"]) + + self.privilege: str = privilege + self.attributes: LuidAttributes = attributes + # self.token_handle: int = token_handle + # self.pid: int = pid + + def title(self, session: "pwncat.manager.Session"): + + if self.attributes == LuidAttributes.DISABLED: + color = "red" + else: + color = "green" + + attrs = str(self.attributes) + attrs = attrs.replace("LuidAttributes.", "") + attrs = attrs.replace("|", "[/cyan]|[cyan]") + attrs = "[cyan]" + attrs + "[/cyan]" + + return f"[{color}]{self.privilege}[/{color}] -> {attrs}" + + +class Module(EnumerateModule): + """Enumerate user privileges using PowerView's Get-ProcessTokenPrivilege""" + + PROVIDES = ["user.privs"] + PLATFORM = [Windows] + SCHEDULE = Schedule.ALWAYS + + def enumerate(self, session: "pwncat.manager.Session"): + + # Ensure that powerview is loaded + session.run( + "manage.powershell.import", + path="PowerShellMafia/PowerSploit/Privesc/PowerUp.ps1", + ) + + # Grab our current token privileges + results = session.platform.powershell("Get-ProcessTokenPrivilege") + if len(results) == 0: + session.log("[red]error[/red]: Get-ProcessTokenPrivilege failed") + return + + # They end up in an array in an array + privs = results[0] + + # Create our enumeration data types + for priv in privs: + yield TokenPrivilegeData( + source=self.name, + privilege=priv["Privilege"], + attributes=LuidAttributes(priv["Attributes"]), + token_handle=priv["TokenHandle"], + pid=priv["ProcessId"], + ) diff --git a/pwncat/modules/windows/manage/powershell/import.py b/pwncat/modules/windows/manage/powershell/import.py index 696817b..aeb38b0 100644 --- a/pwncat/modules/windows/manage/powershell/import.py +++ b/pwncat/modules/windows/manage/powershell/import.py @@ -4,7 +4,6 @@ from io import IOBase, BytesIO from pathlib import Path import requests - from pwncat.modules import Bool, Argument, BaseModule, ModuleFailed from pwncat.platform.windows import Windows @@ -71,14 +70,7 @@ class Module(BaseModule): name, filp = self.resolve_psmodule(session, path) if name in session.platform.psmodules and not force: - session.log( - f"[yellow]warning[/yellow]: {name}: skipping previously loaded module" - ) return - elif name in session.platform.psmodules and force: - session.log(f"[yellow]warning[/yellow]: {name}: reloading module") - else: - session.log(f"executing powershell module: {name}") session.platform.powershell(filp) diff --git a/pwncat/platform/__init__.py b/pwncat/platform/__init__.py index 69ea649..0bb4796 100644 --- a/pwncat/platform/__init__.py +++ b/pwncat/platform/__init__.py @@ -527,8 +527,13 @@ class Platform(ABC): return str(self.channel) @abstractmethod - def getuid(self): - """Get the current user ID""" + def refresh_uid(self) -> Union[int, str]: + """ Refresh the cached UID of the current session. """ + + @abstractmethod + def getuid(self) -> Union[int, str]: + """Get the current user ID. This should not query the target, but should + return the current cached UID as found with `refresh_uid`.""" @abstractmethod def getenv(self, name: str) -> str: diff --git a/pwncat/platform/windows.py b/pwncat/platform/windows.py index e3b019b..74b5dee 100644 --- a/pwncat/platform/windows.py +++ b/pwncat/platform/windows.py @@ -834,7 +834,7 @@ function prompt { ) self.user_info = self.powershell( "([System.DirectoryServices.AccountManagement.UserPrincipal]::Current).SID.Value" - ) + )[0] def getuid(self): diff --git a/test.py b/test.py index dc24835..220ecd5 100755 --- a/test.py +++ b/test.py @@ -1,24 +1,23 @@ #!./env/bin/python +import json +import stat +import time import subprocess import pwncat.manager import pwncat.platform.windows -import time -import stat -import json # Create a manager -manager = pwncat.manager.Manager("data/pwncatrc") +with pwncat.manager.Manager("data/pwncatrc") as manager: -# Tell the manager to create verbose sessions that -# log all commands executed on the remote host -# manager.config.set("verbose", True, glob=True) + # Tell the manager to create verbose sessions that + # log all commands executed on the remote host + # manager.config.set("verbose", True, glob=True) -# Establish a session -# session = manager.create_session("windows", host="192.168.56.10", port=4444) -# session = manager.create_session("windows", host="192.168.122.11", port=4444) -session = manager.create_session("linux", host="pwncat-ubuntu", port=4444) -# session = manager.create_session("windows", host="0.0.0.0", port=4444) + # Establish a session + # session = manager.create_session("windows", host="192.168.56.10", port=4444) + session = manager.create_session("windows", host="192.168.122.11", port=4444) + # session = manager.create_session("linux", host="pwncat-ubuntu", port=4444) + # session = manager.create_session("windows", host="0.0.0.0", port=4444) -while True: - session.platform.getuid() + manager.print(session.platform.powershell("Get-LocalGroupMember Administrators"))