1
0
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:
Caleb Stewart 2020-11-13 12:05:08 -05:00
parent f80d6b65ee
commit c1068ad567
12 changed files with 227 additions and 334 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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