Skip to content

Commit

Permalink
Plugins can now have "commands" and "options".
Browse files Browse the repository at this point in the history
Added --http_get and --hide-rejected-ciphers options in PluginOpenSSLCipherSuites.
Lowered the number of concurrent processes to 5. 10 was too aggressive.

Fixes issue 3
  • Loading branch information
nabla-c0d3 committed Mar 20, 2012
1 parent b4e8644 commit 1edcf19
Show file tree
Hide file tree
Showing 8 changed files with 111 additions and 84 deletions.
66 changes: 31 additions & 35 deletions parse_command_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,16 +114,26 @@ def create_command_line_parser(available_plugins, prog_version, timeout):
pluginoptiongroup = plugin_class.get_commands()

# Get the list of commands implemented by the current plugin
plugin_commands = (zip(*pluginoptiongroup.options))[0]
plugin_commands = (zip(*pluginoptiongroup.commands))[0]
# Keep track of which plugin/module supports which command
for command in plugin_commands:
available_commands[command] = plugin_class

# Add the current plugin's options to the parser
# Add the current plugin's commands to the parser
group = OptionGroup(parser, pluginoptiongroup.title,\
pluginoptiongroup.description)
for option in pluginoptiongroup.commands:
# If dest is something, store it, otherwise just use store_true
if option[2] is not None:
group.add_option('--' + option[0], action="store",
help=option[1], dest=option[2])
else:
group.add_option('--' + option[0], action="store_true",
help=option[1], dest=option[2])

# Add the current plugin's options to the parser
for option in pluginoptiongroup.options:
# If dest is something.. then we store, otherwise just set True
# If dest is something, store it, otherwise just use store_true
if option[2] is not None:
group.add_option('--' + option[0], action="store",
help=option[1], dest=option[2])
Expand All @@ -135,13 +145,16 @@ def create_command_line_parser(available_plugins, prog_version, timeout):
# Add the --regular command line parameter as a shortcut
if parser.has_option('--sslv2') and parser.has_option('--sslv3') \
and parser.has_option('--tlsv1') and parser.has_option('--reneg') \
and parser.has_option('--resum') and parser.has_option('--certinfo'):
and parser.has_option('--resum') and parser.has_option('--certinfo') \
and parser.has_option('--http_get') \
and parser.has_option('--hide_rejected_ciphers'):
parser.add_option(
'--regular',
action="store_true",
help=(
'Regular scan. Shortcut for --sslv2 --sslv3 '
'--tlsv1 --reneg --resum --certinfo=basic'),
'Regular HTTP scan. Shortcut for --sslv2 --sslv3 --tlsv1 '
'--reneg --resum --certinfo=basic --http_get '
'--hide_rejected_ciphers'),
dest=None)

return (parser, available_commands)
Expand All @@ -164,47 +177,29 @@ def parse_command_line(parser):
setattr(args_command_list, 'reneg', True)
setattr(args_command_list, 'resum', True)
setattr(args_command_list, 'certinfo', 'basic')

setattr(args_command_list, 'hide_rejected_ciphers', True)
setattr(args_command_list, 'http_get', True)

return (args_command_list, args_target_list)


def process_parsing_results(args_command_list):

#shared_mgr = Manager()
#shared_settings = shared_mgr.dict() # Will be sent to every plugin process.
# Don't really neeed a manager since shared_settings is read only.
shared_settings = dict()

shared_settings = {}
# Sanity checks on the client cert options
if bool(args_command_list.cert) ^ bool(args_command_list.key):
print PARSING_ERROR_FORMAT.format(
'No private key or certificate file were given! '
'No private key or certificate file were given. '
'See --client_cert and --client_key.')
return
else:
shared_settings['cert'] = args_command_list.cert
shared_settings['key'] = args_command_list.key

# Parse client cert options
if args_command_list.certform in ['DER', 'PEM']:
shared_settings['certform'] = args_command_list.certform
else:
if args_command_list.certform not in ['DER', 'PEM']:
print PARSING_ERROR_FORMAT.format('--certform should be DER or PEM.')
return

if args_command_list.keyform in ['DER', 'PEM']:
shared_settings['keyform'] = args_command_list.keyform
else:
if args_command_list.keyform not in ['DER', 'PEM']:
print PARSING_ERROR_FORMAT.format('--keyform should be DER or PEM.')
return

if args_command_list.keypass:
shared_settings['keypass'] = args_command_list.keypass
else:
shared_settings['keypass'] = None

