From 267061517a7e13d1b3cc8f1dc54c295607cb059d Mon Sep 17 00:00:00 2001
From: cytopia A type-safe data structure for IOStdinStdout options. A type-safe data structure for repeated functions for the Runner class. A type-safe data structure for Timer functions for the Runner class. Set specific options for this IO module. Define an interrupt function which will stop the producer. Various producer might call blocking functions and they won't be able to stop themself
as they hang on that blocking function.
-NOTE: This method is triggered from outside and is supposed to stop/shutdown the producer. You should at least implement it with "self.ssig.raise_stop()"Usage
-n, --nodns Do not resolve DNS.
+ --send-on-eof Buffer data received on stdin until EOF and send
+ everything in one chunk.
+
+ --no-shutdown Do not shutdown into half-duplex mode.
+ If this option is passed, pwncat won't invoke shutdown
+ on a socket after seeing EOF on stdin. This is provided
+ for backward-compatibility with OpenBSD netcat, which
+ exhibits this behavior.
+
-v, --verbose Be verbose and print info to stderr. Use -v, -vv, -vvv
or -vvvv for more verbosity. The server performance will
decrease drastically if you use more than three times.
diff --git a/docs/pwncat.api.html b/docs/pwncat.api.html
index 02a813cf..44fda8d0 100644
--- a/docs/pwncat.api.html
+++ b/docs/pwncat.api.html
@@ -36,7 +36,7 @@ Module
# 4. Transformer
# 5. IO modules
# 6. PSE Store
-# 7. IO Runner
+# 7. IO Runner / InterruptHandler
# 8. Command & Control
# 9. Command line arguments
# 10. Main entrypoint
@@ -76,7 +76,7 @@ pwncat
Module
#
# 4. Signaling / Interrupts
# ------------------------------------
-# The StopSignal instance is distributed across all Threads and and the Runner instance and
+# The InterruptHandler instance is distributed across all Threads and and the Runner instance and
# is a way to let other Threads know that a stop signal has been requested.
# Producer/Consumer can implement their own interrupt function so they can be stopped from
# inside (if they do non-blocking stuff) or from outside (if they do blocking stuff).
@@ -85,17 +85,18 @@ pwncat
Module
from abc import abstractmethod
from abc import ABCMeta
+from datetime import datetime
from subprocess import PIPE
from subprocess import Popen
from subprocess import STDOUT
import argparse
-import atexit
import base64
import logging
import os
import re
import select
+import signal
import socket
import sys
import threading
@@ -106,12 +107,15 @@ pwncat
Module
import ipaddress
except ImportError:
pass
+<<<<<<< HEAD
# Posix terminal
try:
import tty
import termios
except ImportError:
pass
+=======
+>>>>>>> Heavy refactoring
# Windows
try:
import msvcrt
@@ -154,12 +158,16 @@ pwncat
Module
APPNAME = "pwncat"
APPREPO = "https://github.com/cytopia/pwncat"
+<<<<<<< HEAD
VERSION = "0.0.23-alpha"
+=======
+VERSION = "0.1.0"
+>>>>>>> Heavy refactoring
# Default timeout for timeout-based sys.stdin and socket.recv
-TIMEOUT_READ_STDIN = 0.1
-TIMEOUT_RECV_SOCKET = 0.1
-TIMEOUT_RECV_SOCKET_RETRY = 2
+TIMEOUT_READ_STDIN = 0.05
+TIMEOUT_RECV_SOCKET = 0.05
+TIMEOUT_RECV_SOCKET_RETRY = 1
# https://docs.python.org/3/library/subprocess.html#popen-constructor
# * 0 means unbuffered (read and write are one system call and can return short)
@@ -241,7 +249,7 @@ pwncat
Module
# --------------------------------------------------------------------------
@property
def function(self):
- # type: () -> Callable[..., Iterator[str]]
+ # type: () -> Callable[..., Iterator[bytes]]
"""`IO.producer`: Callable funtcion function."""
return self.__function
@@ -261,7 +269,7 @@ pwncat
Module
# Contrcutor
# --------------------------------------------------------------------------
def __init__(self, function, *args, **kwargs):
- # type: (Callable[..., Iterator[str]], Any, Any) -> None
+ # type: (Callable[..., Iterator[bytes]], Any, Any) -> None
self.__function = function
self.__args = args
self.__kwargs = kwargs
@@ -284,7 +292,7 @@ pwncat
Module
@property
def consumer(self):
- # type: () -> Callable[[str], None]
+ # type: () -> Callable[[bytes], None]
"""`IO.consumer`: Data consumer function."""
return self.__consumer
@@ -312,7 +320,7 @@ pwncat
Module
def __init__(
self,
producer, # type: DsCallableProducer
- consumer, # type: Callable[[str], None]
+ consumer, # type: Callable[[bytes], None]
interrupts, # type: List[Callable[[], None]]
transformers, # type: List[Transform]
code, # type: Optional[Union[str, bytes, CodeType]]
@@ -359,10 +367,10 @@ pwncat
Module
return self.__kwargs
@property
- def signal(self):
- # type: () -> StopSignal
- """`StopSignal`: StopSignal instance."""
- return self.__signal
+ def ssig(self):
+ # type: () -> InterruptHandler
+ """`InterruptHandler`: InterruptHandler instance."""
+ return self.__ssig
# --------------------------------------------------------------------------
# Constructor
@@ -370,7 +378,7 @@ pwncat
Module
def __init__(
self,
action, # type: Callable[..., None]
- signal, # type: StopSignal
+ ssig, # type: InterruptHandler
intvl, # type: int
*args, # type: Tuple[Any, ...]
**kwargs # type: Dict[str, Any]
@@ -379,7 +387,7 @@ pwncat
Module
assert type(intvl) is int, type(intvl)
assert type(kwargs) is dict, type(kwargs)
self.__action = action
- self.__signal = signal
+ self.__ssig = ssig
self.__intvl = intvl
self.__args = args
self.__kwargs = kwargs
@@ -425,10 +433,10 @@ pwncat
Module
return self.__kwargs
@property
- def signal(self):
- # type: () -> StopSignal
- """`StopSignal`: StopSignal instance."""
- return self.__signal
+ def ssig(self):
+ # type: () -> InterruptHandler
+ """`InterruptHandler`: InterruptHandler instance."""
+ return self.__ssig
# --------------------------------------------------------------------------
# Constructor
@@ -436,7 +444,7 @@ pwncat
Module
def __init__(
self,
action, # type: Callable[..., None]
- signal, # type: StopSignal
+ ssig, # type: InterruptHandler
repeat, # type: int
pause, # type: float
*args, # type: Tuple[Any, ...]
@@ -447,7 +455,7 @@ pwncat
Module
assert type(pause) is float, type(pause)
assert type(kwargs) is dict, type(kwargs)
self.__action = action
- self.__signal = signal
+ self.__ssig = ssig
self.__repeat = repeat
self.__pause = pause
self.__args = args
@@ -792,8 +800,8 @@ pwncat
Module
# --------------------------------------------------------------------------
@property
def ssig(self):
- # type: () -> StopSignal
- """`StopSignal`: StopSignal instance to trigger a shutdown signal."""
+ # type: () -> InterruptHandler
+ """`InterruptHandler`: InterruptHandler instance to trigger a shutdown signal."""
return self.__ssig
@property
@@ -806,7 +814,7 @@ pwncat
Module
# Constructor
# --------------------------------------------------------------------------
def __init__(self, ssig, safeword):
- # type: (StopSignal, str) -> None
+ # type: (InterruptHandler, str) -> None
super(DsTransformSafeword, self).__init__()
self.__ssig = ssig
self.__safeword = safeword
@@ -833,14 +841,21 @@ pwncat
Module
"""`float`: Input timeout in seconds for non-blocking read or `None` for blocking."""
return self.__input_timeout
+ @property
+ def send_on_eof(self):
+ # type: () -> bool
+ """`float`: Determines if we buffer STDIN until EOF before sending."""
+ return self.__send_on_eof
+
# --------------------------------------------------------------------------
# Constructor
# --------------------------------------------------------------------------
- def __init__(self, encoder, input_timeout):
- # type: (StringEncoder, Optional[float]) -> None
+ def __init__(self, encoder, input_timeout, send_on_eof):
+ # type: (StringEncoder, Optional[float], bool) -> None
super(DsIOStdinStdout, self).__init__()
self.__enc = encoder
self.__input_timeout = input_timeout
+ self.__send_on_eof = send_on_eof
# -------------------------------------------------------------------------------------------------
@@ -896,7 +911,7 @@ pwncat
Module
# #################################################################################################
# -------------------------------------------------------------------------------------------------
-# [3/11 LIBRARY CLASSES]: (1/4) TraceLogger
+# [3/11 LIBRARY CLASSES]: (1/3) TraceLogger
# -------------------------------------------------------------------------------------------------
class TraceLogger(logging.getLoggerClass()): # type: ignore
"""Extend Python's default logger class with TRACE level logging."""
@@ -936,7 +951,7 @@ pwncat
Module
# -------------------------------------------------------------------------------------------------
-# [3/11 LIBRARY CLASSES]: (2/4) ColoredLogFormatter
+# [3/11 LIBRARY CLASSES]: (2/3) ColoredLogFormatter
# -------------------------------------------------------------------------------------------------
class ColoredLogFormatter(logging.Formatter):
"""Custom log formatter which adds different details and color support."""
@@ -1009,7 +1024,7 @@ pwncat
Module
# -------------------------------------------------------------------------------------------------
-# [3/11 LIBRARY CLASSES]: (3/4) StringEncoder
+# [3/11 LIBRARY CLASSES]: (3/3) StringEncoder
# -------------------------------------------------------------------------------------------------
class StringEncoder(object):
"""Takes care about Python 2/3 string encoding/decoding.
@@ -1018,72 +1033,73 @@ pwncat
Module
classes or functions as strings to keep full Python 2/3 compat.
"""
- # --------------------------------------------------------------------------
- # Constructor
- # --------------------------------------------------------------------------
- def __init__(self):
- # type: () -> None
- """Create a StringEncoder instance which converts str/bytes according to Python version."""
- self.__py3 = sys.version_info >= (3, 0) # type: bool
-
- # https://stackoverflow.com/questions/606191/27527728#27527728
- self.__codec = "cp437"
- self.__fallback = "latin-1"
+ CODECS = [
+ "utf-8",
+ "cp437",
+ "latin-1",
+ ]
# --------------------------------------------------------------------------
- # Public Functions
- # --------------------------------------------------------------------------
- def encode(self, data):
+ # Class methods
+ # --------------------------------------------------------------------------
+ @classmethod
+ def rstrip(cls, data, search=None):
+ # type: (Union[bytes, str], Optional[str]) -> Union[bytes, str]
+ """Implementation of rstring which works on bytes or strings."""
+ # We have a bytes object in Python3
+ if sys.version_info >= (3, 0) and type(data) is not str:
+ # Strip whitespace
+ if search is None:
+ while True:
+ new = data
+ new = cls.rstrip(new, " ")
+ new = cls.rstrip(new, "\n")
+ new = cls.rstrip(new, "\r")
+ new = cls.rstrip(new, "\t")
+ # Loop until no more changes occur
+ if new == data:
+ return new
+ else:
+ bsearch = StringEncoder.encode(search)
+ while data[-1:] == bsearch:
+ data = data[:-1]
+ return data
+
+ # Use native function
+ if search is None:
+ return data.rstrip()
+ return data.rstrip(search) # type: ignore
+
+ @classmethod
+ def encode(cls, data):
# type: (str) -> bytes
"""Convert string into a byte type for Python3."""
- if self.__py3:
- try:
- return data.encode(self.__codec)
- except UnicodeEncodeError:
- # TODO: Add logging
- return data.encode(self.__fallback)
+ if sys.version_info >= (3, 0):
+ for codec in cls.CODECS:
+ # On the last codec, do not catch the exception and let it trigger if it fails
+ if codec == cls.CODECS[-1]:
+ return data.encode(codec)
+ try:
+ return data.encode(codec)
+ except UnicodeEncodeError:
+ pass
return data # type: ignore
- def decode(self, data):
+ @classmethod
+ def decode(cls, data):
# type: (bytes) -> str
"""Convert bytes into a string type for Python3."""
- if self.__py3:
- return data.decode(self.__codec)
+ if sys.version_info >= (3, 0):
+ for codec in cls.CODECS:
+ # On the last codec, do not catch the exception and let it trigger if it fails
+ if codec == cls.CODECS[-1]:
+ return data.decode(codec)
+ try:
+ return data.decode(codec)
+ except UnicodeDecodeError:
+ pass
return data # type: ignore
- def base64_encode(self, data):
- # type: (str) -> str
- """Convert string into a base64 encoded string."""
- return self.decode(base64.b64encode(self.encode(data)))
-
-
-# -------------------------------------------------------------------------------------------------
-# [3/11 LIBRARY CLASSES]: (4/4): StopSignal
-# -------------------------------------------------------------------------------------------------
-class StopSignal(object):
- """Provide a simple boolean switch."""
-
- # --------------------------------------------------------------------------
- # Constructor
- # --------------------------------------------------------------------------
- def __init__(self):
- # type: () -> None
- """Create a StopSignal instance."""
- self.__stop = False
-
- # --------------------------------------------------------------------------
- # Public Functions
- # --------------------------------------------------------------------------
- def has_stop(self):
- # type: () -> bool
- """Check if a stop signal has been raised."""
- return self.__stop
-
- def raise_stop(self):
- # type: () -> None
- """Raise a stop signal."""
- self.__stop = True
-
# #################################################################################################
# #################################################################################################
@@ -1097,7 +1113,7 @@ pwncat
Module
# [4/11 NETWORK]: (1/1) Sock
# -------------------------------------------------------------------------------------------------
class Sock(_Singleton("SingletonMeta", (object,), {})): # type: ignore
- """Thread-safe singleton Socket helper to emulate a module within the same file."""
+ """Thread-safe singleton Socket wrapper to emulate a module within the same file."""
def __init__(self):
# type: () -> None
@@ -1557,27 +1573,35 @@ pwncat
Module
self.__log.error(msg)
raise socket.error(msg)
- def accept(self, sockets, fstop):
- # type: (List[socket.socket], Callable[[], bool]) -> Tuple[socket.socket, Tuple[str, int]]
+ def accept(
+ self,
+ sockets, # type: List[socket.socket]
+ has_quit, # type: Callable[[], bool]
+ select_timeout=0.01, # type: float
+ ):
+ # type: (...) -> Tuple[socket.socket, Tuple[str, int]]
"""Accept a single connection from given list of sockets.
Given sockets must be bound to an addr and listening for connections.
Args:
sock ([socket.socket]): List of sockets IPv4 and/or IPv6 to accept on.
- fstop (Callable[[], bool]): A function that returns True if abort is requested.
+ has_quit (Callable[[], bool]): A function that returns True if abort is requested.
+ select_timeout (float): Timeout to poll sockets for connected clients.
Returns:
- socket.socket: Returns the connection socket (whatever protocol was faster).
+ (socket.socket, str, int): Returns tuple of socket, address and port of client.
Raises:
socket.error: Raised if server cannot accept connection or stop signal is requested.
"""
self.__log.debug("Waiting for TCP client")
while True:
- ssockets = select.select(sockets, [], [], 0.01)[0] # type: List[socket.socket]
- if fstop():
- raise socket.error("StopSignal acknknowledged")
+ ssockets = select.select(sockets, [], [], select_timeout)[
+ 0
+ ] # type: List[socket.socket]
+ if has_quit():
+ raise socket.error("SOCK-QUIT signal ACK for accept(): raised socket.error()")
for sock in ssockets:
try:
conn, addr = sock.accept()
@@ -1615,13 +1639,18 @@ pwncat
Module
port (int): Port of server to connect to.
Returns:
- Tuple[str,int]: Adress/port tuple of local bin of the client.
+ Tuple[str,int]: Adress/port tuple of local bind of the client.
Raises:
socker.error: If client cannot connect to remote peer or custom bind did not succeed.
"""
- sock_family_name = self.get_family_name(sock.family)
- sock_type_name = self.get_type_name(sock.type)
+ try:
+ # If the socket was already closed elsewhere, it won't have family or type anymore
+ sock_family_name = self.get_family_name(sock.family)
+ sock_type_name = self.get_type_name(sock.type)
+ except AttributeError as error:
+ raise socket.error(error)
+
# Bind to a custom addr/port
if src_addr is not None and src_port is not None:
try:
@@ -1722,12 +1751,12 @@ pwncat
Module
# Constructor / Destructor
# --------------------------------------------------------------------------
def __init__(self, encoder, ssig, options):
- # type: (StringEncoder, StopSignal, DsSock) -> None
+ # type: (StringEncoder, InterruptHandler, DsSock) -> None
"""Instantiate Sock class.
Args:
encoder (StringEncoder): Instance of StringEncoder (Python2/3 str/byte compat).
- ssig (StopSignal): Used to stop blocking loops.
+ ssig (InterruptHandler): Used to stop blocking loops.
options (DsSock): Instance of DsSock.
"""
self.__log = logging.getLogger(__name__) # type: logging.Logger
@@ -1797,14 +1826,17 @@ pwncat
Module
# Public Send / Receive Functions
# --------------------------------------------------------------------------
def send(self, data):
- # type: (str) -> int
+ # type: (bytes) -> int
"""Send data through a connected (TCP) or unconnected (UDP) socket.
Args:
- data (str): The data to send.
+ data (bytes): The data to send.
Returns:
int: Returns total bytes sent.
+
+ Raises:
+ socket.error: Except here when unconnected or connection was forcibly closed.
"""
# UDP has some specialities as its socket is unconnected.
# See also recv() for specialities on that side.
@@ -1816,16 +1848,18 @@ pwncat
Module
if not self.__active:
self.__log.warning("UDP client has not yet connected. Queueing message")
while not self.__active:
+ if self.__ssig.has_sock_quit():
+ self.__log.trace( # type: ignore
+ "SOCK-QUIT signal ACK in Net.send (while waiting for UDP client)"
+ )
+ return -1
time.sleep(0.01)
curr = 0 # bytes send during one loop iteration
send = 0 # total bytes send
size = len(data) # bytes of data that needs to be send
- byte = self.__enc.encode(data)
- assert size == len(byte), "Encoding messed up string length, might need to do len() after."
# Loop until all bytes have been send
- # TODO: Does this make it impossible to send nullbytes (Ctrl+d)
while send < size:
self.__log.debug(
"Trying to send %d bytes to %s:%d",
@@ -1833,23 +1867,23 @@ pwncat
Module
self.__active["remote_addr"],
self.__active["remote_port"],
)
- self.__log.trace("Trying to send: %s", repr(byte)) # type: ignore
+ self.__log.trace("Trying to send: %s", repr(data)) # type: ignore
try:
# Only UDP server has not made a connect() to the socket, all others
# are already connected and need to use send() instead of sendto()
if self.__udp_mode_server:
curr = self.__active["conn"].sendto(
- byte, (self.__active["remote_addr"], self.__active["remote_port"])
+ data, (self.__active["remote_addr"], self.__active["remote_port"])
)
send += curr
else:
- curr = self.__active["conn"].send(byte)
+ curr = self.__active["conn"].send(data)
send += curr
if curr == 0:
self.__log.error("No bytes send during loop round.")
return 0
# Remove 'curr' many bytes from byte for the next round
- byte = byte[curr:]
+ data = data[curr:]
self.__log.debug(
"Sent %d bytes to %s:%d (%d bytes remaining)",
curr,
@@ -1857,23 +1891,23 @@ pwncat
Module
self.__active["remote_port"],
size - send,
)
- except (OSError, socket.error) as error:
- self.__log.error("Socket OS Error: %s", error)
- return send
+ except (BrokenPipeError, OSError, socket.error) as error:
+ msg = "Socket send Error: {}".format(error)
+ raise socket.error(msg)
return send
def receive(self):
- # type: () -> str
+ # type: () -> bytes
"""Receive and return data from the connected (TCP) or unconnected (UDP) socket.
Returns:
- str: Returns received data from connected (TCP) or unconnected (UDP) socket.
+ bytes: Returns received data from connected (TCP) or unconnected (UDP) socket.
Raises:
socket.timeout: Except here to do an action when the socket is not busy.
AttributeError: Except here when current instance has closed itself (Ctrl+c).
socket.error: Except here when unconnected or connection was forcibly closed.
- EOFError: Except here when upstream has closed the connection.
+ EOFError: Except here when upstream has closed the connection via EOF.
"""
# This is required for a UDP server that has no connected clients yet
# and is waiting for data receival for the first time on either IPv4 or IPv6
@@ -1889,9 +1923,9 @@ pwncat
Module
0
] # type: List[socket.socket]
# E.g.: ValueError: file descriptor cannot be a negative integer (-1)
- except (ValueError, AttributeError):
- msg = "Connection was closed by self."
- self.__log.warning(msg)
+ except (ValueError, AttributeError) as error:
+ msg = "Connection was closed by self: [1]: {}".format(error)
+ self.__log.debug(msg)
raise AttributeError(msg)
if not conns:
# This is raised for the calling function to determine what to do
@@ -1903,12 +1937,12 @@ pwncat
Module
conn = conns[0] # type: socket.socket
try:
# https://manpages.debian.org/buster/manpages-dev/recv.2.en.html
- (byte, addr) = conn.recvfrom(self.__options.bufsize)
+ (data, addr) = conn.recvfrom(self.__options.bufsize)
# [1/5] When closing itself (e.g.: via Ctrl+c and the socket_close() funcs are called)
- except AttributeError:
- msg = "Connection was closed by self."
- self.__log.warning(msg)
+ except AttributeError as error:
+ msg = "Connection was closed by self: [2]: {}".format(error)
+ self.__log.debug(msg)
raise AttributeError(msg)
# [2/5] Connection was forcibly closed
@@ -1916,14 +1950,14 @@ pwncat
Module
# [Errno 10054] An existing connection was forcibly closed by the remote host
# [WinError 10054] An existing connection was forcibly closed by the remote host
except (OSError, socket.error) as error:
- self.__log.warning("Connection error: %s", error)
+ self.__log.debug("Connection error: %s", error)
raise socket.error(error)
# [3/5] Upstream (server or client) is gone.
# In TCP, there is no such thing as an empty message, so zero means a peer disconnect.
# In UDP, there is no such thing as a peer disconnect, so zero means an empty datagram.
- if not byte:
- msg = "Upstream has closed the connection."
+ if not data:
+ msg = "EOF: Remote finished sending."
self.__log.info(msg)
raise EOFError(msg)
@@ -1961,7 +1995,6 @@ pwncat
Module
}
# [5/5] We have data to process
- data = self.__enc.decode(byte)
self.__log.debug(
"Received %d bytes from %s:%d",
len(data),
@@ -2198,24 +2231,27 @@ pwncat
Module
return False
# (2/3) Accept
+ remove = {}
try:
conn, client = self.__sock.accept(
- [conns[family]["sock"] for family in conns], self.__ssig.has_stop
+ [conns[family]["sock"] for family in conns], self.__ssig.has_sock_quit
)
conns[conn.family]["conn"] = conn
conns[conn.family]["remote_addr"] = client[0]
conns[conn.family]["remote_port"] = client[1]
except socket.error as err:
- # On error, remove all bind sockets
- for family in conns:
- self.__log.debug(
- "Removing (family %d/%s) due to: %s",
- family,
- self.__sock.get_family_name(family),
- err,
- )
- self.__sock.close(conns[family]["sock"], self.__sock.get_family_name(family))
- del conns[family]
+ remove = {family: str(err) for family in conns}
+ # On error, remove all bind sockets
+ for family in remove:
+ self.__log.debug(
+ "Removing (family %d/%s) due to: %s",
+ family,
+ self.__sock.get_family_name(family),
+ remove[family],
+ )
+ self.__sock.close(conns[family]["sock"], self.__sock.get_family_name(family))
+ del conns[family]
+ if not conns:
return False
# (3/3) Store connections
@@ -2248,7 +2284,7 @@ pwncat
Module
# [2/3] Accept
try:
conn, client = self.__sock.accept(
- [self.__conns[family]["sock"] for family in self.__conns], self.__ssig.has_stop
+ [self.__conns[family]["sock"] for family in self.__conns], self.__ssig.has_sock_quit
)
except socket.error:
return False
@@ -2289,7 +2325,7 @@ pwncat
Module
# #################################################################################################
# -------------------------------------------------------------------------------------------------
-# [5/11 TRANSFORM]: (1/3): Transform
+# [5/11 TRANSFORM]: (1/5): Transform
# -------------------------------------------------------------------------------------------------
class Transform(ABC): # type: ignore
"""Abstract class to for pwncat I/O transformers.
@@ -2321,16 +2357,19 @@ pwncat
Module
# --------------------------------------------------------------------------
@abstractmethod
def transform(self, data):
- # type: (str) -> str
+ # type: (bytes) -> bytes
"""Implement a transformer function which transforms a string..
+ Args:
+ data (bytes): data to be transformed.
+
Returns:
- str: The transformed string.
+ bytes: The transformed string.
"""
# -------------------------------------------------------------------------------------------------
-# [5/11 TRANSFORM]: (2/3) TransformLinefeed
+# [5/11 TRANSFORM]: (2/5) TransformLinefeed
# -------------------------------------------------------------------------------------------------
class TransformLinefeed(Transform):
"""Implement basic linefeed replacement."""
@@ -2353,7 +2392,7 @@ pwncat
Module
# Public Functions
# --------------------------------------------------------------------------
def transform(self, data):
- # type: (str) -> str
+ # type: (bytes) -> bytes
"""Transform linefeeds to CRLF, LF or CR if requested.
Returns:
@@ -2365,46 +2404,46 @@ pwncat
Module
# ? -> No line feeds
if self.__opts.crlf == "no":
- if data.endswith("\r\n"):
+ if data[-2:] == StringEncoder.encode("\r\n"):
self.log.debug("Removing CRLF")
return data[:-2]
- if data.endswith("\n"):
+ if data[-1:] == StringEncoder.encode("\n"):
self.log.debug("Removing LF")
return data[:-1]
- if data.endswith("\r"):
+ if data[-1:] == StringEncoder.encode("\r"):
self.log.debug("Removing CR")
return data[:-1]
# ? -> CRLF
- if self.__opts.crlf == "crlf" and not data.endswith("\r\n"):
- if data.endswith("\n"):
+ if self.__opts.crlf == "crlf" and data[-2:] != StringEncoder.encode("\r\n"):
+ if data[-1:] == StringEncoder.encode("\n"):
self.log.debug("Replacing LF with CRLF")
- return data[:-1] + "\r\n"
- if data.endswith("\r"):
+ return data[:-1] + StringEncoder.encode("\r\n")
+ if data[-1:] == StringEncoder.encode("\r"):
self.log.debug("Replacing CR with CRLF")
- return data[:-1] + "\r\n"
+ return data[:-1] + StringEncoder.encode("\r\n")
# ? -> LF
if self.__opts.crlf == "lf":
- if data.endswith("\r\n"):
+ if data[-2:] == StringEncoder.encode("\r\n"):
self.log.debug("Replacing CRLF with LF")
- return data[:-2] + "\n"
- if data.endswith("\r"):
+ return data[:-2] + StringEncoder.encode("\n")
+ if data[-1:] == StringEncoder.encode("\r"):
self.log.debug("Replacing CR with LF")
- return data[:-1] + "\n"
+ return data[:-1] + StringEncoder.encode("\n")
# ? -> CR
if self.__opts.crlf == "cr":
- if data.endswith("\r\n"):
+ if data[-2:] == StringEncoder.encode("\r\n"):
self.log.debug("Replacing CRLF with CR")
- return data[:-2] + "\r"
- if data.endswith("\n"):
+ return data[:-2] + StringEncoder.encode("\r")
+ if data[-1:] == StringEncoder.encode("\n"):
self.log.debug("Replacing LF with CR")
- return data[:-1] + "\r"
+ return data[:-1] + StringEncoder.encode("\r")
# Otherwise just return it as it is
return data
# -------------------------------------------------------------------------------------------------
-# [5/11 TRANSFORM]: (3/3) TransformSafeword
+# [5/11 TRANSFORM]: (3/5) TransformSafeword
# -------------------------------------------------------------------------------------------------
class TransformSafeword(Transform):
"""Implement a trigger to emergency shutdown upon receival of a specific safeword."""
@@ -2428,18 +2467,166 @@ pwncat
Module
# Public Functions
# --------------------------------------------------------------------------
def transform(self, data):
- # type: (str) -> str
+ # type: (bytes) -> bytes
"""Raise a stop signal upon receiving the safeword.
Returns:
str: The string as it is without changes
"""
- if self.__opts.safeword in data:
- self.__log.info("Received safeword: raising stop signal.")
- self.__opts.ssig.raise_stop()
+ if StringEncoder.encode(self.__opts.safeword) in data:
+ self.log.trace("TERMINATE signal REQ in TransformSafeword.transform") # type: ignore
+ self.__opts.ssig.raise_terminate()
return data
+# -------------------------------------------------------------------------------------------------
+# [5/11 TRANSFORM]: (4/5) TransformHttpPack
+# -------------------------------------------------------------------------------------------------
+class TransformHttpPack(Transform):
+ """Implement a transformation to pack data into HTTP packets."""
+
+ # --------------------------------------------------------------------------
+ # Constructor / Destructor
+ # --------------------------------------------------------------------------
+ def __init__(self, opts):
+ # type: (Dict[str, str]) -> None
+ """Set specific options for this transformer.
+
+ Args:
+ opts (DsTransformLinefeed): Transformer options.
+
+ """
+ super(TransformHttpPack, self).__init__()
+ self.__opts = opts
+ self.__log = logging.getLogger(__name__)
+
+ assert "reply" in opts
+ assert opts["reply"] in ["request", "response"]
+
+ # Initial default header
+ self.__headers = [
+ "Accept-Charset: utf-8",
+ ]
+
+ self.__response_headers_sent = False
+
+ # --------------------------------------------------------------------------
+ # Public Functions
+ # --------------------------------------------------------------------------
+ def transform(self, data):
+ # type: (bytes) -> bytes
+ """Wrap data into a HTTP packet.
+
+ Returns:
+ bytes: The wrapped string.
+ """
+ request_header = [
+ "POST / HTTP/1.1",
+ "Host: {}".format(self.__opts["host"]),
+ "User-Agent: pwncat",
+ "Accept: */*",
+ "Conent-Length: {}".format(len(data)),
+ "Content-Type: text/plain; charset=UTF-8",
+ ]
+ response_header = [
+ "HTTP/1.1 200 OK",
+ "Date: {}".format(self.__get_date()),
+ "Server: pwncat",
+ "Conent-Length: {}".format(len(data)),
+ "Connection: close",
+ ]
+
+ self.__response_headers_sent = True
+
+ if self.__opts["reply"] == "request":
+ header = StringEncoder.encode(
+ "\n".join(request_header) + "\n" + "\n".join(self.__headers) + "\n\n"
+ )
+ else:
+ header = StringEncoder.encode(
+ "\n".join(response_header) + "\n" + "\n".join(self.__headers) + "\n\n"
+ )
+ return header + data
+
+ # --------------------------------------------------------------------------
+ # Private Functions
+ # --------------------------------------------------------------------------
+ def __get_date(self): # pylint: disable=no-self-use
+ # type: () -> str
+ now = datetime.utcnow()
+ weekday = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"][now.weekday()]
+ month = [
+ "Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec",
+ ][now.month - 1]
+ return "%s, %02d %s %04d %02d:%02d:%02d GMT" % (
+ weekday,
+ now.day,
+ month,
+ now.year,
+ now.hour,
+ now.minute,
+ now.second,
+ )
+
+
+# -------------------------------------------------------------------------------------------------
+# [5/11 TRANSFORM]: (5/5) TransformHttpUnpack
+# -------------------------------------------------------------------------------------------------
+class TransformHttpUnpack(Transform):
+ """Implement a transformation to unpack data from HTTP packets."""
+
+ # --------------------------------------------------------------------------
+ # Constructor / Destructor
+ # --------------------------------------------------------------------------
+ def __init__(self, opts):
+ # type: (Dict[str, str]) -> None
+ """Set specific options for this transformer.
+
+ Args:
+ opts (DsTransformLinefeed): Transformer options.
+
+ """
+ super(TransformHttpUnpack, self).__init__()
+ self.__opts = opts
+ self.__log = logging.getLogger(__name__)
+
+ # --------------------------------------------------------------------------
+ # Public Functions
+ # --------------------------------------------------------------------------
+ def transform(self, data):
+ # type: (bytes) -> bytes
+ """Unwrap data from a HTTP packet.
+
+ Returns:
+ str: The wrapped string.
+ """
+ request = StringEncoder.encode(r"^(GET|HEAD|POST|PUT|DELETE|CONNECT|OPTIONS|TRACE|PATCH)")
+ response = StringEncoder.encode(r"^HTTP/[.0-9]+")
+
+ # Did not receive a valid HTTP request, so we return the original untransformed message
+ if not (re.match(request, data) or re.match(response, data)):
+ return data
+
+ body = StringEncoder.encode(r"(\r\n\r\n|\n\n)(.*)")
+ match = re.search(body, data)
+
+ # Check if we can separate headers and body
+ if match is None or len(match.group()) < 2:
+ return data
+ return match.group(2)
+
+
# #################################################################################################
# #################################################################################################
# ###
@@ -2472,8 +2659,8 @@ pwncat
Module
# --------------------------------------------------------------------------
@property
def ssig(self):
- # type: () -> StopSignal
- """`StopSignal`: Read only property to provide a StopSignal instance to IO."""
+ # type: () -> InterruptHandler
+ """`InterruptHandler`: InterruptHandler instance."""
return self.__ssig
@property
@@ -2487,11 +2674,11 @@ pwncat
Module
# --------------------------------------------------------------------------
@abstractmethod
def __init__(self, ssig):
- # type: (StopSignal) -> None
+ # type: (InterruptHandler) -> None
"""Set specific options for this IO module.
Args:
- ssig (StopSignal): StopSignal instance used by the interrupter.
+ ssig (InterruptHandler): InterruptHandler instance used by the interrupter.
"""
super(IO, self).__init__()
self.__ssig = ssig
@@ -2502,7 +2689,7 @@ pwncat
Module
# --------------------------------------------------------------------------
@abstractmethod
def producer(self, *args, **kwargs):
- # type: (Any, Any) -> Iterator[str]
+ # type: (Any, Any) -> Iterator[bytes]
"""Implement a generator function which constantly yields data.
The data could be from various sources such as: received from a socket,
@@ -2514,7 +2701,7 @@ pwncat
Module
@abstractmethod
def consumer(self, data):
- # type: (str) -> None
+ # type: (bytes) -> None
"""Define a consumer callback which will apply an action on the producer output.
Args:
@@ -2529,8 +2716,6 @@ pwncat
Module
Various producer might call blocking functions and they won't be able to stop themself
as they hang on that blocking function.
NOTE: This method is triggered from outside and is supposed to stop/shutdown the producer.
-
- You should at least implement it with "self.ssig.raise_stop()"
"""
@@ -2545,7 +2730,7 @@ pwncat
Module
# --------------------------------------------------------------------------
def __init__(
self,
- ssig, # type: StopSignal
+ ssig, # type: InterruptHandler
encoder, # type: StringEncoder
host, # type: str
ports, # type: List[int]
@@ -2558,7 +2743,7 @@ pwncat
Module
"""Create a Pwncat instance of either a server or a client.
Args:
- ssig (StopSignal): Stop signal instance
+ ssig (InterruptHandler): Instance of InterruptHandler.
encoder (StringEncoder): Instance of StringEncoder (Python2/3 str/byte compat).
host (str): The hostname to resolve.
ports ([int]): List of ports to connect to or listen on.
@@ -2576,6 +2761,9 @@ pwncat
Module
self.__srv_opts = srv_opts
self.__cli_opts = cli_opts
+ # Did we already run cleanup
+ self.__cleaned_up = False
+
# Internally store addresses for reconn or rebind functions
self.__host = host
self.__ports = ports
@@ -2594,7 +2782,7 @@ pwncat
Module
# Public Functions
# --------------------------------------------------------------------------
def producer(self, *args, **kwargs):
- # type: (Any, Any) -> Iterator[str]
+ # type: (Any, Any) -> Iterator[bytes]
"""Network receive generator which hooks into the receive function and adds features.
Yields:
@@ -2610,9 +2798,9 @@ pwncat
Module
try:
yield self.__net.receive()
# [2/3] Non-blocking socket is finished receiving data and allows us to do some action
- except socket.timeout:
+ except socket.timeout as err:
# Let's ask the interrupter() function if we should terminate?
- if not self.ssig.has_stop():
+ if not self.ssig.has_sock_quit():
continue
# Stop signal is raied when my own side of the network was closed.
# Happened most likely that the user pressed Ctrl+c
@@ -2627,12 +2815,19 @@ pwncat
Module
curr_recv_timeout_retry += 1
continue
# We ware all done reading, shut down
- self.ssig.raise_stop()
+ self.log.trace( # type: ignore
+ "SOCK-QUIT signal ACK in IONetwork.producer [1]: %s", err
+ )
+ self.__cleanup()
return
- # [3/3] Upstream is gone
- except (EOFError, AttributeError, socket.error):
+ # [3/3] Upstream is gone (in one way or another)
+ except (EOFError, AttributeError, socket.error) as err:
# Do we have a stop signal?
- if self.ssig.has_stop():
+ if self.ssig.has_sock_quit():
+ self.log.trace( # type: ignore
+ "SOCK-QUIT signal ACK in IONetwork.producer [2]: %s", err
+ )
+ self.__cleanup()
return
# Do we re-accept new clients?
if self.__sock_opts.udp:
@@ -2642,27 +2837,39 @@ pwncat
Module
continue
if self.__role == "client" and self.__client_reconnect_to_server():
continue
+ # Inform everybody that we are quitting
+ self.log.trace("SOCK-QUIT signal REQ in IONetwork.producer") # type: ignore
+ self.ssig.raise_sock_quit()
+ self.__cleanup()
return
def consumer(self, data):
- # type: (str) -> None
+ # type: (bytes) -> None
"""Send data to a socket."""
- self.__net.send(data)
+ try:
+ self.__net.send(data)
+ except socket.error:
+ pass
def interrupt(self):
# type: () -> None
"""Stop function that can be called externally to close this instance."""
- self.log.trace( # type: ignore
- "[IONetwork] socket.close was raised by calling interrupt() externally."
- )
- self.__net.close_conn_sock()
- self.__net.close_bind_sock()
- # Raise stop signal
- self.ssig.raise_stop()
+ self.log.trace("SOCK-QUIT signal REQ in IONetwork.interrupt") # type: ignore
+ self.ssig.raise_sock_quit()
+ self.__cleanup()
# --------------------------------------------------------------------------
# Private Functions
# --------------------------------------------------------------------------
+ def __cleanup(self):
+ # type: () -> None
+ """Cleanup function."""
+ if not self.__cleaned_up:
+ self.log.trace("SOCK-QUIT-CLEANUP: Closing sockets") # type: ignore
+ self.__net.close_conn_sock()
+ self.__net.close_bind_sock()
+ self.__cleaned_up = True
+
def __client_reconnect_to_server(self):
# type: () -> bool
"""Ensure the client re-connects to the remote server, if the remote server hang up.
@@ -2675,13 +2882,14 @@ pwncat
Module
# reconn < 0 (endlessly)
# reconn > 0 (reconnect until counter reaches zero)
while self.__cli_opts.reconn != 0:
-
# [1/6] Let's ask the interrupter() function if we should terminate?
# We need a little wait here in order for the stop signal to propagate.
# Don't know how fast the other threads are.
- # time.sleep(0.1)
- # if self.ssig.has_stop():
- # return False
+ if self.ssig.has_sock_quit():
+ self.log.trace( # type: ignore
+ "QUIT signal ACK in IONetwork.__clienet_reconnect_to_server [1]"
+ )
+ return False
# [2/6] Wait
time.sleep(self.__cli_opts.reconn_wait)
@@ -2689,7 +2897,10 @@ pwncat
Module
# [3/6] Let's ask the interrupter() function if we should terminate?
# In case the other threads were slower as the sleep time in [1/5]
# we will check again here.
- if self.ssig.has_stop():
+ if self.ssig.has_sock_quit():
+ self.log.trace( # type: ignore
+ "QUIT signal ACK in IONetwork.__clienet_reconnect_to_server [2]"
+ )
return False
# [4/6] Increment the port numer (if --reconn-robin has multiple)
@@ -2733,7 +2944,10 @@ pwncat
Module
while self.__srv_opts.rebind != 0:
# [1/7] Let's ask the interrupter() function if we should terminate?
- if self.ssig.has_stop():
+ if self.ssig.has_sock_quit():
+ self.log.trace( # type: ignore
+ "SOCK-QUIT signal ACK in IONetwork.__server_rebind [1]"
+ )
return False
# [2/7] Increment the port numer (if --reconn-robin has multiple)
@@ -2766,7 +2980,10 @@ pwncat
Module
# [6/7] Let's ask the interrupter() function if we should terminate?
# In case the other threads were slower as the sleep time in [1/7]
# we will check again here.
- if self.ssig.has_stop():
+ if self.ssig.has_sock_quit():
+ self.log.trace( # type: ignore
+ "SOCK-QUIT signal ACK in IONetwork.__server_rebind [2]"
+ )
return False
# [6/7] Recurse until True or reconnect count is used up
@@ -2795,9 +3012,12 @@ pwncat
Module
# [MAYBE] Check stop signal and otherwise try until success.
while True:
- time.sleep(0.1)
+ time.sleep(0.01)
# [NO] We have a stop signal
- if self.ssig.has_stop():
+ if self.ssig.has_sock_quit():
+ self.log.trace( # type: ignore
+ "SOCK-QUIT signal ACK in IONetwork.__server_reaccept_from_client"
+ )
return False
# [YES] Re-accept indefinitely
self.log.info("Re-accepting new clients")
@@ -2838,7 +3058,7 @@ pwncat
Module
# --------------------------------------------------------------------------
def __init__(
self,
- ssig, # type: StopSignal
+ ssig, # type: InterruptHandler
encoder, # type: StringEncoder
host, # type: str
banner, # type: bool
@@ -2849,7 +3069,7 @@ pwncat
Module
"""Create a Pwncat Network Scanner instance.
Args:
- ssig (StopSignal): Stop signal instance
+ ssig (InterruptHandler): Instance of InterruptHandler.
encoder (StringEncoder): Instance of StringEncoder (Python2/3 str/byte compat).
host (str): The hostname to resolve.
banner (bool): Determines if we do banner grabbing as well.
@@ -2858,19 +3078,19 @@ pwncat
Module
"""
super(IONetworkScanner, self).__init__(ssig)
+ self.__ssig = ssig
self.__enc = encoder
self.__cli_opts = cli_opts
self.__sock_opts = sock_opts
self.__banner = banner
self.__log = logging.getLogger(__name__)
- self.__net = Net(encoder, ssig, sock_opts)
self.__sock = Sock()
self.__screen_lock = threading.Semaphore()
# Keep track of local binds (addr-port) of the threaded scanner
# clients as we do not want to treat them as open ports (false posistives)
- self.__local_binds = [] # type: List[str]
+ self.__local_binds = {} # type: Dict[str, socket.socket]
# Compile our regexes if using banner detection
if banner:
@@ -2888,13 +3108,13 @@ pwncat
Module
int(socket.AF_INET),
]
self.__targets = {}
- try:
- for family in families:
+ for family in families:
+ try:
self.__targets[family] = self.__sock.gethostbyname(
host, family, not self.__sock_opts.nodns
)
- except socket.gaierror:
- pass
+ except socket.gaierror:
+ pass
# --------------------------------------------------------------------------
# Public Functions
@@ -2902,18 +3122,24 @@ pwncat
Module
def __get_socket(self, family):
# type: (Union[socket.AddressFamily, int]) -> socket.socket
"""Create socket for specific address family endlessly until resources are available."""
- # The scanner is starting many threads, each creating a single socket
- # and we might hit the max allowed open files limit, so we will
- # endlessly ask the system for a new socket until success.
- # Also adding a delay, which will give other threads the time to
- # release their sockets.
+ # The scanner starts one thread for each port to scan. Each thread will also create
+ # one socket and we might hit the max_allowed_files limit (ulimit).
+ # That's why we loop through creating sockets until we hit a success
+ # as in the meantime, other threads might have already released sockets/fd's.
while True:
+ delay = 0.0
+ if self.__ssig.has_terminate():
+ self.log.trace( # type: ignore
+ "TERMINATE signal ACK for IONetworkScanner._getsocket"
+ )
+ raise socket.error("quit")
try:
if self.__sock_opts.udp:
return self.__sock.create_socket(family, socket.SOCK_DGRAM)
return self.__sock.create_socket(family, socket.SOCK_STREAM)
except socket.error:
- time.sleep(0.1)
+ delay += 0.1
+ time.sleep(delay) # This can be bigger to give the system some time to release fd's
def __get_banner_version(self, banner):
# type: (str) -> Optional[str]
@@ -2930,7 +3156,7 @@ pwncat
Module
for reg in self.BANNER_REG_COMP:
match = re.search(reg, banner)
if match:
- return match.group(1).rstrip()
+ return StringEncoder.rstrip(match.group(1)) # type: ignore
# Nothing found, return first non-empty line
for line in lines:
@@ -2949,25 +3175,30 @@ pwncat
Module
payloads = self.BANNER_PAYLOADS[0]
for payload in payloads:
+ # Break the loop on terminate signal
+ if self.__ssig.has_terminate():
+ self.log.trace( # type: ignore
+ "TERMINATE signal ACK for IONetworkScanner._getbanner: %s-%d", addr, port
+ )
+ return (False, None)
try:
if payload is not None:
sock.send(self.__enc.encode(payload))
self.__log.debug("%s:%d - payload sent: %s", addr, port, repr(payload))
- sock.settimeout(0.1)
+ sock.settimeout(0.5)
banner = sock.recv(self.__sock_opts.bufsize)
version = self.__get_banner_version(self.__enc.decode(banner))
self.__log.debug("%s:%d - respone received: %s", addr, port, repr(banner))
return (True, version)
except socket.timeout:
- time.sleep(0.1)
continue
except (OSError, socket.error):
return (False, None)
return (True, None)
def producer(self, *args, **kwargs):
- # type: (Any, Any) -> Iterator[str]
+ # type: (Any, Any) -> Iterator[bytes]
"""Port scanner yielding open/closed string for given port.
Args:
@@ -2982,13 +3213,25 @@ pwncat
Module
# Loop over adress families
for family in self.__targets:
+ # [1/7] Check for termination request
+ if self.__ssig.has_terminate():
+ self.log.trace( # type: ignore
+ "TERMINATE signal ACK for IONetworkScanner.producer"
+ )
+ return
+
addr = self.__targets[family]
- # [1/5] Get socket
- sock = self.__get_socket(family)
+ # [2/7] Get socket
+ try:
+ sock = self.__get_socket(family)
+ sock_type = sock.type
+ except (AttributeError, socket.error):
+ # Exception is triggered due to stop stignal and we
+ # will abort here in that case.
+ return
- # [2/5] Connect scan
- succ_conn = False
+ # [3/7] Connect scan
try:
laddr, lport = self.__sock.connect(
sock,
@@ -3002,45 +3245,47 @@ pwncat
Module
0.1,
)
# Append local binds (addr-port) to check against during port scan
- self.__local_binds.append(str(laddr + "-" + str(lport)))
- succ_conn = True
+ key = str(laddr + "-" + str(lport))
+ self.__local_binds[key] = sock
except socket.error:
- succ_conn = False
+ self.__sock.close(sock, "[-] closed: {}:{}".format(addr, port))
+ continue
+
+ # [4/7] False positives
+ # Connect was successful, but against a local bind of one of our
+ # port scanners, so this is a false positive.
+ if str(addr + "-" + str(port)) in self.__local_binds:
+ self.__sock.close(sock, "[-] closed: {}:{}".format(addr, port))
+ del self.__local_binds[key]
+ continue
- # [3/5] Banner grabbing
+ # [5/7] Banner grabbing
succ_banner = True
banner = None
if self.__banner:
(succ_banner, banner) = self.__get_banner(sock, addr, port)
- # [4/5] Evaluation
- if banner is not None and (succ_conn and succ_banner):
- if str(addr + "-" + str(port)) not in self.__local_binds:
- sock_type = sock.type
- yield "[+] {:>5}/{} open ({}): {}".format(
- port,
- self.__sock.get_type_name(sock_type),
- self.__sock.get_family_name(family),
- banner,
- )
- if banner is None and (succ_conn and succ_banner):
- if str(addr + "-" + str(port)) not in self.__local_binds:
- sock_type = sock.type
- yield "[+] {:>5}/{} open ({})".format(
- port,
- self.__sock.get_type_name(sock_type),
- self.__sock.get_family_name(family),
- )
+ # [6/7] Evaluation
+ if banner is not None and succ_banner:
+ msg = "[+] {:>5}/{} open ({}): {}".format(
+ port,
+ self.__sock.get_type_name(sock_type),
+ self.__sock.get_family_name(family),
+ banner,
+ )
+ yield self.__enc.encode(msg)
+ if banner is None and succ_banner:
+ msg = "[+] {:>5}/{} open ({})".format(
+ port, self.__sock.get_type_name(sock_type), self.__sock.get_family_name(family),
+ )
+ yield self.__enc.encode(msg)
- # [5/5] Cleanup
- self.__sock.close(sock, addr + "-" + str(port))
- try:
- self.__local_binds.remove(str(addr + "-" + str(port)))
- except ValueError:
- pass
+ # [7/7] Cleanup
+ self.__sock.close(sock, key)
+ del self.__local_binds[key]
def consumer(self, data):
- # type: (str) -> None
+ # type: (bytes) -> None
"""Print received data to stdout."""
# For issues with flush (when using tail -F or equal) see links below:
# https://stackoverflow.com/questions/26692284
@@ -3058,8 +3303,20 @@ pwncat
Module
self.__screen_lock.release()
def interrupt(self):
- # type: (str) -> None
- """Not required."""
+ # type: () -> None
+ """Stop function that can be called externally to close this instance."""
+ self.log.trace("SOCK-QUIT signal REQ in IONetworkScanner.interrupt") # type: ignore
+ self.ssig.raise_sock_quit()
+
+ # NOTE: Closing up to 65535 sockets (single thread) takes very very long
+ # Se we leave this up to Python itself, once the program exits.
+ # self.log.trace("SOCK-QUIT-CLEANUP: Closing sockets") # type: ignore
+ # # Double loop to prevent: Dictionary size changed during iteration
+ # remove = {}
+ # for key in self.__local_binds:
+ # remove[key] = self.__local_binds[key]
+ # for key in remove:
+ # self.__sock.close(remove[key], key)
# -------------------------------------------------------------------------------------------------
@@ -3077,63 +3334,111 @@ pwncat
Module
# Constructor / Destructor
# --------------------------------------------------------------------------
def __init__(self, ssig, opts):
- # type: (StopSignal, DsIOStdinStdout) -> None
+ # type: (InterruptHandler, DsIOStdinStdout) -> None
"""Set specific options for this I/O module.
Args:
- ssig (StopSignal): StopSignal instance.
+ ssig (InterruptHandler): InterruptHandler instance.
opts (DsIOStdinStdout): IO options.
"""
super(IOStdinStdout, self).__init__(ssig)
self.__opts = opts
self.__py3 = sys.version_info >= (3, 0) # type: bool
+<<<<<<< HEAD
self.__win = os.name != "posix" # posix or nt
+=======
+ self.__abort = False
+>>>>>>> Heavy refactoring
# --------------------------------------------------------------------------
# Public Functions
# --------------------------------------------------------------------------
def producer(self, *args, **kwargs):
- # type: (Any, Any) -> Iterator[str]
+ # type: (Any, Any) -> Iterator[bytes]
"""Constantly ask for user input.
Yields:
str: Data read from stdin.
"""
+ # On --send-on-eof we will return all of its contents at once:
+ lines = []
+
# https://stackoverflow.com/questions/1450393/#38670261
# while True: line = sys.stdin.readline() <- reads a whole line (faster)
# for line in sys.stdin.readlin(): <- reads one byte at a time
while True:
- if self.ssig.has_stop():
- self.log.trace("Stop signal acknowledged for reading STDIN-1") # type: ignore
+ if self.__abort:
+ self.log.trace("External interrupt signal triggered. Aborting.") # type: ignore
+ return
+ if self.ssig.has_stdin_quit():
+ self.log.trace( # type: ignore
+ "STDIN-QUIT signal ACK in IOStdinStdout.producer [1]"
+ )
return
try:
+<<<<<<< HEAD
data = self.__read_stdin()
+=======
+ # TODO: select() does not work for windows on stdin/stdout
+ if os.name != "nt":
+ self.__set_input_timeout()
+ if self.__py3:
+ line = sys.stdin.buffer.readline()
+ else:
+ if sys.platform == "win32":
+ # Python 2 on Windows opens sys.stdin in text mode, and
+ # binary data that read from it becomes corrupted on \r\n.
+ # Setting sys.stdin to binary mode fixes that.
+ if hasattr(os, "O_BINARY"):
+ msvcrt.setmode(
+ sys.stdin.fileno(), os.O_BINARY, # pylint: disable=no-member
+ )
+ line = sys.stdin.readline() # type: ignore
+
+>>>>>>> Heavy refactoring
except EOFError:
# When using select() with timeout, we don't have any input
# at this point and simply continue the loop or quit if
# a terminate request has been made by other threads.
- if self.ssig.has_stop():
- self.log.trace("Stop signal acknowledged for reading STDIN-2") # type: ignore
+ if self.ssig.has_stdin_quit():
+ self.log.trace( # type: ignore
+ "STDIN-QUIT signal ACK in IOStdinStdout.producer [2]"
+ )
return
continue
+<<<<<<< HEAD
if data:
self.log.debug("Received %d bytes from STDIN", len(data))
self.log.trace("Received: %s", repr(data)) # type: ignore
yield data
+=======
+ if line:
+ self.log.debug("Received %d bytes from STDIN", len(line))
+ self.log.trace("Received: %s", repr(line)) # type: ignore
+ # [send-on-eof] Append data
+ if self.__opts.send_on_eof:
+ lines.append(line)
+ else:
+ yield line
+>>>>>>> Heavy refactoring
# EOF or <Ctrl>+<d>
else:
- # DO NOT RETURN HERE BLINDLY, THE UPSTREAM CONNECTION MUST GO FIRST!
- if self.ssig.has_stop():
- self.log.trace("Stop signal acknowledged for reading STDIN-3") # type: ignore
- return
+ # [send-on-eof] Dump data before quitting
+ if lines and self.__opts.send_on_eof:
+ yield StringEncoder.encode("").join(lines)
+ self.ssig.raise_stdin_quit()
def consumer(self, data):
- # type: (str) -> None
+ # type: (bytes) -> None
"""Print received data to stdout."""
- # For issues with flush (when using tail -F or equal) see links below:
- # https://stackoverflow.com/questions/26692284
- # https://docs.python.org/3/library/signal.html#note-on-sigpipe
- print(data, end="")
+ if self.__py3:
+ sys.stdout.buffer.write(data)
+ else:
+ # For issues with flush (when using tail -F or equal) see links below:
+ # https://stackoverflow.com/questions/26692284
+ # https://docs.python.org/3/library/signal.html#note-on-sigpipe
+ print(data, end="")
+
try:
sys.stdout.flush()
except (BrokenPipeError, IOError):
@@ -3145,12 +3450,14 @@ pwncat
Module
def interrupt(self):
# type: () -> None
"""Stop function that can be called externally to close this instance."""
+ # TODO: Does not work on windows as it has blocking read of stdin
+ self.log.trace("STDIN-QUIT signal REQ in IOStdinStdout.interrupt") # type: ignore
+ self.ssig.raise_stdin_quit()
+
self.log.trace( # type: ignore
- "[IOStdinStdout] interrupt() invoked"
+ "[IOStdinStdout] setting __abort to True was raised by interrupt() externally"
)
- # Raise stop signal
- # TODO: Check if this is required???
- self.ssig.raise_stop()
+ self.__abort = True
# --------------------------------------------------------------------------
# Private Functions
@@ -3233,19 +3540,19 @@ pwncat
Module
# Constructor / Destructor
# --------------------------------------------------------------------------
def __init__(self, ssig, opts):
- # type: (StopSignal, DsIOCommand) -> None
+ # type: (InterruptHandler, DsIOCommand) -> None
"""Set specific options for this I/O module.
Args:
- ssig (StopSignal): Instance of StopSignal.
+ ssig (InterruptHandler): Instance of InterruptHandler.
opts (DsIOCommand): Custom module options.
"""
super(IOCommand, self).__init__(ssig)
self.__opts = opts
self.log.debug("Setting '%s' as executable", self.__opts.executable)
- # Define destructor
- atexit.register(self.__destruct__)
+ # Did we already run cleanup
+ self.__cleaned_up = False
# Open executable to wait for commands
env = os.environ.copy()
@@ -3267,19 +3574,11 @@ pwncat
Module
self.log.error("Specified executable '%s' not found", self.__opts.executable)
sys.exit(1)
- def __destruct__(self):
- # type: () -> None
- """Destructor."""
- self.log.trace( # type: ignore
- "Killing executable: %s with pid %d", self.__opts.executable, self.proc.pid
- )
- self.proc.kill()
-
# --------------------------------------------------------------------------
# Public Functions
# --------------------------------------------------------------------------
def producer(self, *args, **kwargs):
- # type: (Any, Any) -> Iterator[str]
+ # type: (Any, Any) -> Iterator[bytes]
"""Constantly ask for input.
Yields:
@@ -3287,44 +3586,60 @@ pwncat
Module
"""
assert self.proc.stdout is not None
while True:
- if self.ssig.has_stop():
- self.log.trace("Stop signal acknowledged in Command") # type: ignore
+ if self.ssig.has_command_quit():
+ self.log.trace("CMD-QUIT signal ACK IOCommand.producer") # type: ignore
+ self.__cleanup()
return
self.log.trace("Reading command output") # type: ignore
+<<<<<<< HEAD
# Byte-wise reading is required to make it work for remote ends being in raw mode
# However, the performance of self.proc.stdout.readline() is way faster.
# To improve performance we will get rid of all other logging calls here.
data = self.proc.stdout.read(1)
+=======
+ # Much better performance than self.proc.read(1)
+ # TODO: check if self.proc.read(1) might be better in raw mode
+ data = self.proc.stdout.readline()
+>>>>>>> Heavy refactoring
self.log.trace("Command output: %s", repr(data)) # type: ignore
if not data:
- self.log.trace("Command output was empty. Exiting loop.") # type: ignore
- break
- yield self.__opts.enc.decode(data)
+ self.log.trace("CMD-QUIT signal REQ IOCommand.producer") # type: ignore
+ self.ssig.raise_command_quit()
+ self.__cleanup()
+ return
+ yield data
def consumer(self, data):
- # type: (str) -> None
+ # type: (bytes) -> None
"""Send data received to stdin (command input).
Args:
data (str): Command to execute.
"""
assert self.proc.stdin is not None
- byte = self.__opts.enc.encode(data)
- self.log.trace("Appending to stdin: %s", repr(byte)) # type: ignore
- self.proc.stdin.write(byte)
+ self.log.trace("Appending to stdin: %s", repr(data)) # type: ignore
+ self.proc.stdin.write(data)
self.proc.stdin.flush()
def interrupt(self):
# type: () -> None
"""Stop function that can be called externally to close this instance."""
- self.log.trace( # type: ignore
- "[IOCommand] subprocess.kill() was raised by input_unterrupter()"
- )
- self.proc.kill()
- # Raise stop signal
- # TODO: Check if this is required???
- self.ssig.raise_stop()
-
+ self.log.trace("CMD-QUIT signal REQ IOCommand.interrupt") # type: ignore
+ self.ssig.raise_command_quit()
+ self.__cleanup()
+
+ def __cleanup(self):
+ # type: () -> None
+ """Cleanup function."""
+ if not self.__cleaned_up:
+ self.log.trace( # type: ignore
+ "CMD-QUIT-CLEANUP: killing executable: %s with pid %d",
+ self.__opts.executable,
+ self.proc.pid,
+ )
+ self.proc.kill()
+ self.__cleaned_up = True
+
# #################################################################################################
# #################################################################################################
@@ -3343,18 +3658,18 @@ pwncat
Module
The same instance of this class will be available to your send and receive scripts
that allow you to exchange data or manipulate themselves. You even have access to the
currently used instance of the networking class to manipulate the active socket.
- As well as to the logger and StopSignal instances.
+ As well as to the logger and InterruptHandler instances.
"""
@property
def messages(self):
- # type: () -> Dict[str, List[str]]
- """`Dict[str, List[str]]`: Stores sent and received messages by its thread name."""
+ # type: () -> Dict[str, List[bytes]]
+ """`Dict[str, List[bytes]]`: Stores sent and received messages by its thread name."""
return self.__messages
@messages.setter
def messages(self, value):
- # type: (Dict[str, List[str]]) -> None
+ # type: (Dict[str, List[bytes]]) -> None
self.__messages = value
@property
@@ -3370,8 +3685,8 @@ pwncat
Module
@property
def ssig(self):
- # type: () -> StopSignal
- """`StopSignal`: Instance of Logging.logger class."""
+ # type: () -> InterruptHandler
+ """`InterruptHandler`: Instance of InterruptHandler class."""
return self.__ssig
@property
@@ -3387,11 +3702,11 @@ pwncat
Module
return self.__log
def __init__(self, ssig, net):
- # type: (StopSignal, List[IONetwork]) -> None
+ # type: (InterruptHandler, List[IONetwork]) -> None
"""Instantiate the PSE class.
Args:
- ssig (StopSignal): Instance of the StopSignal class to force a shutdown.
+ ssig (InterruptHandler): Instance InterruptHandler.
net (IONetwork): Instance of the current network class to manipulate the socket.
"""
self.__messages = {}
@@ -3410,7 +3725,104 @@ pwncat
Module
# #################################################################################################
# -------------------------------------------------------------------------------------------------
-# [8/11 IO RUNNER]: (1/1) Runner
+# [8/11 IO RUNNER]: (1/2) InterruptHandler
+# -------------------------------------------------------------------------------------------------
+class InterruptHandler(object):
+ """Pwncat interrupt handler.
+
+ It allows all threads to raise various signal on certain actions,
+ as well as to ask the Interrupt Handler what to do.
+ The Interrupt handler will internally decide (based on pwncat's
+ command line arguments) what to do.
+ """
+
+ # --------------------------------------------------------------------------
+ # Constructor
+ # --------------------------------------------------------------------------
+ def __init__(self, keep_open, no_shutdown):
+ # type: (bool, bool) -> None
+ """Instantiate InterruptHandler.
+
+ Args:
+ keep_open (bool): `--keep-open` command line argument.
+ no_shutdown (bool): `--no-shutdown` command line argument.
+ """
+ self.__log = logging.getLogger(__name__) # type: logging.Logger
+ self.__keep_open = keep_open
+ self.__no_shutdown = no_shutdown
+
+ self.__terminate = False
+ self.__sock_quit = False
+ self.__stdin_quit = False
+ self.__command_quit = False
+
+ def handler(signum, frame): # type: ignore # pylint: disable=unused-argument
+ self.__log.trace("Ctrl+c caught.") # type: ignore
+ self.raise_terminate()
+
+ # Handle Ctrl+C
+ signal.signal(signal.SIGINT, handler)
+
+ # --------------------------------------------------------------------------
+ # Ask for action
+ # --------------------------------------------------------------------------
+ def has_terminate(self):
+ # type: () -> bool
+ """`bool`: Switch to be checked if pwncat should be terminated."""
+ return self.__terminate
+
+ def has_sock_quit(self):
+ # type: () -> bool
+ """`bool`: Switch to be checked if the socket connection should be closed."""
+ return self.__sock_quit
+
+ def has_stdin_quit(self):
+ # type: () -> bool
+ """`bool`: Switch to be checked if the STDIN should be closed."""
+ return self.__stdin_quit
+
+ def has_command_quit(self):
+ # type: () -> bool
+ """`bool`: Switch to be checked if the command should be closed."""
+ return self.__command_quit
+
+ # --------------------------------------------------------------------------
+ # Raise signals
+ # --------------------------------------------------------------------------
+ def raise_terminate(self):
+ # type: () -> None
+ """Signal the application that Socket should be quit."""
+ self.__log.trace("SIGNAL TERMINATE raised") # type: ignore
+ self.__terminate = True
+ self.__sock_quit = True
+ self.__stdin_quit = True
+ self.__command_quit = True
+
+ def raise_sock_quit(self):
+ # type: () -> None
+ """Signal the application that Socket should be quit."""
+ self.__log.trace("SIGNAL SOCK-QUIT raised") # type: ignore
+ self.__sock_quit = True
+ self.raise_terminate()
+
+ def raise_stdin_quit(self):
+ # type: () -> None
+ """Signal the application that STDIN should be quit."""
+ if not (self.__no_shutdown or self.__keep_open):
+ self.__log.trace("SIGNAL STDIN-QUIT raised") # type: ignore
+ self.__stdin_quit = True
+ self.raise_terminate()
+
+ def raise_command_quit(self):
+ # type: () -> None
+ """Signal the application that Command should be quit."""
+ self.__log.trace("SIGNAL CMD-QUIT raised") # type: ignore
+ self.__command_quit = True
+ self.raise_terminate()
+
+
+# -------------------------------------------------------------------------------------------------
+# [8/11 IO RUNNER]: (2/2) Runner
# -------------------------------------------------------------------------------------------------
class Runner(object):
"""Runner class that takes care about putting everything into threads."""
@@ -3418,11 +3830,12 @@ pwncat
Module
# --------------------------------------------------------------------------
# Constructor / Destructor
# --------------------------------------------------------------------------
- def __init__(self, pse):
- # type: (PSEStore) -> None
+ def __init__(self, ssig, pse):
+ # type: (InterruptHandler, PSEStore) -> None
"""Create a new Runner object.
Args:
+ ssig (InterruptHandler): Instance of InterruptHandler.
pse (PSEStore): Pwncat Scripting Engine store.
"""
self.log = logging.getLogger(__name__)
@@ -3444,6 +3857,7 @@ pwncat
Module
# {"name": "<thread>"}
self.__threads = {} # type: Dict[str, threading.Thread]
+ self.__ssig = ssig
self.__pse = pse
# --------------------------------------------------------------------------
@@ -3486,7 +3900,7 @@ pwncat
Module
def run_action(
name, # type: str
producer, # type: DsCallableProducer
- consumer, # type: Callable[[str], None]
+ consumer, # type: Callable[[bytes], None]
transformers, # type: List[Transform]
code, # type: Optional[Union[str, bytes, CodeType]]
):
@@ -3536,22 +3950,24 @@ pwncat
Module
self.log.trace("[%s] Producer Stop", name) # type: ignore
def run_timer(name, action, intvl, ssig, args, **kwargs):
- # type: (str, Callable[..., None], int, StopSignal, Any, Any) -> None
+ # type: (str, Callable[..., None], int, InterruptHandler, Any, Any) -> None
"""Timer run function to be thrown into a thread (Execs periodic tasks).
Args:
name (str): Name for logging output
action (function): Function to be called in a given intervall
intvl (float): Intervall at which the action function will be called
- ssig (StopSignal): Providing has_stop() and raise_stop()
+ ssig (InterruptHandler): Instance of InterruptHandler
args (*args): *args for action func
kwargs (**kwargs): **kwargs for action func
"""
self.log.trace("[%s] Timer Start (exec every %f sec)", name, intvl) # type: ignore
time_last = int(time.time())
while True:
- if ssig.has_stop():
- self.log.trace("Stop signal acknowledged for timer %s", name) # type: ignore
+ if ssig.has_terminate():
+ self.log.trace( # type: ignore
+ "TERMINATE signal ACK for timer action [%s]", name
+ )
return
time_now = int(time.time())
if time_now > time_last + intvl:
@@ -3561,7 +3977,7 @@ pwncat
Module
time.sleep(0.1)
def run_repeater(name, action, repeat, pause, ssig, args, **kwargs):
- # type: (str, Callable[..., None], int, float, StopSignal, Any, Any) -> None
+ # type: (str, Callable[..., None], int, float, InterruptHandler, Any, Any) -> None
"""Repeater run function to be thrown into a thread (Execs periodic tasks).
Args:
@@ -3569,23 +3985,30 @@ pwncat
Module
action (function): Function to be called
repeat (int): Repeat the function so many times before quitting
pause (float): Pause between repeated calls
- ssig (StopSignal): Providing has_stop() and raise_stop()
+ ssig (InterruptHandler): Instance of InterruptHandler
args (*args): *args for action func
kwargs (**kwargs): **kwargs for action func
"""
cycles = 1
self.log.trace("Repeater Start (%d/%d)", cycles, repeat) # type: ignore
while cycles <= repeat:
- if ssig.has_stop():
- self.log.trace("Stop signal acknowledged for timer %s", name) # type: ignore
+ if ssig.has_terminate():
+ self.log.trace( # type: ignore
+ "TERMINATE signal ACK for repeater action [%s]", name
+ )
return
self.log.debug("Executing repeated function (%d/%d)", cycles, repeat)
action(*args, **kwargs)
cycles += 1
time.sleep(pause)
- # Start available action in a thread
+ # [1/3] Start available action in a thread
for key in self.__actions:
+ if self.__ssig.has_terminate():
+ self.log.trace( # type: ignore
+ "TERMINATE signal ACK for Runner.run [1]: [%s]", key
+ )
+ break
# Create Thread object
thread = threading.Thread(
target=run_action,
@@ -3598,16 +4021,27 @@ pwncat
Module
self.__actions[key].code,
),
)
- thread.daemon = False
+ # Daemon threads are easier to kill
+ thread.daemon = True
+
# Add delay if threads cannot be started
+ delay = 0.0
while True:
+ if self.__ssig.has_terminate():
+ self.log.trace( # type: ignore
+ "TERMINATE signal ACK for Runner.run [2]: [%s]", key
+ )
+ break
try:
+ # Start and break the loop upon success to go to the next thread to start
thread.start()
break
except (RuntimeError, Exception): # pylint: disable=broad-except
- time.sleep(0.1)
+ delay += 0.1
+ time.sleep(delay) # Give the system some time to release open fd's
self.__threads[key] = thread
- # Start available timers in a thread
+
+ # [2/3] Start available timers in a thread
for key in self.__timers:
# Create Thread object
thread = threading.Thread(
@@ -3617,14 +4051,15 @@ pwncat
Module
key,
self.__timers[key].action,
self.__timers[key].intvl,
- self.__timers[key].signal,
+ self.__timers[key].ssig,
self.__timers[key].args,
),
kwargs=self.__timers[key].kwargs,
)
thread.daemon = False
thread.start()
- # Start available repeaters in a thread
+
+ # [3/3] Start available repeaters in a thread
for key in self.__repeaters:
# Create Thread object
thread = threading.Thread(
@@ -3635,7 +4070,7 @@ pwncat
Module
self.__repeaters[key].action,
self.__repeaters[key].repeat,
self.__repeaters[key].pause,
- self.__repeaters[key].signal,
+ self.__repeaters[key].ssig,
self.__repeaters[key].args,
),
kwargs=self.__repeaters[key].kwargs,
@@ -3643,22 +4078,14 @@ pwncat
Module
thread.daemon = False
thread.start()
- def check_stop(force):
- # type: (int) -> bool
+ def check_stop():
+ # type: () -> bool
"""Stop threads."""
- for key in self.__threads:
- if not self.__threads[key].is_alive() or force:
- # TODO: How are we gonna call the stop signal now?
- # # [1/3] Inform all threads (inside) about a stop signal.
- # # All threads with non-blocking funcs will be able to stop themselves
- # self.log.trace( # type: ignore
- # "Raise stop signal: StopSignal.stop() for thread [%s]",
- # self.__threads[key].getName(),
- # )
- # self.__actions[key].signal.raise_stop()
- # [2/3] Call external interrupters
- # These will shutdown all blocking functions inside a thread,
- # so that they are actually able to join
+ # TODO: This is a workaround for using daemon threads.
+ # Python < 3.3 is unable to detect Ctrl+c signal during
+ # thread.join() operation in a fast loop (Port scan).
+ if self.__ssig.has_terminate():
+ for key in self.__threads:
for interrupt in self.__actions[key].interrupts:
self.log.trace( # type: ignore
"Call INTERRUPT: %s.%s() for %s",
@@ -3667,24 +4094,53 @@ pwncat
Module
self.__threads[key].getName(),
)
interrupt()
- # [3/3] All blocking events inside the threads are gone, now join them
- self.log.trace("Joining %s", self.__threads[key].getName()) # type: ignore
- self.__threads[key].join(timeout=0.1)
- # If all threads have died or force is requested, then exit
- if not all([self.__threads[key].is_alive() for key in self.__threads]) or force:
+ return True
+ # If all threads are done, also stop
+ if not all([self.__threads[key].is_alive() for key in self.__threads]):
return True
return False
+ # NOTE: This was for previous non-daemon threads
+ # for key in self.__threads:
+ # if not self.__threads[key].is_alive() or force:
+ # # TODO: How are we gonna call the stop signal now?
+ # # # [1/3] Inform all threads (inside) about a stop signal.
+ # # # All threads with non-blocking funcs will be able to stop themselves
+ # # self.log.trace( # type: ignore
+ # # "Raise stop signal: StopSignal.stop() for thread [%s]",
+ # # self.__threads[key].getName(),
+ # # )
+ # # self.__actions[key].signal.signal_quit()
+ # # [2/3] Call external interrupters
+ # # These will shutdown all blocking functions inside a thread,
+ # # so that they are actually able to join
+ # for interrupt in self.__actions[key].interrupts:
+ # self.log.trace( # type: ignore
+ # "Call INTERRUPT: %s.%s() for %s",
+ # getattr(interrupt, "__self__").__class__.__name__,
+ # interrupt.__name__,
+ # self.__threads[key].getName(),
+ # )
+ # interrupt()
+ # # [3/3] All blocking events inside the threads are gone, now join them
+ # try:
+ # self.log.trace("Joining %s", self.__threads[key].getName())# type: ignore
+ # # NOTE: The thread.join() operating will also block the signal handler
+ # #self.__threads[key].join(timeout=0.1)
+ # self.__threads[key].join()
+ # self.log.trace("Joined %s", self.__threads[key].getName()) # type: ignore
+ # except RuntimeError as err:
+ # print(err)
+ # #pass
+ # # If all threads have died or force is requested, then exit
+ # if not all([self.__threads[key].is_alive() for key in self.__threads]) or force:
+ # return True
+ # return False
- try:
- while True:
- if check_stop(False):
- sys.exit(0)
- # Need a timeout to not skyrocket the CPU
- time.sleep(0.1)
- except KeyboardInterrupt:
- print()
- check_stop(True)
- sys.exit(1)
+ while True:
+ if check_stop():
+ sys.exit(0)
+ # Need a timeout to not skyrocket the CPU
+ time.sleep(0.01)
# #################################################################################################
@@ -3724,7 +4180,7 @@ pwncat
Module
"/opt/python3.8/bin",
]
- __PYTHON_VERSIONS = [
+ __PYTHON_NAMES = [
"python",
"python2",
"python2.7",
@@ -3756,7 +4212,7 @@ pwncat
Module
# Constructor
# --------------------------------------------------------------------------
def __init__(self, enc, fsend, frecv):
- # type: (StringEncoder, Callable[[str], None], Callable[[], Iterator[str]]) -> None
+ # type: (StringEncoder, Callable[[bytes], None], Callable[..., Iterator[bytes]]) -> None
"""Instantiate Command and Control class.
Args:
@@ -3808,7 +4264,8 @@ pwncat
Module
command (str): The command to execute on the remote end.
"""
# TODO: determine remote host line feeds and set accordingly.
- self.__fsend(command + "\n")
+ # TODO: handle exception
+ self.__fsend(StringEncoder.encode(command + "\n"))
def create_remote_tmpfile(self):
# type: () -> Optional[str]
@@ -3831,7 +4288,7 @@ pwncat
Module
self.print_info("Creating tmpfile:", False, True)
for response in self.__frecv():
if response:
- tmpfile = response.rstrip()
+ tmpfile = StringEncoder.decode(response).rstrip()
self.print_info("Creating tmpfile: {}".format(tmpfile), True, True)
return tmpfile
@@ -3873,13 +4330,17 @@ pwncat
Module
"""
# TODO: Make windows compatible
for path in self.__PYTHON_PATHS:
- for version in self.__PYTHON_VERSIONS:
- python = path + "/" + version
+ for name in self.__PYTHON_NAMES:
+
+ python = path + "/" + name
self.print_info("Probing for: {}".format(python))
self.remote_command("test -f {p} && echo {p} || echo;".format(p=python))
- for response in self.__frecv():
- reg = re.search(r"^([.0-9]+)", response)
- if response.rstrip() == python.rstrip():
+
+ for byte in self.__frecv():
+
+ response = StringEncoder.decode(byte)
+ match = re.search(r"^([.0-9]+)", response)
+ if StringEncoder.rstrip(response) == StringEncoder.rstrip(python):
self.print_info("Potential path: {}".format(python))
command = []
command.append("{} -c '".format(python))
@@ -3890,18 +4351,18 @@ pwncat
Module
data = "".join(command)
self.remote_command(data)
continue
- if reg:
- match = reg.group(1)
- if match[0] == "2":
+ if match:
+ version = match.group(1)
+ if version[0] == "2":
self.__py3 = False
- elif match[0] == "3":
+ elif version[0] == "3":
self.__py3 = True
else:
- self.print_info(
- "Could not determine major version: {}".format(reg.group(1))
- )
+ self.print_info("Could not determine major version: {}".format(version))
return False
- self.print_info("Found valid Python{} version: {}".format(match[0], match))
+ self.print_info(
+ "Found valid Python{} version: {}".format(version[0], version)
+ )
self.__python = python
return True
# Nothing matched, break the innter loop
@@ -3928,7 +4389,7 @@ pwncat
Module
self.print_info(
"Uploading: {} -> {} ({}/{})".format(lpath, rpath, curr, count), False, True
)
- b64 = self.__enc.base64_encode(line)
+ b64 = StringEncoder.decode(base64.b64encode(StringEncoder.encode(line)))
if first:
self.remote_command('echo "{}" > {}'.format(b64, rpath))
first = False
@@ -3975,8 +4436,8 @@ pwncat
Module
def __init__(
self,
enc, # type: StringEncoder
- send, # type: Callable[[str], None]
- recv, # type: Callable[[], Iterator[str]]
+ send, # type: Callable[[bytes], None]
+ recv, # type: Callable[[], Iterator[bytes]]
cmd, # type: str
host, # type: str
ports, # type: List[int]
@@ -4530,6 +4991,27 @@ pwncat
Module
default=False,
help="""Do not resolve DNS.
+""",
+ )
+ optional.add_argument(
+ "--send-on-eof",
+ action="store_true",
+ default=False,
+ help="""Buffer data received on stdin until EOF and send
+everything in one chunk.
+
+""",
+ )
+ optional.add_argument(
+ "--no-shutdown",
+ action="store_true",
+ default=False,
+ help="""Do not shutdown into half-duplex mode.
+If this option is passed, pwncat won't invoke shutdown
+on a socket after seeing EOF on stdin. This is provided
+for backward-compatibility with OpenBSD netcat, which
+exhibits this behavior.
+
""",
)
optional.add_argument(
@@ -5012,7 +5494,7 @@ pwncat
Module
sys.exit(1)
# Deny unimplemented modes
- if args.http or args.https:
+ if args.https:
print("Unimplemented options", file=sys.stderr)
sys.exit(1)
@@ -5112,8 +5594,8 @@ pwncat
Module
# Initialize encoder
enc = StringEncoder()
- # Initialize StopSignal
- ssig = StopSignal()
+ # Initialize interrupt handler
+ ssig = InterruptHandler(args.keep_open, args.no_shutdown)
# Initialize transformers
transformers = []
@@ -5135,7 +5617,7 @@ pwncat
Module
mod = IOCommand(ssig, DsIOCommand(enc, args.cmd, POPEN_BUFSIZE))
# Use output module
else:
- mod = IOStdinStdout(ssig, DsIOStdinStdout(enc, TIMEOUT_READ_STDIN))
+ mod = IOStdinStdout(ssig, DsIOStdinStdout(enc, TIMEOUT_READ_STDIN, args.send_on_eof))
# Run local port-forward
# -> listen locally and forward traffic to remote (connect)
@@ -5149,7 +5631,7 @@ pwncat
Module
net_srv = IONetwork(ssig, enc, lhost, [lport], "server", srv_opts, cli_opts, sock_opts)
net_cli = IONetwork(ssig, enc, host, ports, "client", srv_opts, cli_opts, sock_opts)
# Create Runner
- run = Runner(PSEStore(ssig, [net_srv, net_cli]))
+ run = Runner(ssig, PSEStore(ssig, [net_srv, net_cli]))
run.add_action(
"TRANSMIT",
DsRunnerAction(
@@ -5184,7 +5666,7 @@ pwncat
Module
net_cli_l = IONetwork(ssig, enc, lhost, [lport], "client", srv_opts, cli_opts, sock_opts)
net_cli_r = IONetwork(ssig, enc, host, ports, "client", srv_opts, cli_opts, sock_opts)
# Create Runner
- run = Runner(PSEStore(ssig, [net_cli_l, net_cli_r]))
+ run = Runner(ssig, PSEStore(ssig, [net_cli_l, net_cli_r]))
run.add_action(
"TRANSMIT",
DsRunnerAction(
@@ -5211,15 +5693,15 @@ pwncat
Module
if mode == "scan":
print("Scanning {} ports".format(len(ports)))
net = IONetworkScanner(ssig, enc, host, args.banner, cli_opts, sock_opts)
- run = Runner(PSEStore(ssig, [net]))
+ run = Runner(ssig, PSEStore(ssig, [net]))
for port in ports:
run.add_action(
"PORT-{}".format(port),
DsRunnerAction(
DsCallableProducer(net.producer, port), # Send port scans
net.consumer, # Output results
+ [net.interrupt],
[],
- transformers,
None,
),
)
@@ -5236,14 +5718,22 @@ pwncat
Module
cnc_cmd, cnc_host, cnc_port = args.self_inject.split(":")
cnc_ports = ArgValidator.get_port_list_from_string(cnc_port)
CNCAutoDeploy(enc, net.consumer, net.producer, cnc_cmd, cnc_host, cnc_ports)
- run = Runner(PSEStore(ssig, [net]))
+
+ if args.http:
+ trans_recv = [TransformHttpUnpack({})] + transformers
+ trans_send = [TransformHttpPack({"host": host, "reply": "response"})] + transformers
+ else:
+ trans_recv = transformers
+ trans_send = transformers
+
+ run = Runner(ssig, PSEStore(ssig, [net]))
run.add_action(
"RECV",
DsRunnerAction(
DsCallableProducer(net.producer), # receive data
mod.consumer,
[net.interrupt, mod.interrupt], # Also force the prod. to stop on net err
- transformers,
+ trans_recv,
code_recv,
),
)
@@ -5253,7 +5743,7 @@ pwncat
Module
DsCallableProducer(mod.producer),
net.consumer, # send data
[mod.interrupt], # Externally stop the produer itself
- transformers,
+ trans_send,
code_send,
),
)
@@ -5264,14 +5754,22 @@ pwncat
Module
net = IONetwork(
ssig, enc, host, ports + args.reconn_robin, "client", srv_opts, cli_opts, sock_opts
)
- run = Runner(PSEStore(ssig, [net]))
+
+ if args.http:
+ trans_recv = [TransformHttpUnpack({})] + transformers
+ trans_send = [TransformHttpPack({"host": host, "reply": "response"})] + transformers
+ else:
+ trans_recv = transformers
+ trans_send = transformers
+
+ run = Runner(ssig, PSEStore(ssig, [net]))
run.add_action(
"RECV",
DsRunnerAction(
DsCallableProducer(net.producer), # receive data
mod.consumer,
[net.interrupt, mod.interrupt], # Also force the prod. to stop on net err
- transformers,
+ trans_recv,
code_recv,
),
)
@@ -5281,7 +5779,7 @@ pwncat
Module
DsCallableProducer(mod.producer),
net.consumer, # send data
[net.interrupt, mod.interrupt], # Externally stop the produer itself
- transformers,
+ trans_send,
code_send,
),
)
@@ -5302,12 +5800,7 @@ pwncat
Module
# [11/11 MAIN ENTRYPOINT]: (2/2) start
# -------------------------------------------------------------------------------------------------
if __name__ == "__main__":
- # Catch Ctrl+c and exit without error message
- try:
- main()
- except KeyboardInterrupt:
- print()
- sys.exit(1)
+ main()
pwncat
Functions
default=False,
help="""Do not resolve DNS.
+""",
+ )
+ optional.add_argument(
+ "--send-on-eof",
+ action="store_true",
+ default=False,
+ help="""Buffer data received on stdin until EOF and send
+everything in one chunk.
+
+""",
+ )
+ optional.add_argument(
+ "--no-shutdown",
+ action="store_true",
+ default=False,
+ help="""Do not shutdown into half-duplex mode.
+If this option is passed, pwncat won't invoke shutdown
+on a socket after seeing EOF on stdin. This is provided
+for backward-compatibility with OpenBSD netcat, which
+exhibits this behavior.
+
""",
)
optional.add_argument(
@@ -5972,7 +6486,7 @@ Functions
sys.exit(1)
# Deny unimplemented modes
- if args.http or args.https:
+ if args.https:
print("Unimplemented options", file=sys.stderr)
sys.exit(1)
@@ -6087,8 +6601,8 @@ Functions
# Initialize encoder
enc = StringEncoder()
- # Initialize StopSignal
- ssig = StopSignal()
+ # Initialize interrupt handler
+ ssig = InterruptHandler(args.keep_open, args.no_shutdown)
# Initialize transformers
transformers = []
@@ -6110,7 +6624,7 @@ Functions
mod = IOCommand(ssig, DsIOCommand(enc, args.cmd, POPEN_BUFSIZE))
# Use output module
else:
- mod = IOStdinStdout(ssig, DsIOStdinStdout(enc, TIMEOUT_READ_STDIN))
+ mod = IOStdinStdout(ssig, DsIOStdinStdout(enc, TIMEOUT_READ_STDIN, args.send_on_eof))
# Run local port-forward
# -> listen locally and forward traffic to remote (connect)
@@ -6124,7 +6638,7 @@ Functions
net_srv = IONetwork(ssig, enc, lhost, [lport], "server", srv_opts, cli_opts, sock_opts)
net_cli = IONetwork(ssig, enc, host, ports, "client", srv_opts, cli_opts, sock_opts)
# Create Runner
- run = Runner(PSEStore(ssig, [net_srv, net_cli]))
+ run = Runner(ssig, PSEStore(ssig, [net_srv, net_cli]))
run.add_action(
"TRANSMIT",
DsRunnerAction(
@@ -6159,7 +6673,7 @@ Functions
net_cli_l = IONetwork(ssig, enc, lhost, [lport], "client", srv_opts, cli_opts, sock_opts)
net_cli_r = IONetwork(ssig, enc, host, ports, "client", srv_opts, cli_opts, sock_opts)
# Create Runner
- run = Runner(PSEStore(ssig, [net_cli_l, net_cli_r]))
+ run = Runner(ssig, PSEStore(ssig, [net_cli_l, net_cli_r]))
run.add_action(
"TRANSMIT",
DsRunnerAction(
@@ -6186,15 +6700,15 @@ Functions
if mode == "scan":
print("Scanning {} ports".format(len(ports)))
net = IONetworkScanner(ssig, enc, host, args.banner, cli_opts, sock_opts)
- run = Runner(PSEStore(ssig, [net]))
+ run = Runner(ssig, PSEStore(ssig, [net]))
for port in ports:
run.add_action(
"PORT-{}".format(port),
DsRunnerAction(
DsCallableProducer(net.producer, port), # Send port scans
net.consumer, # Output results
+ [net.interrupt],
[],
- transformers,
None,
),
)
@@ -6211,14 +6725,22 @@ Functions
cnc_cmd, cnc_host, cnc_port = args.self_inject.split(":")
cnc_ports = ArgValidator.get_port_list_from_string(cnc_port)
CNCAutoDeploy(enc, net.consumer, net.producer, cnc_cmd, cnc_host, cnc_ports)
- run = Runner(PSEStore(ssig, [net]))
+
+ if args.http:
+ trans_recv = [TransformHttpUnpack({})] + transformers
+ trans_send = [TransformHttpPack({"host": host, "reply": "response"})] + transformers
+ else:
+ trans_recv = transformers
+ trans_send = transformers
+
+ run = Runner(ssig, PSEStore(ssig, [net]))
run.add_action(
"RECV",
DsRunnerAction(
DsCallableProducer(net.producer), # receive data
mod.consumer,
[net.interrupt, mod.interrupt], # Also force the prod. to stop on net err
- transformers,
+ trans_recv,
code_recv,
),
)
@@ -6228,7 +6750,7 @@ Functions
DsCallableProducer(mod.producer),
net.consumer, # send data
[mod.interrupt], # Externally stop the produer itself
- transformers,
+ trans_send,
code_send,
),
)
@@ -6239,14 +6761,22 @@ Functions
net = IONetwork(
ssig, enc, host, ports + args.reconn_robin, "client", srv_opts, cli_opts, sock_opts
)
- run = Runner(PSEStore(ssig, [net]))
+
+ if args.http:
+ trans_recv = [TransformHttpUnpack({})] + transformers
+ trans_send = [TransformHttpPack({"host": host, "reply": "response"})] + transformers
+ else:
+ trans_recv = transformers
+ trans_send = transformers
+
+ run = Runner(ssig, PSEStore(ssig, [net]))
run.add_action(
"RECV",
DsRunnerAction(
DsCallableProducer(net.producer), # receive data
mod.consumer,
[net.interrupt, mod.interrupt], # Also force the prod. to stop on net err
- transformers,
+ trans_recv,
code_recv,
),
)
@@ -6256,7 +6786,7 @@ Functions
DsCallableProducer(mod.producer),
net.consumer, # send data
[net.interrupt, mod.interrupt], # Externally stop the produer itself
- transformers,
+ trans_send,
code_send,
),
)
@@ -6804,7 +7334,7 @@ Raises
"/opt/python3.8/bin",
]
- __PYTHON_VERSIONS = [
+ __PYTHON_NAMES = [
"python",
"python2",
"python2.7",
@@ -6836,7 +7366,7 @@ Raises
# Constructor
# --------------------------------------------------------------------------
def __init__(self, enc, fsend, frecv):
- # type: (StringEncoder, Callable[[str], None], Callable[[], Iterator[str]]) -> None
+ # type: (StringEncoder, Callable[[bytes], None], Callable[..., Iterator[bytes]]) -> None
"""Instantiate Command and Control class.
Args:
@@ -6888,7 +7418,8 @@ Raises
command (str): The command to execute on the remote end.
"""
# TODO: determine remote host line feeds and set accordingly.
- self.__fsend(command + "\n")
+ # TODO: handle exception
+ self.__fsend(StringEncoder.encode(command + "\n"))
def create_remote_tmpfile(self):
# type: () -> Optional[str]
@@ -6911,7 +7442,7 @@ Raises
self.print_info("Creating tmpfile:", False, True)
for response in self.__frecv():
if response:
- tmpfile = response.rstrip()
+ tmpfile = StringEncoder.decode(response).rstrip()
self.print_info("Creating tmpfile: {}".format(tmpfile), True, True)
return tmpfile
@@ -6953,13 +7484,17 @@ Raises
"""
# TODO: Make windows compatible
for path in self.__PYTHON_PATHS:
- for version in self.__PYTHON_VERSIONS:
- python = path + "/" + version
+ for name in self.__PYTHON_NAMES:
+
+ python = path + "/" + name
self.print_info("Probing for: {}".format(python))
self.remote_command("test -f {p} && echo {p} || echo;".format(p=python))
- for response in self.__frecv():
- reg = re.search(r"^([.0-9]+)", response)
- if response.rstrip() == python.rstrip():
+
+ for byte in self.__frecv():
+
+ response = StringEncoder.decode(byte)
+ match = re.search(r"^([.0-9]+)", response)
+ if StringEncoder.rstrip(response) == StringEncoder.rstrip(python):
self.print_info("Potential path: {}".format(python))
command = []
command.append("{} -c '".format(python))
@@ -6970,18 +7505,18 @@ Raises
data = "".join(command)
self.remote_command(data)
continue
- if reg:
- match = reg.group(1)
- if match[0] == "2":
+ if match:
+ version = match.group(1)
+ if version[0] == "2":
self.__py3 = False
- elif match[0] == "3":
+ elif version[0] == "3":
self.__py3 = True
else:
- self.print_info(
- "Could not determine major version: {}".format(reg.group(1))
- )
+ self.print_info("Could not determine major version: {}".format(version))
return False
- self.print_info("Found valid Python{} version: {}".format(match[0], match))
+ self.print_info(
+ "Found valid Python{} version: {}".format(version[0], version)
+ )
self.__python = python
return True
# Nothing matched, break the innter loop
@@ -7008,7 +7543,7 @@ Raises
self.print_info(
"Uploading: {} -> {} ({}/{})".format(lpath, rpath, curr, count), False, True
)
- b64 = self.__enc.base64_encode(line)
+ b64 = StringEncoder.decode(base64.b64encode(StringEncoder.encode(line)))
if first:
self.remote_command('echo "{}" > {}'.format(b64, rpath))
first = False
@@ -7117,7 +7652,7 @@ Returns
self.print_info("Creating tmpfile:", False, True)
for response in self.__frecv():
if response:
- tmpfile = response.rstrip()
+ tmpfile = StringEncoder.decode(response).rstrip()
self.print_info("Creating tmpfile: {}".format(tmpfile), True, True)
return tmpfile
@@ -7188,7 +7723,8 @@ Args
command (str): The command to execute on the remote end.
"""
# TODO: determine remote host line feeds and set accordingly.
- self.__fsend(command + "\n")
+ # TODO: handle exception
+ self.__fsend(StringEncoder.encode(command + "\n"))
@@ -7269,8 +7805,8 @@
@@ -7522,7 +8058,7 @@ Raises
def __init__(
self,
enc, # type: StringEncoder
- send, # type: Callable[[str], None]
- recv, # type: Callable[[], Iterator[str]]
+ send, # type: Callable[[bytes], None]
+ recv, # type: Callable[[], Iterator[bytes]]
cmd, # type: str
host, # type: str
ports, # type: List[int]
@@ -7472,7 +8008,7 @@ Methods
# --------------------------------------------------------------------------
@property
def function(self):
- # type: () -> Callable[..., Iterator[str]]
+ # type: () -> Callable[..., Iterator[bytes]]
"""`IO.producer`: Callable funtcion function."""
return self.__function
@@ -7492,7 +8028,7 @@ Methods
# Contrcutor
# --------------------------------------------------------------------------
def __init__(self, function, *args, **kwargs):
- # type: (Callable[..., Iterator[str]], Any, Any) -> None
+ # type: (Callable[..., Iterator[bytes]], Any, Any) -> None
self.__function = function
self.__args = args
self.__kwargs = kwargsInstance variables
@@ -7991,7 +8527,7 @@ @property
def function(self):
- # type: () -> Callable[..., Iterator[str]]
+ # type: () -> Callable[..., Iterator[bytes]]
"""`IO.producer`: Callable funtcion function."""
return self.__function
Instance variables
class DsIOStdinStdout
-(encoder, input_timeout)
+(encoder, input_timeout, send_on_eof)
Instance variables
"""`float`: Input timeout in seconds for non-blocking read or `None` for blocking."""
return self.__input_timeout
+ @property
+ def send_on_eof(self):
+ # type: () -> bool
+ """`float`: Determines if we buffer STDIN until EOF before sending."""
+ return self.__send_on_eof
+
# --------------------------------------------------------------------------
# Constructor
# --------------------------------------------------------------------------
- def __init__(self, encoder, input_timeout):
- # type: (StringEncoder, Optional[float]) -> None
+ def __init__(self, encoder, input_timeout, send_on_eof):
+ # type: (StringEncoder, Optional[float], bool) -> None
super(DsIOStdinStdout, self).__init__()
self.__enc = encoder
- self.__input_timeout = input_timeout
+ self.__input_timeout = input_timeout
+ self.__send_on_eof = send_on_eof
Instance variables
@@ -8056,6 +8599,20 @@
Instance variables
return self.__input_timeout
var send_on_eof
float
: Determines if we buffer STDIN until EOF before sending.
+Expand source code
+
+
+@property
+def send_on_eof(self):
+ # type: () -> bool
+ """`float`: Determines if we buffer STDIN until EOF before sending."""
+ return self.__send_on_eof
@@ -8082,7 +8639,7 @@
-Instance variables
@property
def consumer(self):
- # type: () -> Callable[[str], None]
+ # type: () -> Callable[[bytes], None]
"""`IO.consumer`: Data consumer function."""
return self.__consumer
@@ -8110,7 +8667,7 @@ Instance variables
def __init__(
self,
producer, # type: DsCallableProducer
- consumer, # type: Callable[[str], None]
+ consumer, # type: Callable[[bytes], None]
interrupts, # type: List[Callable[[], None]]
transformers, # type: List[Transform]
code, # type: Optional[Union[str, bytes, CodeType]]
@@ -8147,7 +8704,7 @@ Instance variables
@@ -8198,7 +8755,7 @@ @property
def consumer(self):
- # type: () -> Callable[[str], None]
+ # type: () -> Callable[[bytes], None]
"""`IO.consumer`: Data consumer function."""
return self.__consumer
Instance variables
class DsRunnerRepeater
-(action, signal, repeat, pause, *args, **kwargs)
+(action, ssig, repeat, pause, *args, **kwargs)
Instance variables
return self.__kwargs
@property
- def signal(self):
- # type: () -> StopSignal
- """`StopSignal`: StopSignal instance."""
- return self.__signal
+ def ssig(self):
+ # type: () -> InterruptHandler
+ """`InterruptHandler`: InterruptHandler instance."""
+ return self.__ssig
# --------------------------------------------------------------------------
# Constructor
@@ -8254,7 +8811,7 @@ Instance variables
def __init__(
self,
action, # type: Callable[..., None]
- signal, # type: StopSignal
+ ssig, # type: InterruptHandler
repeat, # type: int
pause, # type: float
*args, # type: Tuple[Any, ...]
@@ -8265,7 +8822,7 @@ Instance variables
assert type(pause) is float, type(pause)
assert type(kwargs) is dict, type(kwargs)
self.__action = action
- self.__signal = signal
+ self.__ssig = ssig
self.__repeat = repeat
self.__pause = pause
self.__args = args
@@ -8343,25 +8900,25 @@ Instance variables
return self.__repeatvar signal
var ssig
StopSignal
: StopSignal instance.InterruptHandler
: InterruptHandler instance.
Expand source code
+def ssig(self):
+ # type: () -> InterruptHandler
+ """`InterruptHandler`: InterruptHandler instance."""
+ return self.__ssig
@property
-def signal(self):
- # type: () -> StopSignal
- """`StopSignal`: StopSignal instance."""
- return self.__signal
class DsRunnerTimer
-(action, signal, intvl, *args, **kwargs)
+(action, ssig, intvl, *args, **kwargs)
Instance variables
return self.__kwargs
@property
- def signal(self):
- # type: () -> StopSignal
- """`StopSignal`: StopSignal instance."""
- return self.__signal
+ def ssig(self):
+ # type: () -> InterruptHandler
+ """`InterruptHandler`: InterruptHandler instance."""
+ return self.__ssig
# --------------------------------------------------------------------------
# Constructor
@@ -8411,7 +8968,7 @@ Instance variables
def __init__(
self,
action, # type: Callable[..., None]
- signal, # type: StopSignal
+ ssig, # type: InterruptHandler
intvl, # type: int
*args, # type: Tuple[Any, ...]
**kwargs # type: Dict[str, Any]
@@ -8420,7 +8977,7 @@ Instance variables
assert type(intvl) is int, type(intvl)
assert type(kwargs) is dict, type(kwargs)
self.__action = action
- self.__signal = signal
+ self.__ssig = ssig
self.__intvl = intvl
self.__args = args
self.__kwargs = kwargs
@@ -8483,18 +9040,18 @@ Instance variables
return self.__kwargs
var signal
var ssig
StopSignal
: StopSignal instance.InterruptHandler
: InterruptHandler instance.
Expand source code
+def ssig(self):
+ # type: () -> InterruptHandler
+ """`InterruptHandler`: InterruptHandler instance."""
+ return self.__ssig
@property
-def signal(self):
- # type: () -> StopSignal
- """`StopSignal`: StopSignal instance."""
- return self.__signal
Instance variables
# --------------------------------------------------------------------------
@property
def ssig(self):
- # type: () -> StopSignal
- """`StopSignal`: StopSignal instance to trigger a shutdown signal."""
+ # type: () -> InterruptHandler
+ """`InterruptHandler`: InterruptHandler instance to trigger a shutdown signal."""
return self.__ssig
@property
@@ -8906,7 +9463,7 @@ Instance variables
# Constructor
# --------------------------------------------------------------------------
def __init__(self, ssig, safeword):
- # type: (StopSignal, str) -> None
+ # type: (InterruptHandler, str) -> None
super(DsTransformSafeword, self).__init__()
self.__ssig = ssig
self.__safeword = safeword
@@ -8929,15 +9486,15 @@ Instance variables
var ssig
StopSignal
: StopSignal instance to trigger a shutdown signal.InterruptHandler
: InterruptHandler instance to trigger a shutdown signal.
Expand source code
@property
def ssig(self):
- # type: () -> StopSignal
- """`StopSignal`: StopSignal instance to trigger a shutdown signal."""
+ # type: () -> InterruptHandler
+ """`InterruptHandler`: InterruptHandler instance to trigger a shutdown signal."""
return self.__ssig
Instance variables
Args
-
ssig
: StopSignal
ssig
: InterruptHandler
@@ -8988,8 +9545,8 @@
Args
# --------------------------------------------------------------------------
@property
def ssig(self):
- # type: () -> StopSignal
- """`StopSignal`: Read only property to provide a StopSignal instance to IO."""
+ # type: () -> InterruptHandler
+ """`InterruptHandler`: InterruptHandler instance."""
return self.__ssig
@property
@@ -9003,11 +9560,11 @@ Args
# --------------------------------------------------------------------------
@abstractmethod
def __init__(self, ssig):
- # type: (StopSignal) -> None
+ # type: (InterruptHandler) -> None
"""Set specific options for this IO module.
Args:
- ssig (StopSignal): StopSignal instance used by the interrupter.
+ ssig (InterruptHandler): InterruptHandler instance used by the interrupter.
"""
super(IO, self).__init__()
self.__ssig = ssig
@@ -9018,7 +9575,7 @@ Args
# --------------------------------------------------------------------------
@abstractmethod
def producer(self, *args, **kwargs):
- # type: (Any, Any) -> Iterator[str]
+ # type: (Any, Any) -> Iterator[bytes]
"""Implement a generator function which constantly yields data.
The data could be from various sources such as: received from a socket,
@@ -9030,7 +9587,7 @@ Args
@abstractmethod
def consumer(self, data):
- # type: (str) -> None
+ # type: (bytes) -> None
"""Define a consumer callback which will apply an action on the producer output.
Args:
@@ -9045,8 +9602,6 @@ Args
Various producer might call blocking functions and they won't be able to stop themself
as they hang on that blocking function.
NOTE: This method is triggered from outside and is supposed to stop/shutdown the producer.
-
- You should at least implement it with "self.ssig.raise_stop()"
"""
Ancestors
@@ -9078,15 +9633,15 @@ Instance variables
var ssig
StopSignal
: Read only property to provide a StopSignal instance to IO.InterruptHandler
: InterruptHandler instance.
Expand source code
@property
def ssig(self):
- # type: () -> StopSignal
- """`StopSignal`: Read only property to provide a StopSignal instance to IO."""
+ # type: () -> InterruptHandler
+ """`InterruptHandler`: InterruptHandler instance."""
return self.__ssig
Args
@abstractmethod
def consumer(self, data):
- # type: (str) -> None
+ # type: (bytes) -> None
"""Define a consumer callback which will apply an action on the producer output.
Args:
@@ -9124,8 +9679,7 @@
Args
@abstractmethod
def producer(self, *args, **kwargs):
- # type: (Any, Any) -> Iterator[str]
+ # type: (Any, Any) -> Iterator[bytes]
"""Implement a generator function which constantly yields data.
The data could be from various sources such as: received from a socket,
@@ -9188,8 +9740,8 @@ Attributes
Set specific options for this I/O module.
Args
-ssig
: StopSignal
-- Instance of StopSignal.
+ssig
: InterruptHandler
+- Instance of InterruptHandler.
opts
: DsIOCommand
- Custom module options.
@@ -9208,19 +9760,19 @@ Args
# Constructor / Destructor
# --------------------------------------------------------------------------
def __init__(self, ssig, opts):
- # type: (StopSignal, DsIOCommand) -> None
+ # type: (InterruptHandler, DsIOCommand) -> None
"""Set specific options for this I/O module.
Args:
- ssig (StopSignal): Instance of StopSignal.
+ ssig (InterruptHandler): Instance of InterruptHandler.
opts (DsIOCommand): Custom module options.
"""
super(IOCommand, self).__init__(ssig)
self.__opts = opts
self.log.debug("Setting '%s' as executable", self.__opts.executable)
- # Define destructor
- atexit.register(self.__destruct__)
+ # Did we already run cleanup
+ self.__cleaned_up = False
# Open executable to wait for commands
env = os.environ.copy()
@@ -9242,19 +9794,11 @@ Args
self.log.error("Specified executable '%s' not found", self.__opts.executable)
sys.exit(1)
- def __destruct__(self):
- # type: () -> None
- """Destructor."""
- self.log.trace( # type: ignore
- "Killing executable: %s with pid %d", self.__opts.executable, self.proc.pid
- )
- self.proc.kill()
-
# --------------------------------------------------------------------------
# Public Functions
# --------------------------------------------------------------------------
def producer(self, *args, **kwargs):
- # type: (Any, Any) -> Iterator[str]
+ # type: (Any, Any) -> Iterator[bytes]
"""Constantly ask for input.
Yields:
@@ -9262,43 +9806,59 @@ Args
"""
assert self.proc.stdout is not None
while True:
- if self.ssig.has_stop():
- self.log.trace("Stop signal acknowledged in Command") # type: ignore
+ if self.ssig.has_command_quit():
+ self.log.trace("CMD-QUIT signal ACK IOCommand.producer") # type: ignore
+ self.__cleanup()
return
self.log.trace("Reading command output") # type: ignore
+<<<<<<< HEAD
# Byte-wise reading is required to make it work for remote ends being in raw mode
# However, the performance of self.proc.stdout.readline() is way faster.
# To improve performance we will get rid of all other logging calls here.
data = self.proc.stdout.read(1)
+=======
+ # Much better performance than self.proc.read(1)
+ # TODO: check if self.proc.read(1) might be better in raw mode
+ data = self.proc.stdout.readline()
+>>>>>>> Heavy refactoring
self.log.trace("Command output: %s", repr(data)) # type: ignore
if not data:
- self.log.trace("Command output was empty. Exiting loop.") # type: ignore
- break
- yield self.__opts.enc.decode(data)
+ self.log.trace("CMD-QUIT signal REQ IOCommand.producer") # type: ignore
+ self.ssig.raise_command_quit()
+ self.__cleanup()
+ return
+ yield data
def consumer(self, data):
- # type: (str) -> None
+ # type: (bytes) -> None
"""Send data received to stdin (command input).
Args:
data (str): Command to execute.
"""
assert self.proc.stdin is not None
- byte = self.__opts.enc.encode(data)
- self.log.trace("Appending to stdin: %s", repr(byte)) # type: ignore
- self.proc.stdin.write(byte)
+ self.log.trace("Appending to stdin: %s", repr(data)) # type: ignore
+ self.proc.stdin.write(data)
self.proc.stdin.flush()
def interrupt(self):
# type: () -> None
"""Stop function that can be called externally to close this instance."""
- self.log.trace( # type: ignore
- "[IOCommand] subprocess.kill() was raised by input_unterrupter()"
- )
- self.proc.kill()
- # Raise stop signal
- # TODO: Check if this is required???
- self.ssig.raise_stop()
+ self.log.trace("CMD-QUIT signal REQ IOCommand.interrupt") # type: ignore
+ self.ssig.raise_command_quit()
+ self.__cleanup()
+
+ def __cleanup(self):
+ # type: () -> None
+ """Cleanup function."""
+ if not self.__cleaned_up:
+ self.log.trace( # type: ignore
+ "CMD-QUIT-CLEANUP: killing executable: %s with pid %d",
+ self.__opts.executable,
+ self.proc.pid,
+ )
+ self.proc.kill()
+ self.__cleaned_up = True
def consumer(self, data):
- # type: (str) -> None
+ # type: (bytes) -> None
"""Send data received to stdin (command input).
Args:
data (str): Command to execute.
"""
assert self.proc.stdin is not None
- byte = self.__opts.enc.encode(data)
- self.log.trace("Appending to stdin: %s", repr(byte)) # type: ignore
- self.proc.stdin.write(byte)
+ self.log.trace("Appending to stdin: %s", repr(data)) # type: ignore
+ self.proc.stdin.write(data)
self.proc.stdin.flush()
@@ -9347,13 +9906,9 @@ def interrupt(self):
# type: () -> None
"""Stop function that can be called externally to close this instance."""
- self.log.trace( # type: ignore
- "[IOCommand] subprocess.kill() was raised by input_unterrupter()"
- )
- self.proc.kill()
- # Raise stop signal
- # TODO: Check if this is required???
- self.ssig.raise_stop()
+ self.log.trace("CMD-QUIT signal REQ IOCommand.interrupt") # type: ignore
+ self.ssig.raise_command_quit()
+ self.__cleanup()
@@ -9371,7 +9926,7 @@ Yields
Expand source code
def producer(self, *args, **kwargs):
- # type: (Any, Any) -> Iterator[str]
+ # type: (Any, Any) -> Iterator[bytes]
"""Constantly ask for input.
Yields:
@@ -9379,19 +9934,28 @@ Yields
"""
assert self.proc.stdout is not None
while True:
- if self.ssig.has_stop():
- self.log.trace("Stop signal acknowledged in Command") # type: ignore
+ if self.ssig.has_command_quit():
+ self.log.trace("CMD-QUIT signal ACK IOCommand.producer") # type: ignore
+ self.__cleanup()
return
self.log.trace("Reading command output") # type: ignore
+<<<<<<< HEAD
# Byte-wise reading is required to make it work for remote ends being in raw mode
# However, the performance of self.proc.stdout.readline() is way faster.
# To improve performance we will get rid of all other logging calls here.
data = self.proc.stdout.read(1)
+=======
+ # Much better performance than self.proc.read(1)
+ # TODO: check if self.proc.read(1) might be better in raw mode
+ data = self.proc.stdout.readline()
+>>>>>>> Heavy refactoring
self.log.trace("Command output: %s", repr(data)) # type: ignore
if not data:
- self.log.trace("Command output was empty. Exiting loop.") # type: ignore
- break
- yield self.__opts.enc.decode(data)
+ self.log.trace("CMD-QUIT signal REQ IOCommand.producer") # type: ignore
+ self.ssig.raise_command_quit()
+ self.__cleanup()
+ return
+ yield data
@@ -9414,8 +9978,8 @@ Create a Pwncat instance of either a server or a client.
ssig
: StopSignal
ssig
: InterruptHandler
encoder
: StringEncoder
host
: str
def consumer(self, data):
- # type: (str) -> None
+ # type: (bytes) -> None
"""Send data to a socket."""
- self.__net.send(data)
+ try:
+ self.__net.send(data)
+ except socket.error:
+ pass
@@ -9736,13 +10338,9 @@ Methods
def interrupt(self):
# type: () -> None
"""Stop function that can be called externally to close this instance."""
- self.log.trace( # type: ignore
- "[IONetwork] socket.close was raised by calling interrupt() externally."
- )
- self.__net.close_conn_sock()
- self.__net.close_bind_sock()
- # Raise stop signal
- self.ssig.raise_stop()
+ self.log.trace("SOCK-QUIT signal REQ in IONetwork.interrupt") # type: ignore
+ self.ssig.raise_sock_quit()
+ self.__cleanup()
@@ -9760,7 +10358,7 @@ Yields
Expand source code
def producer(self, *args, **kwargs):
- # type: (Any, Any) -> Iterator[str]
+ # type: (Any, Any) -> Iterator[bytes]
"""Network receive generator which hooks into the receive function and adds features.
Yields:
@@ -9776,9 +10374,9 @@ Yields
try:
yield self.__net.receive()
# [2/3] Non-blocking socket is finished receiving data and allows us to do some action
- except socket.timeout:
+ except socket.timeout as err:
# Let's ask the interrupter() function if we should terminate?
- if not self.ssig.has_stop():
+ if not self.ssig.has_sock_quit():
continue
# Stop signal is raied when my own side of the network was closed.
# Happened most likely that the user pressed Ctrl+c
@@ -9793,12 +10391,19 @@ Yields
curr_recv_timeout_retry += 1
continue
# We ware all done reading, shut down
- self.ssig.raise_stop()
+ self.log.trace( # type: ignore
+ "SOCK-QUIT signal ACK in IONetwork.producer [1]: %s", err
+ )
+ self.__cleanup()
return
- # [3/3] Upstream is gone
- except (EOFError, AttributeError, socket.error):
+ # [3/3] Upstream is gone (in one way or another)
+ except (EOFError, AttributeError, socket.error) as err:
# Do we have a stop signal?
- if self.ssig.has_stop():
+ if self.ssig.has_sock_quit():
+ self.log.trace( # type: ignore
+ "SOCK-QUIT signal ACK in IONetwork.producer [2]: %s", err
+ )
+ self.__cleanup()
return
# Do we re-accept new clients?
if self.__sock_opts.udp:
@@ -9808,6 +10413,10 @@ Yields
continue
if self.__role == "client" and self.__client_reconnect_to_server():
continue
+ # Inform everybody that we are quitting
+ self.log.trace("SOCK-QUIT signal REQ in IONetwork.producer") # type: ignore
+ self.ssig.raise_sock_quit()
+ self.__cleanup()
return
@@ -9831,8 +10440,8 @@ Inherited members
Create a Pwncat Network Scanner instance.
Args
-ssig
: StopSignal
-- Stop signal instance
+ssig
: InterruptHandler
+- Instance of InterruptHandler.
encoder
: StringEncoder
- Instance of StringEncoder (Python2/3 str/byte compat).
host
: str
@@ -9878,7 +10487,7 @@ Args
# --------------------------------------------------------------------------
def __init__(
self,
- ssig, # type: StopSignal
+ ssig, # type: InterruptHandler
encoder, # type: StringEncoder
host, # type: str
banner, # type: bool
@@ -9889,7 +10498,7 @@ Args
"""Create a Pwncat Network Scanner instance.
Args:
- ssig (StopSignal): Stop signal instance
+ ssig (InterruptHandler): Instance of InterruptHandler.
encoder (StringEncoder): Instance of StringEncoder (Python2/3 str/byte compat).
host (str): The hostname to resolve.
banner (bool): Determines if we do banner grabbing as well.
@@ -9898,19 +10507,19 @@ Args
"""
super(IONetworkScanner, self).__init__(ssig)
+ self.__ssig = ssig
self.__enc = encoder
self.__cli_opts = cli_opts
self.__sock_opts = sock_opts
self.__banner = banner
self.__log = logging.getLogger(__name__)
- self.__net = Net(encoder, ssig, sock_opts)
self.__sock = Sock()
self.__screen_lock = threading.Semaphore()
# Keep track of local binds (addr-port) of the threaded scanner
# clients as we do not want to treat them as open ports (false posistives)
- self.__local_binds = [] # type: List[str]
+ self.__local_binds = {} # type: Dict[str, socket.socket]
# Compile our regexes if using banner detection
if banner:
@@ -9928,13 +10537,13 @@ Args
int(socket.AF_INET),
]
self.__targets = {}
- try:
- for family in families:
+ for family in families:
+ try:
self.__targets[family] = self.__sock.gethostbyname(
host, family, not self.__sock_opts.nodns
)
- except socket.gaierror:
- pass
+ except socket.gaierror:
+ pass
# --------------------------------------------------------------------------
# Public Functions
@@ -9942,18 +10551,24 @@ Args
def __get_socket(self, family):
# type: (Union[socket.AddressFamily, int]) -> socket.socket
"""Create socket for specific address family endlessly until resources are available."""
- # The scanner is starting many threads, each creating a single socket
- # and we might hit the max allowed open files limit, so we will
- # endlessly ask the system for a new socket until success.
- # Also adding a delay, which will give other threads the time to
- # release their sockets.
+ # The scanner starts one thread for each port to scan. Each thread will also create
+ # one socket and we might hit the max_allowed_files limit (ulimit).
+ # That's why we loop through creating sockets until we hit a success
+ # as in the meantime, other threads might have already released sockets/fd's.
while True:
+ delay = 0.0
+ if self.__ssig.has_terminate():
+ self.log.trace( # type: ignore
+ "TERMINATE signal ACK for IONetworkScanner._getsocket"
+ )
+ raise socket.error("quit")
try:
if self.__sock_opts.udp:
return self.__sock.create_socket(family, socket.SOCK_DGRAM)
return self.__sock.create_socket(family, socket.SOCK_STREAM)
except socket.error:
- time.sleep(0.1)
+ delay += 0.1
+ time.sleep(delay) # This can be bigger to give the system some time to release fd's
def __get_banner_version(self, banner):
# type: (str) -> Optional[str]
@@ -9970,7 +10585,7 @@ Args
for reg in self.BANNER_REG_COMP:
match = re.search(reg, banner)
if match:
- return match.group(1).rstrip()
+ return StringEncoder.rstrip(match.group(1)) # type: ignore
# Nothing found, return first non-empty line
for line in lines:
@@ -9989,25 +10604,30 @@ Args
payloads = self.BANNER_PAYLOADS[0]
for payload in payloads:
+ # Break the loop on terminate signal
+ if self.__ssig.has_terminate():
+ self.log.trace( # type: ignore
+ "TERMINATE signal ACK for IONetworkScanner._getbanner: %s-%d", addr, port
+ )
+ return (False, None)
try:
if payload is not None:
sock.send(self.__enc.encode(payload))
self.__log.debug("%s:%d - payload sent: %s", addr, port, repr(payload))
- sock.settimeout(0.1)
+ sock.settimeout(0.5)
banner = sock.recv(self.__sock_opts.bufsize)
version = self.__get_banner_version(self.__enc.decode(banner))
self.__log.debug("%s:%d - respone received: %s", addr, port, repr(banner))
return (True, version)
except socket.timeout:
- time.sleep(0.1)
continue
except (OSError, socket.error):
return (False, None)
return (True, None)
def producer(self, *args, **kwargs):
- # type: (Any, Any) -> Iterator[str]
+ # type: (Any, Any) -> Iterator[bytes]
"""Port scanner yielding open/closed string for given port.
Args:
@@ -10022,13 +10642,25 @@ Args
# Loop over adress families
for family in self.__targets:
+ # [1/7] Check for termination request
+ if self.__ssig.has_terminate():
+ self.log.trace( # type: ignore
+ "TERMINATE signal ACK for IONetworkScanner.producer"
+ )
+ return
+
addr = self.__targets[family]
- # [1/5] Get socket
- sock = self.__get_socket(family)
+ # [2/7] Get socket
+ try:
+ sock = self.__get_socket(family)
+ sock_type = sock.type
+ except (AttributeError, socket.error):
+ # Exception is triggered due to stop stignal and we
+ # will abort here in that case.
+ return
- # [2/5] Connect scan
- succ_conn = False
+ # [3/7] Connect scan
try:
laddr, lport = self.__sock.connect(
sock,
@@ -10042,45 +10674,47 @@ Args
0.1,
)
# Append local binds (addr-port) to check against during port scan
- self.__local_binds.append(str(laddr + "-" + str(lport)))
- succ_conn = True
+ key = str(laddr + "-" + str(lport))
+ self.__local_binds[key] = sock
except socket.error:
- succ_conn = False
+ self.__sock.close(sock, "[-] closed: {}:{}".format(addr, port))
+ continue
+
+ # [4/7] False positives
+ # Connect was successful, but against a local bind of one of our
+ # port scanners, so this is a false positive.
+ if str(addr + "-" + str(port)) in self.__local_binds:
+ self.__sock.close(sock, "[-] closed: {}:{}".format(addr, port))
+ del self.__local_binds[key]
+ continue
- # [3/5] Banner grabbing
+ # [5/7] Banner grabbing
succ_banner = True
banner = None
if self.__banner:
(succ_banner, banner) = self.__get_banner(sock, addr, port)
- # [4/5] Evaluation
- if banner is not None and (succ_conn and succ_banner):
- if str(addr + "-" + str(port)) not in self.__local_binds:
- sock_type = sock.type
- yield "[+] {:>5}/{} open ({}): {}".format(
- port,
- self.__sock.get_type_name(sock_type),
- self.__sock.get_family_name(family),
- banner,
- )
- if banner is None and (succ_conn and succ_banner):
- if str(addr + "-" + str(port)) not in self.__local_binds:
- sock_type = sock.type
- yield "[+] {:>5}/{} open ({})".format(
- port,
- self.__sock.get_type_name(sock_type),
- self.__sock.get_family_name(family),
- )
+ # [6/7] Evaluation
+ if banner is not None and succ_banner:
+ msg = "[+] {:>5}/{} open ({}): {}".format(
+ port,
+ self.__sock.get_type_name(sock_type),
+ self.__sock.get_family_name(family),
+ banner,
+ )
+ yield self.__enc.encode(msg)
+ if banner is None and succ_banner:
+ msg = "[+] {:>5}/{} open ({})".format(
+ port, self.__sock.get_type_name(sock_type), self.__sock.get_family_name(family),
+ )
+ yield self.__enc.encode(msg)
- # [5/5] Cleanup
- self.__sock.close(sock, addr + "-" + str(port))
- try:
- self.__local_binds.remove(str(addr + "-" + str(port)))
- except ValueError:
- pass
+ # [7/7] Cleanup
+ self.__sock.close(sock, key)
+ del self.__local_binds[key]
def consumer(self, data):
- # type: (str) -> None
+ # type: (bytes) -> None
"""Print received data to stdout."""
# For issues with flush (when using tail -F or equal) see links below:
# https://stackoverflow.com/questions/26692284
@@ -10098,8 +10732,10 @@ Args
self.__screen_lock.release()
def interrupt(self):
- # type: (str) -> None
- """Not required."""
+ # type: () -> None
+ """Stop function that can be called externally to close this instance."""
+ self.log.trace("SOCK-QUIT signal REQ in IONetworkScanner.interrupt") # type: ignore
+ self.ssig.raise_sock_quit()
def consumer(self, data):
- # type: (str) -> None
+ # type: (bytes) -> None
"""Print received data to stdout."""
# For issues with flush (when using tail -F or equal) see links below:
# https://stackoverflow.com/questions/26692284
@@ -10155,14 +10791,16 @@ Methods
def interrupt(self)
Not required.
Stop function that can be called externally to close this instance.
def interrupt(self):
- # type: (str) -> None
- """Not required."""
+ # type: () -> None
+ """Stop function that can be called externally to close this instance."""
+ self.log.trace("SOCK-QUIT signal REQ in IONetworkScanner.interrupt") # type: ignore
+ self.ssig.raise_sock_quit()
@@ -10187,7 +10825,7 @@ Yields
Expand source code
def producer(self, *args, **kwargs):
- # type: (Any, Any) -> Iterator[str]
+ # type: (Any, Any) -> Iterator[bytes]
"""Port scanner yielding open/closed string for given port.
Args:
@@ -10202,13 +10840,25 @@ Yields
# Loop over adress families
for family in self.__targets:
+ # [1/7] Check for termination request
+ if self.__ssig.has_terminate():
+ self.log.trace( # type: ignore
+ "TERMINATE signal ACK for IONetworkScanner.producer"
+ )
+ return
+
addr = self.__targets[family]
- # [1/5] Get socket
- sock = self.__get_socket(family)
+ # [2/7] Get socket
+ try:
+ sock = self.__get_socket(family)
+ sock_type = sock.type
+ except (AttributeError, socket.error):
+ # Exception is triggered due to stop stignal and we
+ # will abort here in that case.
+ return
- # [2/5] Connect scan
- succ_conn = False
+ # [3/7] Connect scan
try:
laddr, lport = self.__sock.connect(
sock,
@@ -10222,42 +10872,44 @@ Yields
0.1,
)
# Append local binds (addr-port) to check against during port scan
- self.__local_binds.append(str(laddr + "-" + str(lport)))
- succ_conn = True
+ key = str(laddr + "-" + str(lport))
+ self.__local_binds[key] = sock
except socket.error:
- succ_conn = False
+ self.__sock.close(sock, "[-] closed: {}:{}".format(addr, port))
+ continue
+
+ # [4/7] False positives
+ # Connect was successful, but against a local bind of one of our
+ # port scanners, so this is a false positive.
+ if str(addr + "-" + str(port)) in self.__local_binds:
+ self.__sock.close(sock, "[-] closed: {}:{}".format(addr, port))
+ del self.__local_binds[key]
+ continue
- # [3/5] Banner grabbing
+ # [5/7] Banner grabbing
succ_banner = True
banner = None
if self.__banner:
(succ_banner, banner) = self.__get_banner(sock, addr, port)
- # [4/5] Evaluation
- if banner is not None and (succ_conn and succ_banner):
- if str(addr + "-" + str(port)) not in self.__local_binds:
- sock_type = sock.type
- yield "[+] {:>5}/{} open ({}): {}".format(
- port,
- self.__sock.get_type_name(sock_type),
- self.__sock.get_family_name(family),
- banner,
- )
- if banner is None and (succ_conn and succ_banner):
- if str(addr + "-" + str(port)) not in self.__local_binds:
- sock_type = sock.type
- yield "[+] {:>5}/{} open ({})".format(
- port,
- self.__sock.get_type_name(sock_type),
- self.__sock.get_family_name(family),
- )
+ # [6/7] Evaluation
+ if banner is not None and succ_banner:
+ msg = "[+] {:>5}/{} open ({}): {}".format(
+ port,
+ self.__sock.get_type_name(sock_type),
+ self.__sock.get_family_name(family),
+ banner,
+ )
+ yield self.__enc.encode(msg)
+ if banner is None and succ_banner:
+ msg = "[+] {:>5}/{} open ({})".format(
+ port, self.__sock.get_type_name(sock_type), self.__sock.get_family_name(family),
+ )
+ yield self.__enc.encode(msg)
- # [5/5] Cleanup
- self.__sock.close(sock, addr + "-" + str(port))
- try:
- self.__local_binds.remove(str(addr + "-" + str(port)))
- except ValueError:
- pass
+ # [7/7] Cleanup
+ self.__sock.close(sock, key)
+ del self.__local_binds[key]
Set specific options for this I/O module.
ssig
: StopSignal
ssig
: InterruptHandler
opts
: DsIOStdinStdout
def consumer(self, data):
- # type: (str) -> None
+ # type: (bytes) -> None
"""Print received data to stdout."""
- # For issues with flush (when using tail -F or equal) see links below:
- # https://stackoverflow.com/questions/26692284
- # https://docs.python.org/3/library/signal.html#note-on-sigpipe
- print(data, end="")
+ if self.__py3:
+ sys.stdout.buffer.write(data)
+ else:
+ # For issues with flush (when using tail -F or equal) see links below:
+ # https://stackoverflow.com/questions/26692284
+ # https://docs.python.org/3/library/signal.html#note-on-sigpipe
+ print(data, end="")
+
try:
sys.stdout.flush()
except (BrokenPipeError, IOError):
@@ -10485,12 +11191,14 @@ Methods
def interrupt(self):
# type: () -> None
"""Stop function that can be called externally to close this instance."""
+ # TODO: Does not work on windows as it has blocking read of stdin
+ self.log.trace("STDIN-QUIT signal REQ in IOStdinStdout.interrupt") # type: ignore
+ self.ssig.raise_stdin_quit()
+
self.log.trace( # type: ignore
- "[IOStdinStdout] interrupt() invoked"
+ "[IOStdinStdout] setting __abort to True was raised by interrupt() externally"
)
- # Raise stop signal
- # TODO: Check if this is required???
- self.ssig.raise_stop()
+ self.__abort = True
@@ -10508,39 +11216,79 @@ Yields
Expand source code
def producer(self, *args, **kwargs):
- # type: (Any, Any) -> Iterator[str]
+ # type: (Any, Any) -> Iterator[bytes]
"""Constantly ask for user input.
Yields:
str: Data read from stdin.
"""
+ # On --send-on-eof we will return all of its contents at once:
+ lines = []
+
# https://stackoverflow.com/questions/1450393/#38670261
# while True: line = sys.stdin.readline() <- reads a whole line (faster)
# for line in sys.stdin.readlin(): <- reads one byte at a time
while True:
- if self.ssig.has_stop():
- self.log.trace("Stop signal acknowledged for reading STDIN-1") # type: ignore
+ if self.__abort:
+ self.log.trace("External interrupt signal triggered. Aborting.") # type: ignore
+ return
+ if self.ssig.has_stdin_quit():
+ self.log.trace( # type: ignore
+ "STDIN-QUIT signal ACK in IOStdinStdout.producer [1]"
+ )
return
try:
+<<<<<<< HEAD
data = self.__read_stdin()
+=======
+ # TODO: select() does not work for windows on stdin/stdout
+ if os.name != "nt":
+ self.__set_input_timeout()
+ if self.__py3:
+ line = sys.stdin.buffer.readline()
+ else:
+ if sys.platform == "win32":
+ # Python 2 on Windows opens sys.stdin in text mode, and
+ # binary data that read from it becomes corrupted on \r\n.
+ # Setting sys.stdin to binary mode fixes that.
+ if hasattr(os, "O_BINARY"):
+ msvcrt.setmode(
+ sys.stdin.fileno(), os.O_BINARY, # pylint: disable=no-member
+ )
+ line = sys.stdin.readline() # type: ignore
+
+>>>>>>> Heavy refactoring
except EOFError:
# When using select() with timeout, we don't have any input
# at this point and simply continue the loop or quit if
# a terminate request has been made by other threads.
- if self.ssig.has_stop():
- self.log.trace("Stop signal acknowledged for reading STDIN-2") # type: ignore
+ if self.ssig.has_stdin_quit():
+ self.log.trace( # type: ignore
+ "STDIN-QUIT signal ACK in IOStdinStdout.producer [2]"
+ )
return
continue
+<<<<<<< HEAD
if data:
self.log.debug("Received %d bytes from STDIN", len(data))
self.log.trace("Received: %s", repr(data)) # type: ignore
yield data
+=======
+ if line:
+ self.log.debug("Received %d bytes from STDIN", len(line))
+ self.log.trace("Received: %s", repr(line)) # type: ignore
+ # [send-on-eof] Append data
+ if self.__opts.send_on_eof:
+ lines.append(line)
+ else:
+ yield line
+>>>>>>> Heavy refactoring
# EOF or <Ctrl>+<d>
else:
- # DO NOT RETURN HERE BLINDLY, THE UPSTREAM CONNECTION MUST GO FIRST!
- if self.ssig.has_stop():
- self.log.trace("Stop signal acknowledged for reading STDIN-3") # type: ignore
- return
+ # [send-on-eof] Dump data before quitting
+ if lines and self.__opts.send_on_eof:
+ yield StringEncoder.encode("").join(lines)
+ self.ssig.raise_stdin_quit()
@@ -10554,6 +11302,256 @@
+class InterruptHandler
+(keep_open, no_shutdown)
+
Pwncat interrupt handler.
+It allows all threads to raise various signal on certain actions, +as well as to ask the Interrupt Handler what to do. +The Interrupt handler will internally decide (based on pwncat's +command line arguments) what to do.
+Instantiate InterruptHandler.
+keep_open
: bool
--keep-open
command line argument.no_shutdown
: bool
--no-shutdown
command line argument.class InterruptHandler(object):
+ """Pwncat interrupt handler.
+
+ It allows all threads to raise various signal on certain actions,
+ as well as to ask the Interrupt Handler what to do.
+ The Interrupt handler will internally decide (based on pwncat's
+ command line arguments) what to do.
+ """
+
+ # --------------------------------------------------------------------------
+ # Constructor
+ # --------------------------------------------------------------------------
+ def __init__(self, keep_open, no_shutdown):
+ # type: (bool, bool) -> None
+ """Instantiate InterruptHandler.
+
+ Args:
+ keep_open (bool): `--keep-open` command line argument.
+ no_shutdown (bool): `--no-shutdown` command line argument.
+ """
+ self.__log = logging.getLogger(__name__) # type: logging.Logger
+ self.__keep_open = keep_open
+ self.__no_shutdown = no_shutdown
+
+ self.__terminate = False
+ self.__sock_quit = False
+ self.__stdin_quit = False
+ self.__command_quit = False
+
+ def handler(signum, frame): # type: ignore # pylint: disable=unused-argument
+ self.__log.trace("Ctrl+c caught.") # type: ignore
+ self.raise_terminate()
+
+ # Handle Ctrl+C
+ signal.signal(signal.SIGINT, handler)
+
+ # --------------------------------------------------------------------------
+ # Ask for action
+ # --------------------------------------------------------------------------
+ def has_terminate(self):
+ # type: () -> bool
+ """`bool`: Switch to be checked if pwncat should be terminated."""
+ return self.__terminate
+
+ def has_sock_quit(self):
+ # type: () -> bool
+ """`bool`: Switch to be checked if the socket connection should be closed."""
+ return self.__sock_quit
+
+ def has_stdin_quit(self):
+ # type: () -> bool
+ """`bool`: Switch to be checked if the STDIN should be closed."""
+ return self.__stdin_quit
+
+ def has_command_quit(self):
+ # type: () -> bool
+ """`bool`: Switch to be checked if the command should be closed."""
+ return self.__command_quit
+
+ # --------------------------------------------------------------------------
+ # Raise signals
+ # --------------------------------------------------------------------------
+ def raise_terminate(self):
+ # type: () -> None
+ """Signal the application that Socket should be quit."""
+ self.__log.trace("SIGNAL TERMINATE raised") # type: ignore
+ self.__terminate = True
+ self.__sock_quit = True
+ self.__stdin_quit = True
+ self.__command_quit = True
+
+ def raise_sock_quit(self):
+ # type: () -> None
+ """Signal the application that Socket should be quit."""
+ self.__log.trace("SIGNAL SOCK-QUIT raised") # type: ignore
+ self.__sock_quit = True
+ self.raise_terminate()
+
+ def raise_stdin_quit(self):
+ # type: () -> None
+ """Signal the application that STDIN should be quit."""
+ if not (self.__no_shutdown or self.__keep_open):
+ self.__log.trace("SIGNAL STDIN-QUIT raised") # type: ignore
+ self.__stdin_quit = True
+ self.raise_terminate()
+
+ def raise_command_quit(self):
+ # type: () -> None
+ """Signal the application that Command should be quit."""
+ self.__log.trace("SIGNAL CMD-QUIT raised") # type: ignore
+ self.__command_quit = True
+ self.raise_terminate()
+
+def has_command_quit(self)
+
bool
: Switch to be checked if the command should be closed.
def has_command_quit(self):
+ # type: () -> bool
+ """`bool`: Switch to be checked if the command should be closed."""
+ return self.__command_quit
+
+def has_sock_quit(self)
+
bool
: Switch to be checked if the socket connection should be closed.
def has_sock_quit(self):
+ # type: () -> bool
+ """`bool`: Switch to be checked if the socket connection should be closed."""
+ return self.__sock_quit
+
+def has_stdin_quit(self)
+
bool
: Switch to be checked if the STDIN should be closed.
def has_stdin_quit(self):
+ # type: () -> bool
+ """`bool`: Switch to be checked if the STDIN should be closed."""
+ return self.__stdin_quit
+
+def has_terminate(self)
+
bool
: Switch to be checked if pwncat should be terminated.
def has_terminate(self):
+ # type: () -> bool
+ """`bool`: Switch to be checked if pwncat should be terminated."""
+ return self.__terminate
+
+def raise_command_quit(self)
+
Signal the application that Command should be quit.
def raise_command_quit(self):
+ # type: () -> None
+ """Signal the application that Command should be quit."""
+ self.__log.trace("SIGNAL CMD-QUIT raised") # type: ignore
+ self.__command_quit = True
+ self.raise_terminate()
+
+def raise_sock_quit(self)
+
Signal the application that Socket should be quit.
def raise_sock_quit(self):
+ # type: () -> None
+ """Signal the application that Socket should be quit."""
+ self.__log.trace("SIGNAL SOCK-QUIT raised") # type: ignore
+ self.__sock_quit = True
+ self.raise_terminate()
+
+def raise_stdin_quit(self)
+
Signal the application that STDIN should be quit.
def raise_stdin_quit(self):
+ # type: () -> None
+ """Signal the application that STDIN should be quit."""
+ if not (self.__no_shutdown or self.__keep_open):
+ self.__log.trace("SIGNAL STDIN-QUIT raised") # type: ignore
+ self.__stdin_quit = True
+ self.raise_terminate()
+
+def raise_terminate(self)
+
Signal the application that Socket should be quit.
def raise_terminate(self):
+ # type: () -> None
+ """Signal the application that Socket should be quit."""
+ self.__log.trace("SIGNAL TERMINATE raised") # type: ignore
+ self.__terminate = True
+ self.__sock_quit = True
+ self.__stdin_quit = True
+ self.__command_quit = True
+
class Net
(encoder, ssig, options)
@@ -10565,7 +11563,7 @@ Args
encoder
: StringEncoder
- Instance of StringEncoder (Python2/3 str/byte compat).
-ssig
: StopSignal
+ssig
: InterruptHandler
- Used to stop blocking loops.
options
: DsSock
- Instance of DsSock.
@@ -10581,12 +11579,12 @@ Args
# Constructor / Destructor
# --------------------------------------------------------------------------
def __init__(self, encoder, ssig, options):
- # type: (StringEncoder, StopSignal, DsSock) -> None
+ # type: (StringEncoder, InterruptHandler, DsSock) -> None
"""Instantiate Sock class.
Args:
encoder (StringEncoder): Instance of StringEncoder (Python2/3 str/byte compat).
- ssig (StopSignal): Used to stop blocking loops.
+ ssig (InterruptHandler): Used to stop blocking loops.
options (DsSock): Instance of DsSock.
"""
self.__log = logging.getLogger(__name__) # type: logging.Logger
@@ -10656,14 +11654,17 @@ Args
# Public Send / Receive Functions
# --------------------------------------------------------------------------
def send(self, data):
- # type: (str) -> int
+ # type: (bytes) -> int
"""Send data through a connected (TCP) or unconnected (UDP) socket.
Args:
- data (str): The data to send.
+ data (bytes): The data to send.
Returns:
int: Returns total bytes sent.
+
+ Raises:
+ socket.error: Except here when unconnected or connection was forcibly closed.
"""
# UDP has some specialities as its socket is unconnected.
# See also recv() for specialities on that side.
@@ -10675,16 +11676,18 @@ Args
if not self.__active:
self.__log.warning("UDP client has not yet connected. Queueing message")
while not self.__active:
+ if self.__ssig.has_sock_quit():
+ self.__log.trace( # type: ignore
+ "SOCK-QUIT signal ACK in Net.send (while waiting for UDP client)"
+ )
+ return -1
time.sleep(0.01)
curr = 0 # bytes send during one loop iteration
send = 0 # total bytes send
size = len(data) # bytes of data that needs to be send
- byte = self.__enc.encode(data)
- assert size == len(byte), "Encoding messed up string length, might need to do len() after."
# Loop until all bytes have been send
- # TODO: Does this make it impossible to send nullbytes (Ctrl+d)
while send < size:
self.__log.debug(
"Trying to send %d bytes to %s:%d",
@@ -10692,23 +11695,23 @@ Args
self.__active["remote_addr"],
self.__active["remote_port"],
)
- self.__log.trace("Trying to send: %s", repr(byte)) # type: ignore
+ self.__log.trace("Trying to send: %s", repr(data)) # type: ignore
try:
# Only UDP server has not made a connect() to the socket, all others
# are already connected and need to use send() instead of sendto()
if self.__udp_mode_server:
curr = self.__active["conn"].sendto(
- byte, (self.__active["remote_addr"], self.__active["remote_port"])
+ data, (self.__active["remote_addr"], self.__active["remote_port"])
)
send += curr
else:
- curr = self.__active["conn"].send(byte)
+ curr = self.__active["conn"].send(data)
send += curr
if curr == 0:
self.__log.error("No bytes send during loop round.")
return 0
# Remove 'curr' many bytes from byte for the next round
- byte = byte[curr:]
+ data = data[curr:]
self.__log.debug(
"Sent %d bytes to %s:%d (%d bytes remaining)",
curr,
@@ -10716,23 +11719,23 @@ Args
self.__active["remote_port"],
size - send,
)
- except (OSError, socket.error) as error:
- self.__log.error("Socket OS Error: %s", error)
- return send
+ except (BrokenPipeError, OSError, socket.error) as error:
+ msg = "Socket send Error: {}".format(error)
+ raise socket.error(msg)
return send
def receive(self):
- # type: () -> str
+ # type: () -> bytes
"""Receive and return data from the connected (TCP) or unconnected (UDP) socket.
Returns:
- str: Returns received data from connected (TCP) or unconnected (UDP) socket.
+ bytes: Returns received data from connected (TCP) or unconnected (UDP) socket.
Raises:
socket.timeout: Except here to do an action when the socket is not busy.
AttributeError: Except here when current instance has closed itself (Ctrl+c).
socket.error: Except here when unconnected or connection was forcibly closed.
- EOFError: Except here when upstream has closed the connection.
+ EOFError: Except here when upstream has closed the connection via EOF.
"""
# This is required for a UDP server that has no connected clients yet
# and is waiting for data receival for the first time on either IPv4 or IPv6
@@ -10748,9 +11751,9 @@ Args
0
] # type: List[socket.socket]
# E.g.: ValueError: file descriptor cannot be a negative integer (-1)
- except (ValueError, AttributeError):
- msg = "Connection was closed by self."
- self.__log.warning(msg)
+ except (ValueError, AttributeError) as error:
+ msg = "Connection was closed by self: [1]: {}".format(error)
+ self.__log.debug(msg)
raise AttributeError(msg)
if not conns:
# This is raised for the calling function to determine what to do
@@ -10762,12 +11765,12 @@ Args
conn = conns[0] # type: socket.socket
try:
# https://manpages.debian.org/buster/manpages-dev/recv.2.en.html
- (byte, addr) = conn.recvfrom(self.__options.bufsize)
+ (data, addr) = conn.recvfrom(self.__options.bufsize)
# [1/5] When closing itself (e.g.: via Ctrl+c and the socket_close() funcs are called)
- except AttributeError:
- msg = "Connection was closed by self."
- self.__log.warning(msg)
+ except AttributeError as error:
+ msg = "Connection was closed by self: [2]: {}".format(error)
+ self.__log.debug(msg)
raise AttributeError(msg)
# [2/5] Connection was forcibly closed
@@ -10775,14 +11778,14 @@ Args
# [Errno 10054] An existing connection was forcibly closed by the remote host
# [WinError 10054] An existing connection was forcibly closed by the remote host
except (OSError, socket.error) as error:
- self.__log.warning("Connection error: %s", error)
+ self.__log.debug("Connection error: %s", error)
raise socket.error(error)
# [3/5] Upstream (server or client) is gone.
# In TCP, there is no such thing as an empty message, so zero means a peer disconnect.
# In UDP, there is no such thing as a peer disconnect, so zero means an empty datagram.
- if not byte:
- msg = "Upstream has closed the connection."
+ if not data:
+ msg = "EOF: Remote finished sending."
self.__log.info(msg)
raise EOFError(msg)
@@ -10820,7 +11823,6 @@ Args
}
# [5/5] We have data to process
- data = self.__enc.decode(byte)
self.__log.debug(
"Received %d bytes from %s:%d",
len(data),
@@ -11057,24 +12059,27 @@ Args
return False
# (2/3) Accept
+ remove = {}
try:
conn, client = self.__sock.accept(
- [conns[family]["sock"] for family in conns], self.__ssig.has_stop
+ [conns[family]["sock"] for family in conns], self.__ssig.has_sock_quit
)
conns[conn.family]["conn"] = conn
conns[conn.family]["remote_addr"] = client[0]
conns[conn.family]["remote_port"] = client[1]
except socket.error as err:
- # On error, remove all bind sockets
- for family in conns:
- self.__log.debug(
- "Removing (family %d/%s) due to: %s",
- family,
- self.__sock.get_family_name(family),
- err,
- )
- self.__sock.close(conns[family]["sock"], self.__sock.get_family_name(family))
- del conns[family]
+ remove = {family: str(err) for family in conns}
+ # On error, remove all bind sockets
+ for family in remove:
+ self.__log.debug(
+ "Removing (family %d/%s) due to: %s",
+ family,
+ self.__sock.get_family_name(family),
+ remove[family],
+ )
+ self.__sock.close(conns[family]["sock"], self.__sock.get_family_name(family))
+ del conns[family]
+ if not conns:
return False
# (3/3) Store connections
@@ -11107,7 +12112,7 @@ Args
# [2/3] Accept
try:
conn, client = self.__sock.accept(
- [self.__conns[family]["sock"] for family in self.__conns], self.__ssig.has_stop
+ [self.__conns[family]["sock"] for family in self.__conns], self.__ssig.has_sock_quit
)
except socket.error:
return False
@@ -11204,7 +12209,7 @@ Returns
# [2/3] Accept
try:
conn, client = self.__sock.accept(
- [self.__conns[family]["sock"] for family in self.__conns], self.__ssig.has_stop
+ [self.__conns[family]["sock"] for family in self.__conns], self.__ssig.has_sock_quit
)
except socket.error:
return False
@@ -11229,7 +12234,7 @@ Returns
Receive and return data from the connected (TCP) or unconnected (UDP) socket.
Returns
-str
+bytes
- Returns received data from connected (TCP) or unconnected (UDP) socket.
Raises
@@ -11242,7 +12247,7 @@ Raises
- Except here when unconnected or connection was forcibly closed.
EOFError
-
-
Except here when upstream has closed the connection.
+Except here when upstream has closed the connection via EOF.
@@ -11250,17 +12255,17 @@ Raises
Expand source code
def receive(self):
- # type: () -> str
+ # type: () -> bytes
"""Receive and return data from the connected (TCP) or unconnected (UDP) socket.
Returns:
- str: Returns received data from connected (TCP) or unconnected (UDP) socket.
+ bytes: Returns received data from connected (TCP) or unconnected (UDP) socket.
Raises:
socket.timeout: Except here to do an action when the socket is not busy.
AttributeError: Except here when current instance has closed itself (Ctrl+c).
socket.error: Except here when unconnected or connection was forcibly closed.
- EOFError: Except here when upstream has closed the connection.
+ EOFError: Except here when upstream has closed the connection via EOF.
"""
# This is required for a UDP server that has no connected clients yet
# and is waiting for data receival for the first time on either IPv4 or IPv6
@@ -11276,9 +12281,9 @@ Raises
0
] # type: List[socket.socket]
# E.g.: ValueError: file descriptor cannot be a negative integer (-1)
- except (ValueError, AttributeError):
- msg = "Connection was closed by self."
- self.__log.warning(msg)
+ except (ValueError, AttributeError) as error:
+ msg = "Connection was closed by self: [1]: {}".format(error)
+ self.__log.debug(msg)
raise AttributeError(msg)
if not conns:
# This is raised for the calling function to determine what to do
@@ -11290,12 +12295,12 @@ Raises
conn = conns[0] # type: socket.socket
try:
# https://manpages.debian.org/buster/manpages-dev/recv.2.en.html
- (byte, addr) = conn.recvfrom(self.__options.bufsize)
+ (data, addr) = conn.recvfrom(self.__options.bufsize)
# [1/5] When closing itself (e.g.: via Ctrl+c and the socket_close() funcs are called)
- except AttributeError:
- msg = "Connection was closed by self."
- self.__log.warning(msg)
+ except AttributeError as error:
+ msg = "Connection was closed by self: [2]: {}".format(error)
+ self.__log.debug(msg)
raise AttributeError(msg)
# [2/5] Connection was forcibly closed
@@ -11303,14 +12308,14 @@ Raises
# [Errno 10054] An existing connection was forcibly closed by the remote host
# [WinError 10054] An existing connection was forcibly closed by the remote host
except (OSError, socket.error) as error:
- self.__log.warning("Connection error: %s", error)
+ self.__log.debug("Connection error: %s", error)
raise socket.error(error)
# [3/5] Upstream (server or client) is gone.
# In TCP, there is no such thing as an empty message, so zero means a peer disconnect.
# In UDP, there is no such thing as a peer disconnect, so zero means an empty datagram.
- if not byte:
- msg = "Upstream has closed the connection."
+ if not data:
+ msg = "EOF: Remote finished sending."
self.__log.info(msg)
raise EOFError(msg)
@@ -11348,7 +12353,6 @@ Raises
}
# [5/5] We have data to process
- data = self.__enc.decode(byte)
self.__log.debug(
"Received %d bytes from %s:%d",
len(data),
@@ -11626,24 +12630,27 @@ Returns
return False
# (2/3) Accept
+ remove = {}
try:
conn, client = self.__sock.accept(
- [conns[family]["sock"] for family in conns], self.__ssig.has_stop
+ [conns[family]["sock"] for family in conns], self.__ssig.has_sock_quit
)
conns[conn.family]["conn"] = conn
conns[conn.family]["remote_addr"] = client[0]
conns[conn.family]["remote_port"] = client[1]
except socket.error as err:
- # On error, remove all bind sockets
- for family in conns:
- self.__log.debug(
- "Removing (family %d/%s) due to: %s",
- family,
- self.__sock.get_family_name(family),
- err,
- )
- self.__sock.close(conns[family]["sock"], self.__sock.get_family_name(family))
- del conns[family]
+ remove = {family: str(err) for family in conns}
+ # On error, remove all bind sockets
+ for family in remove:
+ self.__log.debug(
+ "Removing (family %d/%s) due to: %s",
+ family,
+ self.__sock.get_family_name(family),
+ remove[family],
+ )
+ self.__sock.close(conns[family]["sock"], self.__sock.get_family_name(family))
+ del conns[family]
+ if not conns:
return False
# (3/3) Store connections
@@ -11668,27 +12675,35 @@ Returns
Send data through a connected (TCP) or unconnected (UDP) socket.
Args
-data
: str
+data
: bytes
- The data to send.
Returns
int
- Returns total bytes sent.
+
+Raises
+
+socket.error
+- Except here when unconnected or connection was forcibly closed.
Expand source code
def send(self, data):
- # type: (str) -> int
+ # type: (bytes) -> int
"""Send data through a connected (TCP) or unconnected (UDP) socket.
Args:
- data (str): The data to send.
+ data (bytes): The data to send.
Returns:
int: Returns total bytes sent.
+
+ Raises:
+ socket.error: Except here when unconnected or connection was forcibly closed.
"""
# UDP has some specialities as its socket is unconnected.
# See also recv() for specialities on that side.
@@ -11700,16 +12715,18 @@ Returns
if not self.__active:
self.__log.warning("UDP client has not yet connected. Queueing message")
while not self.__active:
+ if self.__ssig.has_sock_quit():
+ self.__log.trace( # type: ignore
+ "SOCK-QUIT signal ACK in Net.send (while waiting for UDP client)"
+ )
+ return -1
time.sleep(0.01)
curr = 0 # bytes send during one loop iteration
send = 0 # total bytes send
size = len(data) # bytes of data that needs to be send
- byte = self.__enc.encode(data)
- assert size == len(byte), "Encoding messed up string length, might need to do len() after."
# Loop until all bytes have been send
- # TODO: Does this make it impossible to send nullbytes (Ctrl+d)
while send < size:
self.__log.debug(
"Trying to send %d bytes to %s:%d",
@@ -11717,23 +12734,23 @@ Returns
self.__active["remote_addr"],
self.__active["remote_port"],
)
- self.__log.trace("Trying to send: %s", repr(byte)) # type: ignore
+ self.__log.trace("Trying to send: %s", repr(data)) # type: ignore
try:
# Only UDP server has not made a connect() to the socket, all others
# are already connected and need to use send() instead of sendto()
if self.__udp_mode_server:
curr = self.__active["conn"].sendto(
- byte, (self.__active["remote_addr"], self.__active["remote_port"])
+ data, (self.__active["remote_addr"], self.__active["remote_port"])
)
send += curr
else:
- curr = self.__active["conn"].send(byte)
+ curr = self.__active["conn"].send(data)
send += curr
if curr == 0:
self.__log.error("No bytes send during loop round.")
return 0
# Remove 'curr' many bytes from byte for the next round
- byte = byte[curr:]
+ data = data[curr:]
self.__log.debug(
"Sent %d bytes to %s:%d (%d bytes remaining)",
curr,
@@ -11741,9 +12758,9 @@ Returns
self.__active["remote_port"],
size - send,
)
- except (OSError, socket.error) as error:
- self.__log.error("Socket OS Error: %s", error)
- return send
+ except (BrokenPipeError, OSError, socket.error) as error:
+ msg = "Socket send Error: {}".format(error)
+ raise socket.error(msg)
return send
@@ -11758,12 +12775,12 @@ Returns
The same instance of this class will be available to your send and receive scripts
that allow you to exchange data or manipulate themselves. You even have access to the
currently used instance of the networking class to manipulate the active socket.
-As well as to the logger and StopSignal instances.
+As well as to the logger and InterruptHandler instances.
Instantiate the PSE class.
Args
-ssig
: StopSignal
-- Instance of the StopSignal class to force a shutdown.
+ssig
: InterruptHandler
+- Instance InterruptHandler.
net
: IONetwork
- Instance of the current network class to manipulate the socket.
@@ -11777,18 +12794,18 @@ Args
The same instance of this class will be available to your send and receive scripts
that allow you to exchange data or manipulate themselves. You even have access to the
currently used instance of the networking class to manipulate the active socket.
- As well as to the logger and StopSignal instances.
+ As well as to the logger and InterruptHandler instances.
"""
@property
def messages(self):
- # type: () -> Dict[str, List[str]]
- """`Dict[str, List[str]]`: Stores sent and received messages by its thread name."""
+ # type: () -> Dict[str, List[bytes]]
+ """`Dict[str, List[bytes]]`: Stores sent and received messages by its thread name."""
return self.__messages
@messages.setter
def messages(self, value):
- # type: (Dict[str, List[str]]) -> None
+ # type: (Dict[str, List[bytes]]) -> None
self.__messages = value
@property
@@ -11804,8 +12821,8 @@ Args
@property
def ssig(self):
- # type: () -> StopSignal
- """`StopSignal`: Instance of Logging.logger class."""
+ # type: () -> InterruptHandler
+ """`InterruptHandler`: Instance of InterruptHandler class."""
return self.__ssig
@property
@@ -11821,11 +12838,11 @@ Args
return self.__log
def __init__(self, ssig, net):
- # type: (StopSignal, List[IONetwork]) -> None
+ # type: (InterruptHandler, List[IONetwork]) -> None
"""Instantiate the PSE class.
Args:
- ssig (StopSignal): Instance of the StopSignal class to force a shutdown.
+ ssig (InterruptHandler): Instance InterruptHandler.
net (IONetwork): Instance of the current network class to manipulate the socket.
"""
self.__messages = {}
@@ -11852,15 +12869,15 @@ Instance variables
var messages
-
-
Dict[str, List[str]]
: Stores sent and received messages by its thread name.
+Dict[str, List[bytes]]
: Stores sent and received messages by its thread name.
Expand source code
@property
def messages(self):
- # type: () -> Dict[str, List[str]]
- """`Dict[str, List[str]]`: Stores sent and received messages by its thread name."""
+ # type: () -> Dict[str, List[bytes]]
+ """`Dict[str, List[bytes]]`: Stores sent and received messages by its thread name."""
return self.__messages
@@ -11880,15 +12897,15 @@ Instance variables
var ssig
-
-
StopSignal
: Instance of Logging.logger class.
+InterruptHandler
: Instance of InterruptHandler class.
Expand source code
@property
def ssig(self):
- # type: () -> StopSignal
- """`StopSignal`: Instance of Logging.logger class."""
+ # type: () -> InterruptHandler
+ """`InterruptHandler`: Instance of InterruptHandler class."""
return self.__ssig
@@ -11910,13 +12927,15 @@ Instance variables
class Runner
-(pse)
+(ssig, pse)
-
Runner class that takes care about putting everything into threads.
Create a new Runner object.
Args
+ssig
: InterruptHandler
+- Instance of InterruptHandler.
pse
: PSEStore
- Pwncat Scripting Engine store.
@@ -11930,11 +12949,12 @@ Args
# --------------------------------------------------------------------------
# Constructor / Destructor
# --------------------------------------------------------------------------
- def __init__(self, pse):
- # type: (PSEStore) -> None
+ def __init__(self, ssig, pse):
+ # type: (InterruptHandler, PSEStore) -> None
"""Create a new Runner object.
Args:
+ ssig (InterruptHandler): Instance of InterruptHandler.
pse (PSEStore): Pwncat Scripting Engine store.
"""
self.log = logging.getLogger(__name__)
@@ -11956,6 +12976,7 @@ Args
# {"name": "<thread>"}
self.__threads = {} # type: Dict[str, threading.Thread]
+ self.__ssig = ssig
self.__pse = pse
# --------------------------------------------------------------------------
@@ -11998,7 +13019,7 @@ Args
def run_action(
name, # type: str
producer, # type: DsCallableProducer
- consumer, # type: Callable[[str], None]
+ consumer, # type: Callable[[bytes], None]
transformers, # type: List[Transform]
code, # type: Optional[Union[str, bytes, CodeType]]
):
@@ -12048,22 +13069,24 @@ Args
self.log.trace("[%s] Producer Stop", name) # type: ignore
def run_timer(name, action, intvl, ssig, args, **kwargs):
- # type: (str, Callable[..., None], int, StopSignal, Any, Any) -> None
+ # type: (str, Callable[..., None], int, InterruptHandler, Any, Any) -> None
"""Timer run function to be thrown into a thread (Execs periodic tasks).
Args:
name (str): Name for logging output
action (function): Function to be called in a given intervall
intvl (float): Intervall at which the action function will be called
- ssig (StopSignal): Providing has_stop() and raise_stop()
+ ssig (InterruptHandler): Instance of InterruptHandler
args (*args): *args for action func
kwargs (**kwargs): **kwargs for action func
"""
self.log.trace("[%s] Timer Start (exec every %f sec)", name, intvl) # type: ignore
time_last = int(time.time())
while True:
- if ssig.has_stop():
- self.log.trace("Stop signal acknowledged for timer %s", name) # type: ignore
+ if ssig.has_terminate():
+ self.log.trace( # type: ignore
+ "TERMINATE signal ACK for timer action [%s]", name
+ )
return
time_now = int(time.time())
if time_now > time_last + intvl:
@@ -12073,7 +13096,7 @@ Args
time.sleep(0.1)
def run_repeater(name, action, repeat, pause, ssig, args, **kwargs):
- # type: (str, Callable[..., None], int, float, StopSignal, Any, Any) -> None
+ # type: (str, Callable[..., None], int, float, InterruptHandler, Any, Any) -> None
"""Repeater run function to be thrown into a thread (Execs periodic tasks).
Args:
@@ -12081,23 +13104,30 @@ Args
action (function): Function to be called
repeat (int): Repeat the function so many times before quitting
pause (float): Pause between repeated calls
- ssig (StopSignal): Providing has_stop() and raise_stop()
+ ssig (InterruptHandler): Instance of InterruptHandler
args (*args): *args for action func
kwargs (**kwargs): **kwargs for action func
"""
cycles = 1
self.log.trace("Repeater Start (%d/%d)", cycles, repeat) # type: ignore
while cycles <= repeat:
- if ssig.has_stop():
- self.log.trace("Stop signal acknowledged for timer %s", name) # type: ignore
+ if ssig.has_terminate():
+ self.log.trace( # type: ignore
+ "TERMINATE signal ACK for repeater action [%s]", name
+ )
return
self.log.debug("Executing repeated function (%d/%d)", cycles, repeat)
action(*args, **kwargs)
cycles += 1
time.sleep(pause)
- # Start available action in a thread
+ # [1/3] Start available action in a thread
for key in self.__actions:
+ if self.__ssig.has_terminate():
+ self.log.trace( # type: ignore
+ "TERMINATE signal ACK for Runner.run [1]: [%s]", key
+ )
+ break
# Create Thread object
thread = threading.Thread(
target=run_action,
@@ -12110,16 +13140,27 @@ Args
self.__actions[key].code,
),
)
- thread.daemon = False
+ # Daemon threads are easier to kill
+ thread.daemon = True
+
# Add delay if threads cannot be started
+ delay = 0.0
while True:
+ if self.__ssig.has_terminate():
+ self.log.trace( # type: ignore
+ "TERMINATE signal ACK for Runner.run [2]: [%s]", key
+ )
+ break
try:
+ # Start and break the loop upon success to go to the next thread to start
thread.start()
break
except (RuntimeError, Exception): # pylint: disable=broad-except
- time.sleep(0.1)
+ delay += 0.1
+ time.sleep(delay) # Give the system some time to release open fd's
self.__threads[key] = thread
- # Start available timers in a thread
+
+ # [2/3] Start available timers in a thread
for key in self.__timers:
# Create Thread object
thread = threading.Thread(
@@ -12129,14 +13170,15 @@ Args
key,
self.__timers[key].action,
self.__timers[key].intvl,
- self.__timers[key].signal,
+ self.__timers[key].ssig,
self.__timers[key].args,
),
kwargs=self.__timers[key].kwargs,
)
thread.daemon = False
thread.start()
- # Start available repeaters in a thread
+
+ # [3/3] Start available repeaters in a thread
for key in self.__repeaters:
# Create Thread object
thread = threading.Thread(
@@ -12147,7 +13189,7 @@ Args
self.__repeaters[key].action,
self.__repeaters[key].repeat,
self.__repeaters[key].pause,
- self.__repeaters[key].signal,
+ self.__repeaters[key].ssig,
self.__repeaters[key].args,
),
kwargs=self.__repeaters[key].kwargs,
@@ -12155,22 +13197,14 @@ Args
thread.daemon = False
thread.start()
- def check_stop(force):
- # type: (int) -> bool
+ def check_stop():
+ # type: () -> bool
"""Stop threads."""
- for key in self.__threads:
- if not self.__threads[key].is_alive() or force:
- # TODO: How are we gonna call the stop signal now?
- # # [1/3] Inform all threads (inside) about a stop signal.
- # # All threads with non-blocking funcs will be able to stop themselves
- # self.log.trace( # type: ignore
- # "Raise stop signal: StopSignal.stop() for thread [%s]",
- # self.__threads[key].getName(),
- # )
- # self.__actions[key].signal.raise_stop()
- # [2/3] Call external interrupters
- # These will shutdown all blocking functions inside a thread,
- # so that they are actually able to join
+ # TODO: This is a workaround for using daemon threads.
+ # Python < 3.3 is unable to detect Ctrl+c signal during
+ # thread.join() operation in a fast loop (Port scan).
+ if self.__ssig.has_terminate():
+ for key in self.__threads:
for interrupt in self.__actions[key].interrupts:
self.log.trace( # type: ignore
"Call INTERRUPT: %s.%s() for %s",
@@ -12179,24 +13213,53 @@ Args
self.__threads[key].getName(),
)
interrupt()
- # [3/3] All blocking events inside the threads are gone, now join them
- self.log.trace("Joining %s", self.__threads[key].getName()) # type: ignore
- self.__threads[key].join(timeout=0.1)
- # If all threads have died or force is requested, then exit
- if not all([self.__threads[key].is_alive() for key in self.__threads]) or force:
+ return True
+ # If all threads are done, also stop
+ if not all([self.__threads[key].is_alive() for key in self.__threads]):
return True
return False
+ # NOTE: This was for previous non-daemon threads
+ # for key in self.__threads:
+ # if not self.__threads[key].is_alive() or force:
+ # # TODO: How are we gonna call the stop signal now?
+ # # # [1/3] Inform all threads (inside) about a stop signal.
+ # # # All threads with non-blocking funcs will be able to stop themselves
+ # # self.log.trace( # type: ignore
+ # # "Raise stop signal: StopSignal.stop() for thread [%s]",
+ # # self.__threads[key].getName(),
+ # # )
+ # # self.__actions[key].signal.signal_quit()
+ # # [2/3] Call external interrupters
+ # # These will shutdown all blocking functions inside a thread,
+ # # so that they are actually able to join
+ # for interrupt in self.__actions[key].interrupts:
+ # self.log.trace( # type: ignore
+ # "Call INTERRUPT: %s.%s() for %s",
+ # getattr(interrupt, "__self__").__class__.__name__,
+ # interrupt.__name__,
+ # self.__threads[key].getName(),
+ # )
+ # interrupt()
+ # # [3/3] All blocking events inside the threads are gone, now join them
+ # try:
+ # self.log.trace("Joining %s", self.__threads[key].getName())# type: ignore
+ # # NOTE: The thread.join() operating will also block the signal handler
+ # #self.__threads[key].join(timeout=0.1)
+ # self.__threads[key].join()
+ # self.log.trace("Joined %s", self.__threads[key].getName()) # type: ignore
+ # except RuntimeError as err:
+ # print(err)
+ # #pass
+ # # If all threads have died or force is requested, then exit
+ # if not all([self.__threads[key].is_alive() for key in self.__threads]) or force:
+ # return True
+ # return False
- try:
- while True:
- if check_stop(False):
- sys.exit(0)
- # Need a timeout to not skyrocket the CPU
- time.sleep(0.1)
- except KeyboardInterrupt:
- print()
- check_stop(True)
- sys.exit(1)
+ while True:
+ if check_stop():
+ sys.exit(0)
+ # Need a timeout to not skyrocket the CPU
+ time.sleep(0.01)
Thread-safe singleton Socket helper to emulate a module within the same file.
Thread-safe singleton Socket wrapper to emulate a module within the same file.
class Sock(_Singleton("SingletonMeta", (object,), {})): # type: ignore
- """Thread-safe singleton Socket helper to emulate a module within the same file."""
+ """Thread-safe singleton Socket wrapper to emulate a module within the same file."""
def __init__(self):
# type: () -> None
@@ -12971,27 +14076,35 @@ Args
self.__log.error(msg)
raise socket.error(msg)
- def accept(self, sockets, fstop):
- # type: (List[socket.socket], Callable[[], bool]) -> Tuple[socket.socket, Tuple[str, int]]
+ def accept(
+ self,
+ sockets, # type: List[socket.socket]
+ has_quit, # type: Callable[[], bool]
+ select_timeout=0.01, # type: float
+ ):
+ # type: (...) -> Tuple[socket.socket, Tuple[str, int]]
"""Accept a single connection from given list of sockets.
Given sockets must be bound to an addr and listening for connections.
Args:
sock ([socket.socket]): List of sockets IPv4 and/or IPv6 to accept on.
- fstop (Callable[[], bool]): A function that returns True if abort is requested.
+ has_quit (Callable[[], bool]): A function that returns True if abort is requested.
+ select_timeout (float): Timeout to poll sockets for connected clients.
Returns:
- socket.socket: Returns the connection socket (whatever protocol was faster).
+ (socket.socket, str, int): Returns tuple of socket, address and port of client.
Raises:
socket.error: Raised if server cannot accept connection or stop signal is requested.
"""
self.__log.debug("Waiting for TCP client")
while True:
- ssockets = select.select(sockets, [], [], 0.01)[0] # type: List[socket.socket]
- if fstop():
- raise socket.error("StopSignal acknknowledged")
+ ssockets = select.select(sockets, [], [], select_timeout)[
+ 0
+ ] # type: List[socket.socket]
+ if has_quit():
+ raise socket.error("SOCK-QUIT signal ACK for accept(): raised socket.error()")
for sock in ssockets:
try:
conn, addr = sock.accept()
@@ -13029,13 +14142,18 @@ Args
port (int): Port of server to connect to.
Returns:
- Tuple[str,int]: Adress/port tuple of local bin of the client.
+ Tuple[str,int]: Adress/port tuple of local bind of the client.
Raises:
socker.error: If client cannot connect to remote peer or custom bind did not succeed.
"""
- sock_family_name = self.get_family_name(sock.family)
- sock_type_name = self.get_type_name(sock.type)
+ try:
+ # If the socket was already closed elsewhere, it won't have family or type anymore
+ sock_family_name = self.get_family_name(sock.family)
+ sock_type_name = self.get_type_name(sock.type)
+ except AttributeError as error:
+ raise socket.error(error)
+
# Bind to a custom addr/port
if src_addr is not None and src_port is not None:
try:
@@ -13243,7 +14361,7 @@ Static methods
Methods
-def accept(self, sockets, fstop)
+def accept(self, sockets, has_quit, select_timeout=0.01)
-
Accept a single connection from given list of sockets.
@@ -13252,14 +14370,13 @@ Args
sock
: [socket.socket]
- List of sockets IPv4 and/or IPv6 to accept on.
-fstop
: Callable[[], bool]
+has_quit
: Callable[[], bool]
- A function that returns True if abort is requested.
+select_timeout
: float
+- Timeout to poll sockets for connected clients.
Returns
-
-socket.socket
-- Returns the connection socket (whatever protocol was faster).
-
+(socket.socket, str, int): Returns tuple of socket, address and port of client.
Raises
socket.error
@@ -13269,27 +14386,35 @@ Raises
Expand source code
-def accept(self, sockets, fstop):
- # type: (List[socket.socket], Callable[[], bool]) -> Tuple[socket.socket, Tuple[str, int]]
+def accept(
+ self,
+ sockets, # type: List[socket.socket]
+ has_quit, # type: Callable[[], bool]
+ select_timeout=0.01, # type: float
+):
+ # type: (...) -> Tuple[socket.socket, Tuple[str, int]]
"""Accept a single connection from given list of sockets.
Given sockets must be bound to an addr and listening for connections.
Args:
sock ([socket.socket]): List of sockets IPv4 and/or IPv6 to accept on.
- fstop (Callable[[], bool]): A function that returns True if abort is requested.
+ has_quit (Callable[[], bool]): A function that returns True if abort is requested.
+ select_timeout (float): Timeout to poll sockets for connected clients.
Returns:
- socket.socket: Returns the connection socket (whatever protocol was faster).
+ (socket.socket, str, int): Returns tuple of socket, address and port of client.
Raises:
socket.error: Raised if server cannot accept connection or stop signal is requested.
"""
self.__log.debug("Waiting for TCP client")
while True:
- ssockets = select.select(sockets, [], [], 0.01)[0] # type: List[socket.socket]
- if fstop():
- raise socket.error("StopSignal acknknowledged")
+ ssockets = select.select(sockets, [], [], select_timeout)[
+ 0
+ ] # type: List[socket.socket]
+ if has_quit():
+ raise socket.error("SOCK-QUIT signal ACK for accept(): raised socket.error()")
for sock in ssockets:
try:
conn, addr = sock.accept()
@@ -13416,7 +14541,7 @@ Args
Returns
Tuple[str,int]
-- Adress/port tuple of local bin of the client.
+- Adress/port tuple of local bind of the client.
Raises
@@ -13448,13 +14573,18 @@ Raises
port (int): Port of server to connect to.
Returns:
- Tuple[str,int]: Adress/port tuple of local bin of the client.
+ Tuple[str,int]: Adress/port tuple of local bind of the client.
Raises:
socker.error: If client cannot connect to remote peer or custom bind did not succeed.
"""
- sock_family_name = self.get_family_name(sock.family)
- sock_type_name = self.get_type_name(sock.type)
+ try:
+ # If the socket was already closed elsewhere, it won't have family or type anymore
+ sock_family_name = self.get_family_name(sock.family)
+ sock_type_name = self.get_type_name(sock.type)
+ except AttributeError as error:
+ raise socket.error(error)
+
# Bind to a custom addr/port
if src_addr is not None and src_port is not None:
try:
@@ -13850,82 +14980,13 @@ Raises
-
-class StopSignal
-
--
-
Provide a simple boolean switch.
-Create a StopSignal instance.
-
-
-Expand source code
-
-class StopSignal(object):
- """Provide a simple boolean switch."""
-
- # --------------------------------------------------------------------------
- # Constructor
- # --------------------------------------------------------------------------
- def __init__(self):
- # type: () -> None
- """Create a StopSignal instance."""
- self.__stop = False
-
- # --------------------------------------------------------------------------
- # Public Functions
- # --------------------------------------------------------------------------
- def has_stop(self):
- # type: () -> bool
- """Check if a stop signal has been raised."""
- return self.__stop
-
- def raise_stop(self):
- # type: () -> None
- """Raise a stop signal."""
- self.__stop = True
-
-Methods
-
-
-def has_stop(self)
-
--
-
Check if a stop signal has been raised.
-
-
-Expand source code
-
-def has_stop(self):
- # type: () -> bool
- """Check if a stop signal has been raised."""
- return self.__stop
-
-
-
-def raise_stop(self)
-
--
-
Raise a stop signal.
-
-
-Expand source code
-
-def raise_stop(self):
- # type: () -> None
- """Raise a stop signal."""
- self.__stop = True
-
-
-
-
class StringEncoder
-
Takes care about Python 2/3 string encoding/decoding.
This allows to parse all string/byte values internally between all
-classes or functions as strings to keep full Python 2/3 compat.
-Create a StringEncoder instance which converts str/bytes according to Python version.
+classes or functions as strings to keep full Python 2/3 compat.
Expand source code
@@ -13937,63 +14998,84 @@ Methods
classes or functions as strings to keep full Python 2/3 compat.
"""
- # --------------------------------------------------------------------------
- # Constructor
- # --------------------------------------------------------------------------
- def __init__(self):
- # type: () -> None
- """Create a StringEncoder instance which converts str/bytes according to Python version."""
- self.__py3 = sys.version_info >= (3, 0) # type: bool
-
- # https://stackoverflow.com/questions/606191/27527728#27527728
- self.__codec = "cp437"
- self.__fallback = "latin-1"
+ CODECS = [
+ "utf-8",
+ "cp437",
+ "latin-1",
+ ]
# --------------------------------------------------------------------------
- # Public Functions
- # --------------------------------------------------------------------------
- def encode(self, data):
+ # Class methods
+ # --------------------------------------------------------------------------
+ @classmethod
+ def rstrip(cls, data, search=None):
+ # type: (Union[bytes, str], Optional[str]) -> Union[bytes, str]
+ """Implementation of rstring which works on bytes or strings."""
+ # We have a bytes object in Python3
+ if sys.version_info >= (3, 0) and type(data) is not str:
+ # Strip whitespace
+ if search is None:
+ while True:
+ new = data
+ new = cls.rstrip(new, " ")
+ new = cls.rstrip(new, "\n")
+ new = cls.rstrip(new, "\r")
+ new = cls.rstrip(new, "\t")
+ # Loop until no more changes occur
+ if new == data:
+ return new
+ else:
+ bsearch = StringEncoder.encode(search)
+ while data[-1:] == bsearch:
+ data = data[:-1]
+ return data
+
+ # Use native function
+ if search is None:
+ return data.rstrip()
+ return data.rstrip(search) # type: ignore
+
+ @classmethod
+ def encode(cls, data):
# type: (str) -> bytes
"""Convert string into a byte type for Python3."""
- if self.__py3:
- try:
- return data.encode(self.__codec)
- except UnicodeEncodeError:
- # TODO: Add logging
- return data.encode(self.__fallback)
+ if sys.version_info >= (3, 0):
+ for codec in cls.CODECS:
+ # On the last codec, do not catch the exception and let it trigger if it fails
+ if codec == cls.CODECS[-1]:
+ return data.encode(codec)
+ try:
+ return data.encode(codec)
+ except UnicodeEncodeError:
+ pass
return data # type: ignore
- def decode(self, data):
+ @classmethod
+ def decode(cls, data):
# type: (bytes) -> str
"""Convert bytes into a string type for Python3."""
- if self.__py3:
- return data.decode(self.__codec)
- return data # type: ignore
-
- def base64_encode(self, data):
- # type: (str) -> str
- """Convert string into a base64 encoded string."""
- return self.decode(base64.b64encode(self.encode(data)))
+ if sys.version_info >= (3, 0):
+ for codec in cls.CODECS:
+ # On the last codec, do not catch the exception and let it trigger if it fails
+ if codec == cls.CODECS[-1]:
+ return data.decode(codec)
+ try:
+ return data.decode(codec)
+ except UnicodeDecodeError:
+ pass
+ return data # type: ignore
-def base64_encode(self, data)
-
var CODECS
Convert string into a base64 encoded string.
def base64_encode(self, data):
- # type: (str) -> str
- """Convert string into a base64 encoded string."""
- return self.decode(base64.b64encode(self.encode(data)))
-
-def decode(self, data)
+def decode(data)
Convert bytes into a string type for Python3.
def decode(self, data):
+@classmethod
+def decode(cls, data):
# type: (bytes) -> str
"""Convert bytes into a string type for Python3."""
- if self.__py3:
- return data.decode(self.__codec)
+ if sys.version_info >= (3, 0):
+ for codec in cls.CODECS:
+ # On the last codec, do not catch the exception and let it trigger if it fails
+ if codec == cls.CODECS[-1]:
+ return data.decode(codec)
+ try:
+ return data.decode(codec)
+ except UnicodeDecodeError:
+ pass
return data # type: ignore
-def encode(self, data)
+def encode(data)
Convert string into a byte type for Python3.
def encode(self, data):
+@classmethod
+def encode(cls, data):
# type: (str) -> bytes
"""Convert string into a byte type for Python3."""
- if self.__py3:
- try:
- return data.encode(self.__codec)
- except UnicodeEncodeError:
- # TODO: Add logging
- return data.encode(self.__fallback)
+ if sys.version_info >= (3, 0):
+ for codec in cls.CODECS:
+ # On the last codec, do not catch the exception and let it trigger if it fails
+ if codec == cls.CODECS[-1]:
+ return data.encode(codec)
+ try:
+ return data.encode(codec)
+ except UnicodeEncodeError:
+ pass
return data # type: ignore
+def rstrip(data, search=None)
+
Implementation of rstring which works on bytes or strings.
@classmethod
+def rstrip(cls, data, search=None):
+ # type: (Union[bytes, str], Optional[str]) -> Union[bytes, str]
+ """Implementation of rstring which works on bytes or strings."""
+ # We have a bytes object in Python3
+ if sys.version_info >= (3, 0) and type(data) is not str:
+ # Strip whitespace
+ if search is None:
+ while True:
+ new = data
+ new = cls.rstrip(new, " ")
+ new = cls.rstrip(new, "\n")
+ new = cls.rstrip(new, "\r")
+ new = cls.rstrip(new, "\t")
+ # Loop until no more changes occur
+ if new == data:
+ return new
+ else:
+ bsearch = StringEncoder.encode(search)
+ while data[-1:] == bsearch:
+ data = data[:-1]
+ return data
+
+ # Use native function
+ if search is None:
+ return data.rstrip()
+ return data.rstrip(search) # type: ignore
+
@@ -14179,11 +15311,14 @@ Args
# --------------------------------------------------------------------------
@abstractmethod
def transform(self, data):
- # type: (str) -> str
+ # type: (bytes) -> bytes
"""Implement a transformer function which transforms a string..
+ Args:
+ data (bytes): data to be transformed.
+
Returns:
- str: The transformed string.
+ bytes: The transformed string.
"""
Implement a transformer function which transforms a string..
+data
: bytes
str
bytes
@abstractmethod
def transform(self, data):
- # type: (str) -> str
+ # type: (bytes) -> bytes
"""Implement a transformer function which transforms a string..
+ Args:
+ data (bytes): data to be transformed.
+
Returns:
- str: The transformed string.
+ bytes: The transformed string.
"""
+class TransformHttpPack
+(opts)
+
Implement a transformation to pack data into HTTP packets.
+Set specific options for this transformer.
+opts
: DsTransformLinefeed
class TransformHttpPack(Transform):
+ """Implement a transformation to pack data into HTTP packets."""
+
+ # --------------------------------------------------------------------------
+ # Constructor / Destructor
+ # --------------------------------------------------------------------------
+ def __init__(self, opts):
+ # type: (Dict[str, str]) -> None
+ """Set specific options for this transformer.
+
+ Args:
+ opts (DsTransformLinefeed): Transformer options.
+
+ """
+ super(TransformHttpPack, self).__init__()
+ self.__opts = opts
+ self.__log = logging.getLogger(__name__)
+
+ assert "reply" in opts
+ assert opts["reply"] in ["request", "response"]
+
+ # Initial default header
+ self.__headers = [
+ "Accept-Charset: utf-8",
+ ]
+
+ self.__response_headers_sent = False
+
+ # --------------------------------------------------------------------------
+ # Public Functions
+ # --------------------------------------------------------------------------
+ def transform(self, data):
+ # type: (bytes) -> bytes
+ """Wrap data into a HTTP packet.
+
+ Returns:
+ bytes: The wrapped string.
+ """
+ request_header = [
+ "POST / HTTP/1.1",
+ "Host: {}".format(self.__opts["host"]),
+ "User-Agent: pwncat",
+ "Accept: */*",
+ "Conent-Length: {}".format(len(data)),
+ "Content-Type: text/plain; charset=UTF-8",
+ ]
+ response_header = [
+ "HTTP/1.1 200 OK",
+ "Date: {}".format(self.__get_date()),
+ "Server: pwncat",
+ "Conent-Length: {}".format(len(data)),
+ "Connection: close",
+ ]
+
+ self.__response_headers_sent = True
+
+ if self.__opts["reply"] == "request":
+ header = StringEncoder.encode(
+ "\n".join(request_header) + "\n" + "\n".join(self.__headers) + "\n\n"
+ )
+ else:
+ header = StringEncoder.encode(
+ "\n".join(response_header) + "\n" + "\n".join(self.__headers) + "\n\n"
+ )
+ return header + data
+
+ # --------------------------------------------------------------------------
+ # Private Functions
+ # --------------------------------------------------------------------------
+ def __get_date(self): # pylint: disable=no-self-use
+ # type: () -> str
+ now = datetime.utcnow()
+ weekday = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"][now.weekday()]
+ month = [
+ "Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec",
+ ][now.month - 1]
+ return "%s, %02d %s %04d %02d:%02d:%02d GMT" % (
+ weekday,
+ now.day,
+ month,
+ now.year,
+ now.hour,
+ now.minute,
+ now.second,
+ )
+
+def transform(self, data)
+
Wrap data into a HTTP packet.
+bytes
def transform(self, data):
+ # type: (bytes) -> bytes
+ """Wrap data into a HTTP packet.
+
+ Returns:
+ bytes: The wrapped string.
+ """
+ request_header = [
+ "POST / HTTP/1.1",
+ "Host: {}".format(self.__opts["host"]),
+ "User-Agent: pwncat",
+ "Accept: */*",
+ "Conent-Length: {}".format(len(data)),
+ "Content-Type: text/plain; charset=UTF-8",
+ ]
+ response_header = [
+ "HTTP/1.1 200 OK",
+ "Date: {}".format(self.__get_date()),
+ "Server: pwncat",
+ "Conent-Length: {}".format(len(data)),
+ "Connection: close",
+ ]
+
+ self.__response_headers_sent = True
+
+ if self.__opts["reply"] == "request":
+ header = StringEncoder.encode(
+ "\n".join(request_header) + "\n" + "\n".join(self.__headers) + "\n\n"
+ )
+ else:
+ header = StringEncoder.encode(
+ "\n".join(response_header) + "\n" + "\n".join(self.__headers) + "\n\n"
+ )
+ return header + data
+
+class TransformHttpUnpack
+(opts)
+
Implement a transformation to unpack data from HTTP packets.
+Set specific options for this transformer.
+opts
: DsTransformLinefeed
class TransformHttpUnpack(Transform):
+ """Implement a transformation to unpack data from HTTP packets."""
+
+ # --------------------------------------------------------------------------
+ # Constructor / Destructor
+ # --------------------------------------------------------------------------
+ def __init__(self, opts):
+ # type: (Dict[str, str]) -> None
+ """Set specific options for this transformer.
+
+ Args:
+ opts (DsTransformLinefeed): Transformer options.
+
+ """
+ super(TransformHttpUnpack, self).__init__()
+ self.__opts = opts
+ self.__log = logging.getLogger(__name__)
+
+ # --------------------------------------------------------------------------
+ # Public Functions
+ # --------------------------------------------------------------------------
+ def transform(self, data):
+ # type: (bytes) -> bytes
+ """Unwrap data from a HTTP packet.
+
+ Returns:
+ str: The wrapped string.
+ """
+ request = StringEncoder.encode(r"^(GET|HEAD|POST|PUT|DELETE|CONNECT|OPTIONS|TRACE|PATCH)")
+ response = StringEncoder.encode(r"^HTTP/[.0-9]+")
+
+ # Did not receive a valid HTTP request, so we return the original untransformed message
+ if not (re.match(request, data) or re.match(response, data)):
+ return data
+
+ body = StringEncoder.encode(r"(\r\n\r\n|\n\n)(.*)")
+ match = re.search(body, data)
+
+ # Check if we can separate headers and body
+ if match is None or len(match.group()) < 2:
+ return data
+ return match.group(2)
+
+def transform(self, data)
+
Unwrap data from a HTTP packet.
+str
def transform(self, data):
+ # type: (bytes) -> bytes
+ """Unwrap data from a HTTP packet.
+
+ Returns:
+ str: The wrapped string.
+ """
+ request = StringEncoder.encode(r"^(GET|HEAD|POST|PUT|DELETE|CONNECT|OPTIONS|TRACE|PATCH)")
+ response = StringEncoder.encode(r"^HTTP/[.0-9]+")
+
+ # Did not receive a valid HTTP request, so we return the original untransformed message
+ if not (re.match(request, data) or re.match(response, data)):
+ return data
+
+ body = StringEncoder.encode(r"(\r\n\r\n|\n\n)(.*)")
+ match = re.search(body, data)
+
+ # Check if we can separate headers and body
+ if match is None or len(match.group()) < 2:
+ return data
+ return match.group(2)
+
class TransformLinefeed
(opts)
@@ -14277,7 +15715,7 @@ Args
# Public Functions
# --------------------------------------------------------------------------
def transform(self, data):
- # type: (str) -> str
+ # type: (bytes) -> bytes
"""Transform linefeeds to CRLF, LF or CR if requested.
Returns:
@@ -14289,39 +15727,39 @@ Args
# ? -> No line feeds
if self.__opts.crlf == "no":
- if data.endswith("\r\n"):
+ if data[-2:] == StringEncoder.encode("\r\n"):
self.log.debug("Removing CRLF")
return data[:-2]
- if data.endswith("\n"):
+ if data[-1:] == StringEncoder.encode("\n"):
self.log.debug("Removing LF")
return data[:-1]
- if data.endswith("\r"):
+ if data[-1:] == StringEncoder.encode("\r"):
self.log.debug("Removing CR")
return data[:-1]
# ? -> CRLF
- if self.__opts.crlf == "crlf" and not data.endswith("\r\n"):
- if data.endswith("\n"):
+ if self.__opts.crlf == "crlf" and data[-2:] != StringEncoder.encode("\r\n"):
+ if data[-1:] == StringEncoder.encode("\n"):
self.log.debug("Replacing LF with CRLF")
- return data[:-1] + "\r\n"
- if data.endswith("\r"):
+ return data[:-1] + StringEncoder.encode("\r\n")
+ if data[-1:] == StringEncoder.encode("\r"):
self.log.debug("Replacing CR with CRLF")
- return data[:-1] + "\r\n"
+ return data[:-1] + StringEncoder.encode("\r\n")
# ? -> LF
if self.__opts.crlf == "lf":
- if data.endswith("\r\n"):
+ if data[-2:] == StringEncoder.encode("\r\n"):
self.log.debug("Replacing CRLF with LF")
- return data[:-2] + "\n"
- if data.endswith("\r"):
+ return data[:-2] + StringEncoder.encode("\n")
+ if data[-1:] == StringEncoder.encode("\r"):
self.log.debug("Replacing CR with LF")
- return data[:-1] + "\n"
+ return data[:-1] + StringEncoder.encode("\n")
# ? -> CR
if self.__opts.crlf == "cr":
- if data.endswith("\r\n"):
+ if data[-2:] == StringEncoder.encode("\r\n"):
self.log.debug("Replacing CRLF with CR")
- return data[:-2] + "\r"
- if data.endswith("\n"):
+ return data[:-2] + StringEncoder.encode("\r")
+ if data[-1:] == StringEncoder.encode("\n"):
self.log.debug("Replacing LF with CR")
- return data[:-1] + "\r"
+ return data[:-1] + StringEncoder.encode("\r")
# Otherwise just return it as it is
return data
@@ -14348,7 +15786,7 @@ def transform(self, data):
- # type: (str) -> str
+ # type: (bytes) -> bytes
"""Transform linefeeds to CRLF, LF or CR if requested.
Returns:
@@ -14360,39 +15798,39 @@ Returns
# ? -> No line feeds
if self.__opts.crlf == "no":
- if data.endswith("\r\n"):
+ if data[-2:] == StringEncoder.encode("\r\n"):
self.log.debug("Removing CRLF")
return data[:-2]
- if data.endswith("\n"):
+ if data[-1:] == StringEncoder.encode("\n"):
self.log.debug("Removing LF")
return data[:-1]
- if data.endswith("\r"):
+ if data[-1:] == StringEncoder.encode("\r"):
self.log.debug("Removing CR")
return data[:-1]
# ? -> CRLF
- if self.__opts.crlf == "crlf" and not data.endswith("\r\n"):
- if data.endswith("\n"):
+ if self.__opts.crlf == "crlf" and data[-2:] != StringEncoder.encode("\r\n"):
+ if data[-1:] == StringEncoder.encode("\n"):
self.log.debug("Replacing LF with CRLF")
- return data[:-1] + "\r\n"
- if data.endswith("\r"):
+ return data[:-1] + StringEncoder.encode("\r\n")
+ if data[-1:] == StringEncoder.encode("\r"):
self.log.debug("Replacing CR with CRLF")
- return data[:-1] + "\r\n"
+ return data[:-1] + StringEncoder.encode("\r\n")
# ? -> LF
if self.__opts.crlf == "lf":
- if data.endswith("\r\n"):
+ if data[-2:] == StringEncoder.encode("\r\n"):
self.log.debug("Replacing CRLF with LF")
- return data[:-2] + "\n"
- if data.endswith("\r"):
+ return data[:-2] + StringEncoder.encode("\n")
+ if data[-1:] == StringEncoder.encode("\r"):
self.log.debug("Replacing CR with LF")
- return data[:-1] + "\n"
+ return data[:-1] + StringEncoder.encode("\n")
# ? -> CR
if self.__opts.crlf == "cr":
- if data.endswith("\r\n"):
+ if data[-2:] == StringEncoder.encode("\r\n"):
self.log.debug("Replacing CRLF with CR")
- return data[:-2] + "\r"
- if data.endswith("\n"):
+ return data[:-2] + StringEncoder.encode("\r")
+ if data[-1:] == StringEncoder.encode("\n"):
self.log.debug("Replacing LF with CR")
- return data[:-1] + "\r"
+ return data[:-1] + StringEncoder.encode("\r")
# Otherwise just return it as it is
return data
@@ -14446,15 +15884,15 @@ def transform(self, data):
- # type: (str) -> str
+ # type: (bytes) -> bytes
"""Raise a stop signal upon receiving the safeword.
Returns:
str: The string as it is without changes
"""
- if self.__opts.safeword in data:
- self.__log.info("Received safeword: raising stop signal.")
- self.__opts.ssig.raise_stop()
+ if StringEncoder.encode(self.__opts.safeword) in data:
+ self.log.trace("TERMINATE signal REQ in TransformSafeword.transform") # type: ignore
+ self.__opts.ssig.raise_terminate()
return data
@@ -14604,6 +16042,7 @@ DsIOS
@@ -14624,7 +16063,7 @@ DsR
kwargs
pause
repeat
-signal
+ssig
@@ -14634,7 +16073,7 @@ DsRunnerT
args
intvl
kwargs
-signal
+ssig
@@ -14714,6 +16153,19 @@ IOStdinSt
+InterruptHandler
+
+
+
Net
close_bind_sock
@@ -14763,18 +16215,12 @@ Sock
-StopSignal
-
-has_stop
-raise_stop
-
-
-
StringEncoder
@@ -14793,6 +16239,18 @@ Transform
+TransformHttpPack
+
+transform
+
+
+
+TransformHttpUnpack
+
+transform
+
+
+
TransformLinefeed
transform
diff --git a/docs/pwncat.man.html b/docs/pwncat.man.html
index 350452f8..99b0dbc2 100644
--- a/docs/pwncat.man.html
+++ b/docs/pwncat.man.html
@@ -155,6 +155,21 @@ DESCRIPTION
Do not resolve DNS.
+
+−−send−on−eof
+
+Buffer data received on stdin
+until EOF and send everything in one chunk.
+
+
+−−no−shutdown
+
+Do not shutdown into
+half−duplex mode. If this option is passed, pwncat
+won’t invoke shutdown on a socket after seeing EOF on
+stdin. This is provided for backward−compatibility
+with OpenBSD netcat, which exhibits this behavior.
+
−v,
−−verbose
diff --git a/docs/pwncat.type.html b/docs/pwncat.type.html
index 9cbc8889..3396a6b8 100644
--- a/docs/pwncat.type.html
+++ b/docs/pwncat.type.html
@@ -15,12 +15,20 @@ Mypy Type Check Coverage Summary
Total
6.44% imprecise
+<<<<<<< HEAD
5283 LOC
+=======
+5687 LOC
+>>>>>>> Heavy refactoring
bin/pwncat
6.44% imprecise
+<<<<<<< HEAD
5283 LOC
+=======
+5687 LOC
+>>>>>>> Heavy refactoring