mirror of
https://github.com/calebstewart/pwncat.git
synced 2024-11-30 12:24:14 +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
|
||||
obj = b.obj if isinstance(b, memoryview) else b
|
||||
n = 0
|
||||
|
||||
try:
|
||||
n = self.channel.recvinto(b)
|
||||
except NotImplementedError:
|
||||
# recvinto was not implemented, fallback recv
|
||||
data = self.channel.recv(len(b))
|
||||
b[: len(data)] = data
|
||||
n = len(data)
|
||||
while n == 0:
|
||||
try:
|
||||
n = self.channel.recvinto(b)
|
||||
except NotImplementedError:
|
||||
# recvinto was not implemented, fallback recv
|
||||
data = self.channel.recv(len(b))
|
||||
b[: len(data)] = data
|
||||
n = len(data)
|
||||
|
||||
obj = bytes(b[:n])
|
||||
|
||||
|
@ -120,7 +120,7 @@ class Session:
|
||||
|
||||
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
|
||||
which may yield multiple modules that match the pattern and
|
||||
base class. """
|
||||
@ -134,7 +134,13 @@ class Session:
|
||||
and type(self.platform) not in module.PLATFORM
|
||||
):
|
||||
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
|
||||
|
||||
def log(self, *args, **kwargs):
|
||||
@ -158,7 +164,10 @@ class Session:
|
||||
self._db_session = self.manager.create_db_session()
|
||||
yield self._db_session
|
||||
finally:
|
||||
self._db_session.commit()
|
||||
try:
|
||||
self._db_session.commit()
|
||||
except:
|
||||
pass
|
||||
|
||||
@contextlib.contextmanager
|
||||
def task(self, *args, **kwargs):
|
||||
@ -273,7 +282,7 @@ class Manager:
|
||||
|
||||
self.engine = create_engine(self.config["db"])
|
||||
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)
|
||||
|
||||
def create_db_session(self):
|
||||
|
@ -163,23 +163,18 @@ def run_decorator(real_run):
|
||||
@functools.wraps(real_run)
|
||||
def decorator(self, session, progress=None, **kwargs):
|
||||
|
||||
if "exec" in kwargs:
|
||||
has_exec = True
|
||||
else:
|
||||
has_exec = False
|
||||
|
||||
# Validate arguments
|
||||
for key in kwargs:
|
||||
if key in self.ARGUMENTS:
|
||||
try:
|
||||
kwargs[key] = self.ARGUMENTS[key].type(kwargs[key])
|
||||
except ValueError:
|
||||
raise ArgumentFormatError(key)
|
||||
except ValueError as exc:
|
||||
raise ArgumentFormatError(key) from exc
|
||||
elif not self.ALLOW_KWARGS:
|
||||
raise InvalidArgument(key)
|
||||
for key in self.ARGUMENTS:
|
||||
if key not in kwargs and key in pwncat.config:
|
||||
kwargs[key] = pwncat.config[key]
|
||||
if key not in kwargs and key in session.config:
|
||||
kwargs[key] = session.config[key]
|
||||
elif key not in kwargs and self.ARGUMENTS[key].default is not NoValue:
|
||||
kwargs[key] = self.ARGUMENTS[key].default
|
||||
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)
|
||||
|
||||
if inspect.isgenerator(result_object):
|
||||
|
||||
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="...")
|
||||
|
||||
with session.task(description=self.name, status="...") as task:
|
||||
# Collect results
|
||||
results = []
|
||||
for item in result_object:
|
||||
self.progress.update(task, status=str(item))
|
||||
session.update_task(task, status=str(item))
|
||||
if not isinstance(item, Status):
|
||||
results.append(item)
|
||||
|
||||
@ -221,18 +199,6 @@ def run_decorator(real_run):
|
||||
return results[0]
|
||||
|
||||
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:
|
||||
return result_object
|
||||
|
||||
@ -296,141 +262,3 @@ class BaseModule(metaclass=BaseModuleMeta):
|
||||
"""
|
||||
|
||||
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
|
||||
# different types of enumeration data
|
||||
PROVIDES = []
|
||||
PLATFORM = [Linux]
|
||||
PLATFORM = []
|
||||
|
||||
# Defines how often to run this enumeration. The default is to
|
||||
# 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.
|
||||
|
||||
Sub-classes should not override this method. Instead, use the
|
||||
@ -58,98 +58,88 @@ class EnumerateModule(BaseModule):
|
||||
|
||||
marker_name = self.name
|
||||
if self.SCHEDULE == Schedule.PER_USER:
|
||||
marker_name += f".{pwncat.victim.current_user.id}"
|
||||
marker_name += f".{session.platform.current_user().id}"
|
||||
|
||||
if clear:
|
||||
# Delete enumerated facts
|
||||
query = (
|
||||
get_session()
|
||||
.query(pwncat.db.Fact)
|
||||
.filter_by(source=self.name, host_id=pwncat.victim.host.id)
|
||||
)
|
||||
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))
|
||||
with session.db as db:
|
||||
|
||||
if clear:
|
||||
# Delete enumerated facts
|
||||
query = db.query(pwncat.db.Fact).filter_by(
|
||||
source=self.name, host_id=session.host
|
||||
)
|
||||
query.delete(synchronize_session=False)
|
||||
return
|
||||
|
||||
# Yield all the know facts which have already been enumerated
|
||||
existing_facts = (
|
||||
get_session()
|
||||
.query(pwncat.db.Fact)
|
||||
.filter_by(source=self.name, host_id=pwncat.victim.host.id)
|
||||
.filter(pwncat.db.Fact.type != "marker")
|
||||
)
|
||||
|
||||
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:
|
||||
# Delete our marker
|
||||
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
|
||||
|
||||
# Get any new facts
|
||||
for item in self.enumerate():
|
||||
if isinstance(item, Status):
|
||||
yield item
|
||||
continue
|
||||
|
||||
typ, data = item
|
||||
|
||||
row = pwncat.db.Fact(
|
||||
host_id=pwncat.victim.host.id, type=typ, data=data, source=self.name
|
||||
# Yield all the know facts which have already been enumerated
|
||||
existing_facts = (
|
||||
db.query(pwncat.db.Fact)
|
||||
.filter_by(source=self.name, host_id=session.host)
|
||||
.filter(pwncat.db.Fact.type != "marker")
|
||||
)
|
||||
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:
|
||||
for typ in types:
|
||||
if fnmatch.fnmatch(row.type, typ):
|
||||
yield row
|
||||
else:
|
||||
yield Status(data)
|
||||
for fact in existing_facts.all():
|
||||
for typ in types:
|
||||
if fnmatch.fnmatch(fact.type, typ):
|
||||
yield fact
|
||||
else:
|
||||
yield row
|
||||
yield from existing_facts.all()
|
||||
|
||||
# Add the marker if needed
|
||||
if self.SCHEDULE != Schedule.ALWAYS:
|
||||
row = pwncat.db.Fact(
|
||||
host_id=pwncat.victim.host.id,
|
||||
type="marker",
|
||||
source=marker_name,
|
||||
data=None,
|
||||
)
|
||||
get_session().add(row)
|
||||
pwncat.victim.host.facts.append(row)
|
||||
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
|
||||
|
||||
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
|
||||
facts. """
|
||||
|
||||
|
@ -14,7 +14,7 @@ class PasswordData:
|
||||
password: str
|
||||
filepath: str
|
||||
lineno: int
|
||||
uid: int = None
|
||||
user: "pwncat.db.User"
|
||||
|
||||
def __str__(self):
|
||||
if self.password is not None:
|
||||
@ -28,8 +28,8 @@ class PasswordData:
|
||||
return result
|
||||
|
||||
@property
|
||||
def user(self):
|
||||
return pwncat.victim.find_user_by_id(self.uid) if self.uid is not None else None
|
||||
def uid(self):
|
||||
return self.user.id
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
@ -37,7 +37,7 @@ class PrivateKeyData:
|
||||
""" A private key found on the remote file system or known
|
||||
to be applicable to this system in some way. """
|
||||
|
||||
uid: int
|
||||
user: "pwncat.db.User"
|
||||
""" The user we believe the private key belongs to """
|
||||
path: str
|
||||
""" The path to the private key on the remote host """
|
||||
@ -58,5 +58,5 @@ class PrivateKeyData:
|
||||
return self.content
|
||||
|
||||
@property
|
||||
def user(self):
|
||||
return pwncat.victim.find_user_by_id(self.uid)
|
||||
def uid(self) -> int:
|
||||
return self.user.id
|
||||
|
@ -19,14 +19,14 @@ class Module(EnumerateModule):
|
||||
SCHEDULE = Schedule.ALWAYS
|
||||
PROVIDES = ["creds.password"]
|
||||
|
||||
def enumerate(self):
|
||||
def enumerate(self, session):
|
||||
|
||||
pam: InstalledModule = None
|
||||
for module in pwncat.modules.run(
|
||||
"persist.gather", progress=self.progress, module="persist.pam_backdoor"
|
||||
):
|
||||
pam = module
|
||||
break
|
||||
# for module in session.run(
|
||||
# "persist.gather", progress=self.progress, module="persist.pam_backdoor"
|
||||
# ):
|
||||
# pam = module
|
||||
# break
|
||||
|
||||
if pam is None:
|
||||
# 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
|
||||
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:
|
||||
with pwncat.victim.open(log_path, "r") as filp:
|
||||
with session.platform.open(log_path, "r") as filp:
|
||||
for line in filp:
|
||||
line = line.rstrip("\n")
|
||||
if line in observed:
|
||||
@ -47,8 +52,10 @@ class Module(EnumerateModule):
|
||||
user, *password = line.split(":")
|
||||
password = ":".join(password)
|
||||
|
||||
# Invalid user name
|
||||
if user not in pwncat.victim.users:
|
||||
try:
|
||||
# Check for valid user name
|
||||
session.platform.find_user(name=user)
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
observed.append(line)
|
||||
|
@ -20,7 +20,7 @@ class Module(EnumerateModule):
|
||||
PLATFORM = [Linux]
|
||||
SCHEDULE = Schedule.PER_USER
|
||||
|
||||
def enumerate(self):
|
||||
def enumerate(self, session):
|
||||
|
||||
# The locations we will search in for passwords
|
||||
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
|
||||
# actual password if it is a literal value (enclosed in single or double quotes)
|
||||
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:
|
||||
return
|
||||
|
||||
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:
|
||||
try:
|
||||
# Decode the line and separate the filename, line number, and content
|
||||
line = line.decode("utf-8").strip().split(":")
|
||||
line = line.strip().split(":")
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
|
||||
@ -108,3 +116,5 @@ class Module(EnumerateModule):
|
||||
# This was a match for the search. We may have extracted a
|
||||
# password. Either way, log it.
|
||||
yield "creds.password", PasswordData(password, path, lineno)
|
||||
|
||||
proc.wait()
|
||||
|
@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
from Crypto.PublicKey import RSA
|
||||
import time
|
||||
|
||||
import pwncat
|
||||
from pwncat.platform.linux import Linux
|
||||
@ -19,17 +20,22 @@ class Module(EnumerateModule):
|
||||
PLATFORM = [Linux]
|
||||
SCHEDULE = Schedule.PER_USER
|
||||
|
||||
def enumerate(self):
|
||||
def enumerate(self, session: "pwncat.manager.Session"):
|
||||
|
||||
facts = []
|
||||
|
||||
# Search for private keys in common locations
|
||||
with pwncat.victim.subprocess(
|
||||
"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:
|
||||
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",
|
||||
shell=True,
|
||||
text=True,
|
||||
stdout=pwncat.subprocess.PIPE,
|
||||
)
|
||||
|
||||
with proc.stdout as pipe:
|
||||
yield Status("searching for private keys")
|
||||
for line in pipe:
|
||||
line = line.strip().decode("utf-8").split(" ")
|
||||
line = line.strip().split(" ")
|
||||
uid, path = int(line[0]), " ".join(line[1:])
|
||||
yield Status(f"found [cyan]{path}[/cyan]")
|
||||
facts.append(PrivateKeyData(uid, path, None, False))
|
||||
@ -37,7 +43,7 @@ class Module(EnumerateModule):
|
||||
for fact in facts:
|
||||
try:
|
||||
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")
|
||||
|
||||
try:
|
||||
@ -53,6 +59,10 @@ class Module(EnumerateModule):
|
||||
else:
|
||||
# Some other error happened, probably not a key
|
||||
continue
|
||||
|
||||
# we set the user field to the id temporarily
|
||||
fact.user = session.platform.find_user(id=fact.user)
|
||||
|
||||
yield "creds.private_key", fact
|
||||
except (PermissionError, FileNotFoundError):
|
||||
continue
|
||||
|
@ -71,7 +71,7 @@ class Module(pwncat.modules.BaseModule):
|
||||
}
|
||||
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 """
|
||||
|
||||
module_names = modules
|
||||
@ -80,15 +80,13 @@ class Module(pwncat.modules.BaseModule):
|
||||
modules = set()
|
||||
for name in module_names:
|
||||
modules = modules | set(
|
||||
pwncat.modules.match(f"enumerate.{name}", base=EnumerateModule)
|
||||
list(session.find_module(f"enumerate.{name}", base=EnumerateModule))
|
||||
)
|
||||
|
||||
if clear:
|
||||
for module in modules:
|
||||
yield pwncat.modules.Status(module.name)
|
||||
module.run(progress=self.progress, clear=True)
|
||||
get_session().commit()
|
||||
pwncat.victim.reload_host()
|
||||
module.run(clear=True)
|
||||
return
|
||||
|
||||
# Enumerate all facts
|
||||
@ -115,7 +113,7 @@ class Module(pwncat.modules.BaseModule):
|
||||
yield pwncat.modules.Status(module.name)
|
||||
|
||||
# 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:
|
||||
yield item
|
||||
elif item.type not in facts:
|
||||
@ -133,7 +131,10 @@ class Module(pwncat.modules.BaseModule):
|
||||
|
||||
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")
|
||||
for typ in facts:
|
||||
filp.write(f"- {typ}\n")
|
||||
|
@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import stat
|
||||
|
||||
import pwncat
|
||||
from pwncat.util import Access
|
||||
@ -17,30 +18,21 @@ class Module(EnumerateModule):
|
||||
SCHEDULE = Schedule.PER_USER
|
||||
PLATFORM = [Linux]
|
||||
|
||||
def enumerate(self):
|
||||
def enumerate(self, session):
|
||||
|
||||
for path in pwncat.victim.getenv("PATH").split(":"):
|
||||
access = pwncat.victim.access(path)
|
||||
if (Access.DIRECTORY | Access.WRITE) in access:
|
||||
yield "misc.writable_path", path
|
||||
elif (
|
||||
Access.EXISTS not in access
|
||||
and (Access.PARENT_EXIST | Access.PARENT_WRITE) in access
|
||||
):
|
||||
yield "misc.writable_path", path
|
||||
elif access == Access.NONE:
|
||||
# This means the parent directory doesn't exist. Check up the chain to see if
|
||||
# We can create this chain of directories
|
||||
dirpath = os.path.dirname(path)
|
||||
access = pwncat.victim.access(dirpath)
|
||||
# Find the first item that either exists or it's parent does
|
||||
while access == Access.NONE:
|
||||
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
|
||||
user = session.platform.current_user()
|
||||
|
||||
for path in session.platform.getenv("PATH").split(":"):
|
||||
|
||||
# Ignore empty components
|
||||
if path == "":
|
||||
continue
|
||||
|
||||
# Find the first item up the path that exists
|
||||
path = session.platform.Path(path)
|
||||
while not path.exists():
|
||||
path = path.parent
|
||||
|
||||
# See if we have write permission
|
||||
if path.is_dir() and path.writable():
|
||||
yield "misc.writable_path", str(path.resolve())
|
||||
|
@ -36,6 +36,28 @@ class Path:
|
||||
_lstat: os.stat_result
|
||||
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:
|
||||
""" Run `stat` on the path and return a stat result """
|
||||
|
||||
@ -410,6 +432,9 @@ class Platform:
|
||||
def __str__(self):
|
||||
return str(self.channel)
|
||||
|
||||
def getenv(self, name: str):
|
||||
""" Get the value of an environment variable """
|
||||
|
||||
def reload_users(self):
|
||||
""" Reload the user and group cache. This is automatically called
|
||||
if the cache hasn't been built yet, but may be called manually
|
||||
@ -467,6 +492,11 @@ class Platform:
|
||||
|
||||
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]:
|
||||
""" Iterate over all groups on the remote system """
|
||||
|
||||
@ -535,9 +565,6 @@ class Platform:
|
||||
def readlink(self, path: str):
|
||||
""" Attempt to read the target of a link """
|
||||
|
||||
def current_user(self):
|
||||
""" Retrieve a user object for the current user """
|
||||
|
||||
def whoami(self):
|
||||
""" Retrieve's only name of the current user (may be faster depending
|
||||
on platform) """
|
||||
|
@ -87,7 +87,14 @@ class PopenLinux(pwncat.subprocess.Popen):
|
||||
# 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.
|
||||
# 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()
|
||||
return self.returncode
|
||||
|
||||
@ -627,6 +634,16 @@ class Linux(Platform):
|
||||
|
||||
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(
|
||||
self,
|
||||
sources: List[Union[str, BinaryIO]],
|
||||
|
Loading…
Reference in New Issue
Block a user