semi-working background listener api

This commit is contained in:
Caleb Stewart 2021-06-19 16:37:58 -04:00
parent 21e9ed3b92
commit 1fda11442a
4 changed files with 262 additions and 17 deletions

View File

@ -66,6 +66,7 @@ from prompt_toolkit.completion import (
) )
from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
from prompt_toolkit.patch_stdout import patch_stdout
from prompt_toolkit.styles.pygments import style_from_pygments_cls from prompt_toolkit.styles.pygments import style_from_pygments_cls
from prompt_toolkit.application.current import get_app from prompt_toolkit.application.current import get_app
@ -556,6 +557,7 @@ class CommandParser:
self.setup_prompt() self.setup_prompt()
running = True running = True
default_text = ""
while running: while running:
try: try:
@ -576,7 +578,10 @@ class CommandParser:
("", "$ "), ("", "$ "),
] ]
line = self.prompt.prompt().strip() with patch_stdout(raw=True):
line = self.prompt.prompt(default=default_text).strip()
default_text = ""
if line == "": if line == "":
continue continue

View File

@ -79,7 +79,8 @@ class Listener(threading.Thread):
self, self,
manager: "Manager", manager: "Manager",
address: Tuple[str, int], address: Tuple[str, int],
platform: Optional[str], protocol: str = "socket",
platform: Optional[str] = None,
count: Optional[int] = None, count: Optional[int] = None,
established: Optional[Callable[["Session"], bool]] = None, established: Optional[Callable[["Session"], bool]] = None,
ssl: bool = False, ssl: bool = False,
@ -92,6 +93,8 @@ class Listener(threading.Thread):
""" The controlling manager object """ """ The controlling manager object """
self.address: Tuple[str, int] = address self.address: Tuple[str, int] = address
""" The address to bind our listener to on the attacking machine """ """ The address to bind our listener to on the attacking machine """
self.protocol: str = protocol
""" Name of the channel protocol to use for incoming connections """
self.platform: Optional[str] = platform self.platform: Optional[str] = platform
""" The platform to use when automatically establishing sessions """ """ The platform to use when automatically establishing sessions """
self.count: Optional[int] = count self.count: Optional[int] = count
@ -105,12 +108,16 @@ class Listener(threading.Thread):
self.ssl_key: Optional[str] = ssl_key self.ssl_key: Optional[str] = ssl_key
""" The SSL server key """ """ The SSL server key """
self.state: ListenerState = ListenerState.STOPPED self.state: ListenerState = ListenerState.STOPPED
""" The current state of the listener; only set internally """
self.failure_exception: Optional[Exception] = None
""" An exception which was caught and put the listener in ListenerState.FAILED state """
self._stop_event: threading.Event = threading.Event() self._stop_event: threading.Event = threading.Event()
""" An event used to signal the listener to stop """ """ An event used to signal the listener to stop """
self._session_queue: queue.Queue = queue.Queue() self._session_queue: queue.Queue = queue.Queue()
""" Queue of newly established sessions. If this queue fills up, it is drained automatically. """ """ Queue of newly established sessions. If this queue fills up, it is drained automatically. """
self._channel_queue: queue.Queue = queue.Queue() self._channel_queue: queue.Queue = queue.Queue()
""" Queue of channels waiting to be initialized in the case of an unidentified platform """ """ Queue of channels waiting to be initialized in the case of an unidentified platform """
self._session_lock: threading.Lock = threading.Lock()
def iter_sessions(count: Optional[int] = None) -> Generator["Session", None, None]: def iter_sessions(count: Optional[int] = None) -> Generator["Session", None, None]:
""" """
@ -125,6 +132,13 @@ class Listener(threading.Thread):
:rtype: Generator[Session, None, None] :rtype: Generator[Session, None, None]
""" """
while count:
try:
yield self._session_queue.get(block=False, timeout=None)
count -= 1
except queue.Empty:
return
def iter_channels(count: Optional[int] = None) -> Generator["Channel", None, None]: def iter_channels(count: Optional[int] = None) -> Generator["Channel", None, None]:
""" """
Synchronously iterate over new channels. This generated will Synchronously iterate over new channels. This generated will
@ -138,12 +152,90 @@ class Listener(threading.Thread):
:rtype: Generator[Channel, None, None] :rtype: Generator[Channel, None, None]
""" """
def _open_socket(self) -> socket.socket: while count:
"""Open the raw socket listener and return the new socket object""" try:
yield self._channel_queue.get(block=False, timeout=None)
count -= 1
except queue.Empty:
return
def _ssl_wrap(self, server: socket.socket) -> ssl.SSLSocket: def bootstrap_session(
"""Wrap the given server socket in an SSL context and return the new socket. self, channel: pwncat.channel.Channel, platform: str
If the ``ssl`` option is not set, this method simply returns the original socket.""" ) -> "pwncat.manager.Session":
"""
Establish a session from an existing channel using the specified platform.
If platform is None, then the given channel is placed onto the uninitialized
channel queue for later initialization.
:param channel: the channel to initialize
:type channel: pwncat.channel.Channel
:param platform: name of the platform to initialize
:type platform: Optional[str]
:rtype: pwncat.manager.Session
:raises:
ListenerError: incorrect platform or channel disconnected
"""
with self._session_lock:
if self.count is not None and self.count <= 0:
raise ListenerError("listener max connections reached")
if platform is None:
# We can't initialize this channel, so we just throw it on the queue
self._channel_queue.put_nowait(channel)
return None
try:
session = self.manager.create_session(
platform=platform, channel=channel
)
self.manager.log(
f"[magenta]listener[/magenta]: [blue]{self.address[0]}[/blue]:[cyan]{self.address[1]}[/cyan]: {platform} session from {channel} established"
)
# Call established callback for session notification
if self.established is not None and not self.established(session):
# The established callback can decide to ignore an established session
session.close()
return None
# Queue the session. This is an obnoxious loop, but
# basically, we attempt to queue the session, and if
# the queue is full, we remove a queued session, and
# retry. We keep doing this until it works. This is
# fine because the queue is just for notification
# purposes, and the sessions are already tracked by
# the manager.
while True:
try:
self._session_queue.put_nowait(session)
except queue.Full:
try:
self._session_queue.get_nowait()
except queue.Empty:
pass
if self.count is not None:
self.count -= 1
if self.count <= 0:
# Drain waiting channels
self.manager.log(
"[magenta]listener[/magenta]: [blue]{self.address[0]}[/blue]:[cyan]{self.address[0]}[/cyan]: max session count reached; shutting down"
)
self._stop_event.set()
return session
except (PlatformError, ChannelError) as exc:
raise ListenerError(str(exc)) from exc
def stop(self):
"""Stop the listener"""
with self._session_lock:
self.count = 0
self._stop_event.set()
def run(self): def run(self):
"""Execute the listener in the background. We have to be careful not """Execute the listener in the background. We have to be careful not
@ -158,14 +250,97 @@ class Listener(threading.Thread):
# Set a short timeout so we don't block the thread # Set a short timeout so we don't block the thread
server.settimeout(0.1) server.settimeout(0.1)
self.state = ListenerState.RUNNING
while not self._stop_event.is_set(): while not self._stop_event.is_set():
try: try:
client = server.accept() # Accept a new client connection
client, address = server.accept()
except socket.timeout: except socket.timeout:
# No connection, loop and check if we've been stopped
continue continue
channel = None
try:
# Construct a channel around the raw client
channel = self._bootstrap_channel(client)
# If we know the platform, create the session
if self.platform is not None:
self.bootstrap_session(channel, platform=self.platform)
except ListenerError as exc:
# this connection didn't establish; log it
self.manager.log(
f"[magenta]listener[/magenta]: [blue]{self.address[0]}[/blue]:[cyan]{self.address[1]}[/cyan]: connection from [blue]{address[0]}[/blue]:[cyan]{address[1]}[/cyan] aborted: {exc}"
)
if channel is not None:
channel.close()
else:
# Close the socket
client.close()
self.state = ListenerState.STOPPED
except Exception as exc:
self.state = ListenerState.FAILED
self.failure_exception = exc
self._stop_event.set()
finally: finally:
pass self._close_socket(raw_server, server)
if self.count is not None and self.count <= 0:
try:
# Drain waiting channels
while True:
self._channel_queue.get_nowait().close()
except queue.Empty:
pass
def _open_socket(self) -> socket.socket:
"""Open the raw socket listener and return the new socket object"""
# Create a listener
try:
server = socket.create_server(
self.address, reuse_port=True, backlog=self.count
)
return server
except socket.error as exc:
raise ListenerError(str(exc))
def _ssl_wrap(self, server: socket.socket) -> ssl.SSLSocket:
"""Wrap the given server socket in an SSL context and return the new socket.
If the ``ssl`` option is not set, this method simply returns the original socket."""
return server
def _close_socket(self, raw_server: socket.socket, server: socket.socket):
"""Close the listener socket"""
if server is not raw_server and server is not None:
server.close()
if raw_server is not None:
raw_server.close()
def _bootstrap_channel(self, client: socket.socket) -> "pwncat.channel.Channel":
"""
Create a channel with the listener parameters around the socket.
:param client: a newly established client socket
:type client: socket.socket
:rtype: pwncat.channel.Channel
"""
try:
channel = pwncat.channel.create(protocol=self.protocol, client=client)
except ChannelError as exc:
raise ListenerError(str(exc))
return channel
class Session: class Session:
@ -179,6 +354,7 @@ class Session:
manager, manager,
platform: Union[str, Platform], platform: Union[str, Platform],
channel: Optional[Channel] = None, channel: Optional[Channel] = None,
active: bool = True,
**kwargs, **kwargs,
): ):
self.id = manager.session_id self.id = manager.session_id
@ -214,7 +390,9 @@ class Session:
# Register this session with the manager # Register this session with the manager
self.manager.sessions[self.id] = self self.manager.sessions[self.id] = self
self.manager.target = self
if active or self.manager.target is None:
self.manager.target = self
# Initialize the host reference # Initialize the host reference
self.hash = self.platform.get_host_hash() self.hash = self.platform.get_host_hash()
@ -488,6 +666,7 @@ class Manager:
self.parser = CommandParser(self) self.parser = CommandParser(self)
self.interactive_running = False self.interactive_running = False
self.db: ZODB.DB = None self.db: ZODB.DB = None
self.prompt_lock = threading.RLock()
# This is needed because pwntools captures the terminal... # This is needed because pwntools captures the terminal...
# there's no way officially to undo it, so this is a nasty # there's no way officially to undo it, so this is a nasty
@ -773,6 +952,64 @@ class Manager:
# probably be configurable somewhere. # probably be configurable somewhere.
pwncat.util.console.print_exception() pwncat.util.console.print_exception()
def create_listener(
self,
protocol: str,
host: str,
port: int,
platform: Optional[str] = None,
ssl: bool = False,
ssl_cert: Optional[str] = None,
ssl_key: Optional[str] = None,
count: Optional[int] = None,
established: Optional[Callable[[Session], bool]] = None,
) -> Listener:
"""
Create and start a new background listener which will wait for connections from
victims and optionally automatically establish sessions. If no platform name is
provided, new ``Channel`` objects will be created and can be initialized by
iterating over them with ``listener.iter_channels`` and initialized with
``listener.bootstrap_session``. If ``ssl`` is true, the socket will be wrapped in
an SSL context. The protocol is normally ``socket``, but can be any channel
protocol which supports a ``client`` parameter holding a socket object.
:param protocol: the name of the channel protocol to use (default: socket)
:type protocol: str
:param host: the host address on which to bind
:type host: str
:param port: the port on which to listen
:type port: int
:param platform: the platform to use when automatically establishing sessions or None
:type platform: Optional[str]
:param ssl: whether to wrap the listener in an SSL context (default: false)
:type ssl: bool
:param ssl_cert: the SSL PEM certificate path
:type ssl_cert: Optional[str]
:param ssl_key: the SSL PEM key path
:type ssl_key: Optional[str]
:param count: the number of sessions to establish before automatically stopping the listener
:type count: Optional[int]
:param established: a callback for when new sessions are established; returning false will
immediately disconnect the new session.
:type established: Optional[Callback[[Session], bool]]
"""
listener = Listener(
manager=self,
address=(host, port),
protocol=protocol,
platform=platform,
count=count,
established=established,
ssl=ssl,
ssl_cert=ssl_cert,
ssl_key=ssl_key,
)
listener.start()
return listener
def create_session(self, platform: str, channel: Channel = None, **kwargs): def create_session(self, platform: str, channel: Channel = None, **kwargs):
r""" r"""
Create a new session from a new or existing channel. The platform specified Create a new session from a new or existing channel. The platform specified

