diff --git a/README.md b/README.md index ff0a826..387229e 100644 --- a/README.md +++ b/README.md @@ -109,4 +109,10 @@ The ProxyRequestHandler class has 3 methods to override: By default, only save_handler is implemented which outputs HTTP(S) headers and some useful data to the standard output. -You can implement your own packets handling plugin by implementing `ProxyHandler` class with methods listed above. Then, you'll have to point the program at your plugin with -p option. \ No newline at end of file +You can implement your own packets handling plugin by implementing `ProxyHandler` class with methods listed above. Then, you'll have to point the program at your plugin with -p option. + + + +## Known bugs + +- Generating SSL certificates on the fly as implemented in `ProxyRequestHandler.generate_ssl_certificate()` fails on Windows most likely due to openssl's error "__unable to write 'random state'__". This needs further investigation. \ No newline at end of file diff --git a/optionsparser.py b/optionsparser.py index 749c6bc..d7e45f5 100755 --- a/optionsparser.py +++ b/optionsparser.py @@ -42,7 +42,7 @@ def parse_options(opts, version): help="Turns off SSL interception/MITM and falls back on straight forwarding.", action="store_true") sslgroup.add_argument('--ssl-certdir', dest='certdir', metavar='DIR', help='Sets the destination for all of the SSL-related files, including keys, certificates (self and of'\ - ' the visited websites). Default: "'+ opts['certdir'] +'"', default=opts['certdir']) + ' the visited websites). If not specified, a default value will be used to create a directory and remove it upon script termination. Default: "'+ opts['certdir'] +'"', default=opts['certdir']) sslgroup.add_argument('--ssl-cakey', dest='cakey', metavar='NAME', help='Sets the name of a CA key file\'s name. Default: "'+ opts['cakey'] +'"', default=opts['cakey']) sslgroup.add_argument('--ssl-cacert', dest='cacert', metavar='NAME', @@ -64,7 +64,7 @@ def parse_options(opts, version): opts.update(vars(params)) if params.list_plugins: - with os.scandir(os.path.join(os.getcwd(), 'plugins/')) as it: + with os.scandir(os.path.normpath(os.path.join(os.getcwd(), 'plugins/'))) as it: for entry in it: if entry.name.endswith(".py") and entry.is_file(): print('[+] Plugin: {}'.format(entry.name)) @@ -76,7 +76,7 @@ def parse_options(opts, version): decomposed = PluginsLoader.decompose_path(opt) if not os.path.isfile(decomposed['path']): opt = opt.replace('.py', '') - opt2 = os.path.join(os.getcwd(), 'plugins/{}.py'.format(opt)) + opt2 = os.path.normpath(os.path.join(os.getcwd(), 'plugins/{}.py'.format(opt))) if not os.path.isfile(opt2): raise Exception('Specified plugin: "%s" does not exist.' % decomposed['path']) else: @@ -94,12 +94,20 @@ def parse_options(opts, version): opts['log'] = 'none' elif params.log and len(params.log) > 0: try: - opts['log'] = open(params.log, 'w') + with open(params.log, 'w') as f: + pass + opts['log'] = params.log except Exception as e: raise Exception('[ERROR] Failed to open log file for writing. Error: "%s"' % e) else: opts['log'] = sys.stdout + if opts['log']: opts['log'] = os.path.normpath(opts['log']) + if opts['plugin']: opts['plugin'] = os.path.normpath(opts['plugin']) + if opts['cakey']: opts['cakey'] = os.path.normpath(opts['cakey']) + if opts['certdir']: opts['certdir'] = os.path.normpath(opts['certdir']) + if opts['certkey']: opts['certkey'] = os.path.normpath(opts['certkey']) + def feed_with_plugin_options(opts, parser): logger = ProxyLogger() plugins = [] diff --git a/proxy2.py b/proxy2.py index 70aec45..8d89331 100755 --- a/proxy2.py +++ b/proxy2.py @@ -66,10 +66,10 @@ 'proxy_self_url': 'http://proxy2.test/', 'timeout': 5, 'no_ssl': False, - 'cakey': 'ca-cert/ca.key', - 'cacert': 'ca-cert/ca.crt', - 'certkey': 'ca-cert/cert.key', - 'certdir': 'certs/', + 'cakey': os.path.normpath(os.path.join(os.getcwd(), 'ca-cert/ca.key')), + 'cacert': os.path.normpath(os.path.join(os.getcwd(), 'ca-cert/ca.crt')), + 'certkey': os.path.normpath(os.path.join(os.getcwd(), 'ca-cert/cert.key')), + 'certdir': os.path.normpath(os.path.join(os.getcwd(), 'certs/')), 'cacn': 'proxy2 CA', 'plugins': set(), 'plugin_class_name': 'ProxyPlugin', @@ -125,6 +125,18 @@ def __init__(self, *args, **kwargs): if options['debug']: raise + def log_message(self, format, *args): + if (self.options['verbose'] or \ + self.options['debug'] or self.options['trace']) or \ + (type(self.options['log']) == str and self.options['log'] != 'none'): + + txt = "%s - - [%s] %s\n" % \ + (self.address_string(), + self.log_date_time_string(), + format%args) + + logger.out(txt, self.options['log'], '') + def log_error(self, format, *args): # Surpress "Request timed out: timeout('timed out',)" if not in debug mode. @@ -143,19 +155,47 @@ def do_CONNECT(self): else: self.connect_relay() + @staticmethod + def generate_ssl_certificate(hostname): + certpath = os.path.normpath("%s/%s.crt" % (options['certdir'].rstrip('/'), hostname)) + stdout = stderr = '' + + if not os.path.isfile(certpath): + logger.dbg('Generating valid SSL certificate...') + epoch = "%d" % (time.time() * 1000) + + # Workaround for the Windows' RANDFILE bug + if not 'RANDFILE' in os.environ.keys(): + os.environ["RANDFILE"] = os.path.normpath("%s/.rnd" % (options['certdir'].rstrip('/'))) + + cmd = ["openssl", "req", "-new", "-key", options['certkey'], "-subj", "/CN=%s" % hostname] + cmd2 = ["openssl", "x509", "-req", "-days", "3650", "-CA", options['cacert'], "-CAkey", options['cakey'], "-set_serial", epoch, "-out", certpath] + + p1 = Popen(cmd, stdout=PIPE, stderr=PIPE) + p2 = Popen(cmd2, stdin=p1.stdout, stdout=PIPE, stderr=PIPE) + (stdout, stderr) = p2.communicate() + + else: + logger.dbg('Using supplied SSL certificate: {}'.format(certpath)) + + if not certpath or not os.path.isfile(certpath): + if stdout or stderr: + logger.err('Openssl x509 crt request failed:\n{}'.format((stdout + stderr).decode())) + logger.fatal('Could not create interception Certificate: "{}"'.format(certpath)) + return '' + + return certpath + def connect_intercept(self): hostname = self.path.split(':')[0] logger.dbg('CONNECT intercepted: "%s"' % self.path) with self.lock: - certpath = "%s/%s.crt" % (self.options['certdir'].rstrip('/'), hostname) - if not os.path.isfile(certpath): - logger.dbg('Generating valid SSL certificate...') - epoch = "%d" % (time.time() * 1000) - p1 = Popen(["openssl", "req", "-new", "-key", self.options['certkey'], "-subj", "/CN=%s" % hostname], stdout=PIPE) - p2 = Popen(["openssl", "x509", "-req", "-days", "3650", "-CA", self.options['cacert'], "-CAkey", self.options['cakey'], "-set_serial", epoch, "-out", certpath], stdin=p1.stdout, stderr=PIPE) - p2.communicate() + certpath = ProxyRequestHandler.generate_ssl_certificate(hostname) + if not certpath: + self.send_response(500, 'Internal Server Error') + self.end_headers() self.send_response(200, 'Connection Established') self.end_headers() @@ -555,9 +595,9 @@ def cleanup(): global options # Close logging file descriptor unless it's stdout - if options['log'] and options['log'] not in (sys.stdout, 'none'): - options['log'].close() - options['log'] = None + #if options['log'] and options['log'] not in (sys.stdout, 'none'): + # options['log'].close() + # options['log'] = None if sslintercept: sslintercept.cleanup() @@ -586,20 +626,15 @@ def serve_proxy(port, _ssl = False): httpd = ThreadingHTTPServer(server_address, ProxyRequestHandler) sa = httpd.socket.getsockname() - s = sa[0] if not sa[0] else '127.0.0.1' + s = sa[0] if not sa[0] else options['hostname'] + logger.info("Serving " + scheme + " proxy on: " + s + ", port: " + str(sa[1]) + "...", color=ProxyLogger.colors_map['yellow']) if scheme == 'https': - certpath = "%s/%s.crt" % (options['certdir'].rstrip('/'), hostname) - if not os.path.isfile(certpath): - logger.dbg('Generating valid SSL certificate...') - epoch = "%d" % (time.time() * 1000) - p1 = Popen(["openssl", "req", "-new", "-key", options['certkey'], "-subj", "/CN=%s" % hostname], stdout=PIPE) - p2 = Popen(["openssl", "x509", "-req", "-days", "3650", "-CA", options['cacert'], "-CAkey", options['cakey'], "-set_serial", epoch, "-out", certpath], stdin=p1.stdout, stderr=PIPE) - p2.communicate() - else: - logger.dbg('Using supplied SSL certificate: {}'.format(certpath)) + certpath = ProxyRequestHandler.generate_ssl_certificate(hostname) + if not certpath: + return False context = ssl._create_unverified_context() context.load_cert_chain(certfile=certpath, keyfile=options['certkey']) @@ -635,8 +670,8 @@ def main(): th.daemon = True th.start() - for t in threads: - t.join() + while True: + time.sleep(1) except KeyboardInterrupt: logger.info('\nProxy serving interrupted by user.', noprefix=True) diff --git a/proxylogger.py b/proxylogger.py index 97737cf..a013e84 100755 --- a/proxylogger.py +++ b/proxylogger.py @@ -66,8 +66,11 @@ def out(txt, fd, mode='info ', **kwargs): tm = str(time.strftime("%H:%M:%S", time.gmtime())) prefix = '' + if mode: + mode = '[%s] ' % mode + if not args['noprefix']: - prefix = ProxyLogger.with_color(ProxyLogger.colors_dict['other'], '[%s] %s: ' + prefix = ProxyLogger.with_color(ProxyLogger.colors_dict['other'], '%s%s: ' % (mode.upper(), tm)) nl = '' @@ -75,21 +78,31 @@ def out(txt, fd, mode='info ', **kwargs): if args['newline']: nl = '\n' - fd.write(prefix + ProxyLogger.with_color(col, txt) + nl) + if type(fd) == str: + with open(fd, 'a') as f: + f.write(prefix + txt + nl) + f.flush() + + sys.stdout.write(prefix + ProxyLogger.with_color(col, txt) + nl) + sys.stdout.flush() + + else: + fd.write(prefix + ProxyLogger.with_color(col, txt) + nl) # Info shall be used as an ordinary logging facility, for every desired output. def info(self, txt, forced = False, **kwargs): - if forced or (self.options['verbose'] or self.options['debug'] or self.options['trace']): + if forced or (self.options['verbose'] or \ + self.options['debug'] or self.options['trace']) or \ + (type(self.options['log']) == str and self.options['log'] != 'none'): ProxyLogger.out(txt, self.options['log'], 'info', **kwargs) # Trace by default does not uses [TRACE] prefix. Shall be used # for dumping packets, headers, metadata and longer technical output. def trace(self, txt, **kwargs): - if self.options['trace']: + if self.options['trace']: kwargs['noprefix'] = True ProxyLogger.out(txt, self.options['log'], 'trace', **kwargs) - def dbg(self, txt, **kwargs): if self.options['debug']: ProxyLogger.out(txt, self.options['log'], 'debug', **kwargs) diff --git a/sslintercept.py b/sslintercept.py index bfbc0d7..4d92ebd 100755 --- a/sslintercept.py +++ b/sslintercept.py @@ -30,7 +30,7 @@ def _setup(self): self.logger.dbg("Creating directory for certificate: '%s'" % self.options['certdir']) os.mkdir(self.options['certdir']) except Exception as e: - self.logger.err("Couldn't make directory for certificates: '%s'" % e) + self.logger.fatal("Couldn't make directory for certificates: '%s'" % e) return False # Step 2: Create CA key @@ -44,7 +44,7 @@ def _setup(self): self.logger.dbg(out + error) if not self.options['cakey']: - self.logger.err('Creating of CA key process has failed.') + self.logger.fatal('Creating of CA key process has failed.') return False else: self.logger.info('Using provided CA key file: {}'.format(self.options['cakey'])) @@ -60,7 +60,7 @@ def _setup(self): self.logger.dbg(out + error) if not self.options['cacert']: - self.logger.err('Creating of CA certificate process has failed.') + self.logger.fatal('Creating of CA certificate process has failed.') return False else: self.logger.info('Using provided CA certificate file: {}'.format(self.options['cacert'])) @@ -76,8 +76,8 @@ def _setup(self): (out, error) = p.communicate() self.logger.dbg(out + error) - if not self.options['certkey']: - self.logger.err('Creating of Certificate key process has failed.') + if not self.options['certkey'] or not os.path.isfile(self.options['certkey']): + self.logger.fatal('Creating of Certificate key process has failed.') return False else: self.logger.info('Using provided Certificate key: {}'.format(self.options['certkey'])) @@ -89,13 +89,12 @@ def _setup(self): self.status = _setup(self) return self.status - def cleanup(self): if not self.status: return try: - shutil.rmtree(self.options['certdir']) + #shutil.rmtree(self.options['certdir']) self.logger.dbg('SSL interception files cleaned up.') except Exception as e: self.logger.err("Couldn't perform SSL interception files cleaning: '%s'" % e)