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:
parent
81e000504a
commit
148c0ba450
@ -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.
|
||||
|
||||
|
@ -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.
|
||||
|
||||
|
@ -20,7 +20,6 @@ from pwncat.commands.base import (
|
||||
)
|
||||
|
||||
from pwncat.modules import PersistError
|
||||
from pwncat.db import get_session
|
||||
|
||||
|
||||
class Command(CommandDefinition):
|
||||
|
@ -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"
|
||||
|
@ -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(
|
||||
|
@ -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="..."
|
||||
),
|
||||
|
@ -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):
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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)})"""
|
||||
|
@ -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()
|
||||
|
@ -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.
|
||||
|
0
pwncat/modules/agnostic/__init__.py
Normal file
0
pwncat/modules/agnostic/__init__.py
Normal 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
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -9,7 +9,7 @@ from pwncat.modules import (
|
||||
ArgumentFormatError,
|
||||
MissingArgument,
|
||||
)
|
||||
from pwncat.modules.escalate import (
|
||||
from pwncat.modules.agnostic.escalate import (
|
||||
EscalateChain,
|
||||
EscalateResult,
|
||||
EscalateModule,
|
||||
|
@ -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")
|
||||
|
0
pwncat/modules/linux/__init__.py
Normal file
0
pwncat/modules/linux/__init__.py
Normal 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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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()
|
@ -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
|
||||
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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,
|
||||
|
@ -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(": ")
|
||||
|
@ -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,
|
||||
|
@ -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):
|
||||
|
@ -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"
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
0
pwncat/modules/windows/__init__.py
Normal file
0
pwncat/modules/windows/__init__.py
Normal 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
103
pwncat/subprocess.py
Normal 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 hasn’t
|
||||
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 """
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -23,7 +23,7 @@ import os
|
||||
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
console = Console(emoji=False)
|
||||
|
||||
CTRL_C = b"\x03"
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user