View File

@ -849,10 +849,9 @@ function prompt {
self.session.manager.log( self.session.manager.log(
"[yellow]warning[/yellow]: Ctrl-C does not work for windows targets" "[yellow]warning[/yellow]: Ctrl-C does not work for windows targets"
) )
except EOFError: except EOFError:
self.channel.send(b"\rexit\r") self.channel.send(b"\rexit\r")
self.channel.recvuntil(INTERACTIVE_END_MARKER) interactive_complete.wait()
raise pwncat.util.RawModeExit
finally: finally:
pwncat.util.pop_term_state() pwncat.util.pop_term_state()

10
test.py
View File

@ -20,10 +20,14 @@ with pwncat.manager.Manager("data/pwncatrc") as manager:
# session = manager.create_session("windows", host="192.168.122.11", port=4444) # session = manager.create_session("windows", host="192.168.122.11", port=4444)
# session = manager.create_session("linux", host="pwncat-ubuntu", port=4444) # session = manager.create_session("linux", host="pwncat-ubuntu", port=4444)
# session = manager.create_session("linux", host="127.0.0.1", port=4444) # session = manager.create_session("linux", host="127.0.0.1", port=4444)
session = manager.create_session( # session = manager.create_session(
"linux", certfile="/tmp/cert.pem", keyfile="/tmp/cert.pem", port=4444 # "linux", certfile="/tmp/cert.pem", keyfile="/tmp/cert.pem", port=4444
) # )
# session.platform.powershell("amsiutils") # session.platform.powershell("amsiutils")
listener = manager.create_listener(
protocol="socket", host="0.0.0.0", port=4444, platform="windows"
)
manager.interactive() manager.interactive()