1
0
mirror of https://github.com/calebstewart/pwncat.git synced 2024-11-27 10:54:14 +01:00

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. 😭
This commit is contained in:
Caleb Stewart 2021-05-25 02:05:23 -04:00
parent 830fe7b211
commit a15577892d
12 changed files with 308 additions and 32 deletions

View File

@ -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

View File

@ -0,0 +1,8 @@
{% extends "generic.md" %}
{% block platform %}
## Windows Specific Info!
{{ [["Hello", "World"], ["Goodbye", "World"]] | table(headers=True) }}
{% endblock %}

62
pwncat/facts/windows.py Normal file
View File

@ -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

View File

@ -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}")

View File

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

View File

@ -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,
)

View File

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

View File

@ -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)

View File

@ -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:

View File

@ -834,7 +834,7 @@ function prompt {
)
self.user_info = self.powershell(
"([System.DirectoryServices.AccountManagement.UserPrincipal]::Current).SID.Value"
)
)[0]
def getuid(self):

27
test.py
View File

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