From ee95381c4ef5830b96bd3c9d7076ff59362ecb1b Mon Sep 17 00:00:00 2001 From: Caleb Stewart Date: Thu, 29 Oct 2020 21:16:57 -0400 Subject: [PATCH] Working on getting interactive working --- pwncat/channel/__init__.py | 51 +++++++++++- pwncat/commands/__init__.py | 124 +++++++++++++++-------------- pwncat/commands/base.py | 10 ++- pwncat/commands/set.py | 25 +++--- pwncat/config.py | 8 ++ pwncat/manager.py | 151 +++++++++++++++++++++++------------- pwncat/platform/__init__.py | 8 ++ pwncat/platform/linux.py | 91 ++++++++++++++-------- 8 files changed, 303 insertions(+), 165 deletions(-) diff --git a/pwncat/channel/__init__.py b/pwncat/channel/__init__.py index 7903636..8dd5986 100644 --- a/pwncat/channel/__init__.py +++ b/pwncat/channel/__init__.py @@ -9,10 +9,29 @@ CHANNEL_TYPES = {} class ChannelError(Exception): """ Raised when a channel fails to connect """ + def __init__(self, ch, msg="generic channel failure"): + super().__init__(msg) + self.channel = ch + class ChannelClosed(ChannelError): """ A channel was closed unexpectedly during communication """ + def __init__(self, ch): + super().__init__(ch, "channel unexpectedly closed") + + def cleanup(self, manager: "pwncat.manager.Manager"): + """ Cleanup this channel from the manager """ + + # If we don't have a session, there's nothing to do + session = manager.find_session_by_channel(self.channel) + if session is None: + return + + # Session takes care of removing itself from the manager + # and unsetting `manager.target` if needed. + session.died() + class ChannelTimeout(ChannelError): """ Raised when a read times out. @@ -21,8 +40,8 @@ class ChannelTimeout(ChannelError): :type data: bytes """ - def __init__(self, data: bytes): - super().__init__("channel recieve timed out") + def __init__(self, ch, data: bytes): + super().__init__(ch, "channel recieve timed out") self.data: bytes = data @@ -185,6 +204,25 @@ class Channel: self.peek_buffer: bytes = b"" + @property + def connected(self): + """ Check if this channel is connected. This should return + false prior to an established connection, and may return + true prior to the ``connect`` method being called for some + channel types. """ + + def connect(self): + """ Utilize the parameters provided at initialization to + connect to the remote host. This is mainly used for channels + which listen for a connection. In that case, `__init__` creates + the listener while connect actually establishes a connection. + For direct connection-type channels, all logic can be implemented + in the constructor. + + This method is called when creating a platform around this channel + to instantiate the session. + """ + def send(self, data: bytes): """ Send data to the remote shell. This is a blocking call that only returns after all data is sent. """ @@ -353,6 +391,15 @@ class Channel: else: return BufferedWriter(raw_io, buffer_size=bufsize) + def close(self): + """ Close this channel. This method should do nothing if + the ``connected`` property returns False. """ + + def __str__(self): + """ Get a representation of this channel """ + + return f"[cyan]{self.address[0]}[/cyan]:[blue]{self.address[1]}[/blue]" + def register(name: str, channel_class): """ diff --git a/pwncat/commands/__init__.py b/pwncat/commands/__init__.py index f7c16c2..508facb 100644 --- a/pwncat/commands/__init__.py +++ b/pwncat/commands/__init__.py @@ -41,7 +41,7 @@ import pwncat import pwncat.db from pwncat.commands.base import CommandDefinition, Complete from pwncat.util import State, console -from pwncat.db import get_session +from pwncat.channel import ChannelClosed def resolve_blocks(source: str): @@ -104,20 +104,27 @@ def resolve_blocks(source: str): class DatabaseHistory(History): """ Yield history from the host entry in the database """ + def __init__(self, manager): + super().__init__() + self.manager = manager + def load_history_strings(self) -> Iterable[str]: """ Load the history from the database """ - for history in ( - get_session() - .query(pwncat.db.History) - .order_by(pwncat.db.History.id.desc()) - .all() - ): - yield history.command + + with self.manager.new_db_session() as session: + for history in ( + session.query(pwncat.db.History) + .order_by(pwncat.db.History.id.desc()) + .all() + ): + yield history.command def store_string(self, string: str) -> None: """ Store a command in the database """ - history = pwncat.db.History(host_id=pwncat.victim.host.id, command=string) - get_session().add(history) + history = pwncat.db.History(command=string) + + with self.manager.new_db_session() as session: + session.add(history) class CommandParser: @@ -127,16 +134,17 @@ class CommandParser: termios modes for the control tty at will in order to support raw vs command mode. """ - def __init__(self): + def __init__(self, manager: "pwncat.manager.Manager"): """ We need to dynamically load commands from pwncat.commands """ + self.manager = manager self.commands: List["CommandDefinition"] = [] for loader, module_name, is_pkg in pkgutil.walk_packages(__path__): if module_name == "base": continue self.commands.append( - loader.find_module(module_name).load_module(module_name).Command() + loader.find_module(module_name).load_module(module_name).Command(self) ) self.prompt: PromptSession = None @@ -153,12 +161,8 @@ class CommandParser: """ This needs to happen after __init__ when the database is fully initialized. """ - if pwncat.victim is not None and pwncat.victim.host is not None: - history = DatabaseHistory() - else: - history = InMemoryHistory() - - completer = CommandCompleter(self.commands) + history = DatabaseHistory(self.manager) + completer = CommandCompleter(self.manager, self.commands) lexer = PygmentsLexer(CommandLexer.build(self.commands)) style = style_from_pygments_cls(get_style_by_name("monokai")) auto_suggest = AutoSuggestFromHistory() @@ -177,28 +181,20 @@ class CommandParser: history=history, ) - @property - def loaded(self): - return self.loading_complete - - @loaded.setter - def loaded(self, value: bool): - assert value == True - self.loading_complete = True - self.eval(pwncat.config["on_load"], "on_load") - def eval(self, source: str, name: str = "