#!/usr/bin/env python
# sslmap.py - enumerate all supported TLS/SSL cipher suites from a target host
# Usage: sslmap.py --host gmail.com --port 443 --allknown
#        sslmap.py -h
# NOTE:  Run with different handshake types (--ssl2 and --tls) to enumerate
#        all of the available cipher suites.
#
# author: iphelix
import socket,binascii,string,sys
from optparse import OptionParser

#Standard TLS/SSL handshake with the following parameters.
handshake_tls =  '\x80\x2c\x01\x03\x01\x00\x03\x00\x00\x00\x20'
handshake_ssl3 = '\x80\x2c\x01\x03\x00\x00\x03\x00\x00\x00\x20'
handshake_ssl2 = '\x80\x2c\x01\x00\x02\x00\x03\x00\x00\x00\x20'

#TLS handshake is used by default
handshake = handshake_tls

#NULL handshake challenge string
challenge = '\x00' * 32
	
#Taken from wireshark/epan/dissectors/packet-ssl-utils.c
cipher_suites_name = {
"010080": "SSL2_RC4_128_WITH_MD5",
"020080": "SSL2_RC4_128_EXPORT40_WITH_MD5",
"030080": "SSL2_RC2_CBC_128_CBC_WITH_MD5",
"040080": "SSL2_RC2_CBC_128_CBC_WITH_MD5",
"050080": "SSL2_IDEA_128_CBC_WITH_MD5",
"060040": "SSL2_DES_64_CBC_WITH_MD5",
"0700c0": "SSL2_DES_192_EDE3_CBC_WITH_MD5",
"080080": "SSL2_RC4_64_WITH_MD5",
"000000": "TLS_NULL_WITH_NULL_NULL",
"000001": "TLS_RSA_WITH_NULL_MD5",
"000002": "TLS_RSA_WITH_NULL_SHA",
"000003": "TLS_RSA_EXPORT_WITH_RC4_40_MD5",
"000004": "TLS_RSA_WITH_RC4_128_MD5",
"000005": "TLS_RSA_WITH_RC4_128_SHA",
"000006": "TLS_RSA_EXPORT_WITH_RC2_CBC_40_MD5",
"000007": "TLS_RSA_WITH_IDEA_CBC_SHA",
"000008": "TLS_RSA_EXPORT_WITH_DES40_CBC_SHA",
"000009": "TLS_RSA_WITH_DES_CBC_SHA",
"00000a": "TLS_RSA_WITH_3DES_EDE_CBC_SHA",
"00000b": "TLS_DH_DSS_EXPORT_WITH_DES40_CBC_SHA",
"00000c": "TLS_DH_DSS_WITH_DES_CBC_SHA",
"00000d": "TLS_DH_DSS_WITH_3DES_EDE_CBC_SHA",
"00000e": "TLS_DH_RSA_EXPORT_WITH_DES40_CBC_SHA",
"00000f": "TLS_DH_RSA_WITH_DES_CBC_SHA",
"000010": "TLS_DH_RSA_WITH_3DES_EDE_CBC_SHA",
"000011": "TLS_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA",
"000012": "TLS_DHE_DSS_WITH_DES_CBC_SHA",
"000013": "TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA",
"000014": "TLS_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA",
"000015": "TLS_DHE_RSA_WITH_DES_CBC_SHA",
"000016": "TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA",
"000017": "TLS_DH_anon_EXPORT_WITH_RC4_40_MD5",
"000018": "TLS_DH_anon_WITH_RC4_128_MD5",
"000019": "TLS_DH_anon_EXPORT_WITH_DES40_CBC_SHA",
"00001a": "TLS_DH_anon_WITH_DES_CBC_SHA",
"00001b": "TLS_DH_anon_WITH_3DES_EDE_CBC_SHA",
"00001c": "SSL_FORTEZZA_KEA_WITH_NULL_SHA",
"00001d": "SSL_FORTEZZA_KEA_WITH_FORTEZZA_CBC_SHA",
"00001e": "SSL_FORTEZZA_KEA_WITH_RC4_128_SHA",
"00002f": "TLS_RSA_WITH_AES_128_CBC_SHA",
"000030": "TLS_DH_DSS_WITH_AES_128_CBC_SHA",
"000031": "TLS_DH_RSA_WITH_AES_128_CBC_SHA",
"000032": "TLS_DHE_DSS_WITH_AES_128_CBC_SHA",
"000033": "TLS_DHE_RSA_WITH_AES_128_CBC_SHA",
"000034": "TLS_DH_anon_WITH_AES_128_CBC_SHA",
"000035": "TLS_RSA_WITH_AES_256_CBC_SHA",
"000036": "TLS_DH_DSS_WITH_AES_256_CBC_SHA",
"000037": "TLS_DH_RSA_WITH_AES_256_CBC_SHA",
"000038": "TLS_DHE_DSS_WITH_AES_256_CBC_SHA",
"000039": "TLS_DHE_RSA_WITH_AES_256_CBC_SHA",
"00003a": "TLS_DH_anon_WITH_AES_256_CBC_SHA",
"000041": "TLS_RSA_WITH_CAMELLIA_128_CBC_SHA",
"000042": "TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA",
"000043": "TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA",
"000044": "TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA",
"000045": "TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA",
"000046": "TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA",
"000047": "TLS_ECDH_ECDSA_WITH_NULL_SHA",
"000048": "TLS_ECDH_ECDSA_WITH_RC4_128_SHA",
"000049": "TLS_ECDH_ECDSA_WITH_DES_CBC_SHA",
"00004a": "TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA",
"00004b": "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA",
"00004c": "TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA",
"000060": "TLS_RSA_EXPORT1024_WITH_RC4_56_MD5",
"000061": "TLS_RSA_EXPORT1024_WITH_RC2_CBC_56_MD5",
"000062": "TLS_RSA_EXPORT1024_WITH_DES_CBC_SHA",
"000063": "TLS_DHE_DSS_EXPORT1024_WITH_DES_CBC_SHA",
"000064": "TLS_RSA_EXPORT1024_WITH_RC4_56_SHA",
"000065": "TLS_DHE_DSS_EXPORT1024_WITH_RC4_56_SHA",
"000066": "TLS_DHE_DSS_WITH_RC4_128_SHA",
"000084": "TLS_RSA_WITH_CAMELLIA_256_CBC_SHA",
"000085": "TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA",
"000086": "TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA",
"000087": "TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA",
"000088": "TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA",
"000089": "TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA",
"00fefe": "SSL_RSA_FIPS_WITH_DES_CBC_SHA",
"00feff": "SSL_RSA_FIPS_WITH_3DES_EDE_CBC_SHA",
"00ffe0": "SSL_RSA_FIPS_WITH_3DES_EDE_CBC_SHA",
"00ffe1": "SSL_RSA_FIPS_WITH_DES_CBC_SHA",
"8f8001": "PCT_SSL_COMPAT | PCT_VERSION_1",
"800003": "PCT_SSL_CERT_TYPE | PCT1_CERT_X509_CHAIN",
"800001": "PCT_SSL_CERT_TYPE | PCT1_CERT_X509",
"810001": "PCT_SSL_HASH_TYPE | PCT1_HASH_MD5",
"810003": "PCT_SSL_HASH_TYPE | PCT1_HASH_SHA",
"820001": "PCT_SSL_EXCH_TYPE | PCT1_EXCH_RSA_PKCS1",
"830004": "PCT_SSL_CIPHER_TYPE_1ST_HALF | PCT1_CIPHER_RC4",
"848040": "PCT_SSL_CIPHER_TYPE_2ND_HALF | PCT1_ENC_BITS_128 | PCT1_MAC_BITS_128",
"842840": "PCT_SSL_CIPHER_TYPE_2ND_HALF | PCT1_ENC_BITS_40 | PCT1_MAC_BITS_128",
}

