diff --git a/.github/workflows/test-linux.yml b/.github/workflows/test-linux.yml index 439a18e1..dd227e8f 100644 --- a/.github/workflows/test-linux.yml +++ b/.github/workflows/test-linux.yml @@ -142,3 +142,15 @@ jobs: shell: bash run: | make test-options-tcp_server_keep_open + + # ------------------------------------------------------------ + # Tests: Modes + # ------------------------------------------------------------ + + ### + ### Port Forward + ### + - name: "[MODES] (TCP) Port Forward" + shell: bash + run: | + make test-modes-forwawrd_tcp-client_make_http_request diff --git a/.github/workflows/test-macos.yml b/.github/workflows/test-macos.yml index 9291b8cc..d40ea902 100644 --- a/.github/workflows/test-macos.yml +++ b/.github/workflows/test-macos.yml @@ -142,3 +142,15 @@ jobs: shell: bash run: | make test-options-tcp_server_keep_open + + # ------------------------------------------------------------ + # Tests: Modes + # ------------------------------------------------------------ + + ### + ### Port Forward + ### + - name: "[MODES] (TCP) Port Forward" + shell: bash + run: | + make test-modes-forwawrd_tcp-client_make_http_request diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index dde65fa4..0f599f91 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -156,3 +156,15 @@ jobs: shell: bash run: | make test-options-tcp_server_keep_open + + # ------------------------------------------------------------ + # Tests: Modes + # ------------------------------------------------------------ + + ### + ### Port Forward + ### + - name: "[MODES] (TCP) Port Forward" + shell: bash + run: | + make test-modes-forwawrd_tcp-client_make_http_request diff --git a/CHANGELOG.md b/CHANGELOG.md index fb43cab1..ebc606af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,20 @@ ## Unreleased +## Release 0.0.7-alpha + +#### Fixed +- Fixed `-L`/`--local` mode to now persist multiple requests +- Fixed `-C`/`--crlf` Only replace `\n` with `\r\n` if `\n` exists and don't blindly add. + +#### Added +- Integration tests for `L`/`--local` mode + +#### Changed +- Plugin architecture has been heavily refactored to make it easier to add new plugins +- Improved logging + + ## Release 0.0.6-alpha #### Fixed diff --git a/Makefile b/Makefile index 88be3db0..969d82a4 100644 --- a/Makefile +++ b/Makefile @@ -137,6 +137,7 @@ test: test-basics-client-udp_send_comand_to_server test: test-options-client-tcp_nodns test: test-options-client-udp_nodns test: test-options-tcp_server_keep_open +test: test-modes-forwawrd_tcp-client_make_http_request # ------------------------------------------------------------------------------------------------- @@ -199,6 +200,13 @@ test-options-tcp_server_keep_open: tests/302-options-tcp_server_keep_open.sh "" +# ------------------------------------------------------------------------------------------------- +# Test Targets: Modes +# ------------------------------------------------------------------------------------------------- +test-modes-forwawrd_tcp-client_make_http_request: + tests/400-mode-forward_tcp-client_make_http_request.sh "" + + # ------------------------------------------------------------------------------------------------- # Documentation # ------------------------------------------------------------------------------------------------- diff --git a/README.md b/README.md index c7a9ce72..df6046b9 100644 --- a/README.md +++ b/README.md @@ -187,7 +187,7 @@ mode arguments: optional arguments: -e cmd, --exec cmd Execute shell command. Only for connect or listen mode. - -C, --crlf Send CRLF line-endings in connect mode (default: LF) + -C, --crlf Replace LF with CRLF from stdin (default: don't) -n, --nodns Do not resolve DNS. -u, --udp Use UDP for the connection instead of TCP. -v, --verbose Be verbose and print info to stderr. Use -v, -vv, -vvv @@ -293,8 +293,8 @@ advanced arguments: --safe-word str All modes: If pwncat is started with this argument, it will shut down as soon as it receives the specified string. The - --keep (server) or --reconn (client) options will be - ignored and it won't listen again or reconnect to you. + --keep-open (server) or --reconn (client) options will + be ignored and it won't listen again or reconnect to you. Use a very unique string to not have it shut down accidentally by other input. diff --git a/bin/pwncat b/bin/pwncat index 5697ccc3..9e9fe284 100755 --- a/bin/pwncat +++ b/bin/pwncat @@ -3,6 +3,7 @@ # # TODO: where to use sys.stdout.flush ?? +# TODO: change docstring to this format: https://sphinxcontrib-napoleon.readthedocs.io/en/latest/ # # Behaviour Server # 1. (tty and non-tty mode) TCP Server should always quit if client disconnects @@ -41,20 +42,15 @@ if os.name != "nt": APPNAME = "pwncat" APPREPO = "https://github.com/cytopia/pwncat" -VERSION = "0.0.6-alpha" - -# Global variable to be used within threads to determine -# if they should exit or not. -# TODO: check if this is best-practice -THREAD_TERMINATE = False +VERSION = "0.0.7-alpha" # Custom loglevel numer for TRACE LOGLEVEL_TRACE_NUM = 9 # Default timeout for timeout-based sys.stdin and socket.recv -TIMEOUT_READ_STDIN = 0.3 -TIMEOUT_RECV_SOCKET = 0.3 -TIMEOUT_RECV_SOCKET_RETRY = 1 +TIMEOUT_READ_STDIN = 0.1 +TIMEOUT_RECV_SOCKET = 0.1 +TIMEOUT_RECV_SOCKET_RETRY = 2 # ################################################################################################# @@ -85,13 +81,13 @@ class StringEncoder(object): def encode(self, data): """Convert string into a byte type for Python3.""" if self.py3: - data = data.encode("cp437") + data = data.encode(self.codec) return data def decode(self, data): """Convert bytes into a string type for Python3.""" if self.py3: - data = data.decode("cp437") + data = data.decode(self.codec) return data @@ -141,18 +137,13 @@ class AbstractSocket(object): "safe_word": False, # Once this is received, the application quits } - # In case the server is running in UDP mode, - # it must wait for the client to connect in order + # Store the address of the remote end. + # If we are in server role and running in UDP mode, + # it must wait for the client to connect first in order # to retrieve its addr and port in order to be able # to send data back to it. - udp_client_addr = None - udp_client_port = None - - # For client role only - # Store the address and port of the remote server to connect to. - # This is required for self.connect() - remote_addr = None remote_addr = None + remote_port = None # ------------------------------------------------------------------------------ # Constructor / Destructor @@ -303,12 +294,6 @@ class AbstractSocket(object): # Get around the "[Errno 98] Address already in use" error, if the socket is still in wait # we instruct it to reuse the address anyway. self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - # TODO: Not sure if SO_REUSEPORT is also required - # try: - # self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) - # except AttributeError: - # # Not available on Windows (and maybe others) - # self.log.debug("socket.SO_REUSEPORT is not available on your platform") def bind(self, addr, port): """Bind the socket to an address.""" @@ -334,6 +319,8 @@ class AbstractSocket(object): self.log.debug("Waiting for TCP client") self.conn, client = self.sock.accept() addr, port = client + self.remote_addr = addr + self.remote_port = port self.log.info("Client connected from {}:{}".format(addr, port)) except (socket.gaierror, socket.error) as error: self.log.error("Accept failed: {}".format(error)) @@ -358,10 +345,10 @@ class AbstractSocket(object): """Send data.""" # In case of sending data back to an udp client we need to wait # until the client has first connected and told us its addr/port - if self.options["udp"] and self.udp_client_addr is None and self.udp_client_port is None: - self.log.info("Waiting for UDP client to connect") - while self.udp_client_addr is None and self.udp_client_port is None: - time.sleep(0.2) # Less wastefull than using 'pass' + if self.options["udp"] and self.remote_addr is None and self.remote_port is None: + self.log.warning("UDP client has not yet connected. Queueing message") + while self.remote_addr is None and self.remote_port is None: + time.sleep(0.1) # Less wastefull than using 'pass' curr = 0 # bytes send during one loop iteration send = 0 # total bytes send @@ -372,9 +359,14 @@ class AbstractSocket(object): # Loop until all bytes have been send while send < size: try: - self.log.trace("Trying to send {} bytes".format(size - send)) + self.log.debug( + "Trying to send {} bytes to {}:{}".format( + size - send, self.remote_addr, self.remote_port + ) + ) + self.log.trace("Trying to send: {}".format(repr(data))) if self.options["udp"]: - curr = self.conn.sendto(data, (self.udp_client_addr, self.udp_client_port)) + curr = self.conn.sendto(data, (self.remote_addr, self.remote_port)) send += curr else: curr = self.conn.send(data) @@ -384,7 +376,11 @@ class AbstractSocket(object): return # Remove 'curr' many bytes from data for the next round data = data[curr:] - self.log.trace("Send {} bytes ({} bytes remaining)".format(curr, size - send)) + self.log.debug( + "Sent {} bytes to {}:{} ({} bytes remaining)".format( + curr, self.remote_addr, self.remote_port, size - send + ) + ) except socket.error as error: if error.errno == socket.errno.EPIPE: self.log.error("TODO:Add desc. Socket error({}): {}".format(error.errno, error)) @@ -401,19 +397,28 @@ class AbstractSocket(object): self.log.error("Socket OS Error: {}".format(error)) return - def receive(self): - """Generator function to receive data endlessly by yielding it.""" + def receive(self, ssig): + """ + Generator function to receive data endlessly by yielding it. + + :param function interrupter: A Func that returns True/False to tell us to stop or not. + """ # Set current receive timeout self.conn.settimeout(self.recv_timeout) - self.log.trace("Socket Timeout: {}".format(self.recv_timeout_retry)) + self.log.trace("Socket Timeout: {}".format(self.recv_timeout)) # Counts how many times we had a ready timeout for later to decide # if we exceeded maximum retires curr_recv_timeout_retry = 0 while True: + # Ensure to signal that we do not stop receiving data + # if ssig.has_stop(): + # self.log.debug("Interrupt has been requested for receive()") + # return if self.conn is None: self.log.error("Exit. Socket is gone in receive()") + ssig.raise_stop() return # Non-blocking socket with timeout. If the timeout threshold is hit, @@ -423,20 +428,27 @@ class AbstractSocket(object): # https://manpages.debian.org/buster/manpages-dev/recv.2.en.html (byte, addr) = self.conn.recvfrom(self.options["bufsize"]) - # [1/5] NOTE: This is the place where we can do any checks in between reads as the + # [1/5] Finished receiving all data + # NOTE: This is the place where we can do any checks in between reads as the # socket has been changed from blocking to time-out based. + # NOTE: This is also the place, where we quit in case --wait was specified. except socket.timeout: - # No other thread has terminated yet, and thus not asked us to quit. - # so we can continue waiting for input on the socket - if not THREAD_TERMINATE: + # Let's ask the interrupter() function if we should terminate? + if not ssig.has_stop(): # No action required, continue the loop and read again. continue + self.log.debug("Interrupt has been requested for receive()") # Other threads are done. Let's try to read a few more times before # returning and ending this function (might be data left) if curr_recv_timeout_retry < self.recv_timeout_retry: - self.log.trace("RECV EOF TIMEOUT: AND THREAD_TERMINATE REQUESTED") + self.log.trace( + "Final socket read: {}/{} before quitting.".format( + curr_recv_timeout_retry, self.recv_timeout_retry + ) + ) curr_recv_timeout_retry += 1 continue + ssig.raise_stop() return # [2/5] Connection was forcibly closed @@ -453,12 +465,20 @@ class AbstractSocket(object): # [3/5] TODO: Still need to figure out what this error is and when it is thrown except AttributeError as error: self.log.error("TODO: What happens here?Attribute Receive Error: {}".format(error)) + ssig.raise_stop() return # We're receiving data again, so let's reset the retry/terminate counter # The counter is incremented in 'except socket.timeout' above. curr_recv_timeout_retry = 0 + # If we're receiving data from a UDP client + # we can firstly/finally set its addr/port in order + # to send data back to it (see send() function) + if self.options["udp"]: + self.remote_addr, self.remote_port = addr + self.log.debug("Client connected: {}:{}".format(self.remote_addr, self.remote_port)) + # [4/5] Upstream (server or client) is gone. Do we reconnect or quit? if not byte: self.log.trace("Socket: Empty data received or otherwise caught.") @@ -466,7 +486,7 @@ class AbstractSocket(object): if self.role == "server": # Yay, we want to continue and allow new clients if self.__reaccept_from_client(): - self.log.trace("Server can continue, because of --keep") + self.log.trace("Server can continue, because of --keep-open") continue if self.role == "client": # Yay, we want to continue and our client will re-connect upstream again @@ -474,29 +494,17 @@ class AbstractSocket(object): self.log.trace("Client can continue, because of --reconn") continue - self.log.warning("Exit. Upstream connection gone. No --keep/--reconn specified.") + self.log.warning("Exit. Upstream connection gone. No --keep-open/--reconn set.") + ssig.raise_stop() return # [5/5] We have data to process data = self.enc.decode(byte) + self.log.debug( + "Received {} bytes from {}:{}".format(len(data), self.remote_addr, self.remote_port) + ) self.log.trace("Received: {}".format(repr(data))) - # If we're receiving data from a UDP client - # we can firstly/finally set its addr/port in order - # to send data back to it (see send() function) - if self.options["udp"]: - self.udp_client_addr, self.udp_client_port = addr - # Avoid the noise on UDP connections to spam on every send - if self.udp_client_addr is None or self.udp_client_port is None: - self.log.info( - "Client connected: {}:{}".format(self.udp_client_addr, self.udp_client_port) - ) - # Find for debug - else: - self.log.debug( - "Client connected: {}:{}".format(self.udp_client_addr, self.udp_client_port) - ) - yield data @@ -549,17 +557,14 @@ class NetcatClient(AbstractSocket): addr = self.gethostbyname(host, port, socket.AF_INET) self.create_socket() self.conn = self.sock - if self.options["udp"]: - self.udp_client_addr = addr - self.udp_client_port = port - else: - self.remote_addr = addr - self.remote_port = port + + self.remote_addr = addr + self.remote_port = port + if not self.options["udp"]: if self.connect(): return - if self.role == "client": - if self._AbstractSocket__reconnect_to_server(): - return + if self._AbstractSocket__reconnect_to_server(): + return sys.exit(1) @@ -595,22 +600,40 @@ class AbstractNetcatPlugin(ABC): @abstractmethod def __init__(self, options={}): - """Set specific options for this plugin.""" + """ + Set specific options for this plugin. + + Args: + options (dict): A dict which allows you to add custom options to your module + """ raise NotImplementedError("Should have implemented this") @abstractmethod - def input_generator(self): - """Implement a generator function which constantly yields data from some input.""" + def producer(self, ssig): + """ + Implement a generator function which constantly yields data. + + The data could be from various sources such as: received from a socket, + received from user input, received from shell command output or anything else. + + Args: + ssig (StopSignal): A StopSignal instance providing has_stop() and raise_stop() functions + """ raise NotImplementedError("Should have implemented this") @abstractmethod - def input_callback(self, data): - """Implement a callback which processes the input which is parsed from input_generator. """ + def consumer(self, data): + """The consumer takes the consumers' output as input and processes it in some form.""" raise NotImplementedError("Should have implemented this") @abstractmethod - def input_interrupter(self): - """Implement a method, which quits the input_generator.""" + def interrupt(self): + """Defines 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. This method is triggered from outside and is + supposed to stop/shutdown the producer. + If no such interrupt is required, imeplemt it empty.""" raise NotImplementedError("Should have implemented this") @@ -625,17 +648,13 @@ class NetcatPluginOutput(AbstractNetcatPlugin): callback that writes to stdout. """ - # Line feeds to use for user input - linefeed = "\n" + # Replace '\n' linefeeds (if they exist) with CRLF ('\r\n')? + crlf = False # Non-blocking read from stdin achieved via timeout. # Specify timeout in seconds. input_timeout = None - # Used by the input_interrupter to set this to true. - # The input_generator will frequently check this value - __quit = False - # ------------------------------------------------------------------------------ # Constructor / Destructor # ------------------------------------------------------------------------------ @@ -644,25 +663,31 @@ class NetcatPluginOutput(AbstractNetcatPlugin): super(AbstractNetcatPlugin, self).__init__() assert "encoder" in options assert "input_timeout" in options - assert "linefeed" in options + assert "crlf" in options self.log = logging.getLogger(__name__) self.enc = options["encoder"] if "input_timeout" in options: self.input_timeout = options["input_timeout"] - if "linefeed" in options: - self.linefeed = options["linefeed"] + if "crlf" in options: + self.crlf = options["crlf"] # ------------------------------------------------------------------------------ # Private Functions # ------------------------------------------------------------------------------ def __use_linefeeds(self, data): """Ensure the user input has the desired linefeeds --crlf or not.""" + # No replacement requested + if not self.crlf: + return data + # Already have CRLF at the end if data.endswith("\r\n"): - data = data[:-2] - elif data.endswith("\n") or data.endswith("\r"): - data = data[:-1] - data += self.linefeed + return data + # Replace current newline character with CRLF + if data.endswith("\n"): + self.log.debug("Replacing LF with CRLF") + return data[:-1] + "\r\n" + # Otherwise just return as it is return data def __set_input_timeout(self): @@ -674,19 +699,15 @@ class NetcatPluginOutput(AbstractNetcatPlugin): # ------------------------------------------------------------------------------ # Public Functions # ------------------------------------------------------------------------------ - def input_interrupter(self): - global THREAD_TERMINATE - """Stop function that can be called externally to close this instance.""" - self.log.trace("[NetcatOutputCommand] quit flag was set by input_interrupter()") - self.__quit = True - def input_generator(self): + def producer(self, ssig): """Constantly ask for user input.""" # 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.__quit: + if ssig.has_stop(): + self.log.trace("Stop signal acknowledged for reading STDIN-1") return try: # TODO: select() does not work for windows on stdin/stdout @@ -697,29 +718,34 @@ class NetcatPluginOutput(AbstractNetcatPlugin): # 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 THREAD_TERMINATE: - self.log.trace("STDIN: terminate") + if ssig.has_stop(): + self.log.trace("Stop signal acknowledged for reading STDIN-2") return # TODO: Re-enable this for very verbose logging # self.log.trace("STDIN: timeout. Waiting for input...") continue if line: - self.log.trace("Yielding stdin") + self.log.debug("Received {} bytes from STDIN".format(len(line))) + self.log.trace("Received: {}".format(repr(line))) yield self.__use_linefeeds(line) # EOF or + else: # DO NOT RETURN HERE BLINDLY, THE UPSTREAM CONNECTION MUST GO FIRST! - if THREAD_TERMINATE: - self.log.trace("No more input generated, quitting.") + if ssig.has_stop(): + self.log.trace("Stop signal acknowledged for reading STDIN-3") return # TODO: Re-enable this for very verbose logging # self.log.trace("STDIN: Reached EOF, repeating") - def input_callback(self, data): + def consumer(self, data): """Print received data to stdout.""" print(data, end="") sys.stdout.flush() # TODO:Is this required? What does this do? Test this! + def interrupt(self): + """Empty interrupt.""" + pass + # ------------------------------------------------------------------------------------------------- # CLASS: NetcatPluginCommand (Module for user-input -> send -> execute -> send-back -> output) @@ -754,6 +780,10 @@ class NetcatPluginCommand(AbstractNetcatPlugin): shell=False, env=env, ) + # Python-2 compat (doesn't have FileNotFoundError) + except OSError: + self.log.error("Specified executable '{}' not found".format(self.executable)) + sys.exit(1) except FileNotFoundError: self.log.error("Specified executable '{}' not found".format(self.executable)) sys.exit(1) @@ -766,14 +796,6 @@ class NetcatPluginCommand(AbstractNetcatPlugin): self.log.trace("Killing executable: {} with pid {}".format(self.executable, self.p.pid)) self.p.kill() - # ------------------------------------------------------------------------------ - # Public Functions - # ------------------------------------------------------------------------------ - def input_interrupter(self): - """Stop function that can be called externally to close this instance.""" - self.log.trace("[NetcatPluginCommand] subprocess.kill() was raised by input_unterrupter()") - self.p.kill() - def __set_input_timeout(self, timeout=0.1): """Throw a TimeOutError Exception for sys.stdin (Linux only).""" # select((rlist, wlist, xlist, timeout) @@ -784,9 +806,20 @@ class NetcatPluginCommand(AbstractNetcatPlugin): if not i: raise BaseException("timed out") - def input_generator(self): + # ------------------------------------------------------------------------------ + # Public Functions + # ------------------------------------------------------------------------------ + def interrupt(self): + """Stop function that can be called externally to close this instance.""" + self.log.trace("[NetcatPluginCommand] subprocess.kill() was raised by input_unterrupter()") + self.p.kill() + + def producer(self, ssig): """Constantly ask for input.""" while True: + if ssig.has_stop(): + self.log.trace("Stop signal acknowledged in Command") + return self.log.trace("Reading command output") # TODO: non-blocking read does not seem to work or? # try: @@ -806,7 +839,7 @@ class NetcatPluginCommand(AbstractNetcatPlugin): break yield data - def input_callback(self, data): + def consumer(self, data): """Send data received to stdin (command input).""" data = self.enc.encode(data) self.log.trace("Appending to stdin: {}".format(data)) @@ -825,9 +858,38 @@ class NetcatPluginCommand(AbstractNetcatPlugin): # ------------------------------------------------------------------------------------------------- # CLASS: Runner # ------------------------------------------------------------------------------------------------- +class StopSignal(object): + + __stop = False + + def has_stop(self): + return self.__stop + + def raise_stop(self): + self.__stop = True + + class Runner(object): """Runner class that takes care about putting everything into threads.""" + # Dict of producer/consumer action to run in a Thread. + # Each list item will be run in a single thread + # { + # "name": { + # { + # "producer": "function", # A func which yields data + # "consumer": "function", # A callback func to process the data + # "interrupter": "function", # A interrupt func to tell the producer to stop + # } + # } + __actions = {} + __timers = {} + + # A dict which holds the threads created from actions. + # The name is based on the __actions name + # {"name": ""} + __threads = {} + # ------------------------------------------------------------------------------ # Constructor / Destructor # ------------------------------------------------------------------------------ @@ -835,140 +897,141 @@ class Runner(object): """Constructor.""" self.log = logging.getLogger(__name__) - # Generator - [ - { - "name": "", - "input_generator": {"fnc": "", "args": "", "kwargs": ""}, - "input_interrupter": {"fnc": "", "args": "", "kwargs": ""}, - "input_callback": {"fnc": "", "args": "", "kwargs": ""}, - } - ] - # Timebased - # ------------------------------------------------------------------------------ # Public Functions # ------------------------------------------------------------------------------ - def set_recv_generator(self, func): - """Set generator func which constantly receives network data.""" - self.recv_generator = func - - def set_input_generator(self, func): - """Set generator func which constantly receives input (shell output/user input).""" - self.input_generator = func - - def set_send_callback(self, func): - """Set the callback for sending data to a socket.""" - self.send_callback = func - - def set_output_callback(self, func): - """Set the callback for outputting data to stdin/stdout.""" - self.output_callback = func - - def set_revc_generator_stop_function(self, func): - self.recv_generator_stop_fn = func - - def set_input_generator_stop_function(self, func): - self.input_generator_stop_fn = func + def add_action(self, action): + """ + Enables a function to run threaded by the producer/consumer runner. + + :param str name: Name for logging output + :param function producer: A generator function which yields data + :param function consumer: A callback which consumes data from the generator + :param function interrupter: A func that signals a stop event to the producer + """ + assert "name" in action + assert "producer" in action + assert "consumer" in action + assert "signal" in action + assert "interrupt" in action + self.__actions[action["name"]] = { + "name": action["name"], + "producer": action["producer"], + "consumer": action["consumer"], + "signal": action["signal"], + "interrupt": action["interrupt"], + } - def set_timed_action(self, intvl, func, *args, **kwargs): - """Set a function that should be called periodically.""" - self.timed_action_intvl = intvl - self.timed_action_func = func - self.timed_action_args = args - self.timed_action_kwargs = kwargs + def add_timer(self, timer): + self.__timers[timer["name"]] = { + "action": timer["action"], + "intvl": timer["intvl"], + "args": timer["args"] if "args" in timer else None, + "kwargs": timer["kwargs"] if "kwargs" in timer else {}, + "signal": timer["signal"], + } def run(self): """Run threaded NetCat.""" - global THREAD_TERMINATE - - assert hasattr(self, "recv_generator"), "Error, recv_generator not set" - assert hasattr(self, "input_generator"), "Error, input_generator not set" - assert hasattr(self, "send_callback"), "Error, send_callback not set" - assert hasattr(self, "output_callback"), "Error, output_callback not set" - - def receiver(): - """Receive data from a socket and process it with a callback. - receive: Must be a generator function to receive network data. - callback: Must be a callback to process received data, e.g.: print to stdin/stdout. + def run_action(name, producer, consumer, ssig): """ - self.log.trace("[Thread-Recv] START") - for data in self.recv_generator(): - self.log.trace("[Thread-Recv] recv_generator() received: {}".format(repr(data))) - self.output_callback(data) - self.log.trace("[Thread-Recv] STOP") + Receive data (network, user-input, shell-output) and process it (send, output). - def sender(): - """Receive data from user-input/command-output and process it with a callback. - - receive: Must be a generator function to receive user-input or command output. - callback: Must be a callback to send this data to a socket. + :param str name: Name for logging output + :param function producer: A generator function which yields data + :param function consumer: A callback which consumes data from the generator + :param StopSignal ssig: Providing has_stop() and raise_stop() """ - self.log.trace("[Thread-Send] START") - for data in self.input_generator(): - self.log.trace("[Thread-Send] input_generator() received: {}".format(repr(data))) - self.send_callback(data) - self.log.trace("[Thread-Send] STOP") + self.log.trace("[{}] Producer Start".format(name)) + for data in producer(ssig): + self.log.trace("[{}] Producer received: {}".format(name, repr(data))) + consumer(data) + self.log.trace("[{}] Producer Stop".format(name)) - def timer(): + def run_timer(name, action, intvl, ssig, *args, **kwargs): """Execute periodic tasks by an optional provided time_action.""" - self.log.trace("[Thread-Time] START") - self.log.debug( - "Ready for timed action every {} seconds".format(self.timed_action_intvl) - ) + self.log.trace("[{}] Timer Start (exec every {} sec)".format(name, intvl)) time_last = int(time.time()) while True: + if ssig.has_stop(): + self.log.trace("Stop signal acknowledged for timer {}".format(name)) + return time_now = int(time.time()) - if time_now > time_last + self.timed_action_intvl: + if time_now > time_last + intvl: self.log.debug("[{}] Executing timed function".format(time_now)) - self.timed_action_func(*self.timed_action_args, **self.timed_action_kwargs) + if args is not None: + if kwargs: + action(*args, **kwargs) + else: + action(*args) + else: + if kwargs: + action(**kwargs) + else: + action() time_last = time_now # Reset previous time time.sleep(1) - # Start sending and receiving threads - self.tr = threading.Thread(target=receiver, name="Thread-Recv") - self.ts = threading.Thread(target=sender, name="Thread-Send") - # If the main thread kills, this thread will be killed too. - self.tr.daemon = False # No daemon, wait for each other (e.g.: data received - self.ts.daemon = False # should also be outputted) - # Start threads - self.tr.start() - # time.sleep(0.1) - self.ts.start() - - if hasattr(self, "timed_action_intvl"): - self.tt = threading.Thread(target=timer, name="Thread-Time") - self.tt.daemon = True - self.tt.start() - - # Cleanup the main program - while True: - # TODO: is this required? (check if need to press Ctrl+c twice) - # if not THREAD_TERMINATE: - # self.input_generator_stop_fn() - # self.recv_generator_stop_fn() - if not self.tr.is_alive(): - self.log.trace("Setting THREAD_TERMINATE=True from Thread-Recv death") - self.log.trace("Waiting for Thread-Send to finish") - # time.sleep(0.1) - THREAD_TERMINATE = True - self.input_generator_stop_fn() - self.ts.join() - sys.exit(0) - if not self.ts.is_alive(): - self.log.trace("Setting THREAD_TERMINATE=True from Thread-Send death") - self.log.trace("Waiting for Thread-Recv to finish") - # time.sleep(0.1) - THREAD_TERMINATE = True - self.recv_generator_stop_fn() - self.tr.join() - sys.exit(0) - # TODO: Yes, also implement the timed function - # if hasattr(self, "tt"): - # if not self.tt.is_alive(): - # sys.exit(0) - # time.sleep(0.1) + # Start available action in a thread + for key in self.__actions: + # Create Thread object + thread = threading.Thread( + target=run_action, + name=key, + args=( + key, + self.__actions[key]["producer"], + self.__actions[key]["consumer"], + self.__actions[key]["signal"], + ), + ) + thread.daemon = False + thread.start() + self.__threads[key] = thread + # Start available timers in a thread + for key in self.__timers: + # Create Thread object + thread = threading.Thread( + target=run_timer, + name=key, + args=( + key, + self.__timers[key]["action"], + self.__timers[key]["intvl"], + self.__timers[key]["signal"], + self.__timers[key]["args"], + ), + kwargs=self.__timers[key]["kwargs"], + ) + thread.daemon = False + thread.start() + + def stop(force): + """Stop threads.""" + for key in self.__threads: + if not self.__threads[key].is_alive() or force: + self.log.trace("Raise stop signal for {}".format(self.__threads[key].getName())) + self.__actions[key]["signal"].raise_stop() + self.log.trace("Call interrupt for {}".format(self.__threads[key].getName())) + self.__actions[key]["interrupt"]() + self.log.trace("Joining {}".format(self.__threads[key].getName())) + self.__threads[key].join(timeout=0.1) + # If all threads have died, exit + if not all([self.__threads[key].is_alive() for key in self.__threads]) or force: + if force: + sys.exit(1) + else: + sys.exit(0) + + try: + while True: + stop(False) + # Need a timeout to not skyrocket the CPU + time.sleep(0.1) + except KeyboardInterrupt: + print() + stop(True) # ------------------------------------------------------------------------------------------------- @@ -1272,7 +1335,7 @@ address given via -L/--local addr:port. "--crlf", action="store_true", default=False, - help="Send CRLF line-endings in connect mode (default: LF)", + help="Replace LF with CRLF from stdin (default: don't)", ) optional.add_argument( "-n", "--nodns", action="store_true", default=False, help="Do not resolve DNS.", @@ -1492,8 +1555,8 @@ Use --udp-ping-intvl 0 to be faster. help="""All modes: If %(prog)s is started with this argument, it will shut down as soon as it receives the specified string. The ---keep (server) or --reconn (client) options will be -ignored and it won't listen again or reconnect to you. +--keep-open (server) or --reconn (client) options will +be ignored and it won't listen again or reconnect to you. Use a very unique string to not have it shut down accidentally by other input. """ @@ -1552,7 +1615,7 @@ def main(): # Set netcat options net_opts = { - "bufsize": 1024, + "bufsize": 8192, "backlog": 0, "nodns": args.nodns, "udp": args.udp, @@ -1587,9 +1650,11 @@ def main(): logging.addLevelName(LOGLEVEL_TRACE_NUM, "TRACE") logging.Logger.trace = logtrace - logformat = "%(levelname)s:%(message)s" + logformat = "%(levelname)s %(message)s" + if args.verbose > 2: + logformat = "%(levelname)s [%(threadName)s]: %(message)s" if args.verbose > 3: - logformat = "%(levelname)s [%(threadName)s] %(funcName)s():%(message)s" + logformat = "%(levelname)s [%(threadName)s] %(lineno)d:%(funcName)s(): %(message)s" logging.basicConfig(format=logformat, level=loglevel) # Initialize encoder @@ -1606,7 +1671,7 @@ def main(): else: module_opts = { "encoder": encoder, - "linefeed": "\r\n" if args.crlf else "\n", + "crlf": args.crlf, "input_timeout": TIMEOUT_READ_STDIN, } mod = NetcatPluginOutput(module_opts) @@ -1614,66 +1679,104 @@ def main(): # Run local port-forward # -> listen locally and forward traffic to remote (connect) if args.local: + ssig = StopSignal() # TODO: Make the listen address optional! - # Create listen and client instances - # FIXME: As there is only one THREAD_TERMINATE, both instances will use it. - # this should go into the runner or so. srv_opts = net_opts.copy() srv_opts["reconn"] = True + srv_opts["keep_open"] = True srv_opts["reconn_wait"] = 0 lhost = args.local.split(":")[0] lport = int(args.local.split(":")[1]) + # Create listen and client instances net_srv = NetcatServer( encoder, lhost, lport, TIMEOUT_RECV_SOCKET, TIMEOUT_RECV_SOCKET_RETRY, srv_opts ) net_cli = NetcatClient( - encoder, host, port, TIMEOUT_RECV_SOCKET, TIMEOUT_RECV_SOCKET_RETRY, net_opts + encoder, host, port, TIMEOUT_RECV_SOCKET, TIMEOUT_RECV_SOCKET_RETRY, srv_opts ) - - # Create Runner (the set_* funcs below are brainfuck and took me 1 hour to figure it out) + # Create Runner run = Runner() - - # [srv] User-Client connects here, sends data and the Server takes it as input - run.set_recv_generator(net_srv.receive) - # [cli] Runner parses data from Server on to Proxy-Client, which sends/connect it further - run.set_output_callback(net_cli.send) - # [cli] Proxy-Client waits for response and receives data back - run.set_input_generator(net_cli.receive) - # [srv] Runner parses data from Proxy-Client onto Server, which sends/back to User-Client - run.set_send_callback(net_srv.send) - - run.set_revc_generator_stop_function(object) - run.set_input_generator_stop_function(object) - # And finally run + run.add_action( + { + "name": "TRANSMIT", + "producer": net_srv.receive, # USER sends data to PC-SERVER + "consumer": net_cli.send, # Data parsed on to PC-CLIENT to send to TARGET + "signal": ssig, + "interrupt": object, + } + ) + run.add_action( + { + "name": "RECEIVE", + "producer": net_cli.receive, # Data comes back from TARGET to PC-CLIENT + "consumer": net_srv.send, # Data parsed on to PC-SERVER to back send to USER + "signal": ssig, + "interrupt": object, + } + ) run.run() # Run server if args.listen: + ssig = StopSignal() net = NetcatServer( encoder, host, port, TIMEOUT_RECV_SOCKET, TIMEOUT_RECV_SOCKET_RETRY, net_opts ) run = Runner() - run.set_recv_generator(net.receive) - run.set_input_generator(mod.input_generator) - run.set_send_callback(net.send) - run.set_output_callback(mod.input_callback) - - run.set_revc_generator_stop_function(object) - run.set_input_generator_stop_function(mod.input_interrupter) + run.add_action( + { + "name": "RECV", + "producer": net.receive, + "consumer": mod.consumer, + "signal": ssig, + "interrupt": mod.interrupt, # Also force the producer to stop on net error + } + ) + run.add_action( + { + "name": "STDIN", + "producer": mod.producer, + "consumer": net.send, + "signal": ssig, + "interrupt": mod.interrupt, # Externally stop the produer itself + } + ) run.run() + # Run client else: + ssig = StopSignal() net = NetcatClient( encoder, host, port, TIMEOUT_RECV_SOCKET, TIMEOUT_RECV_SOCKET_RETRY, net_opts ) run = Runner() - run.set_recv_generator(net.receive) - run.set_input_generator(mod.input_generator) - run.set_send_callback(net.send) - run.set_output_callback(mod.input_callback) + run.add_action( + { + "name": "RECV", + "producer": net.receive, + "consumer": mod.consumer, + "signal": ssig, + "interrupt": mod.interrupt, # Also force the producer to stop on net error + } + ) + run.add_action( + { + "name": "STDIN", + "producer": mod.producer, + "consumer": net.send, + "signal": ssig, + "interrupt": mod.interrupt, # Externally stop the produer itself + } + ) if type(args.udp_ping_intvl) is int and args.udp_ping_intvl > 0: - run.set_timed_action(args.udp_ping_intvl, net.send, "\x00") - run.set_revc_generator_stop_function(object) - run.set_input_generator_stop_function(object) + run.add_timer( + { + "name": "PING", + "action": net.send, + "intvl": args.udp_ping_intvl, + "args": ("\x00"), + "signal": ssig, + } + ) run.run() @@ -1682,6 +1785,5 @@ if __name__ == "__main__": try: main() except KeyboardInterrupt: - THREAD_TERMINATE = True print() sys.exit(1) diff --git a/docs/pwncat.api.html b/docs/pwncat.api.html index 824f789e..1ea91721 100644 --- a/docs/pwncat.api.html +++ b/docs/pwncat.api.html @@ -976,7 +976,6 @@

