diff --git a/README.md b/README.md index 219b0f1..3d672b8 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,20 @@ # tcpproxy.py - An intercepting proxy for TCP data + This tool opens a listening socket, receives data and then runs this data through a chain of proxy modules. After the modules are done, the resulting data is sent to the target server. The response is received and again run through a chain of modules before sending the final data back to the client. To intercept the data, you will either have to be the gateway or do some kind of man-in-the-middle attack. Set up iptables so that the PREROUTING chain will modify the destination and send it to the proxy process. The proxy will then send the data on to whatever target was specified. This tool is inspired by and partially based on the TCP proxy example used in Justin Seitz' book "Black Hat Python" by no starch press. ## Usage + ``` $ ./tcpproxy.py -h usage: tcpproxy.py [-h] [-ti TARGET_IP] [-tp TARGET_PORT] [-li LISTEN_IP] [-lp LISTEN_PORT] [-pi PROXY_IP] [-pp PROXY_PORT] [-pt {SOCKS4,SOCKS5,HTTP}] [-om OUT_MODULES] [-im IN_MODULES] [-v] [-n] [-l LOGFILE] [--list] - [-lo HELP_MODULES] [-s] + [-lo HELP_MODULES] [-s] [-sc SERVER_CERTIFICATE] + [-sk SERVER_KEY] [-cc CLIENT_CERTIFICATE] [-ck CLIENT_KEY] Simple TCP proxy for data interception and modification. Select modules to handle the intercepted traffic. @@ -45,51 +48,68 @@ optional arguments: --list list available modules -lo HELP_MODULES, --list-options HELP_MODULES Print help of selected module - -s, --ssl detect SSL/TLS as well as STARTTLS, certificate is - mitm.pem + -s, --ssl detect SSL/TLS as well as STARTTLS + -sc SERVER_CERTIFICATE, --server-certificate SERVER_CERTIFICATE + server certificate in PEM format (default: mitm.pem) + -sk SERVER_KEY, --server-key SERVER_KEY + server key in PEM format (default: mitm.pem) + -cc CLIENT_CERTIFICATE, --client-certificate CLIENT_CERTIFICATE + client certificate in PEM format in case client + authentication is required by the target + -ck CLIENT_KEY, --client-key CLIENT_KEY + client key in PEM format in case client authentication + is required by the target ``` You will have to provide TARGET_IP and TARGET_PORT, the default listening settings are 0.0.0.0:8080. To make the program actually useful, you will have to decide which modules you want to use on outgoing (client to server) and incoming (server to client) traffic. You can use different modules for each direction. Pass the list of modules as comma-separated list, e.g. -im mod1,mod4,mod2. The data will be passed to the first module, the returned data will be passed to the second module and so on, unless you use the -n/--no/chain switch. In that case, every module will receive the original data. You can also pass options to each module: -im mod1:key1=val1,mod4,mod2:key1=val1:key2=val2. To learn which options you can pass to a module use -lo/--list-options like this: -lo mod1,mod2,mod4 + ## Modules + ``` $ ./tcpproxy.py --list +digestdowngrade - Find HTTP Digest Authentication and replace it with a Basic Auth hexdump - Print a hexdump of the received data http_ok - Prepend HTTP response header http_post - Prepend HTTP header http_strip - Remove HTTP header from data -javaxml - Serialization or deserialization of Java objects (needs jython) log - Log data in the module chain. Use in addition to general logging (-l/--log). removegzip - Replace gzip in the list of accepted encodings in a HTTP request with booo. -replace - Replace text on the fly by using regular expressions in files +replace - Replace text on the fly by using regular expressions in a file or as module parameters +hexreplace - Replace hex data in tcp packets +size - Print the size of the data passed to the module +size404 - Change HTTP responses of a certain size to 404. textdump - Simply print the received data as text ``` + Tcpproxy.py uses modules to view or modify the intercepted data. To see the possibly easiest implementation of a module, have a look at the textdump.py module in the proxymodules directory: + ```python -#!/usr/bin/env python2 +#!/usr/bin/env python3 +import os.path as path class Module: def __init__(self, incoming=False, verbose=False, options=None): # extract the file name from __file__. __file__ is proxymodules/name.py - self.name = __file__.rsplit('/', 1)[1].split('.')[0] + self.name = path.splitext(path.basename(__file__))[0] self.description = 'Simply print the received data as text' self.incoming = incoming # incoming means module is on -im chain self.find = None # if find is not None, this text will be highlighted if options is not None: if 'find' in options.keys(): - self.find = options['find'] # text to highlight + self.find = bytes(options['find'], 'ascii') # text to highlight if 'color' in options.keys(): - self.color = '\033[' + options['color'] + 'm' # highlight color + self.color = bytes('\033[' + options['color'] + 'm', 'ascii') # highlight color else: - self.color = '\033[31;1m' + self.color = b'\033[31;1m' def execute(self, data): if self.find is None: - print data + print(data) else: - pdata = data.replace(self.find, self.color + self.find + '\033[0m') - print pdata + pdata = data.replace(self.find, self.color + self.find + b'\033[0m') + print(pdata.decode('ascii')) return data def help(self): @@ -100,21 +120,27 @@ class Module: if __name__ == '__main__': - print 'This module is not supposed to be executed alone!' + print('This module is not supposed to be executed alone!') ``` + Every module file contains a class named Module. Every module MUST set self.description and MUST implement an execute method that accepts one parameter, the input data. The execute method MUST return something, this something is then either passed to the next module or sent on. Other than that, you are free to do whatever you want inside a module. The incoming parameter in the constructor is set to True when the module is in the incoming chain (-im), otherwise it's False. This way, a module knows in which direction the data is flowing (credits to jbarg for this idea). The verbose parameter is set to True if the proxy is started with -v/--verbose. The options parameter is a dictionary with the keys and values passed to the module on the command line. Note that if you use the options dictionary in your module, you should also implement a help() method. This method must return a string. Use one line per option, make sure each line starts with a \t character for proper indentation. See the hexdump module for an additional options example: + ```python -#!/usr/bin/env python2 +#!/usr/bin/env python3 +import os.path as path class Module: def __init__(self, incoming=False, verbose=False, options=None): - # -- 8< --- snip + # extract the file name from __file__. __file__ is proxymodules/name.py + self.name = path.splitext(path.basename(__file__))[0] + self.description = 'Print a hexdump of the received data' + self.incoming = incoming # incoming means module is on -im chain self.len = 16 if options is not None: if 'length' in options.keys(): @@ -125,16 +151,18 @@ class Module: def execute(self, data): # -- 8< --- snip - for i in xrange(0, len(data), self.len): + for i in range(0, len(data), self.len): s = data[i:i + self.len] # # -- 8< --- snip if __name__ == '__main__': print 'This module is not supposed to be executed alone!' ``` + The above example should give you an idea how to make use of module parameters. A calling example would be: + ``` -python2 tcpproxy.py -om hexdump:length=8,http_post,hexdump:length=12 -ti 127.0.0.1 -tp 12345 +./tcpproxy.py -om hexdump:length=8,http_post,hexdump:length=12 -ti 127.0.0.1 -tp 12345 < < < < out: hexdump 0000 77 6C 6B 66 6A 6C 77 71 wlkfjlwq 0008 6B 66 6A 68 6C 6B 77 71 kfjhlkwq @@ -157,46 +185,15 @@ python2 tcpproxy.py -om hexdump:length=8,http_post,hexdump:length=12 -ti 127.0.0 0060 77 71 6B 65 6A 66 68 77 71 6C 6B 65 wqkejfhwqlke 006C 6A 66 68 0A jfh. ``` -You can see how the first hexdump instance gets a length of 8 bytes per row and the second instance gets a length of 12 bytes. To pass more than one option to a single module, seperate the options with a : character, modname:key1=val1:key2=val2... -## Deserializing and Serializing Java Objects to XML -**Note: at present this does not work due to changes that made the code not compatible with Jython's `socket` implementation. If Java deserialization is what you are looking for: the last compatible commit is e3290261.** - -Using the Java xstream libary, it is possible to deserialize intercepted serialised objects if the .jar with class definitions is known to tcpproxy. - -``` -CLASSPATH=./lib/* jython tcpproxy.py -ti 127.0.0.1 -tp 12346 -lp 12345 -om javaxml:mode=deserial,textdump -``` -If you would like to use a 3rd tool like BurpSuite to manipulate the XStream XML structure use this setup: -``` - - +---------+ - +-------> |BurpSuite+-----+ - | +---------+ | - | V -+------------------+ +--------+--+ +-----------+ +-----------+ -| Java ThickClient +------> |1. tcpproxy| |2. tcpproxy+------------> |Java Server| -+------------------+ +-----------+ +-----------+ +-----------+ -``` -The setup works like this: Let's say you want to intercept an manipulate serialized objects between the thick client and the Java server. The idea is to intercept serialized objects, turn them into XML (deserialize them), pipe them into another tool (BurpSuite in this example) where you manipulate the data, then take that data and send it to the server. The server replies with another object which is again deserialized into XML, fed to the tool and then serialized before sending the response to the client. -``` -$ CLASSPATH=./lib/*:/pathTo/jarFiles/* jython27 tcpproxy.py -ti -tp -lp -om javaxml:mode=deserial,http_post -im http_strip,javaxml:mode=serial -``` -The call above is for the first tcpproxy instance between the client and Burp (or whatever tool you want to use). The target IP is the IP Burp is using, target port tp is Burp's listening port. For listening IP li and listening port lp you either configure the client or do some ARP spoofing/iptables magic. With -om you prepare the data for burp. Since Burp only consumes HTTP, use the http_post module after the deserializer to prepend an HTTP header. Then manipulate the data within burp. Take care to configure Burp to redirect the data to the second tcpproxy instance's listen IP/listen port and enable invisible proxying. -Burp's response will be HTTP with an XML body, so in the incoming chain (-im) first strip the header (http_strip), then serialize the XML before the data is sent to the client. -``` -$ CLASSPATH=./lib/*:/pathTo/jarFiles/* jython27 tcpproxy.py -ti -tp -lp -im javaxml:mode=deserial,http_ok -om http_strip,javaxml:mode=serial -``` -This is the second tcpproxy instance. Burp will send the data there if you correctly configured the request handling in Burp's proxy listener options. Before sending the data to the server in the outgoing chain (-om), first strip the HTTP header, then serialize the XML. The server's response will be handled by the incoming chain (-im), so deserialize it, prepend the HTTP response header, then send the data to burp. - -Using this setup, you are able to take advantage of Burp's capabilities, like the repeater or intruder or simply use it for logging purposes. This was originally the idea of jbarg. - -If you are doing automated modifications and have no need for interactivity, you can simply take advantage of the (de-)serialization modules by writing a module to work on the deserialized XML structure. Then plug your module into the chain by doing -im java_deserializer,your_module,java_serializer (or -om of course). This way you also only need one tcpproxy instance, of course. -Note that when using jython, the SSL mitm does not seem to work. It looks like a jython bug to me, but I haven't yet done extensive debugging so I can't say for sure. +You can see how the first hexdump instance gets a length of 8 bytes per row and the second instance gets a length of 12 bytes. To pass more than one option to a single module, seperate the options with a : character, modname:key1=val1:key2=val2... ## Logging + You can write all data that is sent or received by the proxy to a file using the -l/--log parameter. Data (and some housekeeping info) is written to the log before passing it to the module chains. If you want to log the state of the data during or after the modules are run, you can use the log proxymodule. Using the chain -im http_post,log:file=log.1,http_strip,log would first log the data after the http_post module to the logfile with the name log.1. The second use of the log module at the end of the chain would write the final state of the data to a logfile with the default name in- right before passing it on . + ## TODO + - [ ] make the process interactive by implementing some kind of editor module (will probably complicate matters with regard to timeouts, can be done for now by using the burp solution detailed above and modifying data inside burp) - [ ] Create and maintain a parallel branch that is compatible with jython but also has most of the new stuff introduced after e3290261 @@ -208,3 +205,7 @@ I want to thank the following people for spending their valuable time and energy - [Michael Füllbier](https://github.com/mfuellbier) - [Stefan Grönke](https://github.com/gronke) - [Mattia](https://github.com/sowdust) +- [bjorns163](https://github.com/bjorns163) +- [Pernat1y](https://github.com/Pernat1y) +- [hrzlgnm](https://github.com/hrzlgnm) +- [MKesenheimer](https://github.com/MKesenheimer) diff --git a/lib/kxml2-min-2.3.0.jar b/lib/kxml2-min-2.3.0.jar deleted file mode 100755 index a77dd1d..0000000 Binary files a/lib/kxml2-min-2.3.0.jar and /dev/null differ diff --git a/lib/xmlpull-1.1.3.1.jar b/lib/xmlpull-1.1.3.1.jar deleted file mode 100755 index cbc149d..0000000 Binary files a/lib/xmlpull-1.1.3.1.jar and /dev/null differ diff --git a/lib/xpp3-1.1.4.jar b/lib/xpp3-1.1.4.jar deleted file mode 100755 index 90131a3..0000000 Binary files a/lib/xpp3-1.1.4.jar and /dev/null differ diff --git a/lib/xstream-1.4.4.jar b/lib/xstream-1.4.4.jar deleted file mode 100755 index 137607f..0000000 Binary files a/lib/xstream-1.4.4.jar and /dev/null differ diff --git a/proxymodules/delay.py b/proxymodules/delay.py new file mode 100644 index 0000000..6bed5fa --- /dev/null +++ b/proxymodules/delay.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +import os.path as path +import time +import random + + +class Module: + def __init__(self, incoming=False, verbose=False, options=None): + # extract the file name from __file__. __file__ is proxymodules/name.py + self.name = path.splitext(path.basename(__file__))[0] + self.description = 'Set delay in passing through the packet, used for simulating various network conditions' + self.incoming = incoming # incoming means module is on -im chain + self.random = False + self.seconds = None + self.verbose = verbose + if options is not None: + if 'seconds' in options.keys(): + try: + self.seconds = abs(float(options['seconds'])) + except ValueError: + print(f"Can't parse {options['seconds']} as float") + pass # leave it set to None + if 'random' in options.keys(): + # set random=true to enable delay randomization + self.random = (options['random'].lower() == 'true') + if self.random and self.seconds is None: + # set a upper bound of 1s if seconds is not being used, otherwise keep the seconds value + self.seconds = 1.0 + + def execute(self, data): + delay = None + if self.random: + delay = round(random.uniform(0, self.seconds), 3) # round to milliseconds + else: + delay = self.seconds + # here delay is either None or a positive float + # if the module was instantiated w/o either seconds or random, effectively nothing happens + if delay is not None: + if self.verbose: + print(f"Waiting {delay}s.") + time.sleep(delay) + return data + + def help(self): + h = '\tseconds: number of seconds you want the packet to be delayed\n' + h += ('\trandom: optional; set to true to randomize the delay between 0 and seconds (default: 1.0s)\n') + return h + + +if __name__ == '__main__': + print('This module is not supposed to be executed alone!') diff --git a/proxymodules/digestdowngrade.py b/proxymodules/digestdowngrade.py index 0c554a4..f948c1b 100644 --- a/proxymodules/digestdowngrade.py +++ b/proxymodules/digestdowngrade.py @@ -1,7 +1,5 @@ -#!/usr/bin/env python2 - +#!/usr/bin/env python3 import os -import re class Module: @@ -14,22 +12,21 @@ def __init__(self, incoming=False, verbose=False, options=None): if options is not None: if 'realm' in options.keys(): - self.realm = options['realm'] + self.realm = bytes(options['realm'], 'ascii') def detect_linebreak(self, data): - line = data.split('\n', 1)[0] - if line.endswith('\r'): - return '\r\n' + line = data.split(b'\n', 1)[0] + if line.endswith(b'\r'): + return b'\r\n' else: - return '\n' + return b'\n' def execute(self, data): delimiter = self.detect_linebreak(data) lines = data.split(delimiter) for index, line in enumerate(lines): - if line.lower().startswith('www-authenticate: digest'): - lines[index] = 'WWW-Authenticate: Basic realm="%s"%s' % (self.realm, delimiter) - + if line.lower().startswith(b'www-authenticate: digest'): + lines[index] = b'WWW-Authenticate: Basic realm="%s"' % self.realm return delimiter.join(lines) def help(self): @@ -38,4 +35,4 @@ def help(self): if __name__ == '__main__': - print 'This module is not supposed to be executed alone!' + print('This module is not supposed to be executed alone!') diff --git a/proxymodules/hexdump.py b/proxymodules/hexdump.py index 500dfc1..e9bc3a5 100644 --- a/proxymodules/hexdump.py +++ b/proxymodules/hexdump.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 import os.path as path @@ -20,18 +20,15 @@ def execute(self, data): # this is a pretty hex dumping function directly taken from # http://code.activestate.com/recipes/142812-hex-dumper/ result = [] - digits = 4 if isinstance(data, unicode) else 2 - - for i in xrange(0, len(data), self.len): + digits = 2 + for i in range(0, len(data), self.len): s = data[i:i + self.len] - hexa = b' '.join(["%0*X" % (digits, ord(x)) for x in s]) - text = b''.join([x if 0x20 <= ord(x) < 0x7F else b'.' for x in s]) - result.append(b"%04X %-*s %s" % (i, self.len * (digits + 1), - hexa, text)) - - print b'\n'.join(result) + hexa = ' '.join(['%0*X' % (digits, x) for x in s]) + text = ''.join([chr(x) if 0x20 <= x < 0x7F else '.' for x in s]) + result.append("%04X %-*s %s" % (i, self.len * (digits + 1), hexa, text)) + print("\n".join(result)) return data if __name__ == '__main__': - print 'This module is not supposed to be executed alone!' + print ('This module is not supposed to be executed alone!') diff --git a/proxymodules/hexreplace.py b/proxymodules/hexreplace.py new file mode 100644 index 0000000..4488853 --- /dev/null +++ b/proxymodules/hexreplace.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +import os + + +class Module: + def __init__(self, incoming=False, verbose=False, options=None): + # extract the file name from __file__. __file__ is proxymodules/name.py + self.name = os.path.splitext(os.path.basename(__file__))[0] + self.description = 'Replace hex data on the fly defining search and replace-pairs in a file or as module parameters' + self.verbose = verbose + self.filename = None + self.separator = ':' + self.len = 16 + + search = None + if options is not None: + if 'search' in options.keys(): + search = bytes.fromhex(options['search']) + if 'replace' in options.keys(): + replace = bytes.fromhex(options['replace']) + if 'file' in options.keys(): + self.filename = options['file'] + try: + open(self.filename) + except IOError as ioe: + print("Error opening %s: %s" % (self.filename, ioe.strerror)) + self.filename = None + if 'separator' in options.keys(): + self.separator = options['separator'] + + self.pairs = [] # list of (search, replace) tuples + if search is not None and replace is not None: + self.pairs.append((search, replace)) + + if self.filename is not None: + for line in open(self.filename).readlines(): + try: + search, replace = line.split(self.separator, 1) + self.pairs.append((bytes.fromhex(search.strip()), bytes.fromhex(replace.strip()))) + except ValueError: + # line does not contain separator and will be ignored + pass + + def hexdump(self, data): + result = [] + digits = 2 + for i in range(0, len(data), self.len): + s = data[i:i + self.len] + hexa = ' '.join(['%0*X' % (digits, x) for x in s]) + text = ''.join([chr(x) if 0x20 <= x < 0x7F else '.' for x in s]) + result.append("%04X %-*s %s" % (i, self.len * (digits + 1), hexa, text)) + print("\n".join(result)) + + def execute(self, data): + if self.verbose: + print(f"Incoming packet with size {len(data)}:") + for search, replace in self.pairs: + if search in data: + if self.verbose: + print("########## data found ###########") + print("[Before:]") + self.hexdump(data) + data = data.replace(search, replace) + if self.verbose: + print("[After:]") + self.hexdump(data) + return data + + def help(self): + h = '\tsearch: hex string (i.e. "deadbeef") to search for\n' + h += ('\treplace: hex string the search string should be replaced with\n') + h += ('\tfile: file containing search:replace pairs, one per line\n') + h += ('\tseparator: define a custom search:replace separator in the file, e.g. search#replace\n') + h += ('\n\tUse at least file or search and replace (or both).\n') + return h + + +if __name__ == '__main__': + print('This module is not supposed to be executed alone!') diff --git a/proxymodules/http_ok.py b/proxymodules/http_ok.py index 603a043..02444d8 100644 --- a/proxymodules/http_ok.py +++ b/proxymodules/http_ok.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 import os.path as path @@ -10,21 +10,21 @@ def __init__(self, incoming=False, verbose=False, options=None): self.server = None if options is not None: if 'server' in options.keys(): - self.server = options['server'] + self.server = bytes(options['server'], 'ascii') # source will be set by the proxy thread later on self.source = None def execute(self, data): if self.server is None: - self.server = self.source[0] + self.server = bytes(self.source[0], 'ascii') - http = "HTTP/1.1 200 OK\r\n" - http += "Server: %s\r\n" % self.server - http += "Connection: keep-alive\r\n" - http += "Content-Length: %d\r\n" % len(data) + http = b"HTTP/1.1 200 OK\r\n" + http += b"Server: %s\r\n" % self.server + http += b"Connection: keep-alive\r\n" + http += b"Content-Length: %d\r\n" % len(data) - return http + "\r\n" + data + return http + b"\r\n" + data def help(self): h = '\tserver: remote source, used in response Server header\n' @@ -32,4 +32,4 @@ def help(self): if __name__ == '__main__': - print 'This module is not supposed to be executed alone!' + print('This module is not supposed to be executed alone!') diff --git a/proxymodules/http_post.py b/proxymodules/http_post.py index d08013f..f532d32 100644 --- a/proxymodules/http_post.py +++ b/proxymodules/http_post.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 import os.path as path @@ -12,24 +12,24 @@ def __init__(self, incoming=False, verbose=False, options=None): self.targetport = None if options is not None: if 'host' in options.keys(): - self.targethost = options['host'] + self.targethost = bytes(options['host'], 'ascii') if 'port' in options.keys(): - self.targetport = options['port'] + self.targetport = bytes(options['port'], 'ascii') # destination will be set by the proxy thread later on self.destination = None def execute(self, data): if self.targethost is None: - self.targethost = self.destination[0] + self.targethost = bytes(self.destination[0], 'ascii') if self.targetport is None: - self.targetport = str(self.destination[1]) - http = "POST /to/%s/%s HTTP/1.1\r\n" % (self.targethost, self.targetport) - http += "Host: %s\r\n" % self.targethost + self.targetport = bytes(str(self.destination[1]), 'ascii') + http = b"POST /to/%s/%s HTTP/1.1\r\n" % (self.targethost, self.targetport) + http += b"Host: %s\r\n" % self.targethost - http += "Connection: keep-alive\r\n" - http += "Content-Length: %d\r\n" % len(data) - return http + "\r\n" + data + http += b"Connection: keep-alive\r\n" + http += b"Content-Length: %d\r\n" % len(data) + return http + b"\r\n" + str(data) def help(self): h = '\thost: remote target, used in request URL and Host header\n' @@ -38,4 +38,4 @@ def help(self): if __name__ == '__main__': - print 'This module is not supposed to be executed alone!' + print('This module is not supposed to be executed alone!') diff --git a/proxymodules/http_strip.py b/proxymodules/http_strip.py index ac01d18..e96a63f 100644 --- a/proxymodules/http_strip.py +++ b/proxymodules/http_strip.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 import os.path as path @@ -10,11 +10,11 @@ def __init__(self, incoming=False, verbose=False, options=None): self.incoming = incoming # incoming means module is on -im chain def detect_linebreak(self, data): - line = data.split('\n', 1)[0] - if line.endswith('\r'): - return '\r\n' * 2 + line = data.split(b'\n', 1)[0] + if line.endswith(b'\r'): + return b'\r\n' * 2 else: - return '\n' * 2 + return b'\n' * 2 def execute(self, data): delimiter = self.detect_linebreak(data) @@ -24,4 +24,4 @@ def execute(self, data): if __name__ == '__main__': - print 'This module is not supposed to be executed alone!' + print('This module is not supposed to be executed alone!') diff --git a/proxymodules/javaxml.py b/proxymodules/javaxml.py deleted file mode 100644 index 2635632..0000000 --- a/proxymodules/javaxml.py +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/env python2 -# -*- coding: utf-8 -*- -import os.path as path -import platform -if 'java' in platform.system().lower(): - import java.io as io - from com.thoughtworks.xstream import XStream - from java.lang import Exception - - -class Module: - def __init__(self, incoming=False, verbose=False, options=None): - self.is_jython = 'java' in platform.system().lower() - # extract the file name from __file__. __file__ is proxymodules/name.py - self.name = path.splitext(path.basename(__file__))[0] - self.description = 'Serialization or deserialization of Java objects' if self.is_jython else \ - 'Serialization or deserialization of Java objects (needs jython)' - self.incoming = incoming # incoming means module is on -im chain - self.execute = self.error - - if options is not None: - if 'mode' in options.keys(): - if 'deserial' in options['mode']: - self.execute = self.deserial - elif 'serial' in options['mode']: - self.execute = self.serial - - def help(self): - return '\tmode: [serial|deserial] select deserialization (to XML) or serialization (to Java object)' - - def deserial(self, data): - if not self.is_jython: - print '[!] This module can only be used in jython!' - return data - - try: - # turn data into a Java object - bis = io.ByteArrayInputStream(data) - ois = io.ObjectInputStream(bis) - obj = ois.readObject() - - # converting Java object to XML structure - xs = XStream() - xml = xs.toXML(obj) - return xml - except Exception as e: - print '[!] Caught Exception. Could not convert.\n' - return data - - def serial(self, data): - if not self.is_jython: - print '[!] This module can only be used in jython!' - return data - try: - # Creating XStream object and creating Java object from XML structure - xs = XStream() - serial = xs.fromXML(data) - - # writing created Java object to and serializing it with ObjectOutputStream - bos = io.ByteArrayOutputStream() - oos = io.ObjectOutputStream(bos) - oos.writeObject(serial) - - # I had a problem with signed vs. unsigned bytes, hence the & 0xff - return "".join([chr(x & 0xff) for x in bos.toByteArray().tolist()]) - except Exception as e: - print '[!] Caught Exception. Could not convert.\n' - return data - - def error(self, data): - print '[!] Unknown mode. Please specify mode=[serial|deserial].' - return data - - -if __name__ == '__main__': - print 'This module is not supposed to be executed alone!' diff --git a/proxymodules/log.py b/proxymodules/log.py index 058db60..1af9ee4 100644 --- a/proxymodules/log.py +++ b/proxymodules/log.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 import os.path as path import time @@ -24,11 +24,11 @@ def __del__(self): def execute(self, data): if self.handle is None: - self.handle = open(self.file, 'w', 0) # unbuffered - print 'Logging to file', self.file - logentry = time.strftime('%Y%m%d-%H%M%S') + ' ' + str(time.time()) + '\n' + self.handle = open(self.file, 'wb', 0) # unbuffered + print('Logging to file', self.file) + logentry = bytes(time.strftime('%Y%m%d-%H%M%S') + ' ' + str(time.time()) + '\n', 'ascii') logentry += data - logentry += '-' * 20 + '\n' + logentry += b'-' * 20 + b'\n' self.handle.write(logentry) return data @@ -38,4 +38,4 @@ def help(self): if __name__ == '__main__': - print 'This module is not supposed to be executed alone!' + print('This module is not supposed to be executed alone!') diff --git a/proxymodules/mqtt.py b/proxymodules/mqtt.py new file mode 100644 index 0000000..a0c8071 --- /dev/null +++ b/proxymodules/mqtt.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +import os.path as path +import paho.mqtt.client as mqtt +from distutils.util import strtobool + + +class Module: + def __init__(self, incoming=False, verbose=False, options=None): + # extract the file name from __file__. __file__ is proxymodules/name.py + self.name = path.splitext(path.basename(__file__))[0] + self.description = 'Publish the data to an MQTT server' + self.incoming = incoming # incoming means module is on -im chain + self.client_id = '' + self.username = None + self.password = None + self.server = None + self.port = 1883 + self.topic = '' + self.hex = False + if options is not None: + if 'clientid' in options.keys(): + self.client_id = options['clientid'] + if 'server' in options.keys(): + self.server = options['server'] + if 'username' in options.keys(): + self.username = options['username'] + if 'password' in options.keys(): + self.password = options['password'] + if 'port' in options.keys(): + try: + self.port = int(options['port']) + if self.port not in range(1, 65536): + raise ValueError + except ValueError: + print(f'port: invalid port {options["port"]}, using default {self.port}') + if 'topic' in options.keys(): + self.topic = options['topic'].strip() + if 'hex' in options.keys(): + try: + self.hex = bool(strtobool(options['hex'])) + except ValueError: + print(f'hex: {options["hex"]} is not a bool value, falling back to default value {self.hex}.') + + if self.server is not None: + self.mqtt = mqtt.Client(self.client_id) + if self.username is not None or self.password is not None: + self.mqtt.username_pw_set(self.username, self.password) + self.mqtt.connect(self.server, self.port) + else: + self.mqtt = None + + def execute(self, data): + if self.mqtt is not None: + + if self.hex is True: + self.mqtt.publish(self.topic, data.hex()) + else: + self.mqtt.publish(self.topic, data) + return data + + def help(self): + h = '\tserver: server to connect to, required\n' + h += ('\tclientid: what to use as client_id, default is empty\n' + '\tusername: username\n' + '\tpassword: password\n' + '\tport: port to connect to, default 1883\n' + '\ttopic: topic to publish to, default is empty\n' + '\thex: encode data as hex before sending it. AAAA becomes 41414141.') + return h + + +if __name__ == '__main__': + print('This module is not supposed to be executed alone!') diff --git a/proxymodules/removegzip.py b/proxymodules/removegzip.py index c4923e5..ccd6835 100644 --- a/proxymodules/removegzip.py +++ b/proxymodules/removegzip.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 import os.path as path @@ -15,20 +15,20 @@ def __init__(self, incoming=False, verbose=False, options=None): def execute(self, data): try: # split at \r\n\r\n to split the request into header and body - header, body = data.split('\r\n\r\n', 1) + header, body = data.split(b'\r\n\r\n', 1) except ValueError: # no \r\n\r\n, so probably not HTTP, we can go now return data # now split the header string into its lines - headers = header.split('\r\n') + headers = header.split(b'\r\n') for h in headers: - if h.lower().startswith('accept-encoding:') and 'gzip' in h: - headers[headers.index(h)] = h.replace('gzip', 'booo') + if h.lower().startswith(b'accept-encoding:') and b'gzip' in h: + headers[headers.index(h)] = h.replace(b'gzip', b'booo') break - return '\r\n'.join(headers) + '\r\n\r\n' + body + return b'\r\n'.join(headers) + b'\r\n\r\n' + body if __name__ == '__main__': - print 'This module is not supposed to be executed alone!' + print('This module is not supposed to be executed alone!') diff --git a/proxymodules/replace.py b/proxymodules/replace.py index aad9c75..258f8d2 100644 --- a/proxymodules/replace.py +++ b/proxymodules/replace.py @@ -1,5 +1,4 @@ -#!/usr/bin/env python2 - +#!/usr/bin/env python3 import os import re @@ -17,15 +16,15 @@ def __init__(self, incoming=False, verbose=False, options=None): if options is not None: if 'search' in options.keys(): - self.search = options['search'] + self.search = bytes(options['search'], 'ascii') if 'replace' in options.keys(): - self.replace = options['replace'] + self.replace = bytes(options['replace'], 'ascii') if 'file' in options.keys(): self.filename = options['file'] try: open(self.filename) except IOError as ioe: - print "Error opening %s: %s" % (self.filename, ioe.strerror) + print("Error opening %s: %s" % (self.filename, ioe.strerror)) self.filename = None if 'separator' in options.keys(): self.separator = options['separator'] @@ -39,9 +38,9 @@ def execute(self, data): for line in open(self.filename).readlines(): try: search, replace = line.split(self.separator, 1) - pairs.append((search.strip(), replace.strip())) + pairs.append((bytes(search.strip(), 'ascii'), bytes(replace.strip(), 'ascii'))) except ValueError: - # line does not contain : and will be ignored + # line does not contain separator and will be ignored pass for search, replace in pairs: @@ -60,4 +59,4 @@ def help(self): if __name__ == '__main__': - print 'This module is not supposed to be executed alone!' + print('This module is not supposed to be executed alone!') diff --git a/proxymodules/size.py b/proxymodules/size.py index dca436a..bf6aabe 100644 --- a/proxymodules/size.py +++ b/proxymodules/size.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 import os.path as path from distutils.util import strtobool @@ -22,7 +22,7 @@ def execute(self, data): if self.verbose: msg += " from %s:%d" % self.source msg += " for %s:%d" % self.destination - print msg + print(msg) return data def help(self): @@ -31,4 +31,4 @@ def help(self): if __name__ == '__main__': - print 'This module is not supposed to be executed alone!' + print('This module is not supposed to be executed alone!') diff --git a/proxymodules/size404.py b/proxymodules/size404.py index 039ecd7..cf63a36 100644 --- a/proxymodules/size404.py +++ b/proxymodules/size404.py @@ -1,5 +1,7 @@ -#!/usr/bin/env python3 +#!/usr/bin/env python2 import os.path as path +import time +from distutils.util import strtobool class Module: @@ -10,6 +12,11 @@ def __init__(self, incoming=False, verbose=False, options=None): self.incoming = incoming # incoming means module is on -im chain self.size = 2392 # if a response has this value as content-length, it will become a 404 self.verbose = False + self.custom = False + self.rewriteall = False # will we block the first occurence? + self.firstfound = False # have we found the first occurence yet? + self.resetinterval = None # if we haven't found a fitting response in this many seconds, reset the state and set first to False again + self.timer = time.time() if options is not None: if 'size' in options.keys(): try: @@ -17,19 +24,56 @@ def __init__(self, incoming=False, verbose=False, options=None): except ValueError: pass # use the default if you can't parse the parameter if 'verbose' in options.keys(): - self.verbose=True - + self.verbose = bool(strtobool(options['verbose'])) + if 'custom' in options.keys(): + try: + with open(options['custom'], 'rb') as handle: + self.custom = handle.read() + except Exception: + print('Can\'t open custom error file, not using it.') + self.custom = False + if 'rewriteall' in options.keys(): + self.rewriteall = bool(strtobool(options['rewriteall'])) + if 'reset' in options.keys(): + try: + self.resetinterval = float(options['reset']) + except ValueError: + pass # use the default if you can't parse the parameter def execute(self, data): - contentlength = 'content-length: ' + str(self.size) - if data.startswith('HTTP/1.1 200 OK') and contentlength in data.lower(): - data = data.replace('200 OK', '404 Not Found', 1) - print 'Edited return code' + contentlength = b'content-length: ' + bytes(str(self.size), 'ascii') + if data.startswith(b'HTTP/1.1 200 OK') and contentlength in data.lower(): + if self.resetinterval is not None: + t = time.time() + if t - self.timer >= self.resetinterval: + if self.verbose: + print('Timer elapsed') + self.firstfound = False + self.timer = t + if self.rewriteall is False and self.firstfound is False: + # we have seen this response size for the first time and are not blocking the first one + self.firstfound = True + if self.verbose: + print('Letting this response through') + return data + if self.custom is not False: + data = self.custom + if self.verbose: + print('Replaced response with custom response') + else: + data = data.replace(b'200 OK', b'404 Not Found', 1) + if self.verbose: + print('Edited return code') return data def help(self): - return '\tsize: if a response has this value as content-length, it will become a 404\n\tverbose: print a message if a string is replaced' + h = '\tsize: if a response has this value as content-length, it will become a 404\n' + h += ('\tverbose: print a message if a string is replaced\n' + '\tcustom: path to a file containing a custom response, will replace the received response\n' + '\trewriteall: if set, it will rewrite all responses. Default is to let the first on through' + '\treset: number of seconds after which we will reset the state and will let the next response through.') + return h if __name__ == '__main__': - print 'This module is not supposed to be executed alone!' + print('This module is not supposed to be executed alone!') diff --git a/proxymodules/textdump.py b/proxymodules/textdump.py index c31adef..a192899 100644 --- a/proxymodules/textdump.py +++ b/proxymodules/textdump.py @@ -1,5 +1,6 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 import os.path as path +from codecs import decode, lookup class Module: @@ -9,20 +10,29 @@ def __init__(self, incoming=False, verbose=False, options=None): self.description = 'Simply print the received data as text' self.incoming = incoming # incoming means module is on -im chain self.find = None # if find is not None, this text will be highlighted + self.codec = 'latin_1' if options is not None: if 'find' in options.keys(): - self.find = options['find'] # text to highlight + self.find = bytes(options['find'], 'ascii') # text to highlight if 'color' in options.keys(): - self.color = '\033[' + options['color'] + 'm' # highlight color + self.color = bytes('\033[' + options['color'] + 'm', 'ascii') # highlight color else: - self.color = '\033[31;1m' + self.color = b'\033[31;1m' + if 'codec' in options.keys(): + codec = options['codec'] + try: + lookup(codec) + self.codec = codec + except LookupError: + print(f"{self.name}: {options['codec']} is not a valid codec, using {self.codec}") + def execute(self, data): if self.find is None: - print data + print(decode(data, self.codec)) else: - pdata = data.replace(self.find, self.color + self.find + '\033[0m') - print pdata + pdata = data.replace(self.find, self.color + self.find + b'\033[0m') + print(decode(pdata, self.codec)) return data def help(self): @@ -33,4 +43,4 @@ def help(self): if __name__ == '__main__': - print 'This module is not supposed to be executed alone!' + print('This module is not supposed to be executed alone!') diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ab6e7f6 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +paho-mqtt +PySocks diff --git a/tcpproxy.py b/tcpproxy.py index 25f7d60..4143a14 100755 --- a/tcpproxy.py +++ b/tcpproxy.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 import argparse import pkgutil import os @@ -43,15 +43,21 @@ def parse_args(): parser.add_argument('-lp', '--listenport', dest='listen_port', type=int, default=8080, help='port to listen on') + parser.add_argument('-si', '--sourceip', dest='source_ip', + help='IP address the other end will see') + + parser.add_argument('-sp', '--sourceport', dest='source_port', type=int, + help='source port the other end will see') + parser.add_argument('-pi', '--proxy-ip', dest='proxy_ip', default=None, help='IP address/host name of proxy') - + parser.add_argument('-pp', '--proxy-port', dest='proxy_port', type=int, default=1080, help='proxy port', ) - + parser.add_argument('-pt', '--proxy-type', dest='proxy_type', default='SOCKS5', choices=['SOCKS4', 'SOCKS5', 'HTTP'], - help='proxy type. Options are SOCKS5 (default), SOCKS4, HTTP') - + type = str.upper, help='proxy type. Options are SOCKS5 (default), SOCKS4, HTTP') + parser.add_argument('-om', '--outmodules', dest='out_modules', help='comma-separated list of modules to modify data' + ' before sending to remote target.') @@ -78,9 +84,20 @@ def parse_args(): parser.add_argument('-lo', '--list-options', dest='help_modules', default=None, help='Print help of selected module') - parser.add_argument('-s', '--ssl', dest='use_ssl', action='store_true', - default=False, help='detect SSL/TLS as well as STARTTLS, certificate is mitm.pem') + default=False, help='detect SSL/TLS as well as STARTTLS') + + parser.add_argument('-sc', '--server-certificate', default='mitm.pem', + help='server certificate in PEM format (default: %(default)s)') + + parser.add_argument('-sk', '--server-key', default='mitm.pem', + help='server key in PEM format (default: %(default)s)') + + parser.add_argument('-cc', '--client-certificate', default=None, + help='client certificate in PEM format in case client authentication is required by the target') + + parser.add_argument('-ck', '--client-key', default=None, + help='client key in PEM format in case client authentication is required by the target') return parser.parse_args() @@ -100,7 +117,7 @@ def generate_module_list(modstring, incoming=False, verbose=False): __import__('proxymodules.' + name) modlist.append(sys.modules['proxymodules.' + name].Module(incoming, verbose, options)) except ImportError: - print 'Module %s not found' % name + print('Module %s not found' % name) sys.exit(3) return modlist @@ -120,7 +137,7 @@ def parse_module_options(n): k, v = op.split('=') options[k] = v except ValueError: - print op, ' is not valid!' + print(op, ' is not valid!') sys.exit(23) return name, options @@ -132,7 +149,7 @@ def list_modules(): for _, module, _ in pkgutil.iter_modules([module_path]): __import__('proxymodules.' + module) m = sys.modules['proxymodules.' + module].Module() - print '%s - %s' % (m.name, m.description) + print(f'{m.name} - {m.description}') def print_module_help(modlist): @@ -140,10 +157,10 @@ def print_module_help(modlist): modules = generate_module_list(modlist) for m in modules: try: - print m.name - print m.help() + print(f'{m.name} - {m.description}') + print(m.help()) except AttributeError: - print '\tNo options or missing help() function.' + print('\tNo options or missing help() function.') def update_module_hosts(modules, source, destination): @@ -160,7 +177,7 @@ def update_module_hosts(modules, source, destination): def receive_from(s): # receive data from a socket until no more data is there - b = "" + b = b"" while True: data = s.recv(4096) b += data @@ -185,31 +202,48 @@ def handle_data(data, modules, dont_chain, incoming, verbose): def is_client_hello(sock): firstbytes = sock.recv(128, socket.MSG_PEEK) return (len(firstbytes) >= 3 and - firstbytes[0] == "\x16" and - firstbytes[1:3] in ["\x03\x00", - "\x03\x01", - "\x03\x02", - "\x03\x03", - "\x02\x00"] + firstbytes[0] == 0x16 and + firstbytes[1:3] in [b"\x03\x00", + b"\x03\x01", + b"\x03\x02", + b"\x03\x03", + b"\x02\x00"] ) -def enable_ssl(remote_socket, local_socket): +def enable_ssl(args, remote_socket, local_socket): + sni = None + + def sni_callback(sock, name, ctx): + nonlocal sni + sni = name + try: - local_socket = ssl.wrap_socket(local_socket, + ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + ctx.sni_callback = sni_callback + ctx.load_cert_chain(certfile=args.server_certificate, + keyfile=args.server_key, + ) + local_socket = ctx.wrap_socket(local_socket, server_side=True, - certfile="mitm.pem", - keyfile="mitm.pem", - ssl_version=ssl.PROTOCOL_TLSv1_2, ) except ssl.SSLError as e: - print "SSL handshake failed for listening socket", str(e) + print("SSL handshake failed for listening socket", str(e)) raise try: - remote_socket = ssl.wrap_socket(remote_socket) + ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + if args.client_certificate and args.client_key: + ctx.load_cert_chain(certfile=args.client_certificate, + keyfile=args.client_key, + ) + remote_socket = ctx.wrap_socket(remote_socket, + server_hostname=sni, + ) except ssl.SSLError as e: - print "SSL handshake failed for remote socket", str(e) + print("SSL handshake failed for remote socket", str(e)) raise return [remote_socket, local_socket] @@ -222,6 +256,7 @@ def starttls(args, local_socket, read_sockets): is_client_hello(local_socket) ) + def start_proxy_thread(local_socket, args, in_modules, out_modules): # This method is executed in a thread. It will relay data between the local # host and the remote host, while letting modules work on the data before @@ -229,23 +264,49 @@ def start_proxy_thread(local_socket, args, in_modules, out_modules): remote_socket = socks.socksocket() if args.proxy_ip: - proxy_types = {'SOCKS5' : socks.SOCKS5, 'SOCKS4' : socks.SOCKS4, 'HTTP' : socks.HTTP} + proxy_types = {'SOCKS5': socks.SOCKS5, 'SOCKS4': socks.SOCKS4, 'HTTP': socks.HTTP} remote_socket.set_proxy(proxy_types[args.proxy_type], args.proxy_ip, args.proxy_port) try: + if args.source_ip or args.source_port: + remote_socket.bind((args.source_ip, args.source_port)) remote_socket.connect((args.target_ip, args.target_port)) vprint('Connected to %s:%d' % remote_socket.getpeername(), args.verbose) log(args.logfile, 'Connected to %s:%d' % remote_socket.getpeername()) except socket.error as serr: if serr.errno == errno.ECONNREFUSED: - print '%s:%d - Connection refused' % (args.target_ip, args.target_port) - log(args.logfile, '%s:%d - Connection refused' % (args.target_ip, args.target_port)) + for s in [remote_socket, local_socket]: + s.close() + print(f'{time.strftime("%Y%m%d-%H%M%S")}, {args.target_ip}:{args.target_port}- Connection refused') + log(args.logfile, f'{time.strftime("%Y%m%d-%H%M%S")}, {args.target_ip}:{args.target_port}- Connection refused') + return None + elif serr.errno == errno.ETIMEDOUT: + for s in [remote_socket, local_socket]: + s.close() + print(f'{time.strftime("%Y%m%d-%H%M%S")}, {args.target_ip}:{args.target_port}- Connection timed out') + log(args.logfile, f'{time.strftime("%Y%m%d-%H%M%S")}, {args.target_ip}:{args.target_port}- Connection timed out') return None else: + for s in [remote_socket, local_socket]: + s.close() raise serr - update_module_hosts(out_modules, local_socket.getpeername(), remote_socket.getpeername()) - update_module_hosts(in_modules, remote_socket.getpeername(), local_socket.getpeername()) + try: + update_module_hosts(out_modules, local_socket.getpeername(), remote_socket.getpeername()) + update_module_hosts(in_modules, remote_socket.getpeername(), local_socket.getpeername()) + except socket.error as serr: + if serr.errno == errno.ENOTCONN: + # kind of a blind shot at fixing issue #15 + # I don't yet understand how this error can happen, but if it happens I'll just shut down the thread + # the connection is not in a useful state anymore + for s in [remote_socket, local_socket]: + s.close() + return None + else: + for s in [remote_socket, local_socket]: + s.close() + print(f"{time.strftime('%Y%m%d-%H%M%S')}: Socket exception in start_proxy_thread") + raise serr # This loop ends when no more data is received on either the local or the # remote socket @@ -255,31 +316,45 @@ def start_proxy_thread(local_socket, args, in_modules, out_modules): if starttls(args, local_socket, read_sockets): try: - ssl_sockets = enable_ssl(remote_socket, local_socket) + ssl_sockets = enable_ssl(args, remote_socket, local_socket) remote_socket, local_socket = ssl_sockets vprint("SSL enabled", args.verbose) log(args.logfile, "SSL enabled") except ssl.SSLError as e: - print "SSL handshake failed", str(e) + print("SSL handshake failed", str(e)) log(args.logfile, "SSL handshake failed", str(e)) break read_sockets, _, _ = select.select(ssl_sockets, [], []) for sock in read_sockets: - peer = sock.getpeername() + try: + peer = sock.getpeername() + except socket.error as serr: + if serr.errno == errno.ENOTCONN: + # kind of a blind shot at fixing issue #15 + # I don't yet understand how this error can happen, but if it happens I'll just shut down the thread + # the connection is not in a useful state anymore + for s in [remote_socket, local_socket]: + s.close() + running = False + break + else: + print(f"{time.strftime('%Y%m%d-%H%M%S')}: Socket exception in start_proxy_thread") + raise serr + data = receive_from(sock) log(args.logfile, 'Received %d bytes' % len(data)) if sock == local_socket: if len(data): - log(args.logfile, '< < < out\n' + data) + log(args.logfile, b'< < < out\n' + data) if out_modules is not None: data = handle_data(data, out_modules, args.no_chain_modules, False, # incoming data? args.verbose) - remote_socket.send(data) + remote_socket.send(data.encode() if isinstance(data, str) else data) else: vprint("Connection from local client %s:%d closed" % peer, args.verbose) log(args.logfile, "Connection from local client %s:%d closed" % peer) @@ -288,7 +363,7 @@ def start_proxy_thread(local_socket, args, in_modules, out_modules): break elif sock == remote_socket: if len(data): - log(args.logfile, '> > > in\n' + data) + log(args.logfile, b'> > > in\n' + data) if in_modules is not None: data = handle_data(data, in_modules, args.no_chain_modules, @@ -307,41 +382,46 @@ def log(handle, message, message_only=False): # if message_only is True, only the message will be logged # otherwise the message will be prefixed with a timestamp and a line is # written after the message to make the log file easier to read + if not isinstance(message, bytes): + message = bytes(message, 'ascii') if handle is None: return if not message_only: - logentry = "%s %s\n" % (time.strftime('%Y%m%d-%H%M%S'), - str(time.time())) + logentry = bytes("%s %s\n" % (time.strftime('%Y%m%d-%H%M%S'), str(time.time())), 'ascii') else: - logentry = '' + logentry = b'' logentry += message if not message_only: - logentry += '\n' + '-' * 20 + '\n' + logentry += b'\n' + b'-' * 20 + b'\n' handle.write(logentry) def vprint(msg, is_verbose): # this will print msg, but only if is_verbose is True if is_verbose: - print msg + print(msg) def main(): args = parse_args() if args.list is False and args.help_modules is None: if not args.target_ip: - print 'Target IP is required: -ti' + print('Target IP is required: -ti') sys.exit(6) if not args.target_port: - print 'Target port is required: -tp' + print('Target port is required: -tp') sys.exit(7) + if ((args.client_key is None) ^ (args.client_certificate is None)): + print("You must either specify both the client certificate and client key or leave both empty") + sys.exit(8) + if args.logfile is not None: try: - args.logfile = open(args.logfile, 'a', 0) # unbuffered + args.logfile = open(args.logfile, 'ab', 0) # unbuffered except Exception as ex: - print 'Error opening logfile' - print ex + print('Error opening logfile') + print(ex) sys.exit(4) if args.list: @@ -358,7 +438,7 @@ def main(): except socket.gaierror: ip = False if ip is False: - print '%s is not a valid IP address or host name' % args.listen_ip + print('%s is not a valid IP address or host name' % args.listen_ip) sys.exit(1) else: args.listen_ip = ip @@ -369,7 +449,7 @@ def main(): except socket.gaierror: ip = False if ip is False: - print '%s is not a valid IP address or host name' % args.target_ip + print('%s is not a valid IP address or host name' % args.target_ip) sys.exit(2) else: args.target_ip = ip @@ -391,10 +471,10 @@ def main(): try: proxy_socket.bind((args.listen_ip, args.listen_port)) except socket.error as e: - print e.strerror + print(e.strerror) sys.exit(5) - proxy_socket.listen(10) + proxy_socket.listen(100) log(args.logfile, str(args)) # endless loop until ctrl+c try: @@ -409,7 +489,7 @@ def main(): proxy_thread.start() except KeyboardInterrupt: log(args.logfile, 'Ctrl+C detected, exiting...') - print '\nCtrl+C detected, exiting...' + print('\nCtrl+C detected, exiting...') sys.exit(0)