# Timeout
shared_settings['timeout'] = args_command_list.timeout

# HTTP CONNECT proxy
if args_command_list.https_tunnel:
Expand All @@ -224,22 +219,23 @@ def process_parsing_results(args_command_list):
', discarding all tasks.')
return
else:
pass
shared_settings['https_tunnel_host'] = None
shared_settings['https_tunnel_port'] = None

# STARTTLS
if args_command_list.starttls not in [None,'smtp','xmpp']:
print PARSING_ERROR_FORMAT.format('--starttls should be \'smtp\' or \'xmpp\'.')
return
else:
shared_settings['starttls'] = args_command_list.starttls
shared_settings['xmpp_to'] = args_command_list.xmpp_to

if args_command_list.starttls and args_command_list.https_tunnel:
print PARSING_ERROR_FORMAT.format(
'Cannot have --https_tunnel and --starttls at the same time.')
return


# All good, let's save the data
for key, value in args_command_list.__dict__.iteritems():
shared_settings[key] = value

return shared_settings

48 changes: 12 additions & 36 deletions plugins/PluginBase.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,23 @@ def __init__(self, title, description):
self.title = title
self.description = description
self.options = []
self.commands = []

def add_option(self, command, help, dest):
def add_option(self, option, help, dest):
"""
Options are settings specific to one single plugin.
They are sent to PluginBase._shared_settings.
"""
self.options.append( (option, help, dest) )

def add_command(self, command, help, dest):
"""
Commands are actions/scans the plugin implements, with
PluginXXX.process_task().
Command and help are sent to optparse.OptionGroup.add_option().
Note: dest to None if you don't need arguments
"""
self.options.append( (command, help, dest) )
self.commands.append( (command, help, dest) )


class PluginBase(object):
Expand Down Expand Up @@ -142,38 +152,4 @@ def _create_ssl_connection(self_class, target, ssl=None, ssl_ctx=None):
ssl_connection.ssl_ctx.check_private_key()

return ssl_connection


@classmethod
def _check_ssl_connection_is_alive(self_class, ssl_connection):
"""
Check if the SSL connection is still alive after the handshake.
Will send an HTTP GET for an HTTPS connection.
Will send a NOOP for an SMTP connection.
"""
shared_settings = PluginBase._shared_settings
result = 'N/A'
if shared_settings['starttls'] == 'smtp':
try:
ssl_connection.sock.send('NOOP\r\n')
result = ssl_connection.sock.read(2048).strip()
except socket.timeout:
result = 'Timeout on SMTP NOOP'
elif shared_settings['starttls'] == 'xmpp':
result = 'OK'
else:
try:
# Send an HTTP GET to the server and store the HTTP Status Code
ssl_connection.request("GET", "/", headers={"Connection": "close"})
http_response = ssl_connection.getresponse()
result = 'HTTP ' \
+ str(http_response.status) \
+ ' ' \
+ str(http_response.reason)
except socket.timeout:
result = 'Timeout on HTTP GET'


return result


2 changes: 1 addition & 1 deletion plugins/PluginCertInfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ class PluginCertInfo(PluginBase.PluginBase):
"Verifies the target server's certificate validity against "
"Mozilla's trusted root store, and prints relevant fields of "
"the certificate."))
available_commands.add_option(
available_commands.add_command(
command="certinfo",
help="Should be one of: 'basic', 'full', 'serial', 'cn', 'keysize'.",
dest="certinfo")
Expand Down
2 changes: 1 addition & 1 deletion plugins/PluginEmpty.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class PluginEmpty(PluginBase.PluginBase):
description=(
"PluginEmpty is a sample plugin that does not implement "
"any actual tests. It's designed to show how plugins are written."))
available_commands.add_option(command="empty", help="Do nothing", dest=None)
available_commands.add_command(command="empty", help="Do nothing", dest=None)


def process_task(self, target, command, args):
Expand Down
69 changes: 62 additions & 7 deletions plugins/PluginOpenSSLCipherSuites.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,38 +28,48 @@
ctSSL_cleanup
from utils.CtSSLHelper import SSLHandshakeRejected

import socket

class PluginOpenSSLCipherSuites(PluginBase.PluginBase):


available_commands = PluginBase.AvailableCommands(
"PluginOpenSSLCipherSuites",
"Scans the target server for supported OpenSSL cipher suites.")
available_commands.add_option(
available_commands.add_command(
command="sslv2",
help="Lists the SSL 2.0 OpenSSL cipher suites supported by the server.",
dest=None)
available_commands.add_option(
available_commands.add_command(
command="sslv3",
help="Lists the SSL 3.0 OpenSSL cipher suites supported by the server.",
dest=None)
available_commands.add_option(
available_commands.add_command(
command="tlsv1",
help="Lists the TLS 1.0 OpenSSL cipher suites supported by the server.",
dest=None)
available_commands.add_option(
available_commands.add_command(
command="tlsv1_1",
help="Lists the TLS 1.1 OpenSSL cipher suites supported by the server.",
dest=None)
available_commands.add_option(
available_commands.add_command(
command="tlsv1_2",
help="Lists the TLS 1.2 OpenSSL cipher suites supported by the server.",
dest=None)

available_commands.add_option(
option='http_get',
help="Option - For each cipher suite, sends an HTTP GET request after "
"completing the SSL handshake and returns the HTTP status code.",
dest=None)
available_commands.add_option(
option='hide_rejected_ciphers',
help="Option - Hides the list of cipher suites that were rejected by "
"the server.",
dest=None)

def process_task(self, target, command, args):

MAX_THREADS = 50
MAX_THREADS = 30

if command in ['sslv2', 'sslv3', 'tlsv1', 'tlsv1_1', 'tlsv1_2']:
ssl_version = command
Expand Down Expand Up @@ -108,6 +118,15 @@ def process_task(self, target, command, args):
formatted_results = [
(' * {0} Cipher Suites :'.format(ssl_version.upper()))]

if self._shared_settings['hide_rejected_ciphers']:
# Do not display rejected cipher suites
possible_results = ['Preferred','Accepted', 'Errors']
if len(test_ciphers_results['Rejected']) == len(cipher_list):
possible_results = []
formatted_results = [
(' * {0} Cipher Suites : None'.format(ssl_version.upper()))]


# Print each dictionnary of results
for result_type in possible_results:
if len(test_ciphers_results[result_type]) != 0:
Expand Down Expand Up @@ -228,3 +247,39 @@ def _pref_ciphersuite(self, target, ssl_version):
ssl_connect.close()

return


def _check_ssl_connection_is_alive(self, ssl_connection):
"""
Check if the SSL connection is still alive after the handshake.
Will send an HTTP GET for an HTTPS connection.
Will send a NOOP for an SMTP connection.
"""
shared_settings = self._shared_settings
result = 'N/A'

if shared_settings['starttls'] == 'smtp':
try:
ssl_connection.sock.send('NOOP\r\n')
result = ssl_connection.sock.read(2048).strip()
except socket.timeout:
result = 'Timeout on SMTP NOOP'

elif shared_settings['starttls'] == 'xmpp':
result = ''

elif shared_settings['http_get']:
try: # Send an HTTP GET to the server and store the HTTP Status Code
ssl_connection.request("GET", "/", headers={"Connection": "close"})
http_response = ssl_connection.getresponse()
result = 'HTTP ' \
+ str(http_response.status) \
+ ' ' \
+ str(http_response.reason)
except socket.timeout:
result = 'Timeout on HTTP GET'

else:
result = ''

return result
2 changes: 1 addition & 1 deletion plugins/PluginSessionRenegotiation.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class PluginSessionRenegotiation(PluginBase.PluginBase):
available_commands = PluginBase.AvailableCommands(
title="PluginSessionRenegotiation",
description="Tests the target server for insecure renegotiation.")
available_commands.add_option(
available_commands.add_command(
command="reneg",
help=(
"Tests the target server's support for client-initiated "
Expand Down
4 changes: 2 additions & 2 deletions plugins/PluginSessionResumption.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,13 @@ class PluginSessionResumption(PluginBase.PluginBase):
description=(
"Analyzes the target server's SSL session "
"resumption capabilities."))
available_commands.add_option(
available_commands.add_command(
command="resum",
help=(
"Tests the server for session ressumption support, using "
"session IDs and TLS session tickets (RFC 5077)."),
dest=None)
available_commands.add_option(
available_commands.add_command(
command="resum_rate",
help=(
"Performs 100 session resumptions with the target server, "
Expand Down
2 changes: 1 addition & 1 deletion sslyze.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@


PROG_VERSION = 'SSLyze v0.4 beta'
NB_PROCESSES = 10 # Should be controlled by the user
NB_PROCESSES = 5 # 10 was too aggressive, lowering it to 5
PLUGIN_PATH = "plugins"
DEFAULT_TIMEOUT = 5

Expand Down

0 comments on commit 1edcf19

Please sign in to comment.