From eb3933d26ec97d88472f7ff1eefde06b3c9c3e8a Mon Sep 17 00:00:00 2001 From: Malfurious Date: Sun, 12 Dec 2021 02:51:50 -0500 Subject: sploit: Remove -d/--daemon option A couple of facts have influenced the decision to remove this option: - If a sploit script uses a shebang to launch sploit, it is tricky to specify this option. Specifically, one must add it to their shebang line, which couples more information to the script than was originally intended. - Single-pass pipe mode wasn't all that useful. One can accomplish the same thing by running pipe-daemon, and it is easy to exit after one iteration. Electing to run normal pipe mode requires you to know you only want to run once, which is much more common when running via direct subprocess. As a result of this change, running in pipe mode will now be equivalent to the previous pipe-daemon mode, and subprocess target mode remains single pass. Signed-off-by: Malfurious Signed-off-by: dusoleil --- tools/sploit/sploit/main.py | 33 ++++++++++----------------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/tools/sploit/sploit/main.py b/tools/sploit/sploit/main.py index 77ef49e..8456029 100644 --- a/tools/sploit/sploit/main.py +++ b/tools/sploit/sploit/main.py @@ -1,48 +1,35 @@ -import argparse +from argparse import ArgumentParser, REMAINDER +import gc import tempfile import traceback -import gc from sploit.comm import * def main(): - parser = argparse.ArgumentParser(description='Execute Sploit Script Against Target') - parser.add_argument('-d', '--daemon', action='store_true', - help='run in "daemon" mode with pipes instead of a designated target') - parser.add_argument('script', - help='exploit script to run') - parser.add_argument('target', nargs=argparse.REMAINDER, - help='target program to exploit') + parser = ArgumentParser(description='Execute Sploit script against target') + parser.add_argument('script', help='Exploit script to run') + parser.add_argument('target', nargs=REMAINDER, help='Target program to exploit') args = parser.parse_args() if(len(args.target)>0): - if(args.daemon): - print("Target Given. Ignoring Daemon Flag...") target(args.script, args.target) else: - if(args.daemon): - daemon(args.script) - else: - pipe(args.script) + pipe(args.script) -def daemon(script): - print("Running in Pipe Daemon Mode...") +def pipe(script): + print("Running in Pipe Mode...") with tempfile.TemporaryDirectory() as tmpdir: while(True): try: p = Pipes(tmpdir) except KeyboardInterrupt: break - runscript(script, Comm(p)); + runscript(script, Comm(p)) del p -def pipe(script): - print("Running in Pipe Mode..."); - runscript(script, Comm(Pipes())); - def target(script, target): print("Running in Target Mode...") - runscript(script, Comm(Process(target))); + runscript(script, Comm(Process(target))) def runscript(script, comm): try: -- cgit v1.2.3 From adad3ce320aa774ff274965e001bea5a096a879b Mon Sep 17 00:00:00 2001 From: Malfurious Date: Tue, 14 Dec 2021 04:42:37 -0500 Subject: sploit: Rework logger The log module is updated to support binary encodings, colors, and for improved compatibility with Python's print() builtin. Encoding semantics are switched up, since it seems like some of the more interesting encoding modes (from a CTF perspective) actually use bytes-like objects as their high-level form (that is, bytes are encoded to another form, such as hex, then decoded back to the original form). So the logged value is now passed to encode instead of decode, and only if the object is of type 'bytes', as unicode strings are now considered out-of-scope for this operation. Additionally, the bytes wrapper (b'') is no longer visible in the logged content. For readability, several standard colors have been defined for use within Sploit: - RED: Errors - YELLOW: Warnings - GREEN: Status messages / Startup messages - WHITE: Target output - GRAY: User output / Alt text Logging functions now support an optional color option to select the desired color, and have specific defaults based on who is invoking the log (see below...) Logging functions are now also fully compatible with the builtin print() function. This is because Sploit now replaces the standard print() with a logging function within the user's script (which is done to maintain additional consistency of messages displayed in the console). Function ilog (internal log) has default values tuned for the library's convenience: Text goes to stderr, and is presented as status messages (green). Function elog (external log) has default values tuned for the user: Text goes to stdout, and is presented as alt text to distinguish it from data read from the target. Within the user context, 'print' refers to this function. Signed-off-by: Malfurious Signed-off-by: dusoleil --- tools/sploit/sploit/comm.py | 33 +++++++++++++++++---------------- tools/sploit/sploit/log.py | 36 +++++++++++++++++++++++++++++++----- tools/sploit/sploit/main.py | 15 ++++++++------- 3 files changed, 56 insertions(+), 28 deletions(-) diff --git a/tools/sploit/sploit/comm.py b/tools/sploit/sploit/comm.py index 7d6cd8c..2786c45 100644 --- a/tools/sploit/sploit/comm.py +++ b/tools/sploit/sploit/comm.py @@ -4,7 +4,7 @@ import os import sys import select -from sploit.log import log +from sploit.log import * from sploit.until import bind class Comm: @@ -18,20 +18,20 @@ class Comm: data = os.read(self.back.stdin.fileno(), size) if(data == b''): raise BrokenPipeError('Tried to read on broken pipe') - if self.logonread : log(data) + if self.logonread : ilog(data, file=sys.stdout, color=NORMAL) return data def readline(self): data = self.back.stdin.readline() if(data == b''): raise BrokenPipeError('Tried to read on broken pipe') - if self.logonread : log(data) + if self.logonread : ilog(data, file=sys.stdout, color=NORMAL) return data def readall(self): data = b'' for line in self.back.stdin: - log(line) + ilog(line, file=sys.stdout, color=NORMAL) data += line return data @@ -45,7 +45,7 @@ class Comm: if(pred(data)): break self.logonread = l - if self.logonread : log(data) + if self.logonread : ilog(data, file=sys.stdout, color=NORMAL) return data def readlineuntil(self, pred, /, *args, **kwargs): @@ -65,7 +65,7 @@ class Comm: self.write(data + b'\n') def interact(self): - print("<--Interact Mode-->") + ilog("<--Interact Mode-->") stdin = sys.stdin.buffer os.set_blocking(self.back.stdin.fileno(), False) os.set_blocking(stdin.fileno(), False) @@ -79,9 +79,11 @@ class Comm: if(data == b''): break write(data) + def writeinput(write): + ilog(write, file=sys.stdout, color=NORMAL) readtable = { stdin.fileno() : lambda : readall(stdin.readline, self.write), - self.back.stdin.fileno() : lambda : readall(self.back.stdin.readline, log) + self.back.stdin.fileno() : lambda : readall(self.back.stdin.readline, writeinput) } readtable[self.back.stdin.fileno()]() while(not brk): @@ -97,17 +99,17 @@ class Comm: break os.set_blocking(self.back.stdin.fileno(), True) os.set_blocking(stdin.fileno(), True) - print("<--Interact Mode Done-->") + ilog("<--Interact Mode Done-->") class Process: def __init__(self, args): - print(f"Running: {' '.join(args)}") + ilog(f"Running: {' '.join(args)}") self.proc = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, preexec_fn=lambda : os.setpgrp()) - print(f"PID: {self.proc.pid}") + ilog(f"PID: {self.proc.pid}") self.stdin = self.proc.stdout self.stdout = self.proc.stdin @@ -116,8 +118,8 @@ class Process: if(self.proc.poll() != None): return try: - print("Waiting on Target Program to End...") - print("Press Ctrl+C to Forcefully Kill It...") + ilog("Waiting on Target Program to End...") + ilog("Press Ctrl+C to Forcefully Kill It...") self.proc.wait() except KeyboardInterrupt: self.proc.kill() @@ -135,11 +137,11 @@ class Pipes: self.pathout = os.path.join(dirname, "out") os.mkfifo(self.pathin) os.mkfifo(self.pathout) - print("Waiting on Target to Connect...") - print("<"+self.pathin+" >"+self.pathout) + ilog("Waiting on Target to Connect...", file=sys.stdout) + ilog(f"<{self.pathin} >{self.pathout}", file=sys.stdout) self.stdout = open(self.pathin, "wb") self.stdin = open(self.pathout, "rb") - print("Connected!") + ilog("Connected!") def __del__(self): try: @@ -149,4 +151,3 @@ class Pipes: pass if getattr(self,'pathin',None) and os.path.exists(self.pathin) : os.unlink(self.pathin) if getattr(self,'pathout',None) and os.path.exists(self.pathout) : os.unlink(self.pathout) - diff --git a/tools/sploit/sploit/log.py b/tools/sploit/sploit/log.py index cd9c3be..823b252 100644 --- a/tools/sploit/sploit/log.py +++ b/tools/sploit/sploit/log.py @@ -1,6 +1,32 @@ -ENCODING = '' -def log(s): - if ENCODING != '': - s = s.decode(ENCODING) - print(s) +import codecs +import sys +# https://docs.python.org/3/library/codecs.html#standard-encodings +ENCODING = None + +ERROR = 31 +WARNING = 33 +STATUS = 32 +NORMAL = 0 +ALT = 90 + +def enc_value(value, enc): + if type(value) is bytes: + if enc is not None: + value = codecs.encode(value, enc) + elif ENCODING is not None: + value = codecs.encode(value, ENCODING) + value = str(value)[2:-1] # strip b'' + return str(value) + +def generic_log(*values, sep, end, file, flush, enc, color): + string = sep.join([ enc_value(x, enc) for x in values ]) + print(f'\033[{color}m{string}\033[0m', end=end, file=file, flush=flush) + +# For library internal use +def ilog(*values, sep=' ', end='\n', file=sys.stderr, flush=True, enc=None, color=STATUS): + generic_log(*values, sep=sep, end=end, file=file, flush=flush, enc=enc, color=color) + +# For external use in user script (via print = elog) +def elog(*values, sep=' ', end='\n', file=sys.stdout, flush=True, enc=None, color=ALT): + generic_log(*values, sep=sep, end=end, file=file, flush=flush, enc=enc, color=color) diff --git a/tools/sploit/sploit/main.py b/tools/sploit/sploit/main.py index 8456029..0a34429 100644 --- a/tools/sploit/sploit/main.py +++ b/tools/sploit/sploit/main.py @@ -4,6 +4,7 @@ import tempfile import traceback from sploit.comm import * +from sploit.log import * def main(): parser = ArgumentParser(description='Execute Sploit script against target') @@ -17,7 +18,7 @@ def main(): pipe(args.script) def pipe(script): - print("Running in Pipe Mode...") + ilog("Running in Pipe Mode...") with tempfile.TemporaryDirectory() as tmpdir: while(True): try: @@ -28,21 +29,21 @@ def pipe(script): del p def target(script, target): - print("Running in Target Mode...") + ilog("Running in Target Mode...") runscript(script, Comm(Process(target))) def runscript(script, comm): try: - print("Running Script...") + ilog("Running Script...") code = compile(open(script).read(), script, 'exec') - exec(code, {'io': comm}) - print("Script Finished!") + exec(code, {'io': comm, 'print': elog}) + ilog("Script Finished!") comm.readall() return except KeyboardInterrupt: pass except: - traceback.print_exc() + ilog(traceback.format_exc(), end='', color=ERROR) finally: gc.collect() - print("Script Ended Early!") + ilog("Script Ended Early!", color=WARNING) -- cgit v1.2.3 From 244884bd2d373cecb783135eca51da8530d5f078 Mon Sep 17 00:00:00 2001 From: Malfurious Date: Wed, 15 Dec 2021 05:03:53 -0500 Subject: sploit: Add startup banner This just adds a fancy 'SPLOIT' header to the beginning of Sploit's startup preamble data. It has the ability to display a few lines of text beside itself, but most of the things we've planned to put here are not available yet, so just the operating mode is printed for now. The SPLOIT text has a colored stripe which, at the moment, also indicates the operating mode. This stripe was originally chosen to balance out the amount of color present in the preamble text, but I've grown to like it. Signed-off-by: Malfurious Signed-off-by: dusoleil --- tools/sploit/sploit/main.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tools/sploit/sploit/main.py b/tools/sploit/sploit/main.py index 0a34429..e6c9bb3 100644 --- a/tools/sploit/sploit/main.py +++ b/tools/sploit/sploit/main.py @@ -6,6 +6,16 @@ import traceback from sploit.comm import * from sploit.log import * +def print_banner(color, line1='', line2='', line3=''): + ilog() + ilog(' ░▒█▀▀▀█░▒█▀▀█░▒█░░░░▒█▀▀▀█░▀█▀░▀▀█▀▀ ', end='', color=ALT) + ilog(line1, color=ALT) + ilog(' ░░▀▀▀▄▄░▒█▄▄█░▒█░░░░▒█░░▒█░▒█░░░▒█░░ ', end='', color=color) + ilog(line2, color=ALT) + ilog(' ░▒█▄▄▄█░▒█░░░░▒█▄▄█░▒█▄▄▄█░▄█▄░░▒█░░ ', end='', color=ALT) + ilog(line3, color=ALT) + ilog() + def main(): parser = ArgumentParser(description='Execute Sploit script against target') parser.add_argument('script', help='Exploit script to run') @@ -18,7 +28,7 @@ def main(): pipe(args.script) def pipe(script): - ilog("Running in Pipe Mode...") + print_banner(ERROR, line3='Pipe Mode') with tempfile.TemporaryDirectory() as tmpdir: while(True): try: @@ -29,7 +39,7 @@ def pipe(script): del p def target(script, target): - ilog("Running in Target Mode...") + print_banner(STATUS, line3='Subprocess Mode') runscript(script, Comm(Process(target))) def runscript(script, comm): -- cgit v1.2.3 From e0e1d65caa2244f2f6fd2e329c8431266de4b8d6 Mon Sep 17 00:00:00 2001 From: Malfurious Date: Wed, 15 Dec 2021 05:39:11 -0500 Subject: sploit: Check logonread in function Comm.readall() This function will no longer mistakenly log data when logonread is set to False. Signed-off-by: Malfurious Signed-off-by: dusoleil --- tools/sploit/sploit/comm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/sploit/sploit/comm.py b/tools/sploit/sploit/comm.py index 2786c45..242bc3d 100644 --- a/tools/sploit/sploit/comm.py +++ b/tools/sploit/sploit/comm.py @@ -31,7 +31,7 @@ class Comm: def readall(self): data = b'' for line in self.back.stdin: - ilog(line, file=sys.stdout, color=NORMAL) + if self.logonread : ilog(line, file=sys.stdout, color=NORMAL) data += line return data -- cgit v1.2.3 From 72ae9fd60a4116768eb810a2c961a431da834465 Mon Sep 17 00:00:00 2001 From: Malfurious Date: Wed, 15 Dec 2021 21:12:11 -0500 Subject: sploit: Add logonwrite option to comms If enabled, data sent to the target will be printed/logged as alt text, similar to data directly printed by the user. Feature is off by default. Signed-off-by: Malfurious Signed-off-by: dusoleil --- tools/sploit/sploit/comm.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/sploit/sploit/comm.py b/tools/sploit/sploit/comm.py index 242bc3d..0bc8c6a 100644 --- a/tools/sploit/sploit/comm.py +++ b/tools/sploit/sploit/comm.py @@ -9,6 +9,7 @@ from sploit.until import bind class Comm: logonread = True + logonwrite = False flushonwrite = True def __init__(self, backend): @@ -60,6 +61,7 @@ class Comm: def write(self, data): self.back.stdout.write(data) if self.flushonwrite : self.back.stdout.flush() + if self.logonwrite : ilog(data, file=sys.stdout, color=ALT) def writeline(self, data): self.write(data + b'\n') -- cgit v1.2.3 From 09139a3638812ef8ee7f766010729d75ceabad07 Mon Sep 17 00:00:00 2001 From: Malfurious Date: Wed, 15 Dec 2021 21:20:44 -0500 Subject: sploit: Ensure the logonread option is restored by Comm.readuntil() This function has a momentary side-effect of switching self.logonread to False. This patch ensures its original value is always restored, even if an exception is raised. Signed-off-by: Malfurious Signed-off-by: dusoleil --- tools/sploit/sploit/comm.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tools/sploit/sploit/comm.py b/tools/sploit/sploit/comm.py index 0bc8c6a..afa14e8 100644 --- a/tools/sploit/sploit/comm.py +++ b/tools/sploit/sploit/comm.py @@ -41,11 +41,13 @@ class Comm: pred = bind(pred, *args, **kwargs) l = self.logonread self.logonread = False - while(True): - data += self.read(1) - if(pred(data)): - break - self.logonread = l + try: + while(True): + data += self.read(1) + if(pred(data)): + break + finally: + self.logonread = l if self.logonread : ilog(data, file=sys.stdout, color=NORMAL) return data -- cgit v1.2.3 From c7603c3cd00c565d67b6ee08510720934b34ca2e Mon Sep 17 00:00:00 2001 From: Malfurious Date: Wed, 15 Dec 2021 21:35:18 -0500 Subject: sploit: Catch KeyboardInterrupt in Comm.readall() If execution is stuck inside readall() (for example, due to blocked IO), handling KeyboardInterrupt allows the user a way to get out, without exiting the active script early or losing the data read so far. Signed-off-by: Malfurious Signed-off-by: dusoleil --- tools/sploit/sploit/comm.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tools/sploit/sploit/comm.py b/tools/sploit/sploit/comm.py index afa14e8..789a77e 100644 --- a/tools/sploit/sploit/comm.py +++ b/tools/sploit/sploit/comm.py @@ -31,9 +31,12 @@ class Comm: def readall(self): data = b'' - for line in self.back.stdin: - if self.logonread : ilog(line, file=sys.stdout, color=NORMAL) - data += line + try: + for line in self.back.stdin: + if self.logonread : ilog(line, file=sys.stdout, color=NORMAL) + data += line + except KeyboardInterrupt: + pass return data def readuntil(self, pred, /, *args, **kwargs): -- cgit v1.2.3 From 76e068420455a067d22ef2d4a45d65e2875e58b6 Mon Sep 17 00:00:00 2001 From: Malfurious Date: Wed, 15 Dec 2021 22:02:07 -0500 Subject: sploit: Automatically shutdown outgoing comms after script execution A new function, Comm.shutdown(), is added. It will close only the stdout stream of the communications backend, potentially making the termination of the target program more fluid. The name 'shutdown' is chosen to emulate shutdown(2) from the low-level socket api, which is used to close just part of a full-duplex file descriptor. This is in contrast to 'close', which I would expect to completely terminate the given object IO. comm.shutdown() is now called by main.py, after the user script returns, to ensure that the subsequent readall() doesn't get stuck because our target is blocked reading its stdin. Signed-off-by: Malfurious Signed-off-by: dusoleil --- tools/sploit/sploit/comm.py | 3 +++ tools/sploit/sploit/main.py | 1 + 2 files changed, 4 insertions(+) diff --git a/tools/sploit/sploit/comm.py b/tools/sploit/sploit/comm.py index 789a77e..604045c 100644 --- a/tools/sploit/sploit/comm.py +++ b/tools/sploit/sploit/comm.py @@ -15,6 +15,9 @@ class Comm: def __init__(self, backend): self.back = backend + def shutdown(self): + self.back.stdout.close() + def read(self, size): data = os.read(self.back.stdin.fileno(), size) if(data == b''): diff --git a/tools/sploit/sploit/main.py b/tools/sploit/sploit/main.py index e6c9bb3..b0fe3eb 100644 --- a/tools/sploit/sploit/main.py +++ b/tools/sploit/sploit/main.py @@ -48,6 +48,7 @@ def runscript(script, comm): code = compile(open(script).read(), script, 'exec') exec(code, {'io': comm, 'print': elog}) ilog("Script Finished!") + comm.shutdown() comm.readall() return except KeyboardInterrupt: -- cgit v1.2.3