# Based on recommendations from OpenSSL ciphers man page and a Foundstone SSL paper
cipher_suites_classification = {
"010080": "MEDIUM",
"020080": "EXPORT",
"030080": "MEDIUM",
"040080": "MEDIUM",
"050080": "MEDIUM",
"060040": "LOW",
"0700c0": "HIGH",
"080080": "LOW",
"000000": "NULL",
"000001": "NULL",
"000002": "NULL",
"000003": "EXPORT",
"000004": "MEDIUM",
"000005": "MEDIUM",
"000006": "EXPORT",
"000007": "MEDIUM",
"000008": "EXPORT",
"000009": "MEDIUM",
"00000a": "HIGH",
"00000b": "EXPORT",
"00000c": "MEDIUM",
"00000d": "HIGH",
"00000e": "EXPORT",
"00000f": "MEDIUM",
"000010": "HIGH",
"000011": "EXPORT",
"000012": "MEDIUM",
"000013": "HIGH",
"000014": "EXPORT",
"000015": "MEDIUM",
"000016": "HIGH",
"000017": "ANON",
"000018": "ANON",
"000019": "ANON",
"00001a": "ANON",
"00001b": "ANON",
"00001c": "NULL",
"00001d": "MEDIUM",
"00001e": "MEDIUM",
"00002f": "HIGH",
"000030": "HIGH",
"000031": "HIGH",
"000032": "HIGH",
"000033": "HIGH",
"000034": "ANON",
"000035": "HIGH",
"000036": "HIGH",
"000037": "HIGH",
"000038": "HIGH",
"000039": "HIGH",
"00003a": "ANON",
"000041": "HIGH",
"000042": "HIGH",
"000043": "HIGH",
"000044": "HIGH",
"000045": "HIGH",
"000046": "ANON",
"000047": "NULL",
"000048": "MEDIUM",
"000049": "LOW",
"00004a": "HIGH",
"00004b": "TLS_ECDH_ECDSA",
"00004c": "TLS_ECDH_ECDSA",
"000060": "EXPORT",
"000061": "EXPORT",
"000062": "EXPORT",
"000063": "EXPORT",
"000064": "EXPORT",
"000065": "EXPORT",
"000066": "MEDIUM",
"000084": "HIGH",
"000085": "HIGH",
"000086": "HIGH",
"000087": "HIGH",
"000088": "HIGH",
"000089": "ANON",
"00fefe": "LOW",
"00feff": "HIGH",
"00ffe0": "HIGH",
"00ffe1": "LOW",
"8f8001": "PCT",
"800003": "PCT",
"800001": "PCT",
"810001": "PCT",
"810003": "PCT",
"820001": "PCT",
"830004": "PCT",
"848040": "PCT",
"842840": "PCT",
}