Index

  • APPREPO
  • LOGLEVEL_TRACE_NUM
  • PIPE
  • -
  • THREAD_TERMINATE
  • TIMEOUT_READ_STDIN
  • TIMEOUT_RECV_SOCKET
  • TIMEOUT_RECV_SOCKET_RETRY
  • @@ -1008,9 +1007,9 @@

    Index

    @@ -1054,9 +1053,9 @@

    Index

    @@ -1066,9 +1065,9 @@

    Index

    @@ -1095,14 +1094,19 @@

    Index

    + + +
  • + StopSignal + + +
  • @@ -1141,6 +1145,7 @@

    pwncat module

    # # TODO: where to use sys.stdout.flush ?? +# TODO: change docstring to this format: https://sphinxcontrib-napoleon.readthedocs.io/en/latest/ # # Behaviour Server # 1. (tty and non-tty mode) TCP Server should always quit if client disconnects @@ -1179,20 +1184,15 @@

    pwncat module

    APPNAME = "pwncat" APPREPO = "https://github.com/cytopia/pwncat" -VERSION = "0.0.6-alpha" - -# Global variable to be used within threads to determine -# if they should exit or not. -# TODO: check if this is best-practice -THREAD_TERMINATE = False +VERSION = "0.0.7-alpha" # Custom loglevel numer for TRACE LOGLEVEL_TRACE_NUM = 9 # Default timeout for timeout-based sys.stdin and socket.recv -TIMEOUT_READ_STDIN = 0.3 -TIMEOUT_RECV_SOCKET = 0.3 -TIMEOUT_RECV_SOCKET_RETRY = 1 +TIMEOUT_READ_STDIN = 0.1 +TIMEOUT_RECV_SOCKET = 0.1 +TIMEOUT_RECV_SOCKET_RETRY = 2 # ################################################################################################# @@ -1223,13 +1223,13 @@

    pwncat module

    def encode(self, data): """Convert string into a byte type for Python3.""" if self.py3: - data = data.encode("cp437") + data = data.encode(self.codec) return data def decode(self, data): """Convert bytes into a string type for Python3.""" if self.py3: - data = data.decode("cp437") + data = data.decode(self.codec) return data @@ -1279,18 +1279,13 @@

    pwncat module

    "safe_word": False, # Once this is received, the application quits } - # In case the server is running in UDP mode, - # it must wait for the client to connect in order + # Store the address of the remote end. + # If we are in server role and running in UDP mode, + # it must wait for the client to connect first in order # to retrieve its addr and port in order to be able # to send data back to it. - udp_client_addr = None - udp_client_port = None - - # For client role only - # Store the address and port of the remote server to connect to. - # This is required for self.connect() - remote_addr = None remote_addr = None + remote_port = None # ------------------------------------------------------------------------------ # Constructor / Destructor @@ -1441,12 +1436,6 @@

    pwncat module

    # Get around the "[Errno 98] Address already in use" error, if the socket is still in wait # we instruct it to reuse the address anyway. self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - # TODO: Not sure if SO_REUSEPORT is also required - # try: - # self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) - # except AttributeError: - # # Not available on Windows (and maybe others) - # self.log.debug("socket.SO_REUSEPORT is not available on your platform") def bind(self, addr, port): """Bind the socket to an address.""" @@ -1472,6 +1461,8 @@

    pwncat module

    self.log.debug("Waiting for TCP client") self.conn, client = self.sock.accept() addr, port = client + self.remote_addr = addr + self.remote_port = port self.log.info("Client connected from {}:{}".format(addr, port)) except (socket.gaierror, socket.error) as error: self.log.error("Accept failed: {}".format(error)) @@ -1496,10 +1487,10 @@

    pwncat module

    """Send data.""" # In case of sending data back to an udp client we need to wait # until the client has first connected and told us its addr/port - if self.options["udp"] and self.udp_client_addr is None and self.udp_client_port is None: - self.log.info("Waiting for UDP client to connect") - while self.udp_client_addr is None and self.udp_client_port is None: - time.sleep(0.2) # Less wastefull than using 'pass' + if self.options["udp"] and self.remote_addr is None and self.remote_port is None: + self.log.warning("UDP client has not yet connected. Queueing message") + while self.remote_addr is None and self.remote_port is None: + time.sleep(0.1) # Less wastefull than using 'pass' curr = 0 # bytes send during one loop iteration send = 0 # total bytes send @@ -1510,9 +1501,14 @@

    pwncat module

    # Loop until all bytes have been send while send < size: try: - self.log.trace("Trying to send {} bytes".format(size - send)) + self.log.debug( + "Trying to send {} bytes to {}:{}".format( + size - send, self.remote_addr, self.remote_port + ) + ) + self.log.trace("Trying to send: {}".format(repr(data))) if self.options["udp"]: - curr = self.conn.sendto(data, (self.udp_client_addr, self.udp_client_port)) + curr = self.conn.sendto(data, (self.remote_addr, self.remote_port)) send += curr else: curr = self.conn.send(data) @@ -1522,7 +1518,11 @@

    pwncat module

    return # Remove 'curr' many bytes from data for the next round data = data[curr:] - self.log.trace("Send {} bytes ({} bytes remaining)".format(curr, size - send)) + self.log.debug( + "Sent {} bytes to {}:{} ({} bytes remaining)".format( + curr, self.remote_addr, self.remote_port, size - send + ) + ) except socket.error as error: if error.errno == socket.errno.EPIPE: self.log.error("TODO:Add desc. Socket error({}): {}".format(error.errno, error)) @@ -1539,19 +1539,28 @@

    pwncat module

    self.log.error("Socket OS Error: {}".format(error)) return - def receive(self): - """Generator function to receive data endlessly by yielding it.""" + def receive(self, ssig): + """ + Generator function to receive data endlessly by yielding it. + + :param function interrupter: A Func that returns True/False to tell us to stop or not. + """ # Set current receive timeout self.conn.settimeout(self.recv_timeout) - self.log.trace("Socket Timeout: {}".format(self.recv_timeout_retry)) + self.log.trace("Socket Timeout: {}".format(self.recv_timeout)) # Counts how many times we had a ready timeout for later to decide # if we exceeded maximum retires curr_recv_timeout_retry = 0 while True: + # Ensure to signal that we do not stop receiving data + # if ssig.has_stop(): + # self.log.debug("Interrupt has been requested for receive()") + # return if self.conn is None: self.log.error("Exit. Socket is gone in receive()") + ssig.raise_stop() return # Non-blocking socket with timeout. If the timeout threshold is hit, @@ -1561,20 +1570,27 @@

    pwncat module

    # https://manpages.debian.org/buster/manpages-dev/recv.2.en.html (byte, addr) = self.conn.recvfrom(self.options["bufsize"]) - # [1/5] NOTE: This is the place where we can do any checks in between reads as the + # [1/5] Finished receiving all data + # NOTE: This is the place where we can do any checks in between reads as the # socket has been changed from blocking to time-out based. + # NOTE: This is also the place, where we quit in case --wait was specified. except socket.timeout: - # No other thread has terminated yet, and thus not asked us to quit. - # so we can continue waiting for input on the socket - if not THREAD_TERMINATE: + # Let's ask the interrupter() function if we should terminate? + if not ssig.has_stop(): # No action required, continue the loop and read again. continue + self.log.debug("Interrupt has been requested for receive()") # Other threads are done. Let's try to read a few more times before # returning and ending this function (might be data left) if curr_recv_timeout_retry < self.recv_timeout_retry: - self.log.trace("RECV EOF TIMEOUT: AND THREAD_TERMINATE REQUESTED") + self.log.trace( + "Final socket read: {}/{} before quitting.".format( + curr_recv_timeout_retry, self.recv_timeout_retry + ) + ) curr_recv_timeout_retry += 1 continue + ssig.raise_stop() return # [2/5] Connection was forcibly closed @@ -1591,12 +1607,20 @@

    pwncat module

    # [3/5] TODO: Still need to figure out what this error is and when it is thrown except AttributeError as error: self.log.error("TODO: What happens here?Attribute Receive Error: {}".format(error)) + ssig.raise_stop() return # We're receiving data again, so let's reset the retry/terminate counter # The counter is incremented in 'except socket.timeout' above. curr_recv_timeout_retry = 0 + # If we're receiving data from a UDP client + # we can firstly/finally set its addr/port in order + # to send data back to it (see send() function) + if self.options["udp"]: + self.remote_addr, self.remote_port = addr + self.log.debug("Client connected: {}:{}".format(self.remote_addr, self.remote_port)) + # [4/5] Upstream (server or client) is gone. Do we reconnect or quit? if not byte: self.log.trace("Socket: Empty data received or otherwise caught.") @@ -1604,7 +1628,7 @@

    pwncat module

    if self.role == "server": # Yay, we want to continue and allow new clients if self.__reaccept_from_client(): - self.log.trace("Server can continue, because of --keep") + self.log.trace("Server can continue, because of --keep-open") continue if self.role == "client": # Yay, we want to continue and our client will re-connect upstream again @@ -1612,29 +1636,17 @@

    pwncat module

    self.log.trace("Client can continue, because of --reconn") continue - self.log.warning("Exit. Upstream connection gone. No --keep/--reconn specified.") + self.log.warning("Exit. Upstream connection gone. No --keep-open/--reconn set.") + ssig.raise_stop() return # [5/5] We have data to process data = self.enc.decode(byte) + self.log.debug( + "Received {} bytes from {}:{}".format(len(data), self.remote_addr, self.remote_port) + ) self.log.trace("Received: {}".format(repr(data))) - # If we're receiving data from a UDP client - # we can firstly/finally set its addr/port in order - # to send data back to it (see send() function) - if self.options["udp"]: - self.udp_client_addr, self.udp_client_port = addr - # Avoid the noise on UDP connections to spam on every send - if self.udp_client_addr is None or self.udp_client_port is None: - self.log.info( - "Client connected: {}:{}".format(self.udp_client_addr, self.udp_client_port) - ) - # Find for debug - else: - self.log.debug( - "Client connected: {}:{}".format(self.udp_client_addr, self.udp_client_port) - ) - yield data @@ -1687,17 +1699,14 @@

    pwncat module

    addr = self.gethostbyname(host, port, socket.AF_INET) self.create_socket() self.conn = self.sock - if self.options["udp"]: - self.udp_client_addr = addr - self.udp_client_port = port - else: - self.remote_addr = addr - self.remote_port = port + + self.remote_addr = addr + self.remote_port = port + if not self.options["udp"]: if self.connect(): return - if self.role == "client": - if self._AbstractSocket__reconnect_to_server(): - return + if self._AbstractSocket__reconnect_to_server(): + return sys.exit(1) @@ -1733,22 +1742,40 @@

    pwncat module

    @abstractmethod def __init__(self, options={}): - """Set specific options for this plugin.""" + """ + Set specific options for this plugin. + + Args: + options (dict): A dict which allows you to add custom options to your module + """ raise NotImplementedError("Should have implemented this") @abstractmethod - def input_generator(self): - """Implement a generator function which constantly yields data from some input.""" + def producer(self, ssig): + """ + Implement a generator function which constantly yields data. + + The data could be from various sources such as: received from a socket, + received from user input, received from shell command output or anything else. + + Args: + ssig (StopSignal): A StopSignal instance providing has_stop() and raise_stop() functions + """ raise NotImplementedError("Should have implemented this") @abstractmethod - def input_callback(self, data): - """Implement a callback which processes the input which is parsed from input_generator. """ + def consumer(self, data): + """The consumer takes the consumers' output as input and processes it in some form.""" raise NotImplementedError("Should have implemented this") @abstractmethod - def input_interrupter(self): - """Implement a method, which quits the input_generator.""" + def interrupt(self): + """Defines 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. This method is triggered from outside and is + supposed to stop/shutdown the producer. + If no such interrupt is required, imeplemt it empty.""" raise NotImplementedError("Should have implemented this") @@ -1763,17 +1790,13 @@

    pwncat module

    callback that writes to stdout. """ - # Line feeds to use for user input - linefeed = "\n" + # Replace '\n' linefeeds (if they exist) with CRLF ('\r\n')? + crlf = False # Non-blocking read from stdin achieved via timeout. # Specify timeout in seconds. input_timeout = None - # Used by the input_interrupter to set this to true. - # The input_generator will frequently check this value - __quit = False - # ------------------------------------------------------------------------------ # Constructor / Destructor # ------------------------------------------------------------------------------ @@ -1782,25 +1805,31 @@

    pwncat module

    super(AbstractNetcatPlugin, self).__init__() assert "encoder" in options assert "input_timeout" in options - assert "linefeed" in options + assert "crlf" in options self.log = logging.getLogger(__name__) self.enc = options["encoder"] if "input_timeout" in options: self.input_timeout = options["input_timeout"] - if "linefeed" in options: - self.linefeed = options["linefeed"] + if "crlf" in options: + self.crlf = options["crlf"] # ------------------------------------------------------------------------------ # Private Functions # ------------------------------------------------------------------------------ def __use_linefeeds(self, data): """Ensure the user input has the desired linefeeds --crlf or not.""" + # No replacement requested + if not self.crlf: + return data + # Already have CRLF at the end if data.endswith("\r\n"): - data = data[:-2] - elif data.endswith("\n") or data.endswith("\r"): - data = data[:-1] - data += self.linefeed + return data + # Replace current newline character with CRLF + if data.endswith("\n"): + self.log.debug("Replacing LF with CRLF") + return data[:-1] + "\r\n" + # Otherwise just return as it is return data def __set_input_timeout(self): @@ -1812,19 +1841,15 @@

    pwncat module

    # ------------------------------------------------------------------------------ # Public Functions # ------------------------------------------------------------------------------ - def input_interrupter(self): - global THREAD_TERMINATE - """Stop function that can be called externally to close this instance.""" - self.log.trace("[NetcatOutputCommand] quit flag was set by input_interrupter()") - self.__quit = True - def input_generator(self): + def producer(self, ssig): """Constantly ask for user input.""" # 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.__quit: + if ssig.has_stop(): + self.log.trace("Stop signal acknowledged for reading STDIN-1") return try: # TODO: select() does not work for windows on stdin/stdout @@ -1835,29 +1860,34 @@

    pwncat module

    # 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 THREAD_TERMINATE: - self.log.trace("STDIN: terminate") + if ssig.has_stop(): + self.log.trace("Stop signal acknowledged for reading STDIN-2") return # TODO: Re-enable this for very verbose logging # self.log.trace("STDIN: timeout. Waiting for input...") continue if line: - self.log.trace("Yielding stdin") + self.log.debug("Received {} bytes from STDIN".format(len(line))) + self.log.trace("Received: {}".format(repr(line))) yield self.__use_linefeeds(line) # EOF or + else: # DO NOT RETURN HERE BLINDLY, THE UPSTREAM CONNECTION MUST GO FIRST! - if THREAD_TERMINATE: - self.log.trace("No more input generated, quitting.") + if ssig.has_stop(): + self.log.trace("Stop signal acknowledged for reading STDIN-3") return # TODO: Re-enable this for very verbose logging # self.log.trace("STDIN: Reached EOF, repeating") - def input_callback(self, data): + def consumer(self, data): """Print received data to stdout.""" print(data, end="") sys.stdout.flush() # TODO:Is this required? What does this do? Test this! + def interrupt(self): + """Empty interrupt.""" + pass + # ------------------------------------------------------------------------------------------------- # CLASS: NetcatPluginCommand (Module for user-input -> send -> execute -> send-back -> output) @@ -1892,6 +1922,10 @@

    pwncat module

    shell=False, env=env, ) + # Python-2 compat (doesn't have FileNotFoundError) + except OSError: + self.log.error("Specified executable '{}' not found".format(self.executable)) + sys.exit(1) except FileNotFoundError: self.log.error("Specified executable '{}' not found".format(self.executable)) sys.exit(1) @@ -1904,14 +1938,6 @@

    pwncat module

    self.log.trace("Killing executable: {} with pid {}".format(self.executable, self.p.pid)) self.p.kill() - # ------------------------------------------------------------------------------ - # Public Functions - # ------------------------------------------------------------------------------ - def input_interrupter(self): - """Stop function that can be called externally to close this instance.""" - self.log.trace("[NetcatPluginCommand] subprocess.kill() was raised by input_unterrupter()") - self.p.kill() - def __set_input_timeout(self, timeout=0.1): """Throw a TimeOutError Exception for sys.stdin (Linux only).""" # select((rlist, wlist, xlist, timeout) @@ -1922,9 +1948,20 @@

    pwncat module

    if not i: raise BaseException("timed out") - def input_generator(self): + # ------------------------------------------------------------------------------ + # Public Functions + # ------------------------------------------------------------------------------ + def interrupt(self): + """Stop function that can be called externally to close this instance.""" + self.log.trace("[NetcatPluginCommand] subprocess.kill() was raised by input_unterrupter()") + self.p.kill() + + def producer(self, ssig): """Constantly ask for input.""" while True: + if ssig.has_stop(): + self.log.trace("Stop signal acknowledged in Command") + return self.log.trace("Reading command output") # TODO: non-blocking read does not seem to work or? # try: @@ -1944,7 +1981,7 @@

    pwncat module

    break yield data - def input_callback(self, data): + def consumer(self, data): """Send data received to stdin (command input).""" data = self.enc.encode(data) self.log.trace("Appending to stdin: {}".format(data)) @@ -1963,9 +2000,38 @@

    pwncat module

    # ------------------------------------------------------------------------------------------------- # CLASS: Runner # ------------------------------------------------------------------------------------------------- +class StopSignal(object): + + __stop = False + + def has_stop(self): + return self.__stop + + def raise_stop(self): + self.__stop = True + + class Runner(object): """Runner class that takes care about putting everything into threads.""" + # Dict of producer/consumer action to run in a Thread. + # Each list item will be run in a single thread + # { + # "name": { + # { + # "producer": "function", # A func which yields data + # "consumer": "function", # A callback func to process the data + # "interrupter": "function", # A interrupt func to tell the producer to stop + # } + # } + __actions = {} + __timers = {} + + # A dict which holds the threads created from actions. + # The name is based on the __actions name + # {"name": ""} + __threads = {} + # ------------------------------------------------------------------------------ # Constructor / Destructor # ------------------------------------------------------------------------------ @@ -1973,140 +2039,141 @@

    pwncat module

    """Constructor.""" self.log = logging.getLogger(__name__) - # Generator - [ - { - "name": "", - "input_generator": {"fnc": "", "args": "", "kwargs": ""}, - "input_interrupter": {"fnc": "", "args": "", "kwargs": ""}, - "input_callback": {"fnc": "", "args": "", "kwargs": ""}, - } - ] - # Timebased - # ------------------------------------------------------------------------------ # Public Functions # ------------------------------------------------------------------------------ - def set_recv_generator(self, func): - """Set generator func which constantly receives network data.""" - self.recv_generator = func - - def set_input_generator(self, func): - """Set generator func which constantly receives input (shell output/user input).""" - self.input_generator = func - - def set_send_callback(self, func): - """Set the callback for sending data to a socket.""" - self.send_callback = func - - def set_output_callback(self, func): - """Set the callback for outputting data to stdin/stdout.""" - self.output_callback = func - - def set_revc_generator_stop_function(self, func): - self.recv_generator_stop_fn = func + def add_action(self, action): + """ + Enables a function to run threaded by the producer/consumer runner. - def set_input_generator_stop_function(self, func): - self.input_generator_stop_fn = func + :param str name: Name for logging output + :param function producer: A generator function which yields data + :param function consumer: A callback which consumes data from the generator + :param function interrupter: A func that signals a stop event to the producer + """ + assert "name" in action + assert "producer" in action + assert "consumer" in action + assert "signal" in action + assert "interrupt" in action + self.__actions[action["name"]] = { + "name": action["name"], + "producer": action["producer"], + "consumer": action["consumer"], + "signal": action["signal"], + "interrupt": action["interrupt"], + } - def set_timed_action(self, intvl, func, *args, **kwargs): - """Set a function that should be called periodically.""" - self.timed_action_intvl = intvl - self.timed_action_func = func - self.timed_action_args = args - self.timed_action_kwargs = kwargs + def add_timer(self, timer): + self.__timers[timer["name"]] = { + "action": timer["action"], + "intvl": timer["intvl"], + "args": timer["args"] if "args" in timer else None, + "kwargs": timer["kwargs"] if "kwargs" in timer else {}, + "signal": timer["signal"], + } def run(self): """Run threaded NetCat.""" - global THREAD_TERMINATE - - assert hasattr(self, "recv_generator"), "Error, recv_generator not set" - assert hasattr(self, "input_generator"), "Error, input_generator not set" - assert hasattr(self, "send_callback"), "Error, send_callback not set" - assert hasattr(self, "output_callback"), "Error, output_callback not set" - - def receiver(): - """Receive data from a socket and process it with a callback. - receive: Must be a generator function to receive network data. - callback: Must be a callback to process received data, e.g.: print to stdin/stdout. + def run_action(name, producer, consumer, ssig): """ - self.log.trace("[Thread-Recv] START") - for data in self.recv_generator(): - self.log.trace("[Thread-Recv] recv_generator() received: {}".format(repr(data))) - self.output_callback(data) - self.log.trace("[Thread-Recv] STOP") + Receive data (network, user-input, shell-output) and process it (send, output). - def sender(): - """Receive data from user-input/command-output and process it with a callback. - - receive: Must be a generator function to receive user-input or command output. - callback: Must be a callback to send this data to a socket. + :param str name: Name for logging output + :param function producer: A generator function which yields data + :param function consumer: A callback which consumes data from the generator + :param StopSignal ssig: Providing has_stop() and raise_stop() """ - self.log.trace("[Thread-Send] START") - for data in self.input_generator(): - self.log.trace("[Thread-Send] input_generator() received: {}".format(repr(data))) - self.send_callback(data) - self.log.trace("[Thread-Send] STOP") + self.log.trace("[{}] Producer Start".format(name)) + for data in producer(ssig): + self.log.trace("[{}] Producer received: {}".format(name, repr(data))) + consumer(data) + self.log.trace("[{}] Producer Stop".format(name)) - def timer(): + def run_timer(name, action, intvl, ssig, *args, **kwargs): """Execute periodic tasks by an optional provided time_action.""" - self.log.trace("[Thread-Time] START") - self.log.debug( - "Ready for timed action every {} seconds".format(self.timed_action_intvl) - ) + self.log.trace("[{}] Timer Start (exec every {} sec)".format(name, intvl)) time_last = int(time.time()) while True: + if ssig.has_stop(): + self.log.trace("Stop signal acknowledged for timer {}".format(name)) + return time_now = int(time.time()) - if time_now > time_last + self.timed_action_intvl: + if time_now > time_last + intvl: self.log.debug("[{}] Executing timed function".format(time_now)) - self.timed_action_func(*self.timed_action_args, **self.timed_action_kwargs) + if args is not None: + if kwargs: + action(*args, **kwargs) + else: + action(*args) + else: + if kwargs: + action(**kwargs) + else: + action() time_last = time_now # Reset previous time time.sleep(1) - # Start sending and receiving threads - self.tr = threading.Thread(target=receiver, name="Thread-Recv") - self.ts = threading.Thread(target=sender, name="Thread-Send") - # If the main thread kills, this thread will be killed too. - self.tr.daemon = False # No daemon, wait for each other (e.g.: data received - self.ts.daemon = False # should also be outputted) - # Start threads - self.tr.start() - # time.sleep(0.1) - self.ts.start() - - if hasattr(self, "timed_action_intvl"): - self.tt = threading.Thread(target=timer, name="Thread-Time") - self.tt.daemon = True - self.tt.start() - - # Cleanup the main program - while True: - # TODO: is this required? (check if need to press Ctrl+c twice) - # if not THREAD_TERMINATE: - # self.input_generator_stop_fn() - # self.recv_generator_stop_fn() - if not self.tr.is_alive(): - self.log.trace("Setting THREAD_TERMINATE=True from Thread-Recv death") - self.log.trace("Waiting for Thread-Send to finish") - # time.sleep(0.1) - THREAD_TERMINATE = True - self.input_generator_stop_fn() - self.ts.join() - sys.exit(0) - if not self.ts.is_alive(): - self.log.trace("Setting THREAD_TERMINATE=True from Thread-Send death") - self.log.trace("Waiting for Thread-Recv to finish") - # time.sleep(0.1) - THREAD_TERMINATE = True - self.recv_generator_stop_fn() - self.tr.join() - sys.exit(0) - # TODO: Yes, also implement the timed function - # if hasattr(self, "tt"): - # if not self.tt.is_alive(): - # sys.exit(0) - # time.sleep(0.1) + # Start available action in a thread + for key in self.__actions: + # Create Thread object + thread = threading.Thread( + target=run_action, + name=key, + args=( + key, + self.__actions[key]["producer"], + self.__actions[key]["consumer"], + self.__actions[key]["signal"], + ), + ) + thread.daemon = False + thread.start() + self.__threads[key] = thread + # Start available timers in a thread + for key in self.__timers: + # Create Thread object + thread = threading.Thread( + target=run_timer, + name=key, + args=( + key, + self.__timers[key]["action"], + self.__timers[key]["intvl"], + self.__timers[key]["signal"], + self.__timers[key]["args"], + ), + kwargs=self.__timers[key]["kwargs"], + ) + thread.daemon = False + thread.start() + + def stop(force): + """Stop threads.""" + for key in self.__threads: + if not self.__threads[key].is_alive() or force: + self.log.trace("Raise stop signal for {}".format(self.__threads[key].getName())) + self.__actions[key]["signal"].raise_stop() + self.log.trace("Call interrupt for {}".format(self.__threads[key].getName())) + self.__actions[key]["interrupt"]() + self.log.trace("Joining {}".format(self.__threads[key].getName())) + self.__threads[key].join(timeout=0.1) + # If all threads have died, exit + if not all([self.__threads[key].is_alive() for key in self.__threads]) or force: + if force: + sys.exit(1) + else: + sys.exit(0) + + try: + while True: + stop(False) + # Need a timeout to not skyrocket the CPU + time.sleep(0.1) + except KeyboardInterrupt: + print() + stop(True) # ------------------------------------------------------------------------------------------------- @@ -2410,7 +2477,7 @@

    pwncat module

    "--crlf", action="store_true", default=False, - help="Send CRLF line-endings in connect mode (default: LF)", + help="Replace LF with CRLF from stdin (default: don't)", ) optional.add_argument( "-n", "--nodns", action="store_true", default=False, help="Do not resolve DNS.", @@ -2630,8 +2697,8 @@

    pwncat module

    help="""All modes: If %(prog)s is started with this argument, it will shut down as soon as it receives the specified string. The ---keep (server) or --reconn (client) options will be -ignored and it won't listen again or reconnect to you. +--keep-open (server) or --reconn (client) options will +be ignored and it won't listen again or reconnect to you. Use a very unique string to not have it shut down accidentally by other input. """ @@ -2690,7 +2757,7 @@

    pwncat module

    # Set netcat options net_opts = { - "bufsize": 1024, + "bufsize": 8192, "backlog": 0, "nodns": args.nodns, "udp": args.udp, @@ -2725,9 +2792,11 @@

    pwncat module

    logging.addLevelName(LOGLEVEL_TRACE_NUM, "TRACE") logging.Logger.trace = logtrace - logformat = "%(levelname)s:%(message)s" + logformat = "%(levelname)s %(message)s" + if args.verbose > 2: + logformat = "%(levelname)s [%(threadName)s]: %(message)s" if args.verbose > 3: - logformat = "%(levelname)s [%(threadName)s] %(funcName)s():%(message)s" + logformat = "%(levelname)s [%(threadName)s] %(lineno)d:%(funcName)s(): %(message)s" logging.basicConfig(format=logformat, level=loglevel) # Initialize encoder @@ -2744,7 +2813,7 @@

    pwncat module

    else: module_opts = { "encoder": encoder, - "linefeed": "\r\n" if args.crlf else "\n", + "crlf": args.crlf, "input_timeout": TIMEOUT_READ_STDIN, } mod = NetcatPluginOutput(module_opts) @@ -2752,66 +2821,104 @@

    pwncat module

    # Run local port-forward # -> listen locally and forward traffic to remote (connect) if args.local: + ssig = StopSignal() # TODO: Make the listen address optional! - # Create listen and client instances - # FIXME: As there is only one THREAD_TERMINATE, both instances will use it. - # this should go into the runner or so. srv_opts = net_opts.copy() srv_opts["reconn"] = True + srv_opts["keep_open"] = True srv_opts["reconn_wait"] = 0 lhost = args.local.split(":")[0] lport = int(args.local.split(":")[1]) + # Create listen and client instances net_srv = NetcatServer( encoder, lhost, lport, TIMEOUT_RECV_SOCKET, TIMEOUT_RECV_SOCKET_RETRY, srv_opts ) net_cli = NetcatClient( - encoder, host, port, TIMEOUT_RECV_SOCKET, TIMEOUT_RECV_SOCKET_RETRY, net_opts + encoder, host, port, TIMEOUT_RECV_SOCKET, TIMEOUT_RECV_SOCKET_RETRY, srv_opts ) - - # Create Runner (the set_* funcs below are brainfuck and took me 1 hour to figure it out) + # Create Runner run = Runner() - - # [srv] User-Client connects here, sends data and the Server takes it as input - run.set_recv_generator(net_srv.receive) - # [cli] Runner parses data from Server on to Proxy-Client, which sends/connect it further - run.set_output_callback(net_cli.send) - # [cli] Proxy-Client waits for response and receives data back - run.set_input_generator(net_cli.receive) - # [srv] Runner parses data from Proxy-Client onto Server, which sends/back to User-Client - run.set_send_callback(net_srv.send) - - run.set_revc_generator_stop_function(object) - run.set_input_generator_stop_function(object) - # And finally run + run.add_action( + { + "name": "TRANSMIT", + "producer": net_srv.receive, # USER sends data to PC-SERVER + "consumer": net_cli.send, # Data parsed on to PC-CLIENT to send to TARGET + "signal": ssig, + "interrupt": object, + } + ) + run.add_action( + { + "name": "RECEIVE", + "producer": net_cli.receive, # Data comes back from TARGET to PC-CLIENT + "consumer": net_srv.send, # Data parsed on to PC-SERVER to back send to USER + "signal": ssig, + "interrupt": object, + } + ) run.run() # Run server if args.listen: + ssig = StopSignal() net = NetcatServer( encoder, host, port, TIMEOUT_RECV_SOCKET, TIMEOUT_RECV_SOCKET_RETRY, net_opts ) run = Runner() - run.set_recv_generator(net.receive) - run.set_input_generator(mod.input_generator) - run.set_send_callback(net.send) - run.set_output_callback(mod.input_callback) - - run.set_revc_generator_stop_function(object) - run.set_input_generator_stop_function(mod.input_interrupter) + run.add_action( + { + "name": "RECV", + "producer": net.receive, + "consumer": mod.consumer, + "signal": ssig, + "interrupt": mod.interrupt, # Also force the producer to stop on net error + } + ) + run.add_action( + { + "name": "STDIN", + "producer": mod.producer, + "consumer": net.send, + "signal": ssig, + "interrupt": mod.interrupt, # Externally stop the produer itself + } + ) run.run() + # Run client else: + ssig = StopSignal() net = NetcatClient( encoder, host, port, TIMEOUT_RECV_SOCKET, TIMEOUT_RECV_SOCKET_RETRY, net_opts ) run = Runner() - run.set_recv_generator(net.receive) - run.set_input_generator(mod.input_generator) - run.set_send_callback(net.send) - run.set_output_callback(mod.input_callback) + run.add_action( + { + "name": "RECV", + "producer": net.receive, + "consumer": mod.consumer, + "signal": ssig, + "interrupt": mod.interrupt, # Also force the producer to stop on net error + } + ) + run.add_action( + { + "name": "STDIN", + "producer": mod.producer, + "consumer": net.send, + "signal": ssig, + "interrupt": mod.interrupt, # Externally stop the produer itself + } + ) if type(args.udp_ping_intvl) is int and args.udp_ping_intvl > 0: - run.set_timed_action(args.udp_ping_intvl, net.send, "\x00") - run.set_revc_generator_stop_function(object) - run.set_input_generator_stop_function(object) + run.add_timer( + { + "name": "PING", + "action": net.send, + "intvl": args.udp_ping_intvl, + "args": ("\x00"), + "signal": ssig, + } + ) run.run() @@ -2820,7 +2927,6 @@

    pwncat module

    try: main() except KeyboardInterrupt: - THREAD_TERMINATE = True print() sys.exit(1) @@ -2858,14 +2964,6 @@

    Module variables

    var PIPE

    -
    -
    - - -
    -

    var THREAD_TERMINATE

    - -
    @@ -3009,7 +3107,7 @@

    Functions

    "--crlf", action="store_true", default=False, - help="Send CRLF line-endings in connect mode (default: LF)", + help="Replace LF with CRLF from stdin (default: don't)", ) optional.add_argument( "-n", "--nodns", action="store_true", default=False, help="Do not resolve DNS.", @@ -3229,8 +3327,8 @@

    Functions

    help="""All modes: If %(prog)s is started with this argument, it will shut down as soon as it receives the specified string. The ---keep (server) or --reconn (client) options will be -ignored and it won't listen again or reconnect to you. +--keep-open (server) or --reconn (client) options will +be ignored and it won't listen again or reconnect to you. Use a very unique string to not have it shut down accidentally by other input. """ @@ -3341,7 +3439,7 @@

    Functions

    # Set netcat options net_opts = { - "bufsize": 1024, + "bufsize": 8192, "backlog": 0, "nodns": args.nodns, "udp": args.udp, @@ -3376,9 +3474,11 @@

    Functions

    logging.addLevelName(LOGLEVEL_TRACE_NUM, "TRACE") logging.Logger.trace = logtrace - logformat = "%(levelname)s:%(message)s" + logformat = "%(levelname)s %(message)s" + if args.verbose > 2: + logformat = "%(levelname)s [%(threadName)s]: %(message)s" if args.verbose > 3: - logformat = "%(levelname)s [%(threadName)s] %(funcName)s():%(message)s" + logformat = "%(levelname)s [%(threadName)s] %(lineno)d:%(funcName)s(): %(message)s" logging.basicConfig(format=logformat, level=loglevel) # Initialize encoder @@ -3395,7 +3495,7 @@

    Functions

    else: module_opts = { "encoder": encoder, - "linefeed": "\r\n" if args.crlf else "\n", + "crlf": args.crlf, "input_timeout": TIMEOUT_READ_STDIN, } mod = NetcatPluginOutput(module_opts) @@ -3403,66 +3503,104 @@

    Functions

    # Run local port-forward # -> listen locally and forward traffic to remote (connect) if args.local: + ssig = StopSignal() # TODO: Make the listen address optional! - # Create listen and client instances - # FIXME: As there is only one THREAD_TERMINATE, both instances will use it. - # this should go into the runner or so. srv_opts = net_opts.copy() srv_opts["reconn"] = True + srv_opts["keep_open"] = True srv_opts["reconn_wait"] = 0 lhost = args.local.split(":")[0] lport = int(args.local.split(":")[1]) + # Create listen and client instances net_srv = NetcatServer( encoder, lhost, lport, TIMEOUT_RECV_SOCKET, TIMEOUT_RECV_SOCKET_RETRY, srv_opts ) net_cli = NetcatClient( - encoder, host, port, TIMEOUT_RECV_SOCKET, TIMEOUT_RECV_SOCKET_RETRY, net_opts + encoder, host, port, TIMEOUT_RECV_SOCKET, TIMEOUT_RECV_SOCKET_RETRY, srv_opts ) - - # Create Runner (the set_* funcs below are brainfuck and took me 1 hour to figure it out) + # Create Runner run = Runner() - - # [srv] User-Client connects here, sends data and the Server takes it as input - run.set_recv_generator(net_srv.receive) - # [cli] Runner parses data from Server on to Proxy-Client, which sends/connect it further - run.set_output_callback(net_cli.send) - # [cli] Proxy-Client waits for response and receives data back - run.set_input_generator(net_cli.receive) - # [srv] Runner parses data from Proxy-Client onto Server, which sends/back to User-Client - run.set_send_callback(net_srv.send) - - run.set_revc_generator_stop_function(object) - run.set_input_generator_stop_function(object) - # And finally run + run.add_action( + { + "name": "TRANSMIT", + "producer": net_srv.receive, # USER sends data to PC-SERVER + "consumer": net_cli.send, # Data parsed on to PC-CLIENT to send to TARGET + "signal": ssig, + "interrupt": object, + } + ) + run.add_action( + { + "name": "RECEIVE", + "producer": net_cli.receive, # Data comes back from TARGET to PC-CLIENT + "consumer": net_srv.send, # Data parsed on to PC-SERVER to back send to USER + "signal": ssig, + "interrupt": object, + } + ) run.run() # Run server if args.listen: + ssig = StopSignal() net = NetcatServer( encoder, host, port, TIMEOUT_RECV_SOCKET, TIMEOUT_RECV_SOCKET_RETRY, net_opts ) run = Runner() - run.set_recv_generator(net.receive) - run.set_input_generator(mod.input_generator) - run.set_send_callback(net.send) - run.set_output_callback(mod.input_callback) - - run.set_revc_generator_stop_function(object) - run.set_input_generator_stop_function(mod.input_interrupter) + run.add_action( + { + "name": "RECV", + "producer": net.receive, + "consumer": mod.consumer, + "signal": ssig, + "interrupt": mod.interrupt, # Also force the producer to stop on net error + } + ) + run.add_action( + { + "name": "STDIN", + "producer": mod.producer, + "consumer": net.send, + "signal": ssig, + "interrupt": mod.interrupt, # Externally stop the produer itself + } + ) run.run() + # Run client else: + ssig = StopSignal() net = NetcatClient( encoder, host, port, TIMEOUT_RECV_SOCKET, TIMEOUT_RECV_SOCKET_RETRY, net_opts ) run = Runner() - run.set_recv_generator(net.receive) - run.set_input_generator(mod.input_generator) - run.set_send_callback(net.send) - run.set_output_callback(mod.input_callback) + run.add_action( + { + "name": "RECV", + "producer": net.receive, + "consumer": mod.consumer, + "signal": ssig, + "interrupt": mod.interrupt, # Also force the producer to stop on net error + } + ) + run.add_action( + { + "name": "STDIN", + "producer": mod.producer, + "consumer": net.send, + "signal": ssig, + "interrupt": mod.interrupt, # Externally stop the produer itself + } + ) if type(args.udp_ping_intvl) is int and args.udp_ping_intvl > 0: - run.set_timed_action(args.udp_ping_intvl, net.send, "\x00") - run.set_revc_generator_stop_function(object) - run.set_input_generator_stop_function(object) + run.add_timer( + { + "name": "PING", + "action": net.send, + "intvl": args.udp_ping_intvl, + "args": ("\x00"), + "signal": ssig, + } + ) run.run()
    @@ -3536,22 +3674,40 @@

    Ancestors (in MRO)

    @abstractmethod def __init__(self, options={}): - """Set specific options for this plugin.""" + """ + Set specific options for this plugin. + + Args: + options (dict): A dict which allows you to add custom options to your module + """ raise NotImplementedError("Should have implemented this") @abstractmethod - def input_generator(self): - """Implement a generator function which constantly yields data from some input.""" + def producer(self, ssig): + """ + Implement a generator function which constantly yields data. + + The data could be from various sources such as: received from a socket, + received from user input, received from shell command output or anything else. + + Args: + ssig (StopSignal): A StopSignal instance providing has_stop() and raise_stop() functions + """ raise NotImplementedError("Should have implemented this") @abstractmethod - def input_callback(self, data): - """Implement a callback which processes the input which is parsed from input_generator. """ + def consumer(self, data): + """The consumer takes the consumers' output as input and processes it in some form.""" raise NotImplementedError("Should have implemented this") @abstractmethod - def input_interrupter(self): - """Implement a method, which quits the input_generator.""" + def interrupt(self): + """Defines 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. This method is triggered from outside and is + supposed to stop/shutdown the producer. + If no such interrupt is required, imeplemt it empty.""" raise NotImplementedError("Should have implemented this") @@ -3575,13 +3731,19 @@

    Static methods

    -

    Set specific options for this plugin.

    +

    Set specific options for this plugin.

    +

    Args: + options (dict): A dict which allows you to add custom options to your module

    @abstractmethod
     def __init__(self, options={}):
    -    """Set specific options for this plugin."""
    +    """
    +    Set specific options for this plugin.
    +    Args:
    +        options (dict):    A dict which allows you to add custom options to your module
    +    """
         raise NotImplementedError("Should have implemented this")
     
    @@ -3591,20 +3753,20 @@

    Static methods

    -
    -

    def input_callback(

    self, data)

    +
    +

    def consumer(

    self, data)

    -

    Implement a callback which processes the input which is parsed from input_generator.

    +

    The consumer takes the consumers' output as input and processes it in some form.

    - -
    + +
    @abstractmethod
    -def input_callback(self, data):
    -    """Implement a callback which processes the input which is parsed from input_generator. """
    +def consumer(self, data):
    +    """The consumer takes the consumers' output as input and processes it in some form."""
         raise NotImplementedError("Should have implemented this")
     
    @@ -3614,20 +3776,28 @@

    Static methods

    -
    -

    def input_generator(

    self)

    +
    +

    def interrupt(

    self)

    -

    Implement a generator function which constantly yields data from some input.

    +

    Defines 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. This method is triggered from outside and is +supposed to stop/shutdown the producer. +If no such interrupt is required, imeplemt it empty.

    - -
    + +
    @abstractmethod
    -def input_generator(self):
    -    """Implement a generator function which constantly yields data from some input."""
    +def interrupt(self):
    +    """Defines 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. This method is triggered from outside and is
    +    supposed to stop/shutdown the producer.
    +    If no such interrupt is required, imeplemt it empty."""
         raise NotImplementedError("Should have implemented this")
     
    @@ -3637,20 +3807,30 @@

    Static methods

    -
    -

    def input_interrupter(

    self)

    +
    +

    def producer(

    self, ssig)

    -

    Implement a method, which quits the input_generator.

    +

    Implement a generator function which constantly yields data.

    +

    The data could be from various sources such as: received from a socket, +received from user input, received from shell command output or anything else.

    +

    Args: + ssig (StopSignal): A StopSignal instance providing has_stop() and raise_stop() functions

    - -
    + +
    @abstractmethod
    -def input_interrupter(self):
    -    """Implement a method, which quits the input_generator."""
    +def producer(self, ssig):
    +    """
    +    Implement a generator function which constantly yields data.
    +    The data could be from various sources such as: received from a socket,
    +    received from user input, received from shell command output or anything else.
    +    Args:
    +        ssig (StopSignal): A StopSignal instance providing has_stop() and raise_stop() functions
    +    """
         raise NotImplementedError("Should have implemented this")
     
    @@ -3712,18 +3892,13 @@

    Static methods

    "safe_word": False, # Once this is received, the application quits } - # In case the server is running in UDP mode, - # it must wait for the client to connect in order + # Store the address of the remote end. + # If we are in server role and running in UDP mode, + # it must wait for the client to connect first in order # to retrieve its addr and port in order to be able # to send data back to it. - udp_client_addr = None - udp_client_port = None - - # For client role only - # Store the address and port of the remote server to connect to. - # This is required for self.connect() - remote_addr = None remote_addr = None + remote_port = None # ------------------------------------------------------------------------------ # Constructor / Destructor @@ -3874,12 +4049,6 @@

    Static methods

    # Get around the "[Errno 98] Address already in use" error, if the socket is still in wait # we instruct it to reuse the address anyway. self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - # TODO: Not sure if SO_REUSEPORT is also required - # try: - # self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) - # except AttributeError: - # # Not available on Windows (and maybe others) - # self.log.debug("socket.SO_REUSEPORT is not available on your platform") def bind(self, addr, port): """Bind the socket to an address.""" @@ -3905,6 +4074,8 @@

    Static methods

    self.log.debug("Waiting for TCP client") self.conn, client = self.sock.accept() addr, port = client + self.remote_addr = addr + self.remote_port = port self.log.info("Client connected from {}:{}".format(addr, port)) except (socket.gaierror, socket.error) as error: self.log.error("Accept failed: {}".format(error)) @@ -3929,10 +4100,10 @@

    Static methods

    """Send data.""" # In case of sending data back to an udp client we need to wait # until the client has first connected and told us its addr/port - if self.options["udp"] and self.udp_client_addr is None and self.udp_client_port is None: - self.log.info("Waiting for UDP client to connect") - while self.udp_client_addr is None and self.udp_client_port is None: - time.sleep(0.2) # Less wastefull than using 'pass' + if self.options["udp"] and self.remote_addr is None and self.remote_port is None: + self.log.warning("UDP client has not yet connected. Queueing message") + while self.remote_addr is None and self.remote_port is None: + time.sleep(0.1) # Less wastefull than using 'pass' curr = 0 # bytes send during one loop iteration send = 0 # total bytes send @@ -3943,9 +4114,14 @@

    Static methods

    # Loop until all bytes have been send while send < size: try: - self.log.trace("Trying to send {} bytes".format(size - send)) + self.log.debug( + "Trying to send {} bytes to {}:{}".format( + size - send, self.remote_addr, self.remote_port + ) + ) + self.log.trace("Trying to send: {}".format(repr(data))) if self.options["udp"]: - curr = self.conn.sendto(data, (self.udp_client_addr, self.udp_client_port)) + curr = self.conn.sendto(data, (self.remote_addr, self.remote_port)) send += curr else: curr = self.conn.send(data) @@ -3955,7 +4131,11 @@

    Static methods

    return # Remove 'curr' many bytes from data for the next round data = data[curr:] - self.log.trace("Send {} bytes ({} bytes remaining)".format(curr, size - send)) + self.log.debug( + "Sent {} bytes to {}:{} ({} bytes remaining)".format( + curr, self.remote_addr, self.remote_port, size - send + ) + ) except socket.error as error: if error.errno == socket.errno.EPIPE: self.log.error("TODO:Add desc. Socket error({}): {}".format(error.errno, error)) @@ -3972,19 +4152,28 @@

    Static methods

    self.log.error("Socket OS Error: {}".format(error)) return - def receive(self): - """Generator function to receive data endlessly by yielding it.""" + def receive(self, ssig): + """ + Generator function to receive data endlessly by yielding it. + + :param function interrupter: A Func that returns True/False to tell us to stop or not. + """ # Set current receive timeout self.conn.settimeout(self.recv_timeout) - self.log.trace("Socket Timeout: {}".format(self.recv_timeout_retry)) + self.log.trace("Socket Timeout: {}".format(self.recv_timeout)) # Counts how many times we had a ready timeout for later to decide # if we exceeded maximum retires curr_recv_timeout_retry = 0 while True: + # Ensure to signal that we do not stop receiving data + # if ssig.has_stop(): + # self.log.debug("Interrupt has been requested for receive()") + # return if self.conn is None: self.log.error("Exit. Socket is gone in receive()") + ssig.raise_stop() return # Non-blocking socket with timeout. If the timeout threshold is hit, @@ -3994,20 +4183,27 @@

    Static methods

    # https://manpages.debian.org/buster/manpages-dev/recv.2.en.html (byte, addr) = self.conn.recvfrom(self.options["bufsize"]) - # [1/5] NOTE: This is the place where we can do any checks in between reads as the + # [1/5] Finished receiving all data + # NOTE: This is the place where we can do any checks in between reads as the # socket has been changed from blocking to time-out based. + # NOTE: This is also the place, where we quit in case --wait was specified. except socket.timeout: - # No other thread has terminated yet, and thus not asked us to quit. - # so we can continue waiting for input on the socket - if not THREAD_TERMINATE: + # Let's ask the interrupter() function if we should terminate? + if not ssig.has_stop(): # No action required, continue the loop and read again. continue + self.log.debug("Interrupt has been requested for receive()") # Other threads are done. Let's try to read a few more times before # returning and ending this function (might be data left) if curr_recv_timeout_retry < self.recv_timeout_retry: - self.log.trace("RECV EOF TIMEOUT: AND THREAD_TERMINATE REQUESTED") + self.log.trace( + "Final socket read: {}/{} before quitting.".format( + curr_recv_timeout_retry, self.recv_timeout_retry + ) + ) curr_recv_timeout_retry += 1 continue + ssig.raise_stop() return # [2/5] Connection was forcibly closed @@ -4024,12 +4220,20 @@

    Static methods

    # [3/5] TODO: Still need to figure out what this error is and when it is thrown except AttributeError as error: self.log.error("TODO: What happens here?Attribute Receive Error: {}".format(error)) + ssig.raise_stop() return # We're receiving data again, so let's reset the retry/terminate counter # The counter is incremented in 'except socket.timeout' above. curr_recv_timeout_retry = 0 + # If we're receiving data from a UDP client + # we can firstly/finally set its addr/port in order + # to send data back to it (see send() function) + if self.options["udp"]: + self.remote_addr, self.remote_port = addr + self.log.debug("Client connected: {}:{}".format(self.remote_addr, self.remote_port)) + # [4/5] Upstream (server or client) is gone. Do we reconnect or quit? if not byte: self.log.trace("Socket: Empty data received or otherwise caught.") @@ -4037,7 +4241,7 @@

    Static methods

    if self.role == "server": # Yay, we want to continue and allow new clients if self.__reaccept_from_client(): - self.log.trace("Server can continue, because of --keep") + self.log.trace("Server can continue, because of --keep-open") continue if self.role == "client": # Yay, we want to continue and our client will re-connect upstream again @@ -4045,29 +4249,17 @@

    Static methods

    self.log.trace("Client can continue, because of --reconn") continue - self.log.warning("Exit. Upstream connection gone. No --keep/--reconn specified.") + self.log.warning("Exit. Upstream connection gone. No --keep-open/--reconn set.") + ssig.raise_stop() return # [5/5] We have data to process data = self.enc.decode(byte) + self.log.debug( + "Received {} bytes from {}:{}".format(len(data), self.remote_addr, self.remote_port) + ) self.log.trace("Received: {}".format(repr(data))) - # If we're receiving data from a UDP client - # we can firstly/finally set its addr/port in order - # to send data back to it (see send() function) - if self.options["udp"]: - self.udp_client_addr, self.udp_client_port = addr - # Avoid the noise on UDP connections to spam on every send - if self.udp_client_addr is None or self.udp_client_port is None: - self.log.info( - "Client connected: {}:{}".format(self.udp_client_addr, self.udp_client_port) - ) - # Find for debug - else: - self.log.debug( - "Client connected: {}:{}".format(self.udp_client_addr, self.udp_client_port) - ) - yield data
    @@ -4132,17 +4324,7 @@

    Class variables

    -

    var role

    - - - - -
    -
    - -
    -
    -

    var sock

    +

    var remote_port

    @@ -4152,7 +4334,7 @@

    Class variables

    -

    var udp_client_addr

    +

    var role

    @@ -4162,7 +4344,7 @@

    Class variables

    -

    var udp_client_port

    +

    var sock

    @@ -4228,6 +4410,8 @@

    Static methods

    self.log.debug("Waiting for TCP client") self.conn, client = self.sock.accept() addr, port = client + self.remote_addr = addr + self.remote_port = port self.log.info("Client connected from {}:{}".format(addr, port)) except (socket.gaierror, socket.error) as error: self.log.error("Accept failed: {}".format(error)) @@ -4394,27 +4578,36 @@

    Static methods

    -

    def receive(

    self)

    +

    def receive(

    self, ssig)

    -

    Generator function to receive data endlessly by yielding it.

    +

    Generator function to receive data endlessly by yielding it.

    +

    :param function interrupter: A Func that returns True/False to tell us to stop or not.

    -
    def receive(self):
    -    """Generator function to receive data endlessly by yielding it."""
    +    
    def receive(self, ssig):
    +    """
    +    Generator function to receive data endlessly by yielding it.
    +    :param function interrupter: A Func that returns True/False to tell us to stop or not.
    +    """
         # Set current receive timeout
         self.conn.settimeout(self.recv_timeout)
    -    self.log.trace("Socket Timeout: {}".format(self.recv_timeout_retry))
    +    self.log.trace("Socket Timeout: {}".format(self.recv_timeout))
         # Counts how many times we had a ready timeout for later to decide
         # if we exceeded maximum retires
         curr_recv_timeout_retry = 0
         while True:
    +        # Ensure to signal that we do not stop receiving data
    +        # if ssig.has_stop():
    +        #    self.log.debug("Interrupt has been requested for receive()")
    +        #    return
             if self.conn is None:
                 self.log.error("Exit. Socket is gone in receive()")
    +            ssig.raise_stop()
                 return
             # Non-blocking socket with timeout. If the timeout threshold is hit,
             # it will throw an socket.timeout exception. This is required to see if other
    @@ -4422,20 +4615,27 @@ 

    Static methods

    try: # https://manpages.debian.org/buster/manpages-dev/recv.2.en.html (byte, addr) = self.conn.recvfrom(self.options["bufsize"]) - # [1/5] NOTE: This is the place where we can do any checks in between reads as the + # [1/5] Finished receiving all data + # NOTE: This is the place where we can do any checks in between reads as the # socket has been changed from blocking to time-out based. + # NOTE: This is also the place, where we quit in case --wait was specified. except socket.timeout: - # No other thread has terminated yet, and thus not asked us to quit. - # so we can continue waiting for input on the socket - if not THREAD_TERMINATE: + # Let's ask the interrupter() function if we should terminate? + if not ssig.has_stop(): # No action required, continue the loop and read again. continue + self.log.debug("Interrupt has been requested for receive()") # Other threads are done. Let's try to read a few more times before # returning and ending this function (might be data left) if curr_recv_timeout_retry < self.recv_timeout_retry: - self.log.trace("RECV EOF TIMEOUT: AND THREAD_TERMINATE REQUESTED") + self.log.trace( + "Final socket read: {}/{} before quitting.".format( + curr_recv_timeout_retry, self.recv_timeout_retry + ) + ) curr_recv_timeout_retry += 1 continue + ssig.raise_stop() return # [2/5] Connection was forcibly closed # [Errno 10054] An existing connection was forcibly closed by the remote host @@ -4450,43 +4650,39 @@

    Static methods

    # [3/5] TODO: Still need to figure out what this error is and when it is thrown except AttributeError as error: self.log.error("TODO: What happens here?Attribute Receive Error: {}".format(error)) + ssig.raise_stop() return # We're receiving data again, so let's reset the retry/terminate counter # The counter is incremented in 'except socket.timeout' above. curr_recv_timeout_retry = 0 + # If we're receiving data from a UDP client + # we can firstly/finally set its addr/port in order + # to send data back to it (see send() function) + if self.options["udp"]: + self.remote_addr, self.remote_port = addr + self.log.debug("Client connected: {}:{}".format(self.remote_addr, self.remote_port)) # [4/5] Upstream (server or client) is gone. Do we reconnect or quit? if not byte: self.log.trace("Socket: Empty data received or otherwise caught.") if self.role == "server": # Yay, we want to continue and allow new clients if self.__reaccept_from_client(): - self.log.trace("Server can continue, because of --keep") + self.log.trace("Server can continue, because of --keep-open") continue if self.role == "client": # Yay, we want to continue and our client will re-connect upstream again if self.__reconnect_to_server(): self.log.trace("Client can continue, because of --reconn") continue - self.log.warning("Exit. Upstream connection gone. No --keep/--reconn specified.") + self.log.warning("Exit. Upstream connection gone. No --keep-open/--reconn set.") + ssig.raise_stop() return # [5/5] We have data to process data = self.enc.decode(byte) + self.log.debug( + "Received {} bytes from {}:{}".format(len(data), self.remote_addr, self.remote_port) + ) self.log.trace("Received: {}".format(repr(data))) - # If we're receiving data from a UDP client - # we can firstly/finally set its addr/port in order - # to send data back to it (see send() function) - if self.options["udp"]: - self.udp_client_addr, self.udp_client_port = addr - # Avoid the noise on UDP connections to spam on every send - if self.udp_client_addr is None or self.udp_client_port is None: - self.log.info( - "Client connected: {}:{}".format(self.udp_client_addr, self.udp_client_port) - ) - # Find for debug - else: - self.log.debug( - "Client connected: {}:{}".format(self.udp_client_addr, self.udp_client_port) - ) yield data
    @@ -4511,10 +4707,10 @@

    Static methods

    """Send data.""" # In case of sending data back to an udp client we need to wait # until the client has first connected and told us its addr/port - if self.options["udp"] and self.udp_client_addr is None and self.udp_client_port is None: - self.log.info("Waiting for UDP client to connect") - while self.udp_client_addr is None and self.udp_client_port is None: - time.sleep(0.2) # Less wastefull than using 'pass' + if self.options["udp"] and self.remote_addr is None and self.remote_port is None: + self.log.warning("UDP client has not yet connected. Queueing message") + while self.remote_addr is None and self.remote_port is None: + time.sleep(0.1) # Less wastefull than using 'pass' curr = 0 # bytes send during one loop iteration send = 0 # total bytes send size = len(data) # bytes of data that needs to be send @@ -4523,9 +4719,14 @@

    Static methods

    # Loop until all bytes have been send while send < size: try: - self.log.trace("Trying to send {} bytes".format(size - send)) + self.log.debug( + "Trying to send {} bytes to {}:{}".format( + size - send, self.remote_addr, self.remote_port + ) + ) + self.log.trace("Trying to send: {}".format(repr(data))) if self.options["udp"]: - curr = self.conn.sendto(data, (self.udp_client_addr, self.udp_client_port)) + curr = self.conn.sendto(data, (self.remote_addr, self.remote_port)) send += curr else: curr = self.conn.send(data) @@ -4535,7 +4736,11 @@

    Static methods

    return # Remove 'curr' many bytes from data for the next round data = data[curr:] - self.log.trace("Send {} bytes ({} bytes remaining)".format(curr, size - send)) + self.log.debug( + "Sent {} bytes to {}:{} ({} bytes remaining)".format( + curr, self.remote_addr, self.remote_port, size - send + ) + ) except socket.error as error: if error.errno == socket.errno.EPIPE: self.log.error("TODO:Add desc. Socket error({}): {}".format(error.errno, error)) @@ -4632,17 +4837,14 @@

    Instance variables

    addr = self.gethostbyname(host, port, socket.AF_INET) self.create_socket() self.conn = self.sock - if self.options["udp"]: - self.udp_client_addr = addr - self.udp_client_port = port - else: - self.remote_addr = addr - self.remote_port = port + + self.remote_addr = addr + self.remote_port = port + if not self.options["udp"]: if self.connect(): return - if self.role == "client": - if self._AbstractSocket__reconnect_to_server(): - return + if self._AbstractSocket__reconnect_to_server(): + return sys.exit(1)
    @@ -4683,16 +4885,6 @@

    Class variables

    -
    -
    - -
    -
    -

    var remote_addr

    - - - -
    @@ -4717,33 +4909,13 @@

    Class variables

    -
    -

    var udp_client_addr

    +

    Static methods

    - - - -
    -
    - -
    -
    -

    var udp_client_port

    - - - - -
    -
    - -
    -

    Static methods

    - -
    -
    -

    def __init__(

    self, encoder, host, port, recv_timeout, recv_timeout_retry, options={})

    -
    - +
    +
    +

    def __init__(

    self, encoder, host, port, recv_timeout, recv_timeout_retry, options={})

    +
    + @@ -4760,17 +4932,13 @@

    Static methods

    addr = self.gethostbyname(host, port, socket.AF_INET) self.create_socket() self.conn = self.sock - if self.options["udp"]: - self.udp_client_addr = addr - self.udp_client_port = port - else: - self.remote_addr = addr - self.remote_port = port + self.remote_addr = addr + self.remote_port = port + if not self.options["udp"]: if self.connect(): return - if self.role == "client": - if self._AbstractSocket__reconnect_to_server(): - return + if self._AbstractSocket__reconnect_to_server(): + return sys.exit(1)
    @@ -4797,6 +4965,8 @@

    Static methods

    self.log.debug("Waiting for TCP client") self.conn, client = self.sock.accept() addr, port = client + self.remote_addr = addr + self.remote_port = port self.log.info("Client connected from {}:{}".format(addr, port)) except (socket.gaierror, socket.error) as error: self.log.error("Accept failed: {}".format(error)) @@ -4963,27 +5133,36 @@

    Static methods

    -

    def receive(

    self)

    +

    def receive(

    self, ssig)

    -

    Generator function to receive data endlessly by yielding it.

    +

    Generator function to receive data endlessly by yielding it.

    +

    :param function interrupter: A Func that returns True/False to tell us to stop or not.

    -
    def receive(self):
    -    """Generator function to receive data endlessly by yielding it."""
    +    
    def receive(self, ssig):
    +    """
    +    Generator function to receive data endlessly by yielding it.
    +    :param function interrupter: A Func that returns True/False to tell us to stop or not.
    +    """
         # Set current receive timeout
         self.conn.settimeout(self.recv_timeout)
    -    self.log.trace("Socket Timeout: {}".format(self.recv_timeout_retry))
    +    self.log.trace("Socket Timeout: {}".format(self.recv_timeout))
         # Counts how many times we had a ready timeout for later to decide
         # if we exceeded maximum retires
         curr_recv_timeout_retry = 0
         while True:
    +        # Ensure to signal that we do not stop receiving data
    +        # if ssig.has_stop():
    +        #    self.log.debug("Interrupt has been requested for receive()")
    +        #    return
             if self.conn is None:
                 self.log.error("Exit. Socket is gone in receive()")
    +            ssig.raise_stop()
                 return
             # Non-blocking socket with timeout. If the timeout threshold is hit,
             # it will throw an socket.timeout exception. This is required to see if other
    @@ -4991,20 +5170,27 @@ 

    Static methods

    try: # https://manpages.debian.org/buster/manpages-dev/recv.2.en.html (byte, addr) = self.conn.recvfrom(self.options["bufsize"]) - # [1/5] NOTE: This is the place where we can do any checks in between reads as the + # [1/5] Finished receiving all data + # NOTE: This is the place where we can do any checks in between reads as the # socket has been changed from blocking to time-out based. + # NOTE: This is also the place, where we quit in case --wait was specified. except socket.timeout: - # No other thread has terminated yet, and thus not asked us to quit. - # so we can continue waiting for input on the socket - if not THREAD_TERMINATE: + # Let's ask the interrupter() function if we should terminate? + if not ssig.has_stop(): # No action required, continue the loop and read again. continue + self.log.debug("Interrupt has been requested for receive()") # Other threads are done. Let's try to read a few more times before # returning and ending this function (might be data left) if curr_recv_timeout_retry < self.recv_timeout_retry: - self.log.trace("RECV EOF TIMEOUT: AND THREAD_TERMINATE REQUESTED") + self.log.trace( + "Final socket read: {}/{} before quitting.".format( + curr_recv_timeout_retry, self.recv_timeout_retry + ) + ) curr_recv_timeout_retry += 1 continue + ssig.raise_stop() return # [2/5] Connection was forcibly closed # [Errno 10054] An existing connection was forcibly closed by the remote host @@ -5019,43 +5205,39 @@

    Static methods

    # [3/5] TODO: Still need to figure out what this error is and when it is thrown except AttributeError as error: self.log.error("TODO: What happens here?Attribute Receive Error: {}".format(error)) + ssig.raise_stop() return # We're receiving data again, so let's reset the retry/terminate counter # The counter is incremented in 'except socket.timeout' above. curr_recv_timeout_retry = 0 + # If we're receiving data from a UDP client + # we can firstly/finally set its addr/port in order + # to send data back to it (see send() function) + if self.options["udp"]: + self.remote_addr, self.remote_port = addr + self.log.debug("Client connected: {}:{}".format(self.remote_addr, self.remote_port)) # [4/5] Upstream (server or client) is gone. Do we reconnect or quit? if not byte: self.log.trace("Socket: Empty data received or otherwise caught.") if self.role == "server": # Yay, we want to continue and allow new clients if self.__reaccept_from_client(): - self.log.trace("Server can continue, because of --keep") + self.log.trace("Server can continue, because of --keep-open") continue if self.role == "client": # Yay, we want to continue and our client will re-connect upstream again if self.__reconnect_to_server(): self.log.trace("Client can continue, because of --reconn") continue - self.log.warning("Exit. Upstream connection gone. No --keep/--reconn specified.") + self.log.warning("Exit. Upstream connection gone. No --keep-open/--reconn set.") + ssig.raise_stop() return # [5/5] We have data to process data = self.enc.decode(byte) + self.log.debug( + "Received {} bytes from {}:{}".format(len(data), self.remote_addr, self.remote_port) + ) self.log.trace("Received: {}".format(repr(data))) - # If we're receiving data from a UDP client - # we can firstly/finally set its addr/port in order - # to send data back to it (see send() function) - if self.options["udp"]: - self.udp_client_addr, self.udp_client_port = addr - # Avoid the noise on UDP connections to spam on every send - if self.udp_client_addr is None or self.udp_client_port is None: - self.log.info( - "Client connected: {}:{}".format(self.udp_client_addr, self.udp_client_port) - ) - # Find for debug - else: - self.log.debug( - "Client connected: {}:{}".format(self.udp_client_addr, self.udp_client_port) - ) yield data
    @@ -5080,10 +5262,10 @@

    Static methods

    """Send data.""" # In case of sending data back to an udp client we need to wait # until the client has first connected and told us its addr/port - if self.options["udp"] and self.udp_client_addr is None and self.udp_client_port is None: - self.log.info("Waiting for UDP client to connect") - while self.udp_client_addr is None and self.udp_client_port is None: - time.sleep(0.2) # Less wastefull than using 'pass' + if self.options["udp"] and self.remote_addr is None and self.remote_port is None: + self.log.warning("UDP client has not yet connected. Queueing message") + while self.remote_addr is None and self.remote_port is None: + time.sleep(0.1) # Less wastefull than using 'pass' curr = 0 # bytes send during one loop iteration send = 0 # total bytes send size = len(data) # bytes of data that needs to be send @@ -5092,9 +5274,14 @@

    Static methods

    # Loop until all bytes have been send while send < size: try: - self.log.trace("Trying to send {} bytes".format(size - send)) + self.log.debug( + "Trying to send {} bytes to {}:{}".format( + size - send, self.remote_addr, self.remote_port + ) + ) + self.log.trace("Trying to send: {}".format(repr(data))) if self.options["udp"]: - curr = self.conn.sendto(data, (self.udp_client_addr, self.udp_client_port)) + curr = self.conn.sendto(data, (self.remote_addr, self.remote_port)) send += curr else: curr = self.conn.send(data) @@ -5104,7 +5291,11 @@

    Static methods

    return # Remove 'curr' many bytes from data for the next round data = data[curr:] - self.log.trace("Send {} bytes ({} bytes remaining)".format(curr, size - send)) + self.log.debug( + "Sent {} bytes to {}:{} ({} bytes remaining)".format( + curr, self.remote_addr, self.remote_port, size - send + ) + ) except socket.error as error: if error.errno == socket.errno.EPIPE: self.log.error("TODO:Add desc. Socket error({}): {}".format(error.errno, error)) @@ -5133,6 +5324,26 @@

    Instance variables

    +
    +
    + +
    +
    +

    var remote_addr

    + + + + +
    +
    + +
    +
    +

    var remote_port

    + + + +
    @@ -5178,6 +5389,10 @@

    Instance variables

    shell=False, env=env, ) + # Python-2 compat (doesn't have FileNotFoundError) + except OSError: + self.log.error("Specified executable '{}' not found".format(self.executable)) + sys.exit(1) except FileNotFoundError: self.log.error("Specified executable '{}' not found".format(self.executable)) sys.exit(1) @@ -5190,14 +5405,6 @@

    Instance variables

    self.log.trace("Killing executable: {} with pid {}".format(self.executable, self.p.pid)) self.p.kill() - # ------------------------------------------------------------------------------ - # Public Functions - # ------------------------------------------------------------------------------ - def input_interrupter(self): - """Stop function that can be called externally to close this instance.""" - self.log.trace("[NetcatPluginCommand] subprocess.kill() was raised by input_unterrupter()") - self.p.kill() - def __set_input_timeout(self, timeout=0.1): """Throw a TimeOutError Exception for sys.stdin (Linux only).""" # select((rlist, wlist, xlist, timeout) @@ -5208,9 +5415,20 @@

    Instance variables

    if not i: raise BaseException("timed out") - def input_generator(self): + # ------------------------------------------------------------------------------ + # Public Functions + # ------------------------------------------------------------------------------ + def interrupt(self): + """Stop function that can be called externally to close this instance.""" + self.log.trace("[NetcatPluginCommand] subprocess.kill() was raised by input_unterrupter()") + self.p.kill() + + def producer(self, ssig): """Constantly ask for input.""" while True: + if ssig.has_stop(): + self.log.trace("Stop signal acknowledged in Command") + return self.log.trace("Reading command output") # TODO: non-blocking read does not seem to work or? # try: @@ -5230,7 +5448,7 @@

    Instance variables

    break yield data - def input_callback(self, data): + def consumer(self, data): """Send data received to stdin (command input).""" data = self.enc.encode(data) self.log.trace("Appending to stdin: {}".format(data)) @@ -5294,6 +5512,10 @@

    Static methods

    shell=False, env=env, ) + # Python-2 compat (doesn't have FileNotFoundError) + except OSError: + self.log.error("Specified executable '{}' not found".format(self.executable)) + sys.exit(1) except FileNotFoundError: self.log.error("Specified executable '{}' not found".format(self.executable)) sys.exit(1) @@ -5307,8 +5529,8 @@

    Static methods

    -
    -

    def input_callback(

    self, data)

    +
    +

    def consumer(

    self, data)

    @@ -5316,9 +5538,9 @@

    Static methods

    Send data received to stdin (command input).

    - -
    -
    def input_callback(self, data):
    +  
    +  
    +
    def consumer(self, data):
         """Send data received to stdin (command input)."""
         data = self.enc.encode(data)
         self.log.trace("Appending to stdin: {}".format(data))
    @@ -5332,8 +5554,31 @@ 

    Static methods

    -
    -

    def input_generator(

    self)

    +
    +

    def interrupt(

    self)

    +
    + + + + +

    Stop function that can be called externally to close this instance.

    +
    + +
    +
    def interrupt(self):
    +    """Stop function that can be called externally to close this instance."""
    +    self.log.trace("[NetcatPluginCommand] subprocess.kill() was raised by input_unterrupter()")
    +    self.p.kill()
    +
    +
    +
    + +
    + + +
    +
    +

    def producer(

    self, ssig)

    @@ -5341,11 +5586,14 @@

    Static methods

    Constantly ask for input.

    - -
    -
    def input_generator(self):
    +  
    +  
    +
    def producer(self, ssig):
         """Constantly ask for input."""
         while True:
    +        if ssig.has_stop():
    +            self.log.trace("Stop signal acknowledged in Command")
    +            return
             self.log.trace("Reading command output")
             # TODO: non-blocking read does not seem to work or?
             # try:
    @@ -5368,29 +5616,6 @@ 

    Static methods

    -
    - - -
    -
    -

    def input_interrupter(

    self)

    -
    - - - - -

    Stop function that can be called externally to close this instance.

    -
    - -
    -
    def input_interrupter(self):
    -    """Stop function that can be called externally to close this instance."""
    -    self.log.trace("[NetcatPluginCommand] subprocess.kill() was raised by input_unterrupter()")
    -    self.p.kill()
    -
    -
    -
    -

    Instance variables

    @@ -5446,17 +5671,13 @@

    Instance variables

    callback that writes to stdout. """ - # Line feeds to use for user input - linefeed = "\n" + # Replace '\n' linefeeds (if they exist) with CRLF ('\r\n')? + crlf = False # Non-blocking read from stdin achieved via timeout. # Specify timeout in seconds. input_timeout = None - # Used by the input_interrupter to set this to true. - # The input_generator will frequently check this value - __quit = False - # ------------------------------------------------------------------------------ # Constructor / Destructor # ------------------------------------------------------------------------------ @@ -5465,25 +5686,31 @@

    Instance variables

    super(AbstractNetcatPlugin, self).__init__() assert "encoder" in options assert "input_timeout" in options - assert "linefeed" in options + assert "crlf" in options self.log = logging.getLogger(__name__) self.enc = options["encoder"] if "input_timeout" in options: self.input_timeout = options["input_timeout"] - if "linefeed" in options: - self.linefeed = options["linefeed"] + if "crlf" in options: + self.crlf = options["crlf"] # ------------------------------------------------------------------------------ # Private Functions # ------------------------------------------------------------------------------ def __use_linefeeds(self, data): """Ensure the user input has the desired linefeeds --crlf or not.""" + # No replacement requested + if not self.crlf: + return data + # Already have CRLF at the end if data.endswith("\r\n"): - data = data[:-2] - elif data.endswith("\n") or data.endswith("\r"): - data = data[:-1] - data += self.linefeed + return data + # Replace current newline character with CRLF + if data.endswith("\n"): + self.log.debug("Replacing LF with CRLF") + return data[:-1] + "\r\n" + # Otherwise just return as it is return data def __set_input_timeout(self): @@ -5495,19 +5722,15 @@

    Instance variables

    # ------------------------------------------------------------------------------ # Public Functions # ------------------------------------------------------------------------------ - def input_interrupter(self): - global THREAD_TERMINATE - """Stop function that can be called externally to close this instance.""" - self.log.trace("[NetcatOutputCommand] quit flag was set by input_interrupter()") - self.__quit = True - def input_generator(self): + def producer(self, ssig): """Constantly ask for user input.""" # 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.__quit: + if ssig.has_stop(): + self.log.trace("Stop signal acknowledged for reading STDIN-1") return try: # TODO: select() does not work for windows on stdin/stdout @@ -5518,28 +5741,33 @@

    Instance variables

    # 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 THREAD_TERMINATE: - self.log.trace("STDIN: terminate") + if ssig.has_stop(): + self.log.trace("Stop signal acknowledged for reading STDIN-2") return # TODO: Re-enable this for very verbose logging # self.log.trace("STDIN: timeout. Waiting for input...") continue if line: - self.log.trace("Yielding stdin") + self.log.debug("Received {} bytes from STDIN".format(len(line))) + self.log.trace("Received: {}".format(repr(line))) yield self.__use_linefeeds(line) # EOF or + else: # DO NOT RETURN HERE BLINDLY, THE UPSTREAM CONNECTION MUST GO FIRST! - if THREAD_TERMINATE: - self.log.trace("No more input generated, quitting.") + if ssig.has_stop(): + self.log.trace("Stop signal acknowledged for reading STDIN-3") return # TODO: Re-enable this for very verbose logging # self.log.trace("STDIN: Reached EOF, repeating") - def input_callback(self, data): + def consumer(self, data): """Print received data to stdout.""" print(data, end="") sys.stdout.flush() # TODO:Is this required? What does this do? Test this! + + def interrupt(self): + """Empty interrupt.""" + pass
    @@ -5555,7 +5783,7 @@

    Ancestors (in MRO)

    Class variables

    -

    var input_timeout

    +

    var crlf

    @@ -5565,7 +5793,7 @@

    Class variables

    -

    var linefeed

    +

    var input_timeout

    @@ -5593,13 +5821,13 @@

    Static methods

    super(AbstractNetcatPlugin, self).__init__() assert "encoder" in options assert "input_timeout" in options - assert "linefeed" in options + assert "crlf" in options self.log = logging.getLogger(__name__) self.enc = options["encoder"] if "input_timeout" in options: self.input_timeout = options["input_timeout"] - if "linefeed" in options: - self.linefeed = options["linefeed"] + if "crlf" in options: + self.crlf = options["crlf"]
    @@ -5608,8 +5836,8 @@

    Static methods

    -
    -

    def input_callback(

    self, data)

    +
    +

    def consumer(

    self, data)

    @@ -5617,9 +5845,9 @@

    Static methods

    Print received data to stdout.

    - -
    -
    def input_callback(self, data):
    +  
    +  
    +
    def consumer(self, data):
         """Print received data to stdout."""
         print(data, end="")
         sys.stdout.flush()  # TODO:Is this required? What does this do? Test this!
    @@ -5631,8 +5859,30 @@ 

    Static methods

    -
    -

    def input_generator(

    self)

    +
    +

    def interrupt(

    self)

    +
    + + + + +

    Empty interrupt.

    +
    + +
    +
    def interrupt(self):
    +    """Empty interrupt."""
    +    pass
    +
    +
    +
    + +
    + + +
    +
    +

    def producer(

    self, ssig)

    @@ -5640,15 +5890,16 @@

    Static methods

    Constantly ask for user input.

    - -
    -
    def input_generator(self):
    +  
    +  
    +
    def producer(self, ssig):
         """Constantly ask for user input."""
         # 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.__quit:
    +        if ssig.has_stop():
    +            self.log.trace("Stop signal acknowledged for reading STDIN-1")
                 return
             try:
                 # TODO: select() does not work for windows on stdin/stdout
    @@ -5659,49 +5910,26 @@ 

    Static methods

    # 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 THREAD_TERMINATE: - self.log.trace("STDIN: terminate") + if ssig.has_stop(): + self.log.trace("Stop signal acknowledged for reading STDIN-2") return # TODO: Re-enable this for very verbose logging # self.log.trace("STDIN: timeout. Waiting for input...") continue if line: - self.log.trace("Yielding stdin") + self.log.debug("Received {} bytes from STDIN".format(len(line))) + self.log.trace("Received: {}".format(repr(line))) yield self.__use_linefeeds(line) # EOF or + else: # DO NOT RETURN HERE BLINDLY, THE UPSTREAM CONNECTION MUST GO FIRST! - if THREAD_TERMINATE: - self.log.trace("No more input generated, quitting.") + if ssig.has_stop(): + self.log.trace("Stop signal acknowledged for reading STDIN-3") return
    -
    - - -
    -
    -

    def input_interrupter(

    self)

    -
    - - - - -

    Implement a method, which quits the input_generator.

    -
    - -
    -
    def input_interrupter(self):
    -    global THREAD_TERMINATE
    -    """Stop function that can be called externally to close this instance."""
    -    self.log.trace("[NetcatOutputCommand] quit flag was set by input_interrupter()")
    -    self.__quit = True
    -
    -
    -
    -

    Instance variables

    @@ -5828,17 +6056,7 @@

    Class variables

    -

    var role

    - - - - -
    -
    - -
    -
    -

    var sock

    +

    var remote_port

    @@ -5848,7 +6066,7 @@

    Class variables

    -

    var udp_client_addr

    +

    var role

    @@ -5858,7 +6076,7 @@

    Class variables

    -

    var udp_client_port

    +

    var sock

    @@ -5933,6 +6151,8 @@

    Static methods

    self.log.debug("Waiting for TCP client") self.conn, client = self.sock.accept() addr, port = client + self.remote_addr = addr + self.remote_port = port self.log.info("Client connected from {}:{}".format(addr, port)) except (socket.gaierror, socket.error) as error: self.log.error("Accept failed: {}".format(error)) @@ -6099,27 +6319,36 @@

    Static methods

    -

    def receive(

    self)

    +

    def receive(

    self, ssig)

    -

    Generator function to receive data endlessly by yielding it.

    +

    Generator function to receive data endlessly by yielding it.

    +

    :param function interrupter: A Func that returns True/False to tell us to stop or not.

    -
    def receive(self):
    -    """Generator function to receive data endlessly by yielding it."""
    +    
    def receive(self, ssig):
    +    """
    +    Generator function to receive data endlessly by yielding it.
    +    :param function interrupter: A Func that returns True/False to tell us to stop or not.
    +    """
         # Set current receive timeout
         self.conn.settimeout(self.recv_timeout)
    -    self.log.trace("Socket Timeout: {}".format(self.recv_timeout_retry))
    +    self.log.trace("Socket Timeout: {}".format(self.recv_timeout))
         # Counts how many times we had a ready timeout for later to decide
         # if we exceeded maximum retires
         curr_recv_timeout_retry = 0
         while True:
    +        # Ensure to signal that we do not stop receiving data
    +        # if ssig.has_stop():
    +        #    self.log.debug("Interrupt has been requested for receive()")
    +        #    return
             if self.conn is None:
                 self.log.error("Exit. Socket is gone in receive()")
    +            ssig.raise_stop()
                 return
             # Non-blocking socket with timeout. If the timeout threshold is hit,
             # it will throw an socket.timeout exception. This is required to see if other
    @@ -6127,20 +6356,27 @@ 

    Static methods

    try: # https://manpages.debian.org/buster/manpages-dev/recv.2.en.html (byte, addr) = self.conn.recvfrom(self.options["bufsize"]) - # [1/5] NOTE: This is the place where we can do any checks in between reads as the + # [1/5] Finished receiving all data + # NOTE: This is the place where we can do any checks in between reads as the # socket has been changed from blocking to time-out based. + # NOTE: This is also the place, where we quit in case --wait was specified. except socket.timeout: - # No other thread has terminated yet, and thus not asked us to quit. - # so we can continue waiting for input on the socket - if not THREAD_TERMINATE: + # Let's ask the interrupter() function if we should terminate? + if not ssig.has_stop(): # No action required, continue the loop and read again. continue + self.log.debug("Interrupt has been requested for receive()") # Other threads are done. Let's try to read a few more times before # returning and ending this function (might be data left) if curr_recv_timeout_retry < self.recv_timeout_retry: - self.log.trace("RECV EOF TIMEOUT: AND THREAD_TERMINATE REQUESTED") + self.log.trace( + "Final socket read: {}/{} before quitting.".format( + curr_recv_timeout_retry, self.recv_timeout_retry + ) + ) curr_recv_timeout_retry += 1 continue + ssig.raise_stop() return # [2/5] Connection was forcibly closed # [Errno 10054] An existing connection was forcibly closed by the remote host @@ -6155,43 +6391,39 @@

    Static methods

    # [3/5] TODO: Still need to figure out what this error is and when it is thrown except AttributeError as error: self.log.error("TODO: What happens here?Attribute Receive Error: {}".format(error)) + ssig.raise_stop() return # We're receiving data again, so let's reset the retry/terminate counter # The counter is incremented in 'except socket.timeout' above. curr_recv_timeout_retry = 0 + # If we're receiving data from a UDP client + # we can firstly/finally set its addr/port in order + # to send data back to it (see send() function) + if self.options["udp"]: + self.remote_addr, self.remote_port = addr + self.log.debug("Client connected: {}:{}".format(self.remote_addr, self.remote_port)) # [4/5] Upstream (server or client) is gone. Do we reconnect or quit? if not byte: self.log.trace("Socket: Empty data received or otherwise caught.") if self.role == "server": # Yay, we want to continue and allow new clients if self.__reaccept_from_client(): - self.log.trace("Server can continue, because of --keep") + self.log.trace("Server can continue, because of --keep-open") continue if self.role == "client": # Yay, we want to continue and our client will re-connect upstream again if self.__reconnect_to_server(): self.log.trace("Client can continue, because of --reconn") continue - self.log.warning("Exit. Upstream connection gone. No --keep/--reconn specified.") + self.log.warning("Exit. Upstream connection gone. No --keep-open/--reconn set.") + ssig.raise_stop() return # [5/5] We have data to process data = self.enc.decode(byte) + self.log.debug( + "Received {} bytes from {}:{}".format(len(data), self.remote_addr, self.remote_port) + ) self.log.trace("Received: {}".format(repr(data))) - # If we're receiving data from a UDP client - # we can firstly/finally set its addr/port in order - # to send data back to it (see send() function) - if self.options["udp"]: - self.udp_client_addr, self.udp_client_port = addr - # Avoid the noise on UDP connections to spam on every send - if self.udp_client_addr is None or self.udp_client_port is None: - self.log.info( - "Client connected: {}:{}".format(self.udp_client_addr, self.udp_client_port) - ) - # Find for debug - else: - self.log.debug( - "Client connected: {}:{}".format(self.udp_client_addr, self.udp_client_port) - ) yield data
    @@ -6216,10 +6448,10 @@

    Static methods

    """Send data.""" # In case of sending data back to an udp client we need to wait # until the client has first connected and told us its addr/port - if self.options["udp"] and self.udp_client_addr is None and self.udp_client_port is None: - self.log.info("Waiting for UDP client to connect") - while self.udp_client_addr is None and self.udp_client_port is None: - time.sleep(0.2) # Less wastefull than using 'pass' + if self.options["udp"] and self.remote_addr is None and self.remote_port is None: + self.log.warning("UDP client has not yet connected. Queueing message") + while self.remote_addr is None and self.remote_port is None: + time.sleep(0.1) # Less wastefull than using 'pass' curr = 0 # bytes send during one loop iteration send = 0 # total bytes send size = len(data) # bytes of data that needs to be send @@ -6228,9 +6460,14 @@

    Static methods

    # Loop until all bytes have been send while send < size: try: - self.log.trace("Trying to send {} bytes".format(size - send)) + self.log.debug( + "Trying to send {} bytes to {}:{}".format( + size - send, self.remote_addr, self.remote_port + ) + ) + self.log.trace("Trying to send: {}".format(repr(data))) if self.options["udp"]: - curr = self.conn.sendto(data, (self.udp_client_addr, self.udp_client_port)) + curr = self.conn.sendto(data, (self.remote_addr, self.remote_port)) send += curr else: curr = self.conn.send(data) @@ -6240,7 +6477,11 @@

    Static methods

    return # Remove 'curr' many bytes from data for the next round data = data[curr:] - self.log.trace("Send {} bytes ({} bytes remaining)".format(curr, size - send)) + self.log.debug( + "Sent {} bytes to {}:{} ({} bytes remaining)".format( + curr, self.remote_addr, self.remote_port, size - send + ) + ) except socket.error as error: if error.errno == socket.errno.EPIPE: self.log.error("TODO:Add desc. Socket error({}): {}".format(error.errno, error)) @@ -6276,6 +6517,24 @@

    Static methods

    class Runner(object):
         """Runner class that takes care about putting everything into threads."""
     
    +    # Dict of producer/consumer action to run in a Thread.
    +    # Each list item will be run in a single thread
    +    # {
    +    #   "name": {
    +    #     {
    +    #       "producer": "function",     # A func which yields data
    +    #       "consumer": "function",     # A callback func to process the data
    +    #       "interrupter": "function",  # A interrupt func to tell the producer to stop
    +    #   }
    +    # }
    +    __actions = {}
    +    __timers = {}
    +
    +    # A dict which holds the threads created from actions.
    +    # The name is based on the __actions name
    +    # {"name": ""}
    +    __threads = {}
    +
         # ------------------------------------------------------------------------------
         # Constructor / Destructor
         # ------------------------------------------------------------------------------
    @@ -6283,135 +6542,141 @@ 

    Static methods

    """Constructor.""" self.log = logging.getLogger(__name__) - # Generator - [ - { - "name": "", - "input_generator": {"fnc": "", "args": "", "kwargs": ""}, - "input_interrupter": {"fnc": "", "args": "", "kwargs": ""}, - "input_callback": {"fnc": "", "args": "", "kwargs": ""}, - } - ] - # Timebased - # ------------------------------------------------------------------------------ # Public Functions # ------------------------------------------------------------------------------ - def set_recv_generator(self, func): - """Set generator func which constantly receives network data.""" - self.recv_generator = func - - def set_input_generator(self, func): - """Set generator func which constantly receives input (shell output/user input).""" - self.input_generator = func - - def set_send_callback(self, func): - """Set the callback for sending data to a socket.""" - self.send_callback = func - - def set_output_callback(self, func): - """Set the callback for outputting data to stdin/stdout.""" - self.output_callback = func - - def set_revc_generator_stop_function(self, func): - self.recv_generator_stop_fn = func + def add_action(self, action): + """ + Enables a function to run threaded by the producer/consumer runner. - def set_input_generator_stop_function(self, func): - self.input_generator_stop_fn = func + :param str name: Name for logging output + :param function producer: A generator function which yields data + :param function consumer: A callback which consumes data from the generator + :param function interrupter: A func that signals a stop event to the producer + """ + assert "name" in action + assert "producer" in action + assert "consumer" in action + assert "signal" in action + assert "interrupt" in action + self.__actions[action["name"]] = { + "name": action["name"], + "producer": action["producer"], + "consumer": action["consumer"], + "signal": action["signal"], + "interrupt": action["interrupt"], + } - def set_timed_action(self, intvl, func, *args, **kwargs): - """Set a function that should be called periodically.""" - self.timed_action_intvl = intvl - self.timed_action_func = func - self.timed_action_args = args - self.timed_action_kwargs = kwargs + def add_timer(self, timer): + self.__timers[timer["name"]] = { + "action": timer["action"], + "intvl": timer["intvl"], + "args": timer["args"] if "args" in timer else None, + "kwargs": timer["kwargs"] if "kwargs" in timer else {}, + "signal": timer["signal"], + } def run(self): """Run threaded NetCat.""" - global THREAD_TERMINATE - - assert hasattr(self, "recv_generator"), "Error, recv_generator not set" - assert hasattr(self, "input_generator"), "Error, input_generator not set" - assert hasattr(self, "send_callback"), "Error, send_callback not set" - assert hasattr(self, "output_callback"), "Error, output_callback not set" - def receiver(): - """Receive data from a socket and process it with a callback. - - receive: Must be a generator function to receive network data. - callback: Must be a callback to process received data, e.g.: print to stdin/stdout. + def run_action(name, producer, consumer, ssig): """ - self.log.trace("[Thread-Recv] START") - for data in self.recv_generator(): - self.log.trace("[Thread-Recv] recv_generator() received: {}".format(repr(data))) - self.output_callback(data) - self.log.trace("[Thread-Recv] STOP") - - def sender(): - """Receive data from user-input/command-output and process it with a callback. + Receive data (network, user-input, shell-output) and process it (send, output). - receive: Must be a generator function to receive user-input or command output. - callback: Must be a callback to send this data to a socket. + :param str name: Name for logging output + :param function producer: A generator function which yields data + :param function consumer: A callback which consumes data from the generator + :param StopSignal ssig: Providing has_stop() and raise_stop() """ - self.log.trace("[Thread-Send] START") - for data in self.input_generator(): - self.log.trace("[Thread-Send] input_generator() received: {}".format(repr(data))) - self.send_callback(data) - self.log.trace("[Thread-Send] STOP") + self.log.trace("[{}] Producer Start".format(name)) + for data in producer(ssig): + self.log.trace("[{}] Producer received: {}".format(name, repr(data))) + consumer(data) + self.log.trace("[{}] Producer Stop".format(name)) - def timer(): + def run_timer(name, action, intvl, ssig, *args, **kwargs): """Execute periodic tasks by an optional provided time_action.""" - self.log.trace("[Thread-Time] START") - self.log.debug( - "Ready for timed action every {} seconds".format(self.timed_action_intvl) - ) + self.log.trace("[{}] Timer Start (exec every {} sec)".format(name, intvl)) time_last = int(time.time()) while True: + if ssig.has_stop(): + self.log.trace("Stop signal acknowledged for timer {}".format(name)) + return time_now = int(time.time()) - if time_now > time_last + self.timed_action_intvl: + if time_now > time_last + intvl: self.log.debug("[{}] Executing timed function".format(time_now)) - self.timed_action_func(*self.timed_action_args, **self.timed_action_kwargs) + if args is not None: + if kwargs: + action(*args, **kwargs) + else: + action(*args) + else: + if kwargs: + action(**kwargs) + else: + action() time_last = time_now # Reset previous time time.sleep(1) - # Start sending and receiving threads - self.tr = threading.Thread(target=receiver, name="Thread-Recv") - self.ts = threading.Thread(target=sender, name="Thread-Send") - # If the main thread kills, this thread will be killed too. - self.tr.daemon = False # No daemon, wait for each other (e.g.: data received - self.ts.daemon = False # should also be outputted) - # Start threads - self.tr.start() - # time.sleep(0.1) - self.ts.start() - - if hasattr(self, "timed_action_intvl"): - self.tt = threading.Thread(target=timer, name="Thread-Time") - self.tt.daemon = True - self.tt.start() - - # Cleanup the main program - while True: - # TODO: is this required? (check if need to press Ctrl+c twice) - # if not THREAD_TERMINATE: - # self.input_generator_stop_fn() - # self.recv_generator_stop_fn() - if not self.tr.is_alive(): - self.log.trace("Setting THREAD_TERMINATE=True from Thread-Recv death") - self.log.trace("Waiting for Thread-Send to finish") - # time.sleep(0.1) - THREAD_TERMINATE = True - self.input_generator_stop_fn() - self.ts.join() - sys.exit(0) - if not self.ts.is_alive(): - self.log.trace("Setting THREAD_TERMINATE=True from Thread-Send death") - self.log.trace("Waiting for Thread-Recv to finish") - # time.sleep(0.1) - THREAD_TERMINATE = True - self.recv_generator_stop_fn() - self.tr.join() - sys.exit(0) + # Start available action in a thread + for key in self.__actions: + # Create Thread object + thread = threading.Thread( + target=run_action, + name=key, + args=( + key, + self.__actions[key]["producer"], + self.__actions[key]["consumer"], + self.__actions[key]["signal"], + ), + ) + thread.daemon = False + thread.start() + self.__threads[key] = thread + # Start available timers in a thread + for key in self.__timers: + # Create Thread object + thread = threading.Thread( + target=run_timer, + name=key, + args=( + key, + self.__timers[key]["action"], + self.__timers[key]["intvl"], + self.__timers[key]["signal"], + self.__timers[key]["args"], + ), + kwargs=self.__timers[key]["kwargs"], + ) + thread.daemon = False + thread.start() + + def stop(force): + """Stop threads.""" + for key in self.__threads: + if not self.__threads[key].is_alive() or force: + self.log.trace("Raise stop signal for {}".format(self.__threads[key].getName())) + self.__actions[key]["signal"].raise_stop() + self.log.trace("Call interrupt for {}".format(self.__threads[key].getName())) + self.__actions[key]["interrupt"]() + self.log.trace("Joining {}".format(self.__threads[key].getName())) + self.__threads[key].join(timeout=0.1) + # If all threads have died, exit + if not all([self.__threads[key].is_alive() for key in self.__threads]) or force: + if force: + sys.exit(1) + else: + sys.exit(0) + + try: + while True: + stop(False) + # Need a timeout to not skyrocket the CPU + time.sleep(0.1) + except KeyboardInterrupt: + print() + stop(True)
    @@ -6448,94 +6713,41 @@

    Static methods

    -
    -

    def run(

    self)

    +
    +

    def add_action(

    self, action)

    -

    Run threaded NetCat.

    +

    Enables a function to run threaded by the producer/consumer runner.

    +

    :param str name: Name for logging output +:param function producer: A generator function which yields data +:param function consumer: A callback which consumes data from the generator +:param function interrupter: A func that signals a stop event to the producer

    - -
    -
    def run(self):
    -    """Run threaded NetCat."""
    -    global THREAD_TERMINATE
    -    assert hasattr(self, "recv_generator"), "Error, recv_generator not set"
    -    assert hasattr(self, "input_generator"), "Error, input_generator not set"
    -    assert hasattr(self, "send_callback"), "Error, send_callback not set"
    -    assert hasattr(self, "output_callback"), "Error, output_callback not set"
    -    def receiver():
    -        """Receive data from a socket and process it with a callback.
    -        receive: Must be a generator function to receive network data.
    -        callback: Must be a callback to process received data, e.g.: print to stdin/stdout.
    -        """
    -        self.log.trace("[Thread-Recv] START")
    -        for data in self.recv_generator():
    -            self.log.trace("[Thread-Recv] recv_generator() received: {}".format(repr(data)))
    -            self.output_callback(data)
    -        self.log.trace("[Thread-Recv] STOP")
    -    def sender():
    -        """Receive data from user-input/command-output and process it with a callback.
    -        receive: Must be a generator function to receive user-input or command output.
    -        callback: Must be a callback to send this data to a socket.
    -        """
    -        self.log.trace("[Thread-Send] START")
    -        for data in self.input_generator():
    -            self.log.trace("[Thread-Send] input_generator() received: {}".format(repr(data)))
    -            self.send_callback(data)
    -        self.log.trace("[Thread-Send] STOP")
    -    def timer():
    -        """Execute periodic tasks by an optional provided time_action."""
    -        self.log.trace("[Thread-Time] START")
    -        self.log.debug(
    -            "Ready for timed action every {} seconds".format(self.timed_action_intvl)
    -        )
    -        time_last = int(time.time())
    -        while True:
    -            time_now = int(time.time())
    -            if time_now > time_last + self.timed_action_intvl:
    -                self.log.debug("[{}] Executing timed function".format(time_now))
    -                self.timed_action_func(*self.timed_action_args, **self.timed_action_kwargs)
    -                time_last = time_now  # Reset previous time
    -            time.sleep(1)
    -    # Start sending and receiving threads
    -    self.tr = threading.Thread(target=receiver, name="Thread-Recv")
    -    self.ts = threading.Thread(target=sender, name="Thread-Send")
    -    # If the main thread kills, this thread will be killed too.
    -    self.tr.daemon = False  # No daemon, wait for each other (e.g.: data received
    -    self.ts.daemon = False  # should also be outputted)
    -    # Start threads
    -    self.tr.start()
    -    # time.sleep(0.1)
    -    self.ts.start()
    -    if hasattr(self, "timed_action_intvl"):
    -        self.tt = threading.Thread(target=timer, name="Thread-Time")
    -        self.tt.daemon = True
    -        self.tt.start()
    -    # Cleanup the main program
    -    while True:
    -        # TODO: is this required? (check if need to press Ctrl+c twice)
    -        # if not THREAD_TERMINATE:
    -        #     self.input_generator_stop_fn()
    -        #     self.recv_generator_stop_fn()
    -        if not self.tr.is_alive():
    -            self.log.trace("Setting THREAD_TERMINATE=True from Thread-Recv death")
    -            self.log.trace("Waiting for Thread-Send to finish")
    -            # time.sleep(0.1)
    -            THREAD_TERMINATE = True
    -            self.input_generator_stop_fn()
    -            self.ts.join()
    -            sys.exit(0)
    -        if not self.ts.is_alive():
    -            self.log.trace("Setting THREAD_TERMINATE=True from Thread-Send death")
    -            self.log.trace("Waiting for Thread-Recv to finish")
    -            # time.sleep(0.1)
    -            THREAD_TERMINATE = True
    -            self.recv_generator_stop_fn()
    -            self.tr.join()
    -            sys.exit(0)
    +  
    +  
    +
    def add_action(self, action):
    +    """
    +    Enables a function to run threaded by the producer/consumer runner.
    +    :param str      name:        Name for logging output
    +    :param function producer:    A generator function which yields data
    +    :param function consumer:    A callback which consumes data from the generator
    +    :param function interrupter: A func that signals a stop event to the producer
    +    """
    +    assert "name" in action
    +    assert "producer" in action
    +    assert "consumer" in action
    +    assert "signal" in action
    +    assert "interrupt" in action
    +    self.__actions[action["name"]] = {
    +        "name": action["name"],
    +        "producer": action["producer"],
    +        "consumer": action["consumer"],
    +        "signal": action["signal"],
    +        "interrupt": action["interrupt"],
    +    }
     
    @@ -6544,20 +6756,24 @@

    Static methods

    -
    -

    def set_input_generator(

    self, func)

    +
    +

    def add_timer(

    self, timer)

    -

    Set generator func which constantly receives input (shell output/user input).

    - -
    -
    def set_input_generator(self, func):
    -    """Set generator func which constantly receives input (shell output/user input)."""
    -    self.input_generator = func
    +  
    +  
    +
    def add_timer(self, timer):
    +    self.__timers[timer["name"]] = {
    +        "action": timer["action"],
    +        "intvl": timer["intvl"],
    +        "args": timer["args"] if "args" in timer else None,
    +        "kwargs": timer["kwargs"] if "kwargs" in timer else {},
    +        "signal": timer["signal"],
    +    }
     
    @@ -6566,104 +6782,174 @@

    Static methods

    -
    -

    def set_input_generator_stop_function(

    self, func)

    +
    +

    def run(

    self)

    +

    Run threaded NetCat.

    - -
    -
    def set_input_generator_stop_function(self, func):
    -    self.input_generator_stop_fn = func
    +  
    +  
    +
    def run(self):
    +    """Run threaded NetCat."""
    +    def run_action(name, producer, consumer, ssig):
    +        """
    +        Receive data (network, user-input, shell-output) and process it (send, output).
    +        :param str        name:        Name for logging output
    +        :param function   producer:    A generator function which yields data
    +        :param function   consumer:    A callback which consumes data from the generator
    +        :param StopSignal ssig:        Providing has_stop() and raise_stop()
    +        """
    +        self.log.trace("[{}] Producer Start".format(name))
    +        for data in producer(ssig):
    +            self.log.trace("[{}] Producer received: {}".format(name, repr(data)))
    +            consumer(data)
    +        self.log.trace("[{}] Producer Stop".format(name))
    +    def run_timer(name, action, intvl, ssig, *args, **kwargs):
    +        """Execute periodic tasks by an optional provided time_action."""
    +        self.log.trace("[{}] Timer Start (exec every {} sec)".format(name, intvl))
    +        time_last = int(time.time())
    +        while True:
    +            if ssig.has_stop():
    +                self.log.trace("Stop signal acknowledged for timer {}".format(name))
    +                return
    +            time_now = int(time.time())
    +            if time_now > time_last + intvl:
    +                self.log.debug("[{}] Executing timed function".format(time_now))
    +                if args is not None:
    +                    if kwargs:
    +                        action(*args, **kwargs)
    +                    else:
    +                        action(*args)
    +                else:
    +                    if kwargs:
    +                        action(**kwargs)
    +                    else:
    +                        action()
    +                time_last = time_now  # Reset previous time
    +            time.sleep(1)
    +    # Start available action in a thread
    +    for key in self.__actions:
    +        # Create Thread object
    +        thread = threading.Thread(
    +            target=run_action,
    +            name=key,
    +            args=(
    +                key,
    +                self.__actions[key]["producer"],
    +                self.__actions[key]["consumer"],
    +                self.__actions[key]["signal"],
    +            ),
    +        )
    +        thread.daemon = False
    +        thread.start()
    +        self.__threads[key] = thread
    +    # Start available timers in a thread
    +    for key in self.__timers:
    +        # Create Thread object
    +        thread = threading.Thread(
    +            target=run_timer,
    +            name=key,
    +            args=(
    +                key,
    +                self.__timers[key]["action"],
    +                self.__timers[key]["intvl"],
    +                self.__timers[key]["signal"],
    +                self.__timers[key]["args"],
    +            ),
    +            kwargs=self.__timers[key]["kwargs"],
    +        )
    +        thread.daemon = False
    +        thread.start()
    +    def stop(force):
    +        """Stop threads."""
    +        for key in self.__threads:
    +            if not self.__threads[key].is_alive() or force:
    +                self.log.trace("Raise stop signal for {}".format(self.__threads[key].getName()))
    +                self.__actions[key]["signal"].raise_stop()
    +                self.log.trace("Call interrupt for {}".format(self.__threads[key].getName()))
    +                self.__actions[key]["interrupt"]()
    +                self.log.trace("Joining {}".format(self.__threads[key].getName()))
    +                self.__threads[key].join(timeout=0.1)
    +        # If all threads have died, exit
    +        if not all([self.__threads[key].is_alive() for key in self.__threads]) or force:
    +            if force:
    +                sys.exit(1)
    +            else:
    +                sys.exit(0)
    +    try:
    +        while True:
    +            stop(False)
    +            # Need a timeout to not skyrocket the CPU
    +            time.sleep(0.1)
    +    except KeyboardInterrupt:
    +        print()
    +        stop(True)
     
    +

    Instance variables

    +
    +

    var log

    -
    -
    -

    def set_output_callback(

    self, func)

    -
    - - + -

    Set the callback for outputting data to stdin/stdout.

    - -
    -
    def set_output_callback(self, func):
    -    """Set the callback for outputting data to stdin/stdout."""
    -    self.output_callback = func
    -
    -
    -
    - - -
    -
    -

    def set_recv_generator(

    self, func)

    -
    - - - +
    +
    +
    + +
    +

    class StopSignal

    + -

    Set generator func which constantly receives network data.

    - -
    -
    def set_recv_generator(self, func):
    -    """Set generator func which constantly receives network data."""
    -    self.recv_generator = func
    -
    -
    -
    + +
    +
    class StopSignal(object):
     
    -  
    - - -
    -
    -

    def set_revc_generator_stop_function(

    self, func)

    -
    - + __stop = False - - -
    - -
    -
    def set_revc_generator_stop_function(self, func):
    -    self.recv_generator_stop_fn = func
    +    def has_stop(self):
    +        return self.__stop
    +
    +    def raise_stop(self):
    +        self.__stop = True
     
    -
    - + +
    +

    Ancestors (in MRO)

    + +

    Static methods

    -
    -

    def set_send_callback(

    self, func)

    +
    +

    def has_stop(

    self)

    -

    Set the callback for sending data to a socket.

    - -
    -
    def set_send_callback(self, func):
    -    """Set the callback for sending data to a socket."""
    -    self.send_callback = func
    +  
    +  
    +
    def has_stop(self):
    +    return self.__stop
     
    @@ -6672,40 +6958,24 @@

    Static methods

    -
    -

    def set_timed_action(

    self, intvl, func, *args, **kwargs)

    +
    +

    def raise_stop(

    self)

    -

    Set a function that should be called periodically.

    - -
    -
    def set_timed_action(self, intvl, func, *args, **kwargs):
    -    """Set a function that should be called periodically."""
    -    self.timed_action_intvl = intvl
    -    self.timed_action_func = func
    -    self.timed_action_args = args
    -    self.timed_action_kwargs = kwargs
    +  
    +  
    +
    def raise_stop(self):
    +    self.__stop = True
     
    -

    Instance variables

    -
    -

    var log

    - - - - -
    -
    - -
    @@ -6736,13 +7006,13 @@

    Instance variables

    def encode(self, data): """Convert string into a byte type for Python3.""" if self.py3: - data = data.encode("cp437") + data = data.encode(self.codec) return data def decode(self, data): """Convert bytes into a string type for Python3.""" if self.py3: - data = data.decode("cp437") + data = data.decode(self.codec) return data
    @@ -6804,7 +7074,7 @@

    Static methods

    def decode(self, data):
         """Convert bytes into a string type for Python3."""
         if self.py3:
    -        data = data.decode("cp437")
    +        data = data.decode(self.codec)
         return data
     
    @@ -6828,7 +7098,7 @@

    Static methods

    def encode(self, data):
         """Convert string into a byte type for Python3."""
         if self.py3:
    -        data = data.encode("cp437")
    +        data = data.encode(self.codec)
         return data
     
    diff --git a/docs/pwncat.man.html b/docs/pwncat.man.html index 4aef1272..2663c783 100644 --- a/docs/pwncat.man.html +++ b/docs/pwncat.man.html @@ -125,8 +125,8 @@

    DESCRIPTION

    −C, −−crlf

    -

    Send CRLF line−endings in -connect mode (default: LF)

    +

    Replace LF with CRLF from stdin +(default: don’t)

    −n, −−nodns

    @@ -317,11 +317,12 @@

    DESCRIPTION

    All modes: If pwncat is started with this argument, it will shut down as soon as it receives -the specified string. The −−keep (server) -or −−reconn (client) options will be -ignored and it won’t listen again or reconnect to you. -Use a very unique string to not have it shut down -accidentally by other input.

    +the specified string. The +−−keep−open (server) or +−−reconn (client) options will be ignored +and it won’t listen again or reconnect to you. Use a +very unique string to not have it shut down accidentally by +other input.

    misc arguments:
    diff --git a/man/pwncat.1 b/man/pwncat.1 index 414e8df1..5dd57e7b 100644 --- a/man/pwncat.1 +++ b/man/pwncat.1 @@ -55,7 +55,7 @@ address given via \fB\-L\fR/\-\-local addr:port. Execute shell command. Only for connect or listen mode. .TP \fB\-C\fR, \fB\-\-crlf\fR -Send CRLF line\-endings in connect mode (default: LF) +Replace LF with CRLF from stdin (default: don't) .TP \fB\-n\fR, \fB\-\-nodns\fR Do not resolve DNS. @@ -180,8 +180,8 @@ Use \fB\-\-udp\-ping\-intvl\fR 0 to be faster. All modes: If pwncat is started with this argument, it will shut down as soon as it receives the specified string. The -\fB\-\-keep\fR (server) or \fB\-\-reconn\fR (client) options will be -ignored and it won't listen again or reconnect to you. +\fB\-\-keep\-open\fR (server) or \fB\-\-reconn\fR (client) options will +be ignored and it won't listen again or reconnect to you. Use a very unique string to not have it shut down accidentally by other input. .SS "misc arguments:" diff --git a/setup.py b/setup.py index 273760a9..14f57664 100644 --- a/setup.py +++ b/setup.py @@ -6,8 +6,8 @@ setup( name="pwncat", - version="0.0.6-alpha", - description="Netcat on steroids with FW and IPS evasion, bind and reverse shell, local and remote port-forward.", + version="0.0.7-alpha", + description="Netcat on steroids with Firewall and IPS evasion, bind and reverse shell and port-forwarding.", license="MIT", long_description=long_description, long_description_content_type="text/markdown", diff --git a/tests/.lib.sh b/tests/.lib.sh index dc0a2394..43e471d9 100644 --- a/tests/.lib.sh +++ b/tests/.lib.sh @@ -257,15 +257,6 @@ pid_is_running() { return 0 fi return 1 - #out="$( ps auxw | awk '{print $2}' | grep -E "^${the_pid}\$" )" - #if [ -z "${out}" ]; then - # return 1 - #fi - #if [ "${the_pid}" != "${out}" ]; then - # >&2 echo "Error, 'pid_is_running()' function found a running pid different to input" - # >&2 echo "Error, input pid = ${the_pid} != output pid = ${out}" - # exit 1 - #fi } diff --git a/tests/400-mode-forward_tcp-client_make_http_request.sh b/tests/400-mode-forward_tcp-client_make_http_request.sh new file mode 100755 index 00000000..ec3e02c4 --- /dev/null +++ b/tests/400-mode-forward_tcp-client_make_http_request.sh @@ -0,0 +1,215 @@ +#!/usr/bin/env bash + +set -e +set -u +set -o pipefail + +SCRIPTPATH="$( cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" +BINARY="${SCRIPTPATH}/../bin/pwncat" +# shellcheck disable=SC1090 +source "${SCRIPTPATH}/.lib.sh" + + +# ------------------------------------------------------------------------------------------------- +# GLOBALS +# ------------------------------------------------------------------------------------------------- + +PYTHON="python${1:-}" +PYVER="$( eval "${PYTHON} -V" 2>&1 | head -1 )" + +RHOST="www.google.com" +RPORT="80" + +LHOST="localhost" +LPORT="${2:-4444}" +RUNS=1 +SRV_WAIT=2 +TRANS_WAIT=2 + + +# ------------------------------------------------------------------------------------------------- +# TEST FUNCTIONS +# ------------------------------------------------------------------------------------------------- + +print_test_case "[400] Mode: (TCP) Forward: Client makes HTTP request (${PYVER})" + +# 1. Start Forward Server in background +# 2. Run Client without proxy +# 3. Run Client through proxy +# 4. Run Client through proxy (see if server stays alive) +# 5. Compare data contents + +run_test() { + local host="${1}" + local port="${2}" + local srv_opts="${3:-}" + local cli_opts="${4:-}" + local tround="${5}" + local sround="${6}" + local data= + + echo;echo + print_h1 "[${tround}/${RUNS}] (${sround}/13) Starting Test Round (${host}:${port}) (cli '${cli_opts}' vs srv '${srv_opts}')" + + kill_process "pwncat" >/dev/null 2>&1 || true + + ### + ### Create data and files + ### + data="$(tmp_file)" + printf "HEAD / HTTP/1.1\\n\\n" > "${data}" + srv_stdout="$(tmp_file)" + srv_stderr="$(tmp_file)" + cli1_stdout="$(tmp_file)" + cli1_stderr="$(tmp_file)" + cli2_stdout="$(tmp_file)" + cli2_stderr="$(tmp_file)" + cli3_stdout="$(tmp_file)" + cli3_stderr="$(tmp_file)" + + + # -------------------------------------------------------------------------------- + # START: SERVER + # -------------------------------------------------------------------------------- + echo;print_h2 "(1/5) Start: Server" + + # Start Server + print_info "Start Server" + # shellcheck disable=SC2086 + srv_pid="$( run_bg "" "${PYTHON}" "${BINARY}" ${srv_opts} "--local" "${LHOST}:${LPORT}" "${RHOST}" "${RPORT}" "${srv_stdout}" "${srv_stderr}" )" + + # Wait until Server is up + run "sleep ${SRV_WAIT}" + + # Ensure Server is started in background + test_case_instance_is_started_in_bg "Server" "${srv_pid}" "${srv_stdout}" "${srv_stderr}" + + # Ensure Server has no errors + test_case_instance_has_no_errors "Server" "${srv_pid}" "${srv_stdout}" "${srv_stderr}" + + + # -------------------------------------------------------------------------------- + # START: CLIENT-1 (NO PROXY) + # -------------------------------------------------------------------------------- + echo;print_h2 "(2/5) Start: Client-1 (without Proxy)" + + # Start Client + print_info "Start Client-1" + # shellcheck disable=SC2086 + cli1_pid="$( run_bg "cat ${data}" "${PYTHON}" "${BINARY}" ${cli_opts} "${RHOST}" "${RPORT}" "${cli1_stdout}" "${cli1_stderr}" )" + run "sleep ${TRANS_WAIT}" + test_case_instance_is_started_in_bg "Client-1" "${cli1_pid}" "${cli1_stdout}" "${cli1_stderr}" + test_case_instance_has_no_errors "Client-1" "${cli1_pid}" "${cli1_stdout}" "${cli1_stderr}" + test_case_instance_is_running "Client-1" "${cli1_pid}" "${cli1_stdout}" "${cli1_stderr}" + action_stop_instance "Client-1" "${cli1_pid}" "${cli1_stdout}" "${cli1_stderr}" + + + # -------------------------------------------------------------------------------- + # START: CLIENT-2 (WITH PROXY) + # -------------------------------------------------------------------------------- + echo;print_h2 "(3/5) Start: Client-2 (with Proxy)" + + # Start Client + print_info "Start Client-2" + # shellcheck disable=SC2086 + cli2_pid="$( run_bg "cat ${data}" "${PYTHON}" "${BINARY}" ${cli_opts} "${LHOST}" "${LPORT}" "${cli2_stdout}" "${cli2_stderr}" )" + run "sleep ${TRANS_WAIT}" + test_case_instance_is_started_in_bg "Client-2" "${cli2_pid}" "${cli2_stdout}" "${cli2_stderr}" + test_case_instance_has_no_errors "Client-2" "${cli2_pid}" "${cli2_stdout}" "${cli2_stderr}" + test_case_instance_is_running "Client-2" "${cli2_pid}" "${cli2_stdout}" "${cli2_stderr}" + action_stop_instance "Client-2" "${cli2_pid}" "${cli2_stdout}" "${cli2_stderr}" + + + # -------------------------------------------------------------------------------- + # START: CLIENT-3 (WITH PROXY) + # -------------------------------------------------------------------------------- + echo;print_h2 "(4/5) Start: Client-3 (with Proxy)" + + # TODO: USE WAIT MODE HOERE + # Start Client + print_info "Start Client-3" + # shellcheck disable=SC2086 + cli3_pid="$( run_bg "cat ${data}" "${PYTHON}" "${BINARY}" ${cli_opts} "${LHOST}" "${LPORT}" "${cli3_stdout}" "${cli3_stderr}" )" + run "sleep ${TRANS_WAIT}" + test_case_instance_is_started_in_bg "Client-2" "${cli3_pid}" "${cli3_stdout}" "${cli3_stderr}" + test_case_instance_has_no_errors "Client-2" "${cli3_pid}" "${cli3_stdout}" "${cli3_stderr}" + test_case_instance_is_running "Client-2" "${cli3_pid}" "${cli3_stdout}" "${cli3_stderr}" + action_stop_instance "Client-2" "${cli3_pid}" "${cli3_stdout}" "${cli3_stderr}" + + + # -------------------------------------------------------------------------------- + # COMPARE + # -------------------------------------------------------------------------------- + echo;print_h2 "(5/5) Check and Compare results" + + test_case_instance_is_running "Server" "${srv_pid}" "${srv_stdout}" "${srv_stderr}" + action_stop_instance "Server" "${srv_pid}" "${srv_stdout}" "${srv_stderr}" + + # Sanity check we have at least some data in the file + print_info "Ensure we have some data in Client-1 available" + if ! run "cat '${cli1_stdout}' | grep 'Set-Cookie' >/dev/null"; then + print_file "CLIENT-1 STDERR" "${cli1_stderr}" + print_file "CLIENT-1 STDOUT" "${cli1_stdout}" + print_error "[Receive Error] Client-1 did not receive any data. Cannot compare results" + exit 1 + fi + + # Client-1 vs Client-2 + print_info "Compare Client-1 and Client-2" + if ! run "diff <(cat '${cli1_stdout}' | sed 's/^Set-Cookie:.*//g' | sed 's/^Date:.*//g') \ + <(cat '${cli2_stdout}' | sed 's/^Set-Cookie:.*//g' | sed 's/^Date:.*//g')"; then + print_file "CLIENT-1 STDERR" "${cli1_stderr}" + print_file "CLIENT-1 STDOUT" "${cli1_stdout}" + print_file "CLIENT-2 STDERR" "${cli2_stderr}" + print_file "CLIENT-2 STDOUT" "${cli2_stdout}" + print_file "SERVER STDERR" "${srv_stderr}" + print_file "SERVER STDOUT" "${srv_stdout}" + diff "${cli1_stdout}" "${cli2_stdout}" 2>&1 || true + print_error "[Receive Error] Client-1 and Client-2 data don't match" + exit 1 + fi + + # Client-2 vs Client-3 + print_info "Compare Client-2 and Client-3" + if ! run "diff <(cat '${cli2_stdout}' | sed 's/^Set-Cookie:.*//g' | sed 's/^Date:.*//g') \ + <(cat '${cli3_stdout}' | sed 's/^Set-Cookie:.*//g' | sed 's/^Date:.*//g')"; then + print_file "CLIENT-1 STDERR" "${cli2_stderr}" + print_file "CLIENT-1 STDOUT" "${cli2_stdout}" + print_file "CLIENT-2 STDERR" "${cli3_stderr}" + print_file "CLIENT-2 STDOUT" "${cli3_stdout}" + print_file "SERVER STDERR" "${srv_stderr}" + print_file "SERVER STDOUT" "${srv_stdout}" + diff "${cli2_stdout}" "${cli3_stdout}" 2>&1 || true + print_error "[Receive Error] Client-2 and Client-3 data don't match" + exit 1 + fi + + # Show received data + print_file "Client-1 received data" "${cli1_stdout}" + print_file "Client-2 received data" "${cli2_stdout}" + print_file "Client-3 received data" "${cli3_stdout}" +} + + +# ------------------------------------------------------------------------------------------------- +# MAIN ENTRYPOINT +# ------------------------------------------------------------------------------------------------- + +for i in $(seq "${RUNS}"); do + echo + run_test "${RHOST}" "${RPORT}" "-vvvv" "-vvvv" "${i}" "1" + run_test "${RHOST}" "${RPORT}" "-vvv " "-vvvv" "${i}" "2" + run_test "${RHOST}" "${RPORT}" "-vv " "-vvvv" "${i}" "3" + run_test "${RHOST}" "${RPORT}" "-v " "-vvvv" "${i}" "4" + run_test "${RHOST}" "${RPORT}" " " "-vvvv" "${i}" "5" + + run_test "${RHOST}" "${RPORT}" "-vvvv" "-vvv " "${i}" "6" + run_test "${RHOST}" "${RPORT}" "-vvvv" "-vv " "${i}" "7" + run_test "${RHOST}" "${RPORT}" "-vvvv" "-v " "${i}" "8" + run_test "${RHOST}" "${RPORT}" "-vvvv" " " "${i}" "9" + + run_test "${RHOST}" "${RPORT}" "-vvv " "-vvv " "${i}" "10" + run_test "${RHOST}" "${RPORT}" "-vv " "-vv " "${i}" "11" + run_test "${RHOST}" "${RPORT}" "-v " "-v " "${i}" "12" + run_test "${RHOST}" "${RPORT}" " " " " "${i}" "13" +done diff --git a/tests/README.md b/tests/README.md index e4960bfe..3ab84174 100644 --- a/tests/README.md +++ b/tests/README.md @@ -9,3 +9,4 @@ Test files start with the following numbers: * `1xx`: Testing Behaviour (socket, auto-shutdown, etc) * `2xx`: Testing Basics * `3xx`: Testing Options +* `4xx`: Testing modes