Skip to content

Commit

Permalink
Fixed issues related to Windows platform, enhanced SSL certificate re…
Browse files Browse the repository at this point in the history
…generation and improved logging to output file.
  • Loading branch information
mgeeky committed Jan 23, 2020
1 parent 3cf0f8c commit 35416b4
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 43 deletions.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
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.
16 changes: 12 additions & 4 deletions optionsparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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))
Expand All @@ -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:
Expand All @@ -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 = []
Expand Down
87 changes: 61 additions & 26 deletions proxy2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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.
Expand All @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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'])
Expand Down Expand Up @@ -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)
Expand Down
23 changes: 18 additions & 5 deletions proxylogger.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,30 +66,43 @@ 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 = ''
if 'newline' in args:
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)
Expand Down
13 changes: 6 additions & 7 deletions sslintercept.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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']))
Expand All @@ -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']))
Expand All @@ -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']))
Expand All @@ -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)
Expand Down

0 comments on commit 35416b4

Please sign in to comment.