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.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.application.current import get_app
@ -556,6 +557,7 @@ class CommandParser:
self.setup_prompt()
running = True
default_text = ""
while running:
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 == "":
continue

View File

@ -79,7 +79,8 @@ class Listener(threading.Thread):
self,
manager: "Manager",
address: Tuple[str, int],
platform: Optional[str],
protocol: str = "socket",
platform: Optional[str] = None,
count: Optional[int] = None,
established: Optional[Callable[["Session"], bool]] = None,
ssl: bool = False,
@ -92,6 +93,8 @@ class Listener(threading.Thread):
""" The controlling manager object """
self.address: Tuple[str, int] = address
""" 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
""" The platform to use when automatically establishing sessions """
self.count: Optional[int] = count
@ -105,12 +108,16 @@ class Listener(threading.Thread):
self.ssl_key: Optional[str] = ssl_key
""" The SSL server key """
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()
""" An event used to signal the listener to stop """
self._session_queue: queue.Queue = queue.Queue()
""" Queue of newly established sessions. If this queue fills up, it is drained automatically. """
self._channel_queue: queue.Queue = queue.Queue()
""" 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]:
"""
@ -125,6 +132,13 @@ class Listener(threading.Thread):
: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]:
"""
Synchronously iterate over new channels. This generated will
@ -138,12 +152,90 @@ class Listener(threading.Thread):
:rtype: Generator[Channel, None, None]
"""
def _open_socket(self) -> socket.socket:
"""Open the raw socket listener and return the new socket object"""
while count:
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:
"""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."""
def bootstrap_session(
self, channel: pwncat.channel.Channel, platform: str
) -> "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):
"""Execute the listener in the background. We have to be careful not
@ -158,15 +250,98 @@ class Listener(threading.Thread):
# Set a short timeout so we don't block the thread
server.settimeout(0.1)
self.state = ListenerState.RUNNING
while not self._stop_event.is_set():
try:
client = server.accept()
# Accept a new client connection
client, address = server.accept()
except socket.timeout:
# No connection, loop and check if we've been stopped
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:
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:
"""This class represents the container by which ``pwncat`` references
@ -179,6 +354,7 @@ class Session:
manager,
platform: Union[str, Platform],
channel: Optional[Channel] = None,
active: bool = True,
**kwargs,
):
self.id = manager.session_id
@ -214,6 +390,8 @@ class Session:
# Register this session with the manager
self.manager.sessions[self.id] = self
if active or self.manager.target is None:
self.manager.target = self
# Initialize the host reference
@ -488,6 +666,7 @@ class Manager:
self.parser = CommandParser(self)
self.interactive_running = False
self.db: ZODB.DB = None
self.prompt_lock = threading.RLock()
# This is needed because pwntools captures the terminal...
# there's no way officially to undo it, so this is a nasty
@ -773,6 +952,64 @@ class Manager:
# probably be configurable somewhere.
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):
r"""
Create a new session from a new or existing channel. The platform specified

View File

@ -851,8 +851,7 @@ function prompt {
)
except EOFError:
self.channel.send(b"\rexit\r")
self.channel.recvuntil(INTERACTIVE_END_MARKER)
raise pwncat.util.RawModeExit
interactive_complete.wait()
finally:
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("linux", host="pwncat-ubuntu", port=4444)
# session = manager.create_session("linux", host="127.0.0.1", port=4444)
session = manager.create_session(
"linux", certfile="/tmp/cert.pem", keyfile="/tmp/cert.pem", port=4444
)
# session = manager.create_session(
# "linux", certfile="/tmp/cert.pem", keyfile="/tmp/cert.pem", port=4444
# )
# session.platform.powershell("amsiutils")
listener = manager.create_listener(
protocol="socket", host="0.0.0.0", port=4444, platform="windows"
)
manager.interactive()