1
0
mirror of https://github.com/calebstewart/pwncat.git synced 2024-11-23 09:05:37 +01:00

Implemented User enumeration

This commit is contained in:
Caleb Stewart 2021-05-02 14:03:52 -04:00
parent 81e000504a
commit 148c0ba450
60 changed files with 559 additions and 308 deletions

View File

@ -14,14 +14,13 @@ victim: Optional["pwncat.remote.Victim"] = None
from .config import Config
from .commands import parser
from .util import console
from .db import get_session
from .tamper import TamperManager
tamper: TamperManager = TamperManager()
def interactive(platform):
""" Run the interactive pwncat shell with the given initialized victim.
"""Run the interactive pwncat shell with the given initialized victim.
This function handles the pwncat and remote prompts and does not return
until explicitly exited by the user.

View File

@ -121,6 +121,21 @@ def RemoteFileType(file_exist=True, directory_exist=False):
return _type
def get_module_choices(command):
"""Yields a list of module choices to be used which command argument
choices to select a valid module for the current target"""
if command.manager.target is None:
return
yield from [
module.name.removeprefix("agnostic.").removeprefix(
command.manager.target.platform.name + "."
)
for module in command.manager.target.find_module("*")
]
class Parameter:
"""Generic parameter definition for commands.

View File

@ -20,7 +20,6 @@ from pwncat.commands.base import (
)
from pwncat.modules import PersistError
from pwncat.db import get_session
class Command(CommandDefinition):

View File

@ -5,19 +5,18 @@ from rich.table import Table
from rich import box
import pwncat
from pwncat.commands.base import CommandDefinition, Complete, Parameter
from pwncat.commands.base import (
CommandDefinition,
Complete,
Parameter,
get_module_choices,
)
from pwncat.util import console
class Command(CommandDefinition):
""" View info about a module """
def get_module_choices(self):
if self.manager.target is None:
return
yield from [module.name for module in self.manager.target.find_module("*")]
PROG = "info"
ARGS = {
"module": Parameter(
@ -37,15 +36,21 @@ class Command(CommandDefinition):
if args.module:
try:
module = list(manager.target.find_module(args.module, exact=True))[0]
except IndexError:
module = next(manager.target.find_module(args.module, exact=True))
module_name = args.module
except StopIteration:
console.log(f"[red]error[/red]: {args.module}: no such module")
return
else:
module = manager.config.module
module_name = module.name.removeprefix("agnostic.")
if self.manager.target is not None:
module_name = module_name.removeprefix(
self.manager.target.platform.name + "."
)
console.print(
f"[bold underline]Module [cyan]{module.name}[/cyan][/bold underline]"
f"[bold underline]Module [cyan]{module_name}[/cyan][/bold underline]"
)
console.print(
textwrap.indent(textwrap.dedent(module.__doc__.strip("\n")), " ") + "\n"

View File

@ -4,7 +4,12 @@ import textwrap
import pwncat
import pwncat.modules
from pwncat.util import console
from pwncat.commands.base import CommandDefinition, Complete, Parameter
from pwncat.commands.base import (
CommandDefinition,
Complete,
Parameter,
get_module_choices,
)
class Command(CommandDefinition):
@ -22,12 +27,6 @@ class Command(CommandDefinition):
arguments, you can use the `info` command.
"""
def get_module_choices(self):
if self.manager.target is None:
return
yield from [module.name for module in self.manager.target.find_module("*")]
PROG = "run"
ARGS = {
"--raw,-r": Parameter(

View File

@ -12,12 +12,6 @@ from pwncat.util import console
class Command(CommandDefinition):
""" View info about a module """
def get_module_choices(self):
if self.manager.target is None:
return
yield from [module.name for module in self.manager.target.find_module("*")]
PROG = "search"
ARGS = {
"module": Parameter(
@ -42,8 +36,15 @@ class Command(CommandDefinition):
# the easiest way to do that, so we use a large size for
# width.
description = module.__doc__ if module.__doc__ is not None else ""
module_name = module.name.removeprefix("agnostic.")
if self.manager.target is not None:
module_name = module_name.removeprefix(
self.manager.target.platform.name + "."
)
table.add_row(
f"[cyan]{module.name}[/cyan]",
f"[cyan]{module_name}[/cyan]",
textwrap.shorten(
description.replace("\n", " "), width=200, placeholder="..."
),

View File

@ -6,7 +6,6 @@ from sqlalchemy.orm import sessionmaker
import pwncat
from pwncat.commands.base import CommandDefinition, Complete, Parameter
from pwncat.util import console, State
from pwncat.db import get_session, reset_engine
class Command(CommandDefinition):

View File

@ -1,16 +1,18 @@
#!/usr/bin/env python3
import pwncat
from pwncat.commands.base import CommandDefinition, Complete, Parameter
from pwncat.commands.base import (
CommandDefinition,
Complete,
Parameter,
get_module_choices,
)
from pwncat.util import console
class Command(CommandDefinition):
""" Set the currently used module in the config handler """
def get_module_choices(self):
yield from [module.name for module in self.manager.target.find_module("*")]
PROG = "use"
ARGS = {
"module": Parameter(

View File

@ -1,12 +1,10 @@
#!/usr/bin/env python3
import pwncat
from pwncat.db.base import Base
from pwncat.db.binary import Binary
from pwncat.db.history import History
from pwncat.db.host import Host
from pwncat.db.persist import Persistence
from pwncat.db.suid import SUID
from pwncat.db.tamper import Tamper
from pwncat.db.user import User, Group, SecondaryGroupAssociation
from pwncat.db.user import User, Group
from pwncat.db.fact import Fact

View File

@ -1,25 +1,38 @@
#!/usr/bin/env python3
import persistent
from typing import Optional
import persistent
from persistent.list import PersistentList
class Fact(persistent.Persistent):
"""Store enumerated facts. The pwncat.enumerate.Fact objects are pickled and
stored in the "data" column. The enumerator is arbitrary, but allows for
organizations based on the source enumerator."""
from pwncat.modules import Result
def __init__(self, arg_type, source):
class Fact(Result, persistent.Persistent):
"""Abstract enumerated facts about an enumerated target. Individual
enumeration modules will create subclasses containing the data for
the fact. A generic fact is guaranteed to have a list of types, a
module source, a __repr__ implementation, a __str__ implementation.
By default, a category property is defined which is the first type
in the list of types. This can be overloaded if needed, and is used
when formatted and displaying enumeration results.
Lastly, if the description property is not None, it indicates that
the fact has a "long form" description as opposed to a single-line
content. This only effects the way reports are generated.
"""
def __init__(self, types, source):
super().__init__()
if not isinstance(types, PersistentList):
types = PersistentList(types)
# The type of fact (e.g.., "system.user")
self.type: Optional[str] = arg_type
self.types: PersistentList = types
# The original procedure that found this fact
self.source: Optional[str] = source
# The original SQLAlchemy-style code held a property, "data",
# which was a pickle object. We will re-implement that as a subclass
# but that may need to include the class properties used previously.
self.source: str = source
@property
def category(self) -> str:
return f"{self.type}"
return f"{self.types[0]} facts"

View File

@ -1,38 +1,51 @@
#!/usr/bin/env python3
import persistent
import persistent.list
from typing import Optional
from persistent.list import PersistentList
class Group(persistent.Persistent):
"""
Stores a record of changes on the target (i.e., things that have been
tampered with)
"""
from pwncat.db.fact import Fact
def __init__(self, name, members):
self.name: Optional[str] = name
self.members: persistent.list.PersistentList = persistent.list.PersistentList()
class Group(Fact):
"""Basic representation of a user group on the target system. Individual
platform enumeration modules may subclass this to implement other user
properties as needed for their platform."""
def __init__(self, source: str, name: str, gid, members):
super().__init__(["group"], source)
self.name: str = name
self.id = gid
self.members: PersistentList = PersistentList(members)
def __repr__(self):
return f"""Group(gid={self.id}, name={repr(self.name)}), members={repr(",".join(m.name for m in self.members))})"""
return f"""Group(gid={self.id}, name={repr(self.name)}), members={repr(",".join(m for m in self.members))})"""
class User(persistent.Persistent):
def __init__(self, name, gid, fullname, homedir, password, hash, shell, groups):
class User(Fact):
"""Basic representation of a user on the target system. Individual platform
enumeration modules may subclass this to implement other user properties as
needed for their platform."""
self.name: Optional[str] = name
self.gid: Optional[int] = gid
self.fullname: Optional[str] = fullname
self.homedir: Optional[str] = homedir
self.password: Optional[str] = password
self.hash: Optional[str] = hash
self.shell: Optional[str] = shell
self.groups: persistent.list.PersistentList = persistent.list.PersistentList(
groups
)
def __init__(
self,
source: str,
name,
uid,
password: Optional[str] = None,
hash: Optional[str] = None,
):
super().__init__(["user"], source)
self.name: str = name
self.id = uid
self.password: Optional[str] = None
self.hash: Optional[str] = None
def __repr__(self):
return f"""User(uid={self.id}, gid={self.gid}, name={repr(self.name)})"""
if self.password is None and self.hash is None:
return f"""User(uid={self.id}, name={repr(self.name)})"""
elif self.password is not None:
return f"""User(uid={repr(self.id)}, name={repr(self.name)}, password={repr(self.password)})"""
else:
return f"""User(uid={repr(self.id)}, name={repr(self.name)}, hash={repr(self.hash)})"""

View File

@ -114,6 +114,7 @@ class Session:
target.guid = self.hash
# Add the target to the database
self.db.transaction_manager.begin()
self.db.root.targets.append(target)
self.db.transaction_manager.commit()
@ -122,19 +123,22 @@ class Session:
def run(self, module: str, **kwargs):
""" Run a module on this session """
if module not in self.manager.modules:
raise pwncat.modules.ModuleNotFound(module)
module_name = module
module = self.manager.modules.get(module_name)
if module is None:
module = self.manager.modules.get(self.platform.name + "." + module_name)
if module is None:
module = self.manager.modules.get("agnostic." + module_name)
if module is None:
raise pwncat.modules.ModuleNotFound(module_name)
if (
self.manager.modules[module].PLATFORM is not None
and type(self.platform) not in self.manager.modules[module].PLATFORM
):
raise pwncat.modules.IncorrectPlatformError(module)
if module.PLATFORM is not None and type(self.platform) not in module.PLATFORM:
raise pwncat.modules.IncorrectPlatformError(module_name)
# Ensure that our database connection is up to date
self.db.begin()
self.db.transaction_manager.begin()
return self.manager.modules[module].run(self, **kwargs)
return module.run(self, **kwargs)
def find_module(self, pattern: str, base=None, exact: bool = False):
"""Locate a module by a glob pattern. This is an generator
@ -150,14 +154,22 @@ class Session:
and type(self.platform) not in module.PLATFORM
):
continue
if (
not exact
and fnmatch.fnmatch(name, pattern)
and isinstance(module, base)
):
yield module
elif exact and name == pattern and isinstance(module, base):
yield module
if not isinstance(module, base):
continue
if not exact:
if (
fnmatch.fnmatch(name, pattern)
or fnmatch.fnmatch(name, f"agnostic.{pattern}")
or fnmatch.fnmatch(name, f"{self.platform.name}.{pattern}")
):
yield module
elif exact:
if (
name == pattern
or name == f"agnostic.{pattern}"
or name == f"{self.platform.name}.{pattern}"
):
yield module
def log(self, *args, **kwargs):
"""Log to the console. This utilizes the active sessions
@ -326,9 +338,8 @@ class Manager:
self.db = ZODB.DB(storage, **factory_args)
conn = self.db.open()
try:
conn.root.targets
except AttributeError:
if not hasattr(conn.root, "targets"):
conn.root.targets = persistent.list.PersistentList()
conn.transaction_manager.commit()
conn.close()

View File

@ -82,8 +82,8 @@ class Argument:
def List(_type=str):
""" Argument list type, which accepts a list of the provided
type. """
"""Argument list type, which accepts a list of the provided
type."""
def _ListType(value):
if isinstance(value, list):
@ -96,9 +96,9 @@ def List(_type=str):
def Bool(value: str):
""" Argument of type "bool". Accepts true/false (case-insensitive)
"""Argument of type "bool". Accepts true/false (case-insensitive)
as well as 1/0. The presence of an argument of type "Bool" with no
assignment (e.g. run module arg) is equivalent to `run module arg=true`. """
assignment (e.g. run module arg) is equivalent to `run module arg=true`."""
if isinstance(value, bool):
return value
@ -115,27 +115,27 @@ def Bool(value: str):
class Result:
""" This is a module result. Modules can return standard python objects,
"""This is a module result. Modules can return standard python objects,
but if they need to be formatted when displayed, each result should
implement this interface. """
implement this interface."""
@property
def category(self) -> str:
""" Return a "categry" of object. Categories will be grouped.
"""Return a "categry" of object. Categories will be grouped.
If this returns None or is not defined, this result will be "uncategorized"
"""
return None
@property
def title(self) -> str:
""" Return a short-form description/title of the object. If not defined,
this defaults to the object converted to a string. """
raise NotImplementedError
"""Return a short-form description/title of the object. If not defined,
this defaults to the object converted to a string."""
return str(self)
@property
def description(self) -> str:
""" Returns a long-form description. If not defined, the result is assumed
to not be a long-form result. """
"""Returns a long-form description. If not defined, the result is assumed
to not be a long-form result."""
return None
def is_long_form(self) -> bool:
@ -147,12 +147,9 @@ class Result:
return False
return True
def __str__(self) -> str:
return self.title
class Status(str):
""" A result which isn't actually returned, but simply updates
"""A result which isn't actually returned, but simply updates
the progress bar. It is equivalent to a string, so this is valid:
``yield Status("module status update")``"""
@ -206,8 +203,8 @@ def run_decorator(real_run):
class BaseModuleMeta(type):
""" Ensures that type-checking is done on all "run" functions
of sub-classes """
"""Ensures that type-checking is done on all "run" functions
of sub-classes"""
def __new__(cls, name, bases, local):
if "run" in local:
@ -216,10 +213,10 @@ class BaseModuleMeta(type):
class BaseModule(metaclass=BaseModuleMeta):
""" Generic module class. This class allows to easily create
"""Generic module class. This class allows to easily create
new modules. Any new module must inherit from this class. The
run method is guaranteed to receive as key-word arguments any
arguments specified in the ``ARGUMENTS`` dictionary. """
arguments specified in the ``ARGUMENTS`` dictionary."""
ARGUMENTS = {
# "name": Argument(int, default="value"),
@ -248,7 +245,7 @@ class BaseModule(metaclass=BaseModuleMeta):
self.name = None
def run(self, session, progress=None, **kwargs):
""" The run method is called via keyword-arguments with all the
"""The run method is called via keyword-arguments with all the
parameters specified in the ``ARGUMENTS`` dictionary. If ``ALLOW_KWARGS``
was True, then other keyword arguments may also be passed. Any
types specified in ``ARGUMENTS`` will already have been checked.

View File

View File

@ -8,7 +8,6 @@ import time
import pwncat
from pwncat.platform.linux import Linux
from pwncat.modules import BaseModule, Status, Argument, List
from pwncat.db import get_session
class Schedule(Enum):
@ -56,90 +55,78 @@ class EnumerateModule(BaseModule):
ensure enumeration modules aren't re-run.
"""
marker_name = self.name
if self.SCHEDULE == Schedule.PER_USER:
marker_name += f".{session.platform.current_user().id}"
# Retrieve the DB target object
target = session.target
with session.db as db:
if clear:
# Filter out all facts which were generated by this module
target.facts = persistent.list.PersistentList(
(f for f in target.facts if f.source != self.name)
)
if clear:
# Delete enumerated facts
session.target.facts = persistent.list.PersistentList(
(f for f in session.target.facts if f.source != self.name)
# Remove the enumeration state if available
del target.enumerate_state[self.name]
# Commit database changes
session.db.transaction_manager.commit()
return
# Yield all the know facts which have already been enumerated
if types:
yield from (
f
for f in target.facts
if f.source == self.name
and any(
any(fnmatch.fnmatch(item_type, req_type) for req_type in types)
for item_type in f.types
)
)
else:
yield from (f for f in target.facts if f.source == self.name)
# Delete our marker
#### We aren't positive how to recreate this in ZODB yet
# if self.SCHEDULE != Schedule.ALWAYS:
# query = (
# db.query(pwncat.db.Fact)
# .filter_by(host_id=session.host, type="marker")
# .filter(pwncat.db.Fact.source.startswith(self.name))
# )
# query.delete(synchronize_session=False)
return
# Check if the module is scheduled to run now
if (self.SCHEDULE == Schedule.ONCE and self.name in target.enumerate_state) or (
self.SCHEDULE == Schedule.PER_USER
and session.platform.current_user().id in target.enumerate_state[self.name]
):
return
# Yield all the know facts which have already been enumerated
existing_facts = (f for f in session.target.facts if f.source == self.name)
if types:
for fact in existing_facts:
for typ in types:
if fnmatch.fnmatch(fact.type, typ):
yield fact
else:
yield from existing_facts
if self.SCHEDULE != Schedule.ALWAYS:
exists = (
db.query(pwncat.db.Fact.id)
.filter_by(host_id=session.host, type="marker", source=marker_name)
.scalar()
is not None
)
if exists:
return
# Get any new facts
# Get any new facts
try:
for item in self.enumerate(session):
# Allow non-fact status updates
if isinstance(item, Status):
yield item
continue
typ, data = item
# session.target.facts.append(fact)
# row = pwncat.db.Fact(
# host_id=session.host, type=typ, data=data, source=self.name
# )
try:
db.add(row)
db.commit()
except sqlalchemy.exc.IntegrityError:
db.rollback()
yield Status(data)
continue
# Only add the item if it doesn't exist
if item not in target.facts:
target.facts.append(item)
# Don't yield the actual fact if we didn't ask for this type
if types:
for typ in types:
if fnmatch.fnmatch(row.type, typ):
yield row
else:
yield Status(data)
if not types or any(
any(fnmatch.fnmatch(item_type, req_type) for req_type in types)
for item_type in item.types
):
yield item
else:
yield row
yield Status(item)
# Add the marker if needed
if self.SCHEDULE != Schedule.ALWAYS:
row = pwncat.db.Fact(
host_id=session.host,
type="marker",
source=marker_name,
data=None,
# Update state for restricted modules
if self.SCHEDULE == Schedule.ONCE:
target.enumerate_state[self.name] = True
elif self.SCHEDULE == Schedule.PER_USER:
if not self.name in target.enumerate_state:
target.enumerate_state[self.name] = persistent.list.PersistentList()
target.enumerate_state[self.name].append(
session.platform.current_user().id
)
db.add(row)
# session.db.transaction_manager.commit()
finally:
# Commit database changes
session.db.transaction_manager.commit()
def enumerate(self, session):
"""
@ -149,4 +136,4 @@ class EnumerateModule(BaseModule):
# This makes `run enumerate` initiate a quick scan
from pwncat.modules.enumerate.quick import Module
from pwncat.modules.agnostic.enumerate.quick import Module

View File

@ -12,8 +12,7 @@ from rich import markup
import pwncat.modules
from pwncat import util
from pwncat.util import console
from pwncat.modules.enumerate import EnumerateModule
from pwncat.db import get_session
from pwncat.modules.agnostic.enumerate import EnumerateModule
def strip_markup(styled_text: str) -> str:

View File

@ -21,7 +21,7 @@ from pwncat.modules import (
ArgumentFormatError,
ModuleFailed,
)
from pwncat.modules.persist import PersistType, PersistModule, PersistError
from pwncat.modules.agnostic.persist import PersistType, PersistModule, PersistError
from pwncat.gtfobins import Capability
from pwncat.file import RemoteBinaryPipe
from pwncat.util import CompilationError
@ -34,7 +34,7 @@ class EscalateError(ModuleFailed):
def fix_euid_mismatch(
escalate: "EscalateModule", exit_cmd: str, target_uid: int, target_gid: int
):
""" Attempt to gain EUID=UID=target_uid.
"""Attempt to gain EUID=UID=target_uid.
This is intended to fix EUID/UID mismatches after a escalation.
"""
@ -161,7 +161,7 @@ def euid_fix(technique_class):
@dataclasses.dataclass
class Technique:
""" Describes a technique possible through some module.
"""Describes a technique possible through some module.
Modules should subclass this class in order to implement
their techniques. Only the methods corresponding to the
@ -176,7 +176,7 @@ class Technique:
""" The module which provides these capabilities """
def write(self, filepath: str, data: bytes):
""" Write the given data to the specified file as another user.
"""Write the given data to the specified file as another user.
:param filepath: path to the target file
:type filepath: str
@ -186,7 +186,7 @@ class Technique:
raise NotImplementedError
def read(self, filepath: str):
""" Read the given file as the specified user
"""Read the given file as the specified user
:param filepath: path to the target file
:type filepath: str
@ -196,7 +196,7 @@ class Technique:
raise NotImplementedError
def exec(self, binary: str):
""" Execute a shell as the specified user.
"""Execute a shell as the specified user.
:param binary: the shell to execute
:type binary: str
@ -220,7 +220,7 @@ class Technique:
class GTFOTechnique(Technique):
""" A technique which is based on a GTFO binary capability.
"""A technique which is based on a GTFO binary capability.
This is mainly used for sudo and setuid techniques, but could theoretically
be used for other techniques.
@ -314,11 +314,11 @@ class GTFOTechnique(Technique):
@dataclasses.dataclass
class FileContentsResult(Result):
""" Result which contains the contents of a file. This is the
"""Result which contains the contents of a file. This is the
result returned from an ``EscalateModule`` when the ``read``
parameter is true. It allows for the file to be used as a
stream programmatically, and also nicely formats the file data
if run from the prompt. """
if run from the prompt."""
filepath: str
""" Path to the file which this data came from """
@ -369,7 +369,7 @@ class FileContentsResult(Result):
@dataclasses.dataclass
class EscalateChain(Result):
""" Chain of techniques used to escalate. When escalating
"""Chain of techniques used to escalate. When escalating
through multiple users, this allows ``pwncat`` to easily
track the different techniques and users that were traversed.
When ``exec`` is used, this object is returned instead of
@ -408,8 +408,8 @@ class EscalateChain(Result):
self.chain.append((technique, exit_cmd))
def extend(self, chain: "EscalateChain"):
""" Extend this chain with another chain. The two chains
are concatenated. """
"""Extend this chain with another chain. The two chains
are concatenated."""
self.chain.extend(chain.chain)
def pop(self):
@ -420,7 +420,7 @@ class EscalateChain(Result):
pwncat.victim.update_user()
def unwrap(self):
""" Exit each shell in the chain with the provided exit script.
"""Exit each shell in the chain with the provided exit script.
This should return the state of the remote shell to prior to
escalation."""
@ -434,7 +434,7 @@ class EscalateChain(Result):
class EscalateResult(Result):
""" The result of running an escalate module. This object contains
"""The result of running an escalate module. This object contains
all the enumerated techniques and provides an abstract way to employ
the techniques to attempt privilege escalation. This is the meat and
bones of the automatic escalation logic, and shouldn't generally need
@ -458,7 +458,7 @@ class EscalateResult(Result):
@property
def category(self):
""" EscalateResults are uncategorized
"""EscalateResults are uncategorized
:meta private:
"""
@ -466,7 +466,7 @@ class EscalateResult(Result):
@property
def title(self):
""" The title of the section when displayed on the terminal
"""The title of the section when displayed on the terminal
:meta private:
"""
@ -474,7 +474,7 @@ class EscalateResult(Result):
@property
def description(self):
""" Description of these results (list of techniques)
"""Description of these results (list of techniques)
:meta private:
"""
@ -487,9 +487,9 @@ class EscalateResult(Result):
return "\n".join(result)
def extend(self, result: "EscalateResult"):
""" Extend this result with another escalation enumeration result.
"""Extend this result with another escalation enumeration result.
This allows you to enumerate multiple modules and utilize all their
techniques together to perform escalation. """
techniques together to perform escalation."""
for key, value in result.techniques.items():
if key not in self.techniques:
@ -562,7 +562,7 @@ class EscalateResult(Result):
return exit_cmd.chain[0][0]
def read(self, user: str, filepath: str, progress, no_exec: bool = False):
""" Attempt to use all the techniques enumerated to read a file
"""Attempt to use all the techniques enumerated to read a file
as the given user. This method returns a file-like object capable
of reading the file.
@ -599,7 +599,10 @@ class EscalateResult(Result):
# We are now running in a shell as this user, just write the file
try:
filp = pwncat.victim.open(filepath, "r",)
filp = pwncat.victim.open(
filepath,
"r",
)
# Our exit command needs to be run as well when the file is
# closed
original_close = filp.close
@ -701,7 +704,7 @@ class EscalateResult(Result):
def _write_authorized_key(
self, user: str, pubkey: str, authkeys: List[str], authkeys_path: str, progress
):
""" Attempt to Write the given public key to the user's authorized
"""Attempt to Write the given public key to the user's authorized
keys file. Return True if successful, otherwise return False.
The authorized keys file will be overwritten with the contents of the given
@ -722,7 +725,7 @@ class EscalateResult(Result):
return technique
def exec(self, user: str, shell: str, progress):
""" Attempt to use all the techniques enumerated to execute a
"""Attempt to use all the techniques enumerated to execute a
shell as the specified user.
:param user: The user to execute a shell as
@ -934,7 +937,7 @@ class EscalateResult(Result):
class EscalateModule(BaseModule):
""" The base module for all escalation modules. This module
"""The base module for all escalation modules. This module
is responsible for enumerating ``Technique`` objects which
can be used to attempt various escalation actions.
@ -978,7 +981,7 @@ class EscalateModule(BaseModule):
escalation. Lower values execute first. """
def run(self, user, exec, read, write, shell, path, data, **kwargs):
""" This method is not overriden by subclasses. Subclasses should
"""This method is not overriden by subclasses. Subclasses should
should implement the ``enumerate`` method which yields techniques.
Running a module results in an EnumerateResult object which can be
@ -1021,7 +1024,7 @@ class EscalateModule(BaseModule):
yield result
def enumerate(self, **kwargs) -> "Generator[Technique, None, None]":
""" Enumerate techniques for this module. Each technique must
"""Enumerate techniques for this module. Each technique must
implement at least one capability, and all techniques will be
used together to escalate privileges. Any custom arguments
are passed to this method through keyword arguments. None of

View File

@ -9,7 +9,7 @@ from pwncat.modules import (
ArgumentFormatError,
MissingArgument,
)
from pwncat.modules.escalate import (
from pwncat.modules.agnostic.escalate import (
EscalateChain,
EscalateResult,
EscalateModule,

View File

@ -16,41 +16,10 @@ from pwncat.modules import (
PersistType,
ArgumentFormatError,
)
from pwncat.db import get_session
def host_type(ident: str) -> pwncat.db.Host:
if isinstance(ident, pwncat.db.Host):
return ident
if ident is None and pwncat.victim is None:
raise ArgumentFormatError("invalid host")
elif ident is None:
return pwncat.victim.host
try:
host = get_session().query(pwncat.db.Host).filter_by(id=int(ident)).first()
except ValueError:
host = None
if host is None:
host = get_session().query(pwncat.db.Host).filter_by(ip=ident).first()
if host is None:
try:
host = (
get_session()
.query(pwncat.db.Host)
.filter_by(ip=socket.gethostbyname(ident))
.first()
)
except socket.gaierror:
host = None
if host is None:
raise ArgumentFormatError("invalid host")
return host
def host_type(ident: str):
return ident
class PersistModule(BaseModule):
@ -120,9 +89,9 @@ class PersistModule(BaseModule):
].help = "Ignored for install/remove. Defaults to root for escalate."
def run(self, remove, escalate, connect, host, **kwargs):
""" This method should not be overriden by subclasses. It handles all logic
"""This method should not be overriden by subclasses. It handles all logic
for installation, escalation, connection, and removal. The standard interface
of this method allows abstract interactions across all persistence modules. """
of this method allows abstract interactions across all persistence modules."""
if "user" not in kwargs:
raise RuntimeError(f"{self.__class__} must take a user argument")

View File

View File

@ -2,9 +2,9 @@
import pwncat
from pwncat.platform.linux import Linux
from pwncat.modules.enumerate import EnumerateModule, Schedule
from pwncat.modules.enumerate.creds import PasswordData
from pwncat.modules.persist.gather import InstalledModule
from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule
from pwncat.modules.linux.enumerate.creds import PasswordData
from pwncat.modules.linux.persist.gather import InstalledModule
class Module(EnumerateModule):

View File

@ -4,8 +4,8 @@ import re
import pwncat
from pwncat.platform.linux import Linux
from pwncat.modules.enumerate import EnumerateModule, Schedule
from pwncat.modules.enumerate.creds import PasswordData
from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule
from pwncat.modules.linux.enumerate.creds import PasswordData
class Module(EnumerateModule):

View File

@ -5,8 +5,8 @@ import time
import pwncat
from pwncat.platform.linux import Linux
from pwncat.modules import Status
from pwncat.modules.enumerate import EnumerateModule, Schedule
from pwncat.modules.enumerate.creds import PrivateKeyData
from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule
from pwncat.modules.linux.enumerate.creds import PrivateKeyData
class Module(EnumerateModule):

View File

@ -5,7 +5,7 @@ import dataclasses
import pwncat
from pwncat.platform.linux import Linux
from pwncat import util
from pwncat.modules.enumerate import EnumerateModule, Schedule
from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule
@dataclasses.dataclass

View File

@ -6,7 +6,7 @@ import pwncat
from pwncat.platform.linux import Linux
from pwncat import util
from pwncat.modules import Status
from pwncat.modules.enumerate import EnumerateModule, Schedule
from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule
@dataclasses.dataclass

View File

@ -5,7 +5,7 @@ import stat
import pwncat
from pwncat.util import Access
from pwncat.platform.linux import Linux
from pwncat.modules.enumerate import EnumerateModule, Schedule
from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule
class Module(EnumerateModule):

View File

@ -6,7 +6,7 @@ import re
import pwncat
from pwncat.platform.linux import Linux
from pwncat.modules import Status
from pwncat.modules.enumerate import EnumerateModule, Schedule
from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule
@dataclasses.dataclass

View File

@ -5,7 +5,7 @@ import re
import shlex
import pwncat
from pwncat.modules.enumerate import EnumerateModule, Schedule
from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule
from pwncat.platform.linux import Linux

View File

@ -6,7 +6,7 @@ from typing import Generator, Optional, List
import pwncat
from pwncat.platform.linux import Linux
from pwncat import util
from pwncat.modules.enumerate import EnumerateModule, Schedule
from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule
per_user = True
sudo_pattern = re.compile(
@ -129,14 +129,23 @@ def LineParser(line):
commands = re.split(r"""(?<!\\), ?""", command)
return SudoSpec(
line, True, user, group, host, runas_user, runas_group, options, hash, commands,
line,
True,
user,
group,
host,
runas_user,
runas_group,
options,
hash,
commands,
)
class Module(EnumerateModule):
""" Enumerate sudo privileges for the current user. If allowed,
"""Enumerate sudo privileges for the current user. If allowed,
this module will also enumerate sudo rules for other users. Normally,
root permissions are needed to read /etc/sudoers. """
root permissions are needed to read /etc/sudoers."""
PROVIDES = ["software.sudo.rule"]
PLATFORM = [Linux]

View File

@ -2,7 +2,7 @@
import dataclasses
import re
from pwncat.modules.enumerate import EnumerateModule, Schedule
from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule
import pwncat
from pwncat.platform.linux import Linux

View File

@ -5,7 +5,7 @@ import dataclasses
import pwncat
from pwncat.platform.linux import Linux
from pwncat import util
from pwncat.modules.enumerate import EnumerateModule, Schedule
from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule
@dataclasses.dataclass

View File

@ -5,7 +5,7 @@ import dataclasses
import pwncat
from pwncat.platform.linux import Linux
from pwncat import util
from pwncat.modules.enumerate import EnumerateModule, Schedule
from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule
@dataclasses.dataclass

View File

@ -5,7 +5,7 @@ import dataclasses
import pwncat
from pwncat.platform.linux import Linux
from pwncat import util
from pwncat.modules.enumerate import EnumerateModule, Schedule
from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule
@dataclasses.dataclass

View File

@ -4,7 +4,7 @@ from typing import List
import pwncat
from pwncat.platform.linux import Linux
from pwncat.modules.enumerate import EnumerateModule, Schedule
from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule
@dataclasses.dataclass

View File

@ -6,7 +6,7 @@ import re
import pwncat
from pwncat.platform.linux import Linux
from pwncat import util
from pwncat.modules.enumerate import EnumerateModule, Schedule
from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule
@dataclasses.dataclass

View File

@ -6,7 +6,7 @@ import pwncat
from pwncat.platform.linux import Linux
from pwncat import util
from pwncat.modules import Result
from pwncat.modules.enumerate import EnumerateModule, Schedule
from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule
@dataclasses.dataclass

View File

@ -3,7 +3,7 @@ import dataclasses
import pwncat
from pwncat.platform.linux import Linux
from pwncat.modules.enumerate import EnumerateModule, Schedule
from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule
@dataclasses.dataclass

View File

@ -5,7 +5,7 @@ import shlex
import pwncat
from pwncat.platform.linux import Linux
from pwncat.modules.enumerate import EnumerateModule, Schedule
from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule
@dataclasses.dataclass

View File

@ -4,7 +4,7 @@ from typing import Dict
import pwncat
from pwncat.platform.linux import Linux
from pwncat.modules.enumerate import EnumerateModule, Schedule
from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule
@dataclasses.dataclass

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python3
import dataclasses
from pwncat.modules.enumerate import EnumerateModule, Schedule
from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule
import pwncat
from pwncat.platform.linux import Linux
from pwncat.util import Init

View File

@ -7,7 +7,7 @@ import json
import pwncat
from pwncat.platform.linux import Linux
from pwncat import util
from pwncat.modules.enumerate import EnumerateModule, Schedule
from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule
@dataclasses.dataclass

View File

@ -0,0 +1,92 @@
#!/usr/bin/env python3
from pwncat.modules import ModuleFailed, Status
from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule
from pwncat.platform.linux import Linux, LinuxUser
class Module(EnumerateModule):
""" Enumerate users from a linux target """
PROVIDES = ["user"]
PLATFORM = [Linux]
SCHEDULE = Schedule.ONCE
def enumerate(self, session: "pwncat.manager.Session"):
passwd = session.platform.Path("/etc/passwd")
shadow = session.platform.Path("/etc/shadow")
users = {}
try:
with passwd.open("r") as filp:
for user_info in filp:
try:
# Extract the user fields
(
name,
hash,
uid,
gid,
comment,
home,
shell,
) = user_info.split(":")
# Build a user object
user = LinuxUser(
self.name,
name,
hash,
int(uid),
int(gid),
comment,
home,
shell,
)
users[name] = user
yield Status(user)
except Exception as exc:
# Bad passwd line
continue
except (FileNotFoundError, PermissionError) as exc:
raise ModuleFailed(str(exc)) from exc
try:
with shadow.open("r") as filp:
for user_info in filp:
try:
(
name,
hash,
last_change,
min_age,
max_age,
warn_period,
inactive_period,
expir_date,
reserved,
) = user_info.split(":")
if users[name].hash is None:
users[name].hash = hash if hash != "" else None
if users[name].password is None and hash == "":
users[name].password = ""
users[name].last_change = int(last_change)
users[name].min_age = int(min_age)
users[name].max_age = int(max_age)
users[name].warn_period = int(warn_period)
users[name].inactive_period = int(inactive_period)
users[name].expiration = int(expir_date)
users[name].reserved = reserved
except:
continue
except (FileNotFoundError, PermissionError):
pass
except Exception as exc:
raise ModuleFailed(str(exc)) from exc
# Yield all the known users after attempting to parse /etc/shadow
yield from users.values()

View File

@ -4,7 +4,7 @@ import dataclasses
from rich.table import Table
from pwncat.modules.enumerate import EnumerateModule, Schedule
from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule
from pwncat.platform.windows import Windows

View File

@ -2,7 +2,7 @@
import pwncat
from pwncat.gtfobins import Capability, Stream, BinaryNotFound
from pwncat.modules.escalate import (
from pwncat.modules.agnostic.escalate import (
EscalateModule,
EscalateError,
GTFOTechnique,

View File

@ -4,14 +4,14 @@ import pwncat
from pwncat.modules import Status
from pwncat.platform.linux import Linux
from pwncat.gtfobins import Capability
from pwncat.modules.persist import PersistError, PersistType
from pwncat.modules.persist.gather import InstalledModule
from pwncat.modules.escalate import EscalateError, EscalateModule, Technique
from pwncat.modules.agnostic.persist import PersistError, PersistType
from pwncat.modules.agnostic.persist.gather import InstalledModule
from pwncat.modules.agnostic.escalate import EscalateError, EscalateModule, Technique
class PersistenceTechnique(Technique):
""" Escalates privileges utilizing an installed persistence
technique. """
"""Escalates privileges utilizing an installed persistence
technique."""
def __init__(self, module: EscalateModule, user: str, persist: InstalledModule):
super(PersistenceTechnique, self).__init__(Capability.SHELL, user, module)
@ -29,8 +29,8 @@ class PersistenceTechnique(Technique):
class Module(EscalateModule):
""" This module will enumerate all installed persistence methods which
offer local escalation. """
"""This module will enumerate all installed persistence methods which
offer local escalation."""
PLATFORM = None
PRIORITY = -1

View File

@ -5,7 +5,7 @@ from io import StringIO
import pwncat
from pwncat.gtfobins import Capability
from pwncat.modules.escalate import EscalateError, EscalateModule, Technique
from pwncat.modules.agnostic.escalate import EscalateError, EscalateModule, Technique
class ScreenTechnique(Technique):

View File

@ -3,7 +3,7 @@
import pwncat
from pwncat.util import Access
from pwncat.gtfobins import Capability, Stream, BinaryNotFound
from pwncat.modules.escalate import (
from pwncat.modules.agnostic.escalate import (
EscalateModule,
EscalateError,
GTFOTechnique,

View File

@ -3,7 +3,12 @@
import pwncat
from pwncat.gtfobins import BinaryNotFound, Capability, Stream
from pwncat.modules import Status
from pwncat.modules.escalate import EscalateError, EscalateModule, Technique, euid_fix
from pwncat.modules.agnostic.escalate import (
EscalateError,
EscalateModule,
Technique,
euid_fix,
)
from pwncat.util import Access
@ -24,7 +29,8 @@ class SuTechnique(Technique):
if current_user.name != "root":
# Send the su command, and check if it succeeds
pwncat.victim.run(
f'su {self.user} -c "echo good"', wait=False,
f'su {self.user} -c "echo good"',
wait=False,
)
pwncat.victim.recvuntil(": ")

View File

@ -2,7 +2,7 @@
import pwncat
from pwncat.gtfobins import Capability, Stream, BinaryNotFound
from pwncat.modules.escalate import (
from pwncat.modules.agnostic.escalate import (
EscalateModule,
EscalateError,
GTFOTechnique,

View File

@ -11,7 +11,7 @@ import pwncat.tamper
from pwncat.util import Access
from pwncat.platform.linux import Linux
from pwncat.modules import Argument, PersistType, PersistError
from pwncat.modules.persist import PersistModule
from pwncat.modules.agnostic.persist import PersistModule
class Module(PersistModule):

View File

@ -5,14 +5,13 @@ import socket
import pwncat
from pwncat.util import console
from pwncat.modules import BaseModule, Argument, Status, Bool, Result
import pwncat.modules.persist
from pwncat.db import get_session
import pwncat.modules.agnostic.persist
@dataclasses.dataclass
class InstalledModule(Result):
""" Represents an installed module. It contains the persistence
database object and the underlying module object. """
"""Represents an installed module. It contains the persistence
database object and the underlying module object."""
persist: pwncat.db.Persistence
module: "pwncat.modules.persist.PersistModule"

View File

@ -13,7 +13,7 @@ import pwncat
from pwncat.util import CompilationError, Access
from pwncat.platform.linux import Linux
from pwncat.modules import Argument, Status, PersistError, PersistType
from pwncat.modules.persist import PersistModule
from pwncat.modules.agnostic.persist import PersistModule
class Module(PersistModule):

View File

@ -6,7 +6,7 @@ import paramiko
import pwncat
from pwncat.modules import Argument, Status, PersistType, PersistError
from pwncat.modules.persist import PersistModule
from pwncat.modules.agnostic.persist import PersistModule
class Module(PersistModule):

View File

View File

@ -17,6 +17,7 @@ import pwncat.subprocess
from pwncat import util
from pwncat.gtfobins import GTFOBins, Capability, Stream, MissingBinary
from pwncat.platform import Platform, PlatformError, Path
from pwncat.db.user import User
class PopenLinux(pwncat.subprocess.Popen):
@ -442,6 +443,34 @@ class LinuxWriter(BufferedIOBase):
self.detach()
class LinuxUser(User):
""" Linux-specific user definition """
def __init__(
self,
source,
name,
hash,
uid,
gid,
comment,
home,
shell,
password: Optional[str] = None,
):
# Normally, the hash is only stored in /etc/shadow
if hash == "x":
hash = None
super().__init__(source, name, uid, password=password, hash=hash)
self.gid = gid
self.comment = comment
self.home = home
self.shell = shell
class Linux(Platform):
"""
Concrete platform class abstracting interaction with a GNU/Linux remote

103
pwncat/subprocess.py Normal file
View File

@ -0,0 +1,103 @@
#!/usr/bin/env python3
from typing import List, IO, Optional
from subprocess import (
CompletedProcess,
SubprocessError,
TimeoutExpired,
CalledProcessError,
DEVNULL,
PIPE,
)
import io
import pwncat
class Popen:
"""Base class for Popen objects defining the interface.
Individual platforms will subclass this object to implement
the correct logic. This is an abstract class."""
stdin: IO
"""
If the stdin argument was PIPE, this attribute is a writeable
stream object as returned by open(). If the encoding or errors
arguments were specified or the universal_newlines argument was
True, the stream is a text stream, otherwise it is a byte
stream. If the stdin argument was not PIPE, this attribute is
None.
"""
stdout: IO
"""
If the stdout argument was PIPE, this attribute is a readable
stream object as returned by open(). Reading from the stream
provides output from the child process. If the encoding or
errors arguments were specified or the universal_newlines
argument was True, the stream is a text stream, otherwise it
is a byte stream. If the stdout argument was not PIPE, this
attribute is None.
"""
stderr: IO
"""
If the stderr argument was PIPE, this attribute is a readable
stream object as returned by open(). Reading from the stream
provides error output from the child process. If the encoding
or errors arguments were specified or the universal_newlines
argument was True, the stream is a text stream, otherwise it
is a byte stream. If the stderr argument was not PIPE, this
attribute is None.
"""
args: List[str]
"""
The args argument as it was passed to Popen a sequence of
program arguments or else a single string.
"""
pid: int
""" The process ID of the child process. """
returncode: int
"""
The child return code, set by poll() and wait() (and indirectly by
communicate()). A None value indicates that the process hasnt
terminated yet.
"""
def __init__(self):
self.pid = None
self.returncode = None
self.args = None
self.stderr = None
self.stdout = None
self.stdin = None
def poll(self) -> Optional[int]:
"""Check if the child process has terminated. Set and return
``returncode`` attribute. Otherwise, returns None."""
def wait(self, timeout: float = None) -> int:
"""Wait for child process to terminate. Set and return
``returncode`` attribute.
If the process does not terminate after ``timeout`` seconds,
raise a ``TimeoutExpired`` exception. It is safe to catch
this exception and retry the wait.
"""
def communicate(self, input: bytes = None, timeout: float = None):
"""Interact with process: Send data to stdin. Read data from stdout
and stderr, until end-of-file is readched. Wait for the process to
terminate and set the ``returncode`` attribute. The optional ``input``
argument should be data to be sent to the child process, or None, if
no data should be sent to the child. If streams were opened in text mode,
``input`` must be a string. Otherwise, it must be ``bytes``."""
def send_signal(self, signal: int):
"""Sends the signal ``signal`` to the child.
Does nothing if the process completed.
"""
def terminate(self):
""" Stop the child. """
def kill(self):
""" Kills the child """

View File

@ -6,7 +6,8 @@ from colorama import Fore
import pwncat
from pwncat.util import Access
from pwncat.db import get_session
# from pwncat.db import get_session
class Action(Enum):
@ -47,10 +48,10 @@ class CreatedFile(Tamper):
class ModifiedFile(Tamper):
""" File modification tamper. This tamper needs either a specific line which
"""File modification tamper. This tamper needs either a specific line which
should be removed from a text file, or the original original_content as bytes which
will be replaced. If neither is provided, we will track the modification but be unable
to revert it. """
to revert it."""
def __init__(
self, path: str, added_lines: List[str] = None, original_content: bytes = None
@ -113,12 +114,12 @@ class LambdaTamper(Tamper):
class TamperManager:
""" TamperManager not only provides some automated ability to tamper with
"""TamperManager not only provides some automated ability to tamper with
properties of the remote system, but also a tracker for all modifications
on the remote system with the ability to remove previous changes. Other modules
can register system changes with `PtyHandler.tamper` in order to allow the
user to get a wholistic view of all modifications of the remote system, and
attempt revert all modifications automatically. """
attempt revert all modifications automatically."""
def __init__(self):
# List of tampers registered with this manager
@ -173,8 +174,8 @@ class TamperManager:
return pickle.loads(pwncat.victim.host.tampers[item].data)
def remove(self, tamper: Tamper):
""" Pop a tamper from the list of known tampers. This does not revert the tamper.
It removes the tracking for this tamper. """
"""Pop a tamper from the list of known tampers. This does not revert the tamper.
It removes the tracking for this tamper."""
tracker = (
get_session().query(pwncat.db.Tamper).filter_by(name=str(tamper)).first()

View File

@ -4,7 +4,7 @@ import enum
import persistent
import persistent.list
from BTrees.OOBTree import TreeSet
from BTrees.OOBTree import TreeSet, OOBTree
class NAT(enum.Enum):
@ -75,6 +75,8 @@ class Target(persistent.Persistent):
""" Target host operating system """
self.facts: persistent.list.PersistentList = persistent.list.PersistentList()
""" List of enumerated facts about the target host """
self.enumerate_state: OOBTree = OOBTree()
""" The state of all enumeration modules which drives the module schedule """
self.tampers: persistent.list.PersistentList = persistent.list.PersistentList()
""" List of files/properties of the target that have been modified and/or created. """
self.users: persistent.list.PersistentList = persistent.list.PersistentList()

View File

@ -23,7 +23,7 @@ import os
from rich.console import Console
console = Console()
console = Console(emoji=False)
CTRL_C = b"\x03"

1
pwncatrc Symbolic link
View File

@ -0,0 +1 @@
./data/pwncatrc