mirror of
https://github.com/calebstewart/pwncat.git
synced 2024-12-02 13:24:15 +01:00
Most of enumerate modules are working with platforms/sessions/managers
This commit is contained in:
parent
f80d6b65ee
commit
c1068ad567
@ -118,14 +118,16 @@ class ChannelFile(RawIOBase):
|
|||||||
|
|
||||||
# Check the type of the argument, and grab the relevant part
|
# Check the type of the argument, and grab the relevant part
|
||||||
obj = b.obj if isinstance(b, memoryview) else b
|
obj = b.obj if isinstance(b, memoryview) else b
|
||||||
|
n = 0
|
||||||
|
|
||||||
try:
|
while n == 0:
|
||||||
n = self.channel.recvinto(b)
|
try:
|
||||||
except NotImplementedError:
|
n = self.channel.recvinto(b)
|
||||||
# recvinto was not implemented, fallback recv
|
except NotImplementedError:
|
||||||
data = self.channel.recv(len(b))
|
# recvinto was not implemented, fallback recv
|
||||||
b[: len(data)] = data
|
data = self.channel.recv(len(b))
|
||||||
n = len(data)
|
b[: len(data)] = data
|
||||||
|
n = len(data)
|
||||||
|
|
||||||
obj = bytes(b[:n])
|
obj = bytes(b[:n])
|
||||||
|
|
||||||
|
@ -120,7 +120,7 @@ class Session:
|
|||||||
|
|
||||||
return self.manager.modules[module].run(self, **kwargs)
|
return self.manager.modules[module].run(self, **kwargs)
|
||||||
|
|
||||||
def find_module(self, pattern: str, base=None):
|
def find_module(self, pattern: str, base=None, exact: bool = False):
|
||||||
""" Locate a module by a glob pattern. This is an generator
|
""" Locate a module by a glob pattern. This is an generator
|
||||||
which may yield multiple modules that match the pattern and
|
which may yield multiple modules that match the pattern and
|
||||||
base class. """
|
base class. """
|
||||||
@ -134,7 +134,13 @@ class Session:
|
|||||||
and type(self.platform) not in module.PLATFORM
|
and type(self.platform) not in module.PLATFORM
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
if fnmatch.fnmatch(name, pattern) and isinstance(module, base):
|
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
|
yield module
|
||||||
|
|
||||||
def log(self, *args, **kwargs):
|
def log(self, *args, **kwargs):
|
||||||
@ -158,7 +164,10 @@ class Session:
|
|||||||
self._db_session = self.manager.create_db_session()
|
self._db_session = self.manager.create_db_session()
|
||||||
yield self._db_session
|
yield self._db_session
|
||||||
finally:
|
finally:
|
||||||
self._db_session.commit()
|
try:
|
||||||
|
self._db_session.commit()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def task(self, *args, **kwargs):
|
def task(self, *args, **kwargs):
|
||||||
@ -273,7 +282,7 @@ class Manager:
|
|||||||
|
|
||||||
self.engine = create_engine(self.config["db"])
|
self.engine = create_engine(self.config["db"])
|
||||||
pwncat.db.Base.metadata.create_all(self.engine)
|
pwncat.db.Base.metadata.create_all(self.engine)
|
||||||
self.SessionBuilder = sessionmaker(bind=self.engine)
|
self.SessionBuilder = sessionmaker(bind=self.engine, expire_on_commit=False)
|
||||||
self.parser = CommandParser(self)
|
self.parser = CommandParser(self)
|
||||||
|
|
||||||
def create_db_session(self):
|
def create_db_session(self):
|
||||||
|
@ -163,23 +163,18 @@ def run_decorator(real_run):
|
|||||||
@functools.wraps(real_run)
|
@functools.wraps(real_run)
|
||||||
def decorator(self, session, progress=None, **kwargs):
|
def decorator(self, session, progress=None, **kwargs):
|
||||||
|
|
||||||
if "exec" in kwargs:
|
|
||||||
has_exec = True
|
|
||||||
else:
|
|
||||||
has_exec = False
|
|
||||||
|
|
||||||
# Validate arguments
|
# Validate arguments
|
||||||
for key in kwargs:
|
for key in kwargs:
|
||||||
if key in self.ARGUMENTS:
|
if key in self.ARGUMENTS:
|
||||||
try:
|
try:
|
||||||
kwargs[key] = self.ARGUMENTS[key].type(kwargs[key])
|
kwargs[key] = self.ARGUMENTS[key].type(kwargs[key])
|
||||||
except ValueError:
|
except ValueError as exc:
|
||||||
raise ArgumentFormatError(key)
|
raise ArgumentFormatError(key) from exc
|
||||||
elif not self.ALLOW_KWARGS:
|
elif not self.ALLOW_KWARGS:
|
||||||
raise InvalidArgument(key)
|
raise InvalidArgument(key)
|
||||||
for key in self.ARGUMENTS:
|
for key in self.ARGUMENTS:
|
||||||
if key not in kwargs and key in pwncat.config:
|
if key not in kwargs and key in session.config:
|
||||||
kwargs[key] = pwncat.config[key]
|
kwargs[key] = session.config[key]
|
||||||
elif key not in kwargs and self.ARGUMENTS[key].default is not NoValue:
|
elif key not in kwargs and self.ARGUMENTS[key].default is not NoValue:
|
||||||
kwargs[key] = self.ARGUMENTS[key].default
|
kwargs[key] = self.ARGUMENTS[key].default
|
||||||
elif key not in kwargs and self.ARGUMENTS[key].default is NoValue:
|
elif key not in kwargs and self.ARGUMENTS[key].default is NoValue:
|
||||||
@ -192,28 +187,11 @@ def run_decorator(real_run):
|
|||||||
result_object = real_run(self, session, **kwargs)
|
result_object = real_run(self, session, **kwargs)
|
||||||
|
|
||||||
if inspect.isgenerator(result_object):
|
if inspect.isgenerator(result_object):
|
||||||
|
with session.task(description=self.name, status="...") as task:
|
||||||
try:
|
|
||||||
if progress is None:
|
|
||||||
# We weren't given a progress instance, so start one ourselves
|
|
||||||
self.progress = Progress(
|
|
||||||
"collecting results",
|
|
||||||
"•",
|
|
||||||
"[yellow]{task.fields[module]}",
|
|
||||||
"•",
|
|
||||||
"[cyan]{task.fields[status]}",
|
|
||||||
transient=True,
|
|
||||||
console=console,
|
|
||||||
)
|
|
||||||
self.progress.start()
|
|
||||||
|
|
||||||
# Added a task to this progress bar
|
|
||||||
task = self.progress.add_task("", module=self.name, status="...")
|
|
||||||
|
|
||||||
# Collect results
|
# Collect results
|
||||||
results = []
|
results = []
|
||||||
for item in result_object:
|
for item in result_object:
|
||||||
self.progress.update(task, status=str(item))
|
session.update_task(task, status=str(item))
|
||||||
if not isinstance(item, Status):
|
if not isinstance(item, Status):
|
||||||
results.append(item)
|
results.append(item)
|
||||||
|
|
||||||
@ -221,18 +199,6 @@ def run_decorator(real_run):
|
|||||||
return results[0]
|
return results[0]
|
||||||
|
|
||||||
return results
|
return results
|
||||||
finally:
|
|
||||||
if progress is None:
|
|
||||||
# If we are the last task/this is our progress bar,
|
|
||||||
# we don't hide ourselves. This makes the progress bar
|
|
||||||
# empty, and "transient" ends up remove an extra line in
|
|
||||||
# the terminal.
|
|
||||||
self.progress.stop()
|
|
||||||
else:
|
|
||||||
# This task is done, hide it.
|
|
||||||
self.progress.update(
|
|
||||||
task, completed=True, visible=False, status="complete"
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
return result_object
|
return result_object
|
||||||
|
|
||||||
@ -296,141 +262,3 @@ class BaseModule(metaclass=BaseModuleMeta):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
def reload(where: typing.Optional[typing.List[str]] = None):
|
|
||||||
""" Reload modules from the given directory. If no directory
|
|
||||||
is specified, then the default modules are reloaded. This
|
|
||||||
function will not remove or un-load any existing modules, but
|
|
||||||
may overwrite existing modules with conflicting names.
|
|
||||||
|
|
||||||
:param where: Directories which contain pwncat modules
|
|
||||||
:type where: List[str]
|
|
||||||
"""
|
|
||||||
|
|
||||||
# We need to load built-in modules first
|
|
||||||
if not LOADED_MODULES and where is not None:
|
|
||||||
reload()
|
|
||||||
|
|
||||||
# If no paths were specified, load built-ins
|
|
||||||
if where is None:
|
|
||||||
where = __path__
|
|
||||||
|
|
||||||
for loader, module_name, _ in pkgutil.walk_packages(where, prefix=__name__ + "."):
|
|
||||||
module = loader.find_module(module_name).load_module(module_name)
|
|
||||||
|
|
||||||
if getattr(module, "Module", None) is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
module_name = module_name.split(__name__ + ".")[1]
|
|
||||||
|
|
||||||
LOADED_MODULES[module_name] = module.Module()
|
|
||||||
|
|
||||||
setattr(LOADED_MODULES[module_name], "name", module_name)
|
|
||||||
|
|
||||||
|
|
||||||
def find(name: str, base=BaseModule, ignore_platform: bool = False):
|
|
||||||
""" Locate a module with this exact name. Optionally filter
|
|
||||||
modules based on their class type. By default, this will search
|
|
||||||
for any module implementing BaseModule which is applicable to
|
|
||||||
the current platform.
|
|
||||||
|
|
||||||
:param name: Name of the module to locate
|
|
||||||
:type name: str
|
|
||||||
:param base: Base class which the module must implement
|
|
||||||
:type base: type
|
|
||||||
:param ignore_platform: Whether to ignore the victim's platform in the search
|
|
||||||
:type ignore_platform: bool
|
|
||||||
:raises ModuleNotFoundError: Raised if the module does not exist or the platform/base class do not match.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if not LOADED_MODULES:
|
|
||||||
reload()
|
|
||||||
|
|
||||||
if name not in LOADED_MODULES:
|
|
||||||
raise ModuleNotFoundError(f"{name}: module not found")
|
|
||||||
|
|
||||||
if not isinstance(LOADED_MODULES[name], base):
|
|
||||||
raise ModuleNotFoundError(f"{name}: incorrect base class")
|
|
||||||
|
|
||||||
# Grab the module
|
|
||||||
module = LOADED_MODULES[name]
|
|
||||||
|
|
||||||
if not ignore_platform:
|
|
||||||
if module.PLATFORM != Platform.NO_HOST and pwncat.victim.host is None:
|
|
||||||
raise ModuleNotFoundError(f"{module.name}: no connected victim")
|
|
||||||
elif (
|
|
||||||
module.PLATFORM != Platform.NO_HOST
|
|
||||||
and pwncat.victim.host.platform not in module.PLATFORM
|
|
||||||
):
|
|
||||||
raise ModuleNotFoundError(f"{module.name}: incorrect platform")
|
|
||||||
|
|
||||||
return module
|
|
||||||
|
|
||||||
|
|
||||||
def match(pattern: str, base=BaseModule):
|
|
||||||
""" Locate modules who's name matches the given glob pattern.
|
|
||||||
This function will only return modules which implement a subclass
|
|
||||||
of the given base class and which are applicable to the current
|
|
||||||
target's platform.
|
|
||||||
|
|
||||||
:param pattern: A Unix glob-like pattern for the module name
|
|
||||||
:type pattern: str
|
|
||||||
:param base: The base class for modules you are looking for (defaults to BaseModule)
|
|
||||||
:type base: type
|
|
||||||
:return: A generator yielding module objects which at least implement ``base``
|
|
||||||
:rtype: Generator[base, None, None]
|
|
||||||
"""
|
|
||||||
|
|
||||||
if not LOADED_MODULES:
|
|
||||||
reload()
|
|
||||||
|
|
||||||
for module_name, module in LOADED_MODULES.items():
|
|
||||||
|
|
||||||
# NOTE - this should be cleaned up. It's gross.
|
|
||||||
if not isinstance(module, base):
|
|
||||||
continue
|
|
||||||
if module.PLATFORM != Platform.NO_HOST and pwncat.victim.host is None:
|
|
||||||
continue
|
|
||||||
elif (
|
|
||||||
module.PLATFORM != Platform.NO_HOST
|
|
||||||
and pwncat.victim.host.platform not in module.PLATFORM
|
|
||||||
):
|
|
||||||
continue
|
|
||||||
if not fnmatch.fnmatch(module_name, pattern):
|
|
||||||
continue
|
|
||||||
|
|
||||||
yield module
|
|
||||||
|
|
||||||
|
|
||||||
def run(name: str, **kwargs):
|
|
||||||
""" Locate a module by name and execute it. The module can be of any
|
|
||||||
type and is guaranteed to match the current platform. If no module can
|
|
||||||
be found which matches those criteria, an exception is thrown.
|
|
||||||
|
|
||||||
:param name: The name of the module to run
|
|
||||||
:type name: str
|
|
||||||
:param kwargs: Keyword arguments for the module
|
|
||||||
:type kwargs: Dict[str, Any]
|
|
||||||
:returns: The result from the module's ``run`` method.
|
|
||||||
:raises ModuleNotFoundError: If no module with that name matches the required criteria
|
|
||||||
"""
|
|
||||||
|
|
||||||
if not LOADED_MODULES:
|
|
||||||
reload()
|
|
||||||
|
|
||||||
if name not in LOADED_MODULES:
|
|
||||||
raise ModuleNotFoundError(f"{name}: module not found")
|
|
||||||
|
|
||||||
# Grab the module
|
|
||||||
module = LOADED_MODULES[name]
|
|
||||||
|
|
||||||
if module.PLATFORM != Platform.NO_HOST and pwncat.victim.host is None:
|
|
||||||
raise ModuleNotFoundError(f"{module.name}: no connected victim")
|
|
||||||
elif (
|
|
||||||
module.PLATFORM != Platform.NO_HOST
|
|
||||||
and pwncat.victim.host.platform not in module.PLATFORM
|
|
||||||
):
|
|
||||||
raise ModuleNotFoundError(f"{module.name}: incorrect platform")
|
|
||||||
|
|
||||||
return module.run(**kwargs)
|
|
||||||
|
@ -26,7 +26,7 @@ class EnumerateModule(BaseModule):
|
|||||||
# This should be set by the sub-classes to know where to find
|
# This should be set by the sub-classes to know where to find
|
||||||
# different types of enumeration data
|
# different types of enumeration data
|
||||||
PROVIDES = []
|
PROVIDES = []
|
||||||
PLATFORM = [Linux]
|
PLATFORM = []
|
||||||
|
|
||||||
# Defines how often to run this enumeration. The default is to
|
# Defines how often to run this enumeration. The default is to
|
||||||
# only run once per system/target.
|
# only run once per system/target.
|
||||||
@ -48,7 +48,7 @@ class EnumerateModule(BaseModule):
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
def run(self, types, clear):
|
def run(self, session, types, clear):
|
||||||
""" Locate all facts this module provides.
|
""" Locate all facts this module provides.
|
||||||
|
|
||||||
Sub-classes should not override this method. Instead, use the
|
Sub-classes should not override this method. Instead, use the
|
||||||
@ -58,98 +58,88 @@ class EnumerateModule(BaseModule):
|
|||||||
|
|
||||||
marker_name = self.name
|
marker_name = self.name
|
||||||
if self.SCHEDULE == Schedule.PER_USER:
|
if self.SCHEDULE == Schedule.PER_USER:
|
||||||
marker_name += f".{pwncat.victim.current_user.id}"
|
marker_name += f".{session.platform.current_user().id}"
|
||||||
|
|
||||||
if clear:
|
with session.db as db:
|
||||||
# Delete enumerated facts
|
|
||||||
query = (
|
if clear:
|
||||||
get_session()
|
# Delete enumerated facts
|
||||||
.query(pwncat.db.Fact)
|
query = db.query(pwncat.db.Fact).filter_by(
|
||||||
.filter_by(source=self.name, host_id=pwncat.victim.host.id)
|
source=self.name, host_id=session.host
|
||||||
)
|
|
||||||
query.delete(synchronize_session=False)
|
|
||||||
# Delete our marker
|
|
||||||
if self.SCHEDULE != Schedule.ALWAYS:
|
|
||||||
query = (
|
|
||||||
get_session()
|
|
||||||
.query(pwncat.db.Fact)
|
|
||||||
.filter_by(host_id=pwncat.victim.host.id, type="marker")
|
|
||||||
.filter(pwncat.db.Fact.source.startswith(self.name))
|
|
||||||
)
|
)
|
||||||
query.delete(synchronize_session=False)
|
query.delete(synchronize_session=False)
|
||||||
return
|
# Delete our marker
|
||||||
|
if self.SCHEDULE != Schedule.ALWAYS:
|
||||||
# Yield all the know facts which have already been enumerated
|
query = (
|
||||||
existing_facts = (
|
db.query(pwncat.db.Fact)
|
||||||
get_session()
|
.filter_by(host_id=session.host, type="marker")
|
||||||
.query(pwncat.db.Fact)
|
.filter(pwncat.db.Fact.source.startswith(self.name))
|
||||||
.filter_by(source=self.name, host_id=pwncat.victim.host.id)
|
)
|
||||||
.filter(pwncat.db.Fact.type != "marker")
|
query.delete(synchronize_session=False)
|
||||||
)
|
|
||||||
|
|
||||||
if types:
|
|
||||||
for fact in existing_facts.all():
|
|
||||||
for typ in types:
|
|
||||||
if fnmatch.fnmatch(fact.type, typ):
|
|
||||||
yield fact
|
|
||||||
else:
|
|
||||||
yield from existing_facts.all()
|
|
||||||
|
|
||||||
if self.SCHEDULE != Schedule.ALWAYS:
|
|
||||||
exists = (
|
|
||||||
get_session()
|
|
||||||
.query(pwncat.db.Fact.id)
|
|
||||||
.filter_by(
|
|
||||||
host_id=pwncat.victim.host.id, type="marker", source=marker_name
|
|
||||||
)
|
|
||||||
.scalar()
|
|
||||||
is not None
|
|
||||||
)
|
|
||||||
if exists:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get any new facts
|
# Yield all the know facts which have already been enumerated
|
||||||
for item in self.enumerate():
|
existing_facts = (
|
||||||
if isinstance(item, Status):
|
db.query(pwncat.db.Fact)
|
||||||
yield item
|
.filter_by(source=self.name, host_id=session.host)
|
||||||
continue
|
.filter(pwncat.db.Fact.type != "marker")
|
||||||
|
|
||||||
typ, data = item
|
|
||||||
|
|
||||||
row = pwncat.db.Fact(
|
|
||||||
host_id=pwncat.victim.host.id, type=typ, data=data, source=self.name
|
|
||||||
)
|
)
|
||||||
try:
|
|
||||||
get_session().add(row)
|
|
||||||
pwncat.victim.host.facts.append(row)
|
|
||||||
get_session().commit()
|
|
||||||
except sqlalchemy.exc.IntegrityError:
|
|
||||||
get_session().rollback()
|
|
||||||
yield Status(data)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Don't yield the actual fact if we didn't ask for this type
|
|
||||||
if types:
|
if types:
|
||||||
for typ in types:
|
for fact in existing_facts.all():
|
||||||
if fnmatch.fnmatch(row.type, typ):
|
for typ in types:
|
||||||
yield row
|
if fnmatch.fnmatch(fact.type, typ):
|
||||||
else:
|
yield fact
|
||||||
yield Status(data)
|
|
||||||
else:
|
else:
|
||||||
yield row
|
yield from existing_facts.all()
|
||||||
|
|
||||||
# Add the marker if needed
|
if self.SCHEDULE != Schedule.ALWAYS:
|
||||||
if self.SCHEDULE != Schedule.ALWAYS:
|
exists = (
|
||||||
row = pwncat.db.Fact(
|
db.query(pwncat.db.Fact.id)
|
||||||
host_id=pwncat.victim.host.id,
|
.filter_by(host_id=session.host, type="marker", source=marker_name)
|
||||||
type="marker",
|
.scalar()
|
||||||
source=marker_name,
|
is not None
|
||||||
data=None,
|
)
|
||||||
)
|
if exists:
|
||||||
get_session().add(row)
|
return
|
||||||
pwncat.victim.host.facts.append(row)
|
|
||||||
|
|
||||||
def enumerate(self):
|
# Get any new facts
|
||||||
|
for item in self.enumerate(session):
|
||||||
|
if isinstance(item, Status):
|
||||||
|
yield item
|
||||||
|
continue
|
||||||
|
|
||||||
|
typ, data = item
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
else:
|
||||||
|
yield row
|
||||||
|
|
||||||
|
# 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,
|
||||||
|
)
|
||||||
|
db.add(row)
|
||||||
|
|
||||||
|
def enumerate(self, session):
|
||||||
""" Defined by sub-classes to do the actual enumeration of
|
""" Defined by sub-classes to do the actual enumeration of
|
||||||
facts. """
|
facts. """
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ class PasswordData:
|
|||||||
password: str
|
password: str
|
||||||
filepath: str
|
filepath: str
|
||||||
lineno: int
|
lineno: int
|
||||||
uid: int = None
|
user: "pwncat.db.User"
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
if self.password is not None:
|
if self.password is not None:
|
||||||
@ -28,8 +28,8 @@ class PasswordData:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def user(self):
|
def uid(self):
|
||||||
return pwncat.victim.find_user_by_id(self.uid) if self.uid is not None else None
|
return self.user.id
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
@ -37,7 +37,7 @@ class PrivateKeyData:
|
|||||||
""" A private key found on the remote file system or known
|
""" A private key found on the remote file system or known
|
||||||
to be applicable to this system in some way. """
|
to be applicable to this system in some way. """
|
||||||
|
|
||||||
uid: int
|
user: "pwncat.db.User"
|
||||||
""" The user we believe the private key belongs to """
|
""" The user we believe the private key belongs to """
|
||||||
path: str
|
path: str
|
||||||
""" The path to the private key on the remote host """
|
""" The path to the private key on the remote host """
|
||||||
@ -58,5 +58,5 @@ class PrivateKeyData:
|
|||||||
return self.content
|
return self.content
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def user(self):
|
def uid(self) -> int:
|
||||||
return pwncat.victim.find_user_by_id(self.uid)
|
return self.user.id
|
||||||
|
@ -19,14 +19,14 @@ class Module(EnumerateModule):
|
|||||||
SCHEDULE = Schedule.ALWAYS
|
SCHEDULE = Schedule.ALWAYS
|
||||||
PROVIDES = ["creds.password"]
|
PROVIDES = ["creds.password"]
|
||||||
|
|
||||||
def enumerate(self):
|
def enumerate(self, session):
|
||||||
|
|
||||||
pam: InstalledModule = None
|
pam: InstalledModule = None
|
||||||
for module in pwncat.modules.run(
|
# for module in session.run(
|
||||||
"persist.gather", progress=self.progress, module="persist.pam_backdoor"
|
# "persist.gather", progress=self.progress, module="persist.pam_backdoor"
|
||||||
):
|
# ):
|
||||||
pam = module
|
# pam = module
|
||||||
break
|
# break
|
||||||
|
|
||||||
if pam is None:
|
if pam is None:
|
||||||
# The pam persistence module isn't installed.
|
# The pam persistence module isn't installed.
|
||||||
@ -37,8 +37,13 @@ class Module(EnumerateModule):
|
|||||||
# Just in case we have multiple of the same password logged
|
# Just in case we have multiple of the same password logged
|
||||||
observed = []
|
observed = []
|
||||||
|
|
||||||
|
# This ensures our user database is fetched prior to opening the file.
|
||||||
|
# otherwise, we may attempt to read the user database while the file is
|
||||||
|
# open
|
||||||
|
session.platform.current_user()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with pwncat.victim.open(log_path, "r") as filp:
|
with session.platform.open(log_path, "r") as filp:
|
||||||
for line in filp:
|
for line in filp:
|
||||||
line = line.rstrip("\n")
|
line = line.rstrip("\n")
|
||||||
if line in observed:
|
if line in observed:
|
||||||
@ -47,8 +52,10 @@ class Module(EnumerateModule):
|
|||||||
user, *password = line.split(":")
|
user, *password = line.split(":")
|
||||||
password = ":".join(password)
|
password = ":".join(password)
|
||||||
|
|
||||||
# Invalid user name
|
try:
|
||||||
if user not in pwncat.victim.users:
|
# Check for valid user name
|
||||||
|
session.platform.find_user(name=user)
|
||||||
|
except KeyError:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
observed.append(line)
|
observed.append(line)
|
||||||
|
@ -20,7 +20,7 @@ class Module(EnumerateModule):
|
|||||||
PLATFORM = [Linux]
|
PLATFORM = [Linux]
|
||||||
SCHEDULE = Schedule.PER_USER
|
SCHEDULE = Schedule.PER_USER
|
||||||
|
|
||||||
def enumerate(self):
|
def enumerate(self, session):
|
||||||
|
|
||||||
# The locations we will search in for passwords
|
# The locations we will search in for passwords
|
||||||
locations = ["/var/www", "$HOME", "/opt", "/etc"]
|
locations = ["/var/www", "$HOME", "/opt", "/etc"]
|
||||||
@ -29,17 +29,25 @@ class Module(EnumerateModule):
|
|||||||
# The types of files which are "code". This means that we only recognize the
|
# The types of files which are "code". This means that we only recognize the
|
||||||
# actual password if it is a literal value (enclosed in single or double quotes)
|
# actual password if it is a literal value (enclosed in single or double quotes)
|
||||||
code_types = [".c", ".php", ".py", ".sh", ".pl", ".js", ".ini", ".json"]
|
code_types = [".c", ".php", ".py", ".sh", ".pl", ".js", ".ini", ".json"]
|
||||||
grep = pwncat.victim.which("grep")
|
# grep = pwncat.victim.which("grep")
|
||||||
|
grep = "grep"
|
||||||
|
|
||||||
if grep is None:
|
if grep is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
command = f"{grep} -InriE 'password[\"'\"'\"']?\\s*(=>|=|:)' {' '.join(locations)} 2>/dev/null"
|
command = f"{grep} -InriE 'password[\"'\"'\"']?\\s*(=>|=|:)' {' '.join(locations)} 2>/dev/null"
|
||||||
with pwncat.victim.subprocess(command, "r") as filp:
|
|
||||||
|
# Run the command on the remote host
|
||||||
|
proc = session.platform.Popen(
|
||||||
|
command, shell=True, text=True, stdout=pwncat.subprocess.PIPE
|
||||||
|
)
|
||||||
|
|
||||||
|
# Iterate through the output
|
||||||
|
with proc.stdout as filp:
|
||||||
for line in filp:
|
for line in filp:
|
||||||
try:
|
try:
|
||||||
# Decode the line and separate the filename, line number, and content
|
# Decode the line and separate the filename, line number, and content
|
||||||
line = line.decode("utf-8").strip().split(":")
|
line = line.strip().split(":")
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -108,3 +116,5 @@ class Module(EnumerateModule):
|
|||||||
# This was a match for the search. We may have extracted a
|
# This was a match for the search. We may have extracted a
|
||||||
# password. Either way, log it.
|
# password. Either way, log it.
|
||||||
yield "creds.password", PasswordData(password, path, lineno)
|
yield "creds.password", PasswordData(password, path, lineno)
|
||||||
|
|
||||||
|
proc.wait()
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
from Crypto.PublicKey import RSA
|
from Crypto.PublicKey import RSA
|
||||||
|
import time
|
||||||
|
|
||||||
import pwncat
|
import pwncat
|
||||||
from pwncat.platform.linux import Linux
|
from pwncat.platform.linux import Linux
|
||||||
@ -19,17 +20,22 @@ class Module(EnumerateModule):
|
|||||||
PLATFORM = [Linux]
|
PLATFORM = [Linux]
|
||||||
SCHEDULE = Schedule.PER_USER
|
SCHEDULE = Schedule.PER_USER
|
||||||
|
|
||||||
def enumerate(self):
|
def enumerate(self, session: "pwncat.manager.Session"):
|
||||||
|
|
||||||
facts = []
|
facts = []
|
||||||
|
|
||||||
# Search for private keys in common locations
|
# Search for private keys in common locations
|
||||||
with pwncat.victim.subprocess(
|
proc = session.platform.Popen(
|
||||||
"grep -l -I -D skip -rE '^-+BEGIN .* PRIVATE KEY-+$' /home /etc /opt 2>/dev/null | xargs stat -c '%u %n' 2>/dev/null"
|
"grep -l -I -D skip -rE '^-+BEGIN .* PRIVATE KEY-+$' /home /etc /opt 2>/dev/null | xargs stat -c '%u %n' 2>/dev/null",
|
||||||
) as pipe:
|
shell=True,
|
||||||
|
text=True,
|
||||||
|
stdout=pwncat.subprocess.PIPE,
|
||||||
|
)
|
||||||
|
|
||||||
|
with proc.stdout as pipe:
|
||||||
yield Status("searching for private keys")
|
yield Status("searching for private keys")
|
||||||
for line in pipe:
|
for line in pipe:
|
||||||
line = line.strip().decode("utf-8").split(" ")
|
line = line.strip().split(" ")
|
||||||
uid, path = int(line[0]), " ".join(line[1:])
|
uid, path = int(line[0]), " ".join(line[1:])
|
||||||
yield Status(f"found [cyan]{path}[/cyan]")
|
yield Status(f"found [cyan]{path}[/cyan]")
|
||||||
facts.append(PrivateKeyData(uid, path, None, False))
|
facts.append(PrivateKeyData(uid, path, None, False))
|
||||||
@ -37,7 +43,7 @@ class Module(EnumerateModule):
|
|||||||
for fact in facts:
|
for fact in facts:
|
||||||
try:
|
try:
|
||||||
yield Status(f"reading [cyan]{fact.path}[/cyan]")
|
yield Status(f"reading [cyan]{fact.path}[/cyan]")
|
||||||
with pwncat.victim.open(fact.path, "r") as filp:
|
with session.platform.open(fact.path, "r") as filp:
|
||||||
fact.content = filp.read().strip().replace("\r\n", "\n")
|
fact.content = filp.read().strip().replace("\r\n", "\n")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -53,6 +59,10 @@ class Module(EnumerateModule):
|
|||||||
else:
|
else:
|
||||||
# Some other error happened, probably not a key
|
# Some other error happened, probably not a key
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# we set the user field to the id temporarily
|
||||||
|
fact.user = session.platform.find_user(id=fact.user)
|
||||||
|
|
||||||
yield "creds.private_key", fact
|
yield "creds.private_key", fact
|
||||||
except (PermissionError, FileNotFoundError):
|
except (PermissionError, FileNotFoundError):
|
||||||
continue
|
continue
|
||||||
|
@ -71,7 +71,7 @@ class Module(pwncat.modules.BaseModule):
|
|||||||
}
|
}
|
||||||
PLATFORM = None
|
PLATFORM = None
|
||||||
|
|
||||||
def run(self, output, modules, types, clear):
|
def run(self, session, output, modules, types, clear):
|
||||||
""" Perform a enumeration of the given moduels and save the output """
|
""" Perform a enumeration of the given moduels and save the output """
|
||||||
|
|
||||||
module_names = modules
|
module_names = modules
|
||||||
@ -80,15 +80,13 @@ class Module(pwncat.modules.BaseModule):
|
|||||||
modules = set()
|
modules = set()
|
||||||
for name in module_names:
|
for name in module_names:
|
||||||
modules = modules | set(
|
modules = modules | set(
|
||||||
pwncat.modules.match(f"enumerate.{name}", base=EnumerateModule)
|
list(session.find_module(f"enumerate.{name}", base=EnumerateModule))
|
||||||
)
|
)
|
||||||
|
|
||||||
if clear:
|
if clear:
|
||||||
for module in modules:
|
for module in modules:
|
||||||
yield pwncat.modules.Status(module.name)
|
yield pwncat.modules.Status(module.name)
|
||||||
module.run(progress=self.progress, clear=True)
|
module.run(clear=True)
|
||||||
get_session().commit()
|
|
||||||
pwncat.victim.reload_host()
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Enumerate all facts
|
# Enumerate all facts
|
||||||
@ -115,7 +113,7 @@ class Module(pwncat.modules.BaseModule):
|
|||||||
yield pwncat.modules.Status(module.name)
|
yield pwncat.modules.Status(module.name)
|
||||||
|
|
||||||
# Iterate over facts from the sub-module with our progress manager
|
# Iterate over facts from the sub-module with our progress manager
|
||||||
for item in module.run(progress=self.progress, types=types):
|
for item in module.run(session, types=types):
|
||||||
if output is None:
|
if output is None:
|
||||||
yield item
|
yield item
|
||||||
elif item.type not in facts:
|
elif item.type not in facts:
|
||||||
@ -133,7 +131,10 @@ class Module(pwncat.modules.BaseModule):
|
|||||||
|
|
||||||
with output as filp:
|
with output as filp:
|
||||||
|
|
||||||
filp.write(f"# {pwncat.victim.host.ip} - Enumeration Report\n\n")
|
with session.db as db:
|
||||||
|
host = db.query(pwncat.db.Host).filter_by(id=session.host).first()
|
||||||
|
|
||||||
|
filp.write(f"# {host.ip} - Enumeration Report\n\n")
|
||||||
filp.write("Enumerated Types:\n")
|
filp.write("Enumerated Types:\n")
|
||||||
for typ in facts:
|
for typ in facts:
|
||||||
filp.write(f"- {typ}\n")
|
filp.write(f"- {typ}\n")
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import os
|
import os
|
||||||
|
import stat
|
||||||
|
|
||||||
import pwncat
|
import pwncat
|
||||||
from pwncat.util import Access
|
from pwncat.util import Access
|
||||||
@ -17,30 +18,21 @@ class Module(EnumerateModule):
|
|||||||
SCHEDULE = Schedule.PER_USER
|
SCHEDULE = Schedule.PER_USER
|
||||||
PLATFORM = [Linux]
|
PLATFORM = [Linux]
|
||||||
|
|
||||||
def enumerate(self):
|
def enumerate(self, session):
|
||||||
|
|
||||||
for path in pwncat.victim.getenv("PATH").split(":"):
|
user = session.platform.current_user()
|
||||||
access = pwncat.victim.access(path)
|
|
||||||
if (Access.DIRECTORY | Access.WRITE) in access:
|
for path in session.platform.getenv("PATH").split(":"):
|
||||||
yield "misc.writable_path", path
|
|
||||||
elif (
|
# Ignore empty components
|
||||||
Access.EXISTS not in access
|
if path == "":
|
||||||
and (Access.PARENT_EXIST | Access.PARENT_WRITE) in access
|
continue
|
||||||
):
|
|
||||||
yield "misc.writable_path", path
|
# Find the first item up the path that exists
|
||||||
elif access == Access.NONE:
|
path = session.platform.Path(path)
|
||||||
# This means the parent directory doesn't exist. Check up the chain to see if
|
while not path.exists():
|
||||||
# We can create this chain of directories
|
path = path.parent
|
||||||
dirpath = os.path.dirname(path)
|
|
||||||
access = pwncat.victim.access(dirpath)
|
# See if we have write permission
|
||||||
# Find the first item that either exists or it's parent does
|
if path.is_dir() and path.writable():
|
||||||
while access == Access.NONE:
|
yield "misc.writable_path", str(path.resolve())
|
||||||
dirpath = os.path.dirname(dirpath)
|
|
||||||
access = pwncat.victim.access(dirpath)
|
|
||||||
# This item exists. Is it a directory and can we write to it?
|
|
||||||
if (Access.DIRECTORY | Access.WRITE) in access:
|
|
||||||
yield "misc.writable_path", path
|
|
||||||
elif (
|
|
||||||
Access.PARENT_EXIST | Access.PARENT_WRITE
|
|
||||||
) in access and Access.EXISTS not in access:
|
|
||||||
yield "misc.writable_path", path
|
|
||||||
|
@ -36,6 +36,28 @@ class Path:
|
|||||||
_lstat: os.stat_result
|
_lstat: os.stat_result
|
||||||
parts = []
|
parts = []
|
||||||
|
|
||||||
|
def writable(self) -> bool:
|
||||||
|
""" This is non-standard, but is useful """
|
||||||
|
|
||||||
|
user = self._target.current_user()
|
||||||
|
mode = self.stat().st_mode
|
||||||
|
uid = self.stat().st_uid
|
||||||
|
gid = self.stat().st_gid
|
||||||
|
|
||||||
|
if uid == user.id and (mode & stat.S_IWUSR):
|
||||||
|
return True
|
||||||
|
elif user.group.id == gid and (mode & stat.S_IWGRP):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
for group in user.groups:
|
||||||
|
if group.id == gid and (mode & stat.S_IWGRP):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
if mode & stat.S_IWOTH:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
def stat(self) -> os.stat_result:
|
def stat(self) -> os.stat_result:
|
||||||
""" Run `stat` on the path and return a stat result """
|
""" Run `stat` on the path and return a stat result """
|
||||||
|
|
||||||
@ -410,6 +432,9 @@ class Platform:
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self.channel)
|
return str(self.channel)
|
||||||
|
|
||||||
|
def getenv(self, name: str):
|
||||||
|
""" Get the value of an environment variable """
|
||||||
|
|
||||||
def reload_users(self):
|
def reload_users(self):
|
||||||
""" Reload the user and group cache. This is automatically called
|
""" Reload the user and group cache. This is automatically called
|
||||||
if the cache hasn't been built yet, but may be called manually
|
if the cache hasn't been built yet, but may be called manually
|
||||||
@ -467,6 +492,11 @@ class Platform:
|
|||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
def current_user(self):
|
||||||
|
""" Retrieve a user object for the current user """
|
||||||
|
|
||||||
|
return self.find_user(name=self.whoami())
|
||||||
|
|
||||||
def iter_groups(self) -> Generator["pwncat.db.Group", None, None]:
|
def iter_groups(self) -> Generator["pwncat.db.Group", None, None]:
|
||||||
""" Iterate over all groups on the remote system """
|
""" Iterate over all groups on the remote system """
|
||||||
|
|
||||||
@ -535,9 +565,6 @@ class Platform:
|
|||||||
def readlink(self, path: str):
|
def readlink(self, path: str):
|
||||||
""" Attempt to read the target of a link """
|
""" Attempt to read the target of a link """
|
||||||
|
|
||||||
def current_user(self):
|
|
||||||
""" Retrieve a user object for the current user """
|
|
||||||
|
|
||||||
def whoami(self):
|
def whoami(self):
|
||||||
""" Retrieve's only name of the current user (may be faster depending
|
""" Retrieve's only name of the current user (may be faster depending
|
||||||
on platform) """
|
on platform) """
|
||||||
|
@ -87,7 +87,14 @@ class PopenLinux(pwncat.subprocess.Popen):
|
|||||||
# Drain buffer, don't wait for more data. The user didn't ask
|
# Drain buffer, don't wait for more data. The user didn't ask
|
||||||
# for the data with `stdout=PIPE`, so we can safely ignore it.
|
# for the data with `stdout=PIPE`, so we can safely ignore it.
|
||||||
# This returns true if we hit EOF
|
# This returns true if we hit EOF
|
||||||
if self.stdout_raw.peek(len(self.end_delim)) == b"" and self.stdout_raw.raw.eof:
|
try:
|
||||||
|
if (
|
||||||
|
self.stdout_raw.peek(len(self.end_delim)) == b""
|
||||||
|
and self.stdout_raw.raw.eof
|
||||||
|
):
|
||||||
|
self._receive_returncode()
|
||||||
|
return self.returncode
|
||||||
|
except ValueError:
|
||||||
self._receive_returncode()
|
self._receive_returncode()
|
||||||
return self.returncode
|
return self.returncode
|
||||||
|
|
||||||
@ -627,6 +634,16 @@ class Linux(Platform):
|
|||||||
|
|
||||||
return stdout
|
return stdout
|
||||||
|
|
||||||
|
def getenv(self, name: str):
|
||||||
|
|
||||||
|
try:
|
||||||
|
proc = self.run(
|
||||||
|
["echo", f"${name}"], capture_output=True, text=True, check=True
|
||||||
|
)
|
||||||
|
return proc.stdout.rstrip("\n")
|
||||||
|
except CalledProcessError:
|
||||||
|
return ""
|
||||||
|
|
||||||
def compile(
|
def compile(
|
||||||
self,
|
self,
|
||||||
sources: List[Union[str, BinaryIO]],
|
sources: List[Union[str, BinaryIO]],
|
||||||
|
Loading…
Reference in New Issue
Block a user