def check_cipher(cipher_id, host, port):
	cipher = binascii.unhexlify(cipher_id)
	
	s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
	
	try:   s.connect((host, port))		
	except socket.error, msg:
		print "[!] Could not connect to target host: %s" % msg
		s.close()
		sys.exit()
	
	s.send(handshake+cipher+challenge)
	
	try:	data = s.recv(1)
	except socket.error, msg:
		print "[!] Could not receive response from our request: %s" %msg
		s.close()
		sys.exit()
		
	state = False
	
	#TLS/SSLv3 Server Hello
	if data == '\x16':   state = True   # Server Hello Code
	elif data == '\x15': state =  False # Server Alert Code
	
	#SSLv2 Server Hello
	else:
		data = s.recv(8)
		data = s.recv(2)
		if data == '\x00\x03': state = True # Server Matching Cipher Length
		else: state = False
				
	s.close()
	return state

def print_cipher(cipher_id):
	if cipher_suites_name.has_key(cipher_id): 
		print "\t%s\t- %s (0x%s)" % (cipher_suites_classification[cipher_id], cipher_suites_name[cipher_id], cipher_id)
	else: 
		print "\tUNKNOWN\t- UNKNOWN (0x%s)\t%s" % (cipher_id, cipher_suites_classification[cipher_id])
	
def scan_fuzz_ciphers(host,port):
	print "[*] Fuzzing %s:%d for all possible cipher suite identifiers" % (host, port)
	for i in range(0,16777215):
		cipher_id = '%06x' % i
		if check_cipher(cipher_id,host,port): print_cipher(cipher_id)

def scan_known_ciphers(host,port):
	print "[*] Scanning %s:%d for known cipher suites" % (host,port)
	for cipher_id in cipher_suites_name.keys():
		if check_cipher(cipher_id,host,port): print_cipher(cipher_id)

if __name__ == '__main__':
	# Parse scan parameters
	parser = OptionParser()
	parser.add_option("--host", dest="host", help="target host", default = "gmail.com",  metavar="gmail.com")
	parser.add_option("--port", dest="port", help="target port", default = 443,          metavar="443")
	parser.add_option("--allknown",   action="store_true", dest="allknown", default=False, help="scan all known cipher suites (quick)")
	parser.add_option("--fuzz",       action="store_true", dest="fuzz",     default=False, help="fuzz all possible cipher values (lengthy)")
	parser.add_option("--tls",        action="store_true", dest="tls",      default=False, help="use TLS handshake (default)")
	parser.add_option("--ssl3",       action="store_true", dest="ssl3",     default=False, help="use SSL3 handshake")
	parser.add_option("--ssl2",       action="store_true", dest="ssl2",     default=False, help="use SSL2 handshake (for SSLv2 ciphers)")
	(options, args) = parser.parse_args()
	
	# Perform checks on user input
	if not options.host: parser.error("No host was specified. Please use --host flag")
	else: HOST = options.host
		
	if not (options.allknown or options.fuzz): parser.error("No scan method was specified. Please use --allknown flag")

	# Run scans
	if options.tls:  handshake = handshake_tls
	if options.ssl3: handshake = handshake_ssl3
	if options.ssl2: handshake = handshake_ssl2
	
	if options.allknown: scan_known_ciphers(options.host, int(options.port))		
	if options.fuzz: scan_fuzz_ciphers(options.host, int(options.port))
