mirror of
				https://github.com/jtesta/ssh-audit.git
				synced 2025-10-30 21:15:27 +01:00 
			
		
		
		
	Added RSA certificate auditing.
This commit is contained in:
		
							
								
								
									
										323
									
								
								ssh-audit.py
									
									
									
									
									
								
							
							
						
						
									
										323
									
								
								ssh-audit.py
									
									
									
									
									
								
							| @@ -462,14 +462,14 @@ class SSH2(object):  # pylint: disable=too-few-public-methods | |||||||
| 			# type: () -> int | 			# type: () -> int | ||||||
| 			return self.__unused | 			return self.__unused | ||||||
|  |  | ||||||
| 		def set_rsa_hostkey_size(self, rsa_type, rsa_hostkey_size): | 		def set_rsa_key_size(self, rsa_type, hostkey_size, ca_size=-1): | ||||||
| 			self.__rsa_key_sizes[rsa_type] = rsa_hostkey_size; | 			self.__rsa_key_sizes[rsa_type] = (hostkey_size, ca_size) | ||||||
|  |  | ||||||
| 		def rsa_hostkey_sizes(self): | 		def rsa_key_sizes(self): | ||||||
| 			return self.__rsa_key_sizes | 			return self.__rsa_key_sizes | ||||||
|  |  | ||||||
| 		def set_dh_modulus_size(self, gex_alg, modulus_size): | 		def set_dh_modulus_size(self, gex_alg, modulus_size): | ||||||
| 			self.__dh_modulus_sizes[gex_alg] = modulus_size | 			self.__dh_modulus_sizes[gex_alg] = (modulus_size, -1) | ||||||
|  |  | ||||||
| 		def dh_modulus_sizes(self): | 		def dh_modulus_sizes(self): | ||||||
| 			return self.__dh_modulus_sizes | 			return self.__dh_modulus_sizes | ||||||
| @@ -522,9 +522,10 @@ class SSH2(object):  # pylint: disable=too-few-public-methods | |||||||
| 	# Obtains RSA host keys and checks their size. | 	# Obtains RSA host keys and checks their size. | ||||||
| 	class RSAKeyTest(object): | 	class RSAKeyTest(object): | ||||||
| 		RSA_TYPES = ['ssh-rsa', 'rsa-sha2-256', 'rsa-sha2-512'] | 		RSA_TYPES = ['ssh-rsa', 'rsa-sha2-256', 'rsa-sha2-512'] | ||||||
|  | 		RSA_CA_TYPES = ['ssh-rsa-cert-v01@openssh.com'] | ||||||
|  |  | ||||||
| 		@staticmethod | 		@staticmethod | ||||||
| 		def run(s, kex): | 		def run(s, server_kex): | ||||||
| 			KEX_TO_DHGROUP = { | 			KEX_TO_DHGROUP = { | ||||||
| 				'diffie-hellman-group1-sha1': KexGroup1, | 				'diffie-hellman-group1-sha1': KexGroup1, | ||||||
| 				'diffie-hellman-group14-sha1': KexGroup14_SHA1, | 				'diffie-hellman-group14-sha1': KexGroup14_SHA1, | ||||||
| @@ -543,25 +544,47 @@ class SSH2(object):  # pylint: disable=too-few-public-methods | |||||||
|  |  | ||||||
| 			# Pick the first kex algorithm that the server supports, which we | 			# Pick the first kex algorithm that the server supports, which we | ||||||
| 			# happen to support as well. | 			# happen to support as well. | ||||||
| 			selected_kex_str = None | 			kex_str = None | ||||||
| 			kex_group = None | 			kex_group = None | ||||||
| 			for server_kex_alg in kex.kex_algorithms: | 			for server_kex_alg in server_kex.kex_algorithms: | ||||||
| 				if server_kex_alg in KEX_TO_DHGROUP: | 				if server_kex_alg in KEX_TO_DHGROUP: | ||||||
| 					selected_kex_str = server_kex_alg | 					kex_str = server_kex_alg | ||||||
| 					kex_group = KEX_TO_DHGROUP[server_kex_alg]() | 					kex_group = KEX_TO_DHGROUP[kex_str]() | ||||||
| 					break | 					break | ||||||
|  |  | ||||||
|  | 			if kex_str is not None: | ||||||
|  | 				SSH2.RSAKeyTest.__test(s, server_kex, kex_str, kex_group, SSH2.RSAKeyTest.RSA_TYPES) | ||||||
|  | 				SSH2.RSAKeyTest.__test(s, server_kex, kex_str, kex_group, SSH2.RSAKeyTest.RSA_CA_TYPES, ca=True) | ||||||
|  |  | ||||||
|  | 		@staticmethod | ||||||
|  | 		def __test(s, server_kex, kex_str, kex_group, rsa_types, ca=False): | ||||||
| 			# If the server supports one of the RSA types, extract its key size. | 			# If the server supports one of the RSA types, extract its key size. | ||||||
| 			modulus_size = 0 | 			hostkey_modulus_size = 0 | ||||||
| 			if selected_kex_str is not None: | 			ca_modulus_size = 0 | ||||||
| 				for rsa_type in SSH2.RSAKeyTest.RSA_TYPES: | 			ran_test = False | ||||||
| 					if rsa_type in kex.key_algorithms: |  | ||||||
|  | 			# If the connection is closed, re-open it and get the kex again. | ||||||
|  | 			if not s.is_connected(): | ||||||
|  | 				s.connect() | ||||||
|  | 				unused1, unused2, err = s.get_banner() | ||||||
|  | 				if err is not None: | ||||||
|  | 					s.close() | ||||||
|  | 					return | ||||||
|  |  | ||||||
|  | 				# Parse the server's initial KEX. | ||||||
|  | 				packet_type, payload = s.read_packet() | ||||||
|  | 				SSH2.Kex.parse(payload) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 			for rsa_type in rsa_types: | ||||||
|  | 				if rsa_type in server_kex.key_algorithms: | ||||||
|  | 					ran_test = True | ||||||
|  |  | ||||||
| 					# Send the server our KEXINIT message, using only our | 					# Send the server our KEXINIT message, using only our | ||||||
| 					# selected kex and RSA type.  Send the server's own | 					# selected kex and RSA type.  Send the server's own | ||||||
| 					# list of ciphers and MACs back to it (this doesn't | 					# list of ciphers and MACs back to it (this doesn't | ||||||
| 					# matter, really). | 					# matter, really). | ||||||
| 						client_kex = SSH2.Kex(os.urandom(16), [selected_kex_str], [rsa_type], kex.client, kex.server, 0, 0) | 					client_kex = SSH2.Kex(os.urandom(16), [kex_str], [rsa_type], server_kex.client, server_kex.server, 0, 0) | ||||||
|  |  | ||||||
| 					s.write_byte(SSH.Protocol.MSG_KEXINIT) | 					s.write_byte(SSH.Protocol.MSG_KEXINIT) | ||||||
| 					client_kex.write(s) | 					client_kex.write(s) | ||||||
| @@ -571,29 +594,47 @@ class SSH2(object):  # pylint: disable=too-few-public-methods | |||||||
| 					# with the host key and its length.  Bingo. | 					# with the host key and its length.  Bingo. | ||||||
| 					kex_group.send_init(s) | 					kex_group.send_init(s) | ||||||
| 					kex_group.recv_reply(s) | 					kex_group.recv_reply(s) | ||||||
| 						modulus_size = kex_group.get_hostkey_size() |  | ||||||
|  |  | ||||||
| 						# We only need to test one RSA type, since the others | 					hostkey_modulus_size = kex_group.get_hostkey_size() | ||||||
| 						# will all be the same. | 					ca_modulus_size = kex_group.get_ca_size() | ||||||
|  |  | ||||||
|  | 					# If we're not working with the CA types, we only need to | ||||||
|  | 					# test one RSA key, since the others will all be the same. | ||||||
|  | 					if ca is False: | ||||||
| 						break | 						break | ||||||
|  |  | ||||||
| 			if modulus_size > 0: | 			if hostkey_modulus_size > 0 or ca_modulus_size > 0: | ||||||
|  |  | ||||||
| 				# Set the hostkey size for all RSA key types since 'ssh-rsa', | 				# Set the hostkey size for all RSA key types since 'ssh-rsa', | ||||||
| 				# 'rsa-sha2-256', etc. are all using the same host key. | 				# 'rsa-sha2-256', etc. are all using the same host key. | ||||||
| 				for rsa_type in SSH2.RSAKeyTest.RSA_TYPES: | 				# Note, however, that this may change in the future. | ||||||
| 					kex.set_rsa_hostkey_size(rsa_type, modulus_size) | 				if ca is False: | ||||||
|  | 					for rsa_type in rsa_types: | ||||||
|  | 						server_kex.set_rsa_key_size(rsa_type, hostkey_modulus_size) | ||||||
|  | 				else: | ||||||
|  | 					server_kex.set_rsa_key_size(rsa_type, hostkey_modulus_size, ca_modulus_size) | ||||||
|  |  | ||||||
| 				# Keys smaller than 2048 result in a failure. | 				# Keys smaller than 2048 result in a failure. | ||||||
| 				fail = False | 				fail = False | ||||||
| 				if modulus_size < 2048: | 				if hostkey_modulus_size < 2048 or (ca_modulus_size < 2048 and ca_modulus_size > 0): | ||||||
| 					fail = True | 					fail = True | ||||||
|  |  | ||||||
| 				# If this is a bad key size, update the database accordingly. | 				# If this is a bad key size, update the database accordingly. | ||||||
| 				if fail: | 				if fail: | ||||||
|  | 					if ca is False: | ||||||
| 						for rsa_type in SSH2.RSAKeyTest.RSA_TYPES: | 						for rsa_type in SSH2.RSAKeyTest.RSA_TYPES: | ||||||
| 							alg_list = SSH2.KexDB.ALGORITHMS['key'][rsa_type] | 							alg_list = SSH2.KexDB.ALGORITHMS['key'][rsa_type] | ||||||
| 						alg_list.append(['using small %d-bit modulus' % modulus_size]) | 							alg_list.append(['using small %d-bit modulus' % hostkey_modulus_size]) | ||||||
|  | 					else: | ||||||
|  | 						alg_list = SSH2.KexDB.ALGORITHMS['key'][rsa_type] | ||||||
|  |  | ||||||
|  | 						min_modulus = min(hostkey_modulus_size, ca_modulus_size) | ||||||
|  | 						min_modulus = min_modulus if min_modulus > 0 else max(hostkey_modulus_size, ca_modulus_size) | ||||||
|  | 						alg_list.append(['using small %d-bit modulus' % min_modulus]) | ||||||
|  |  | ||||||
|  | 			# If we ran any tests, close the socket, as the connection has | ||||||
|  | 			# been put in a state that later tests can't use. | ||||||
|  | 			if ran_test: | ||||||
|  | 				s.close() | ||||||
|  |  | ||||||
| 	# Performs DH group exchanges to find what moduli are supported, and checks | 	# Performs DH group exchanges to find what moduli are supported, and checks | ||||||
| 	# their size. | 	# their size. | ||||||
| @@ -602,13 +643,15 @@ class SSH2(object):  # pylint: disable=too-few-public-methods | |||||||
| 		# Creates a new connection to the server.  Returns an SSH.Socket, or | 		# Creates a new connection to the server.  Returns an SSH.Socket, or | ||||||
| 		# None on failure. | 		# None on failure. | ||||||
| 		@staticmethod | 		@staticmethod | ||||||
| 		def reconnect(ipvo, host, port, gex_alg): | 		def reconnect(s, gex_alg): | ||||||
| 			s = SSH.Socket(host, port) | 			if s.is_connected(): | ||||||
| 			s.connect(ipvo) | 				return | ||||||
|  |  | ||||||
|  | 			s.connect() | ||||||
| 			unused, unused, err = s.get_banner() | 			unused, unused, err = s.get_banner() | ||||||
| 			if err is not None: | 			if err is not None: | ||||||
| 				s.close() | 				s.close() | ||||||
| 				return None | 				return False | ||||||
|  |  | ||||||
| 			# Parse the server's initial KEX. | 			# Parse the server's initial KEX. | ||||||
| 			packet_type, payload = s.read_packet(2) | 			packet_type, payload = s.read_packet(2) | ||||||
| @@ -620,32 +663,31 @@ class SSH2(object):  # pylint: disable=too-few-public-methods | |||||||
| 			s.write_byte(SSH.Protocol.MSG_KEXINIT) | 			s.write_byte(SSH.Protocol.MSG_KEXINIT) | ||||||
| 			client_kex.write(s) | 			client_kex.write(s) | ||||||
| 			s.send_packet() | 			s.send_packet() | ||||||
| 			return s | 			return True | ||||||
|  |  | ||||||
| 		# Runs the DH moduli test against the specified target. | 		# Runs the DH moduli test against the specified target. | ||||||
| 		@staticmethod | 		@staticmethod | ||||||
| 		def run(ipvo, host, port, s, kex): | 		def run(s, kex): | ||||||
| 			GEX_ALGS = { | 			GEX_ALGS = { | ||||||
| 				'diffie-hellman-group-exchange-sha1': KexGroupExchange_SHA1, | 				'diffie-hellman-group-exchange-sha1': KexGroupExchange_SHA1, | ||||||
| 				'diffie-hellman-group-exchange-sha256': KexGroupExchange_SHA256, | 				'diffie-hellman-group-exchange-sha256': KexGroupExchange_SHA256, | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
|  | 			# The previous RSA tests put the server in a state we can't | ||||||
|  | 			# test.  So we need a new connection to start with a clean | ||||||
|  | 			# slate. | ||||||
|  | 			if s.is_connected(): | ||||||
|  | 				s.close() | ||||||
|  |  | ||||||
| 			# Check if the server supports any of the group-exchange | 			# Check if the server supports any of the group-exchange | ||||||
| 			# algorithms.  If so, test each one. | 			# algorithms.  If so, test each one. | ||||||
| 			for gex_alg in GEX_ALGS: | 			for gex_alg in GEX_ALGS: | ||||||
| 				if gex_alg in kex.kex_algorithms: | 				if gex_alg in kex.kex_algorithms: | ||||||
|  |  | ||||||
| 					# The previous RSA tests put the server in a state we can't | 					if SSH2.GEXTest.reconnect(s, gex_alg) is False: | ||||||
| 					# test.  So we need a new connection to start with a clean |  | ||||||
| 					# slate. |  | ||||||
| 					if s is not None: |  | ||||||
| 						s.close() |  | ||||||
| 						s = None |  | ||||||
|  |  | ||||||
| 					s = SSH2.GEXTest.reconnect(ipvo, host, port, gex_alg) |  | ||||||
| 					if s is None: |  | ||||||
| 						break | 						break | ||||||
|  |  | ||||||
|  |  | ||||||
| 					kex_group = GEX_ALGS[gex_alg]() | 					kex_group = GEX_ALGS[gex_alg]() | ||||||
| 					smallest_modulus = -1 | 					smallest_modulus = -1 | ||||||
|  |  | ||||||
| @@ -657,12 +699,11 @@ class SSH2(object):  # pylint: disable=too-few-public-methods | |||||||
| 						# Its been observed that servers will return a group | 						# Its been observed that servers will return a group | ||||||
| 						# larger than the requested max.  So just because we | 						# larger than the requested max.  So just because we | ||||||
| 						# got here, doesn't mean the server is vulnerable... | 						# got here, doesn't mean the server is vulnerable... | ||||||
| 						smallest_modulus = kex_group.get_modulus_size() | 						smallest_modulus = kex_group.get_dh_modulus_size() | ||||||
| 					except Exception as e: | 					except Exception as e: | ||||||
| 						pass | 						pass | ||||||
| 					finally: | 					finally: | ||||||
| 						s.close() | 						s.close() | ||||||
| 						s = None |  | ||||||
|  |  | ||||||
| 					# Try an array of specific modulus sizes... one at a time. | 					# Try an array of specific modulus sizes... one at a time. | ||||||
| 					reconnect_failed = False | 					reconnect_failed = False | ||||||
| @@ -673,21 +714,22 @@ class SSH2(object):  # pylint: disable=too-few-public-methods | |||||||
| 						if smallest_modulus > 0 and bits >= smallest_modulus: | 						if smallest_modulus > 0 and bits >= smallest_modulus: | ||||||
| 							break | 							break | ||||||
|  |  | ||||||
| 						if s is None: | 						if SSH2.GEXTest.reconnect(s, gex_alg) is False: | ||||||
| 							s = SSH2.GEXTest.reconnect(ipvo, host, port, gex_alg) |  | ||||||
| 							if s is None: |  | ||||||
| 							reconnect_failed = True | 							reconnect_failed = True | ||||||
| 							break | 							break | ||||||
|  |  | ||||||
| 						try: | 						try: | ||||||
| 							kex_group.send_init(s, bits, bits, bits) | 							kex_group.send_init(s, bits, bits, bits) | ||||||
| 							kex_group.recv_reply(s) | 							kex_group.recv_reply(s) | ||||||
| 							smallest_modulus = kex_group.get_modulus_size() | 							smallest_modulus = kex_group.get_dh_modulus_size() | ||||||
| 						except Exception as e: | 						except Exception as e: | ||||||
| 							pass | 							pass | ||||||
| 						finally: | 						finally: | ||||||
|  | 							# The server is in a state that is not re-testable, | ||||||
|  | 							# so there's nothing else to do with this open | ||||||
|  | 							# connection. | ||||||
| 							s.close() | 							s.close() | ||||||
| 							s = None |  | ||||||
|  |  | ||||||
| 					if smallest_modulus > 0: | 					if smallest_modulus > 0: | ||||||
| 						kex.set_dh_modulus_size(gex_alg, smallest_modulus) | 						kex.set_dh_modulus_size(gex_alg, smallest_modulus) | ||||||
| @@ -699,7 +741,7 @@ class SSH2(object):  # pylint: disable=too-few-public-methods | |||||||
| 							# For 'diffie-hellman-group-exchange-sha256', add | 							# For 'diffie-hellman-group-exchange-sha256', add | ||||||
| 							# a failure reason. | 							# a failure reason. | ||||||
| 							if len(lst) == 1: | 							if len(lst) == 1: | ||||||
| 								lst.append(text) | 								lst.append([text]) | ||||||
| 							# For 'diffie-hellman-group-exchange-sha1', delete | 							# For 'diffie-hellman-group-exchange-sha1', delete | ||||||
| 							# the existing failure reason (which is vague), and | 							# the existing failure reason (which is vague), and | ||||||
| 							# insert our own. | 							# insert our own. | ||||||
| @@ -975,6 +1017,10 @@ class ReadBuf(object): | |||||||
| 		# type: () -> text_type | 		# type: () -> text_type | ||||||
| 		return self._buf.readline().rstrip().decode('utf-8', 'replace') | 		return self._buf.readline().rstrip().decode('utf-8', 'replace') | ||||||
|  |  | ||||||
|  | 	def reset(self): | ||||||
|  | 		self._buf = BytesIO() | ||||||
|  | 		self._len = 0 | ||||||
|  | 		super(ReadBuf, self).reset() | ||||||
|  |  | ||||||
| class WriteBuf(object): | class WriteBuf(object): | ||||||
| 	def __init__(self, data=None): | 	def __init__(self, data=None): | ||||||
| @@ -1064,6 +1110,9 @@ class WriteBuf(object): | |||||||
| 		self._wbuf.seek(0) | 		self._wbuf.seek(0) | ||||||
| 		return payload | 		return payload | ||||||
|  |  | ||||||
|  | 	def reset(self): | ||||||
|  | 		self._wbuf = BytesIO() | ||||||
|  |  | ||||||
|  |  | ||||||
| class SSH(object):  # pylint: disable=too-few-public-methods | class SSH(object):  # pylint: disable=too-few-public-methods | ||||||
| 	class Protocol(object):  # pylint: disable=too-few-public-methods | 	class Protocol(object):  # pylint: disable=too-few-public-methods | ||||||
| @@ -1567,10 +1616,8 @@ class SSH(object):  # pylint: disable=too-few-public-methods | |||||||
| 				for alg_type, alg_list in alg_pair.items(): | 				for alg_type, alg_list in alg_pair.items(): | ||||||
| 					if alg_type == 'aut': | 					if alg_type == 'aut': | ||||||
| 						continue | 						continue | ||||||
| 					rec[sshv][alg_type] = {'add': {}, 'del': {}} | 					rec[sshv][alg_type] = {'add': {}, 'del': {}, 'chg': {}} | ||||||
| 					for n, alg_desc in alg_db[alg_type].items(): | 					for n, alg_desc in alg_db[alg_type].items(): | ||||||
| 						if alg_type == 'key' and '-cert-' in n: |  | ||||||
| 							continue |  | ||||||
| 						versions = alg_desc[0] | 						versions = alg_desc[0] | ||||||
| 						if len(versions) == 0 or versions[0] is None: | 						if len(versions) == 0 or versions[0] is None: | ||||||
| 							continue | 							continue | ||||||
| @@ -1597,18 +1644,19 @@ class SSH(object):  # pylint: disable=too-few-public-methods | |||||||
| 							if fc > 0: | 							if fc > 0: | ||||||
| 								faults += pow(10, 2 - i) * fc | 								faults += pow(10, 2 - i) * fc | ||||||
| 						if n not in alg_list: | 						if n not in alg_list: | ||||||
| 							if faults > 0: | 							if faults > 0 or (alg_type == 'key' and '-cert-' in n): | ||||||
| 								continue | 								continue | ||||||
| 							rec[sshv][alg_type]['add'][n] = 0 | 							rec[sshv][alg_type]['add'][n] = 0 | ||||||
| 						else: | 						else: | ||||||
| 							if faults == 0: | 							if faults == 0: | ||||||
| 								continue | 								continue | ||||||
| 							if n == 'diffie-hellman-group-exchange-sha256': | 							if n in ['diffie-hellman-group-exchange-sha256', 'ssh-rsa', 'rsa-sha2-256', 'rsa-sha2-512', 'ssh-rsa-cert-v01@openssh.com']: | ||||||
| 								if software.compare_version('7.3') < 0: | 								rec[sshv][alg_type]['chg'][n] = faults | ||||||
| 									continue | 							else: | ||||||
| 								rec[sshv][alg_type]['del'][n] = faults | 								rec[sshv][alg_type]['del'][n] = faults | ||||||
| 					add_count = len(rec[sshv][alg_type]['add']) | 					add_count = len(rec[sshv][alg_type]['add']) | ||||||
| 					del_count = len(rec[sshv][alg_type]['del']) | 					del_count = len(rec[sshv][alg_type]['del']) | ||||||
|  | 					chg_count = len(rec[sshv][alg_type]['chg']) | ||||||
| 					new_alg_count = len(alg_list) + add_count - del_count | 					new_alg_count = len(alg_list) + add_count - del_count | ||||||
| 					if new_alg_count < 1 and del_count > 0: | 					if new_alg_count < 1 and del_count > 0: | ||||||
| 						mf = min(rec[sshv][alg_type]['del'].values()) | 						mf = min(rec[sshv][alg_type]['del'].values()) | ||||||
| @@ -1626,6 +1674,8 @@ class SSH(object):  # pylint: disable=too-few-public-methods | |||||||
| 							del rec[sshv][alg_type]['add'] | 							del rec[sshv][alg_type]['add'] | ||||||
| 						if del_count == 0: | 						if del_count == 0: | ||||||
| 							del rec[sshv][alg_type]['del'] | 							del rec[sshv][alg_type]['del'] | ||||||
|  | 						if chg_count == 0: | ||||||
|  | 							del rec[sshv][alg_type]['chg'] | ||||||
| 						if len(rec[sshv][alg_type]) == 0: | 						if len(rec[sshv][alg_type]) == 0: | ||||||
| 							del rec[sshv][alg_type] | 							del rec[sshv][alg_type] | ||||||
| 				if len(rec[sshv]) == 0: | 				if len(rec[sshv]) == 0: | ||||||
| @@ -1770,6 +1820,7 @@ class SSH(object):  # pylint: disable=too-few-public-methods | |||||||
| 				raise ValueError('invalid port: {0}'.format(port)) | 				raise ValueError('invalid port: {0}'.format(port)) | ||||||
| 			self.__host = host | 			self.__host = host | ||||||
| 			self.__port = nport | 			self.__port = nport | ||||||
|  | 			self.__ipvo = () | ||||||
| 		 | 		 | ||||||
| 		def _resolve(self, ipvo): | 		def _resolve(self, ipvo): | ||||||
| 			# type: (Sequence[int]) -> Iterable[Tuple[int, Tuple[Any, ...]]] | 			# type: (Sequence[int]) -> Iterable[Tuple[int, Tuple[Any, ...]]] | ||||||
| @@ -1794,10 +1845,12 @@ class SSH(object):  # pylint: disable=too-few-public-methods | |||||||
| 				out.fail('[exception] {0}'.format(e)) | 				out.fail('[exception] {0}'.format(e)) | ||||||
| 				sys.exit(1) | 				sys.exit(1) | ||||||
| 		 | 		 | ||||||
| 		def connect(self, ipvo=(), cto=3.0, rto=5.0): | 		def connect(self, ipvo=None, cto=3.0, rto=5.0): | ||||||
| 			# type: (Sequence[int], float, float) -> None | 			# type: (Sequence[int], float, float) -> None | ||||||
| 			err = None | 			err = None | ||||||
| 			for af, addr in self._resolve(ipvo): | 			if ipvo is not None: | ||||||
|  | 				self.__ipvo = ipvo | ||||||
|  | 			for af, addr in self._resolve(self.__ipvo): | ||||||
| 				s = None | 				s = None | ||||||
| 				try: | 				try: | ||||||
| 					s = socket.socket(af, socket.SOCK_STREAM) | 					s = socket.socket(af, socket.SOCK_STREAM) | ||||||
| @@ -1956,8 +2009,19 @@ class SSH(object):  # pylint: disable=too-few-public-methods | |||||||
| 			data = struct.pack('>Ib', plen, padding) + payload + pad_bytes | 			data = struct.pack('>Ib', plen, padding) + payload + pad_bytes | ||||||
| 			return self.send(data) | 			return self.send(data) | ||||||
|  |  | ||||||
|  | 		# Returns True if this Socket is connected, otherwise False. | ||||||
|  | 		def is_connected(self): | ||||||
|  | 			return (self.__sock is not None) | ||||||
|  |  | ||||||
| 		def close(self): | 		def close(self): | ||||||
| 			self.__cleanup() | 			self.__cleanup() | ||||||
|  | 			self.reset() | ||||||
|  | 			self.__state = 0 | ||||||
|  | 			self.__header = [] | ||||||
|  | 			self.__banner = None | ||||||
|  |  | ||||||
|  | 		def reset(self): | ||||||
|  | 			super(SSH.Socket, self).reset() | ||||||
|  |  | ||||||
| 		def _close_socket(self, s): | 		def _close_socket(self, s): | ||||||
| 			# type: (Optional[socket.socket]) -> None | 			# type: (Optional[socket.socket]) -> None | ||||||
| @@ -1975,6 +2039,7 @@ class SSH(object):  # pylint: disable=too-few-public-methods | |||||||
| 		def __cleanup(self): | 		def __cleanup(self): | ||||||
| 			# type: () -> None | 			# type: () -> None | ||||||
| 			self._close_socket(self.__sock) | 			self._close_socket(self.__sock) | ||||||
|  | 			self.__sock = None | ||||||
|  |  | ||||||
|  |  | ||||||
| class KexDH(object):  # pragma: nocover | class KexDH(object):  # pragma: nocover | ||||||
| @@ -1988,7 +2053,8 @@ class KexDH(object):  # pragma: nocover | |||||||
| 		self.__hostkey_type = None | 		self.__hostkey_type = None | ||||||
| 		self.__hostkey_e = 0 | 		self.__hostkey_e = 0 | ||||||
| 		self.__hostkey_n = 0 | 		self.__hostkey_n = 0 | ||||||
| 		self.__hostkey_n_len = 0 # This is the length of the host key modulus. | 		self.__hostkey_n_len = 0 # Length of the host key modulus. | ||||||
|  | 		self.__ca_n_len = 0 # Length of the CA key modulus (if hostkey is a cert). | ||||||
| 		self.__f = 0 | 		self.__f = 0 | ||||||
| 		self.__h_sig = 0 | 		self.__h_sig = 0 | ||||||
|  |  | ||||||
| @@ -2017,47 +2083,97 @@ class KexDH(object):  # pragma: nocover | |||||||
| 			# TODO: change Exception to something more specific. | 			# TODO: change Exception to something more specific. | ||||||
| 			raise Exception('Expected MSG_KEXDH_REPLY (%d) or MSG_KEXDH_GEX_REPLY (%d), but got %d instead.' % (SSH.Protocol.MSG_KEXDH_REPLY, SSH.Protocol.MSG_KEXDH_GEX_REPLY, packet_type)) | 			raise Exception('Expected MSG_KEXDH_REPLY (%d) or MSG_KEXDH_GEX_REPLY (%d), but got %d instead.' % (SSH.Protocol.MSG_KEXDH_REPLY, SSH.Protocol.MSG_KEXDH_GEX_REPLY, packet_type)) | ||||||
|  |  | ||||||
| 		host_key_len = struct.unpack('>I', payload[0:4])[0] | 		# Get the host key blob, F, and signature. | ||||||
| 		ptr = 4 | 		ptr = 0 | ||||||
|  | 		hostkey, hostkey_len, ptr = KexDH.__get_bytes(payload, ptr) | ||||||
| 		hostkey = payload[ptr:ptr + host_key_len] | 		self.__f, f_len, ptr = KexDH.__get_bytes(payload, ptr) | ||||||
| 		ptr += host_key_len | 		self.__h_sig, h_sig_len, ptr = KexDH.__get_bytes(payload, ptr) | ||||||
|  |  | ||||||
| 		f_len = struct.unpack('>I', payload[ptr:ptr+4])[0] |  | ||||||
| 		ptr += 4 |  | ||||||
|  |  | ||||||
| 		self.__f = payload[ptr:ptr + f_len] |  | ||||||
| 		ptr += f_len |  | ||||||
|  |  | ||||||
| 		h_sig_len = struct.unpack('>I', payload[ptr:ptr+4])[0] |  | ||||||
| 		ptr += 4 |  | ||||||
|  |  | ||||||
| 		self.__h_sig = payload[ptr:ptr + h_sig_len] |  | ||||||
| 		ptr += h_sig_len |  | ||||||
|  |  | ||||||
| 		# Now pick apart the host key blob. | 		# Now pick apart the host key blob. | ||||||
| 		hostkey_type_len = struct.unpack('>I', hostkey[0:4])[0] |  | ||||||
| 		ptr = 4 |  | ||||||
|  |  | ||||||
| 		# Get the host key type (i.e.: 'ssh-rsa', 'ssh-ed25519', etc). | 		# Get the host key type (i.e.: 'ssh-rsa', 'ssh-ed25519', etc). | ||||||
| 		self.__hostkey_type = hostkey[ptr:ptr + hostkey_type_len] | 		ptr = 0 | ||||||
| 		ptr += hostkey_type_len | 		self.__hostkey_type, hostkey_type_len, ptr = KexDH.__get_bytes(hostkey, ptr) | ||||||
|  |  | ||||||
| 		hostkey_e_len = struct.unpack('>I', hostkey[ptr:ptr + 4])[0] | 		# If this is an RSA certificate, skip over the nonce. | ||||||
| 		ptr += 4 | 		if self.__hostkey_type.startswith('ssh-rsa-cert-v0'): | ||||||
|  | 			nonce, nonce_len, ptr = KexDH.__get_bytes(hostkey, ptr) | ||||||
|  |  | ||||||
| 		self.__hostkey_e = int(binascii.hexlify(hostkey[ptr:ptr + hostkey_e_len]), 16) | 		# The public key exponent. | ||||||
| 		ptr += hostkey_e_len | 		hostkey_e, hostkey_e_len, ptr = KexDH.__get_bytes(hostkey, ptr) | ||||||
|  | 		self.__hostkey_e = int(binascii.hexlify(hostkey_e), 16) | ||||||
|  |  | ||||||
| 		# Here is the modulus size & actual modulus of the host key public key. | 		# Here is the modulus size & actual modulus of the host key public key. | ||||||
| 		self.__hostkey_n_len = struct.unpack('>I', hostkey[ptr:ptr + 4])[0] | 		hostkey_n, self.__hostkey_n_len, ptr = KexDH.__get_bytes(hostkey, ptr) | ||||||
|  | 		self.__hostkey_n = int(binascii.hexlify(hostkey_n), 16) | ||||||
|  |  | ||||||
|  | 		# If this is an RSA certificate, continue parsing to extract the CA | ||||||
|  | 		# key. | ||||||
|  | 		if self.__hostkey_type.startswith('ssh-rsa-cert-v0'): | ||||||
|  | 			# Skip over the serial number. | ||||||
|  | 			ptr += 8 | ||||||
|  |  | ||||||
|  | 			# Get the certificate type. | ||||||
|  | 			cert_type = int(binascii.hexlify(hostkey[ptr:ptr + 4]), 16) | ||||||
| 			ptr += 4 | 			ptr += 4 | ||||||
| 		self.__hostkey_n = int(binascii.hexlify(hostkey[ptr:ptr + self.__hostkey_n_len]), 16) |  | ||||||
|  |  | ||||||
| 	# Returns the size of the hostkey, in bits. | 			# Only SSH2_CERT_TYPE_HOST (2) makes sense in this context. | ||||||
| 	def get_hostkey_size(self): | 			if cert_type == 2: | ||||||
| 		size = self.__hostkey_n_len * 8 |  | ||||||
|  |  | ||||||
|  | 				# Skip the key ID (this is the serial number of the | ||||||
|  | 				# certificate). | ||||||
|  | 				key_id, key_id_len, ptr = KexDH.__get_bytes(hostkey, ptr) | ||||||
|  |  | ||||||
|  | 				# The principles, which are... I don't know what. | ||||||
|  | 				principles, printicples_len, ptr = KexDH.__get_bytes(hostkey, ptr) | ||||||
|  |  | ||||||
|  | 				# The timestamp that this certificate is valid after. | ||||||
|  | 				valid_after = hostkey[ptr:ptr + 8] | ||||||
|  | 				ptr += 8 | ||||||
|  |  | ||||||
|  | 				# The timestamp that this certificate is valid before. | ||||||
|  | 				valid_before = hostkey[ptr:ptr + 8] | ||||||
|  | 				ptr += 8 | ||||||
|  |  | ||||||
|  | 				# TODO: validate the principles, and time range. | ||||||
|  |  | ||||||
|  | 				# The critical options. | ||||||
|  | 				critical_options, critical_options_len, ptr = KexDH.__get_bytes(hostkey, ptr) | ||||||
|  |  | ||||||
|  | 				# Certificate extensions. | ||||||
|  | 				extensions, extensions_len, ptr = KexDH.__get_bytes(hostkey, ptr) | ||||||
|  |  | ||||||
|  | 				# Another nonce. | ||||||
|  | 				nonce, nonce_len, ptr = KexDH.__get_bytes(hostkey, ptr) | ||||||
|  |  | ||||||
|  | 				# Finally, we get to the CA key. | ||||||
|  | 				ca_key, ca_key_len, ptr = KexDH.__get_bytes(hostkey, ptr) | ||||||
|  |  | ||||||
|  | 				# Last in the host key blob is the CA signature.  It isn't | ||||||
|  | 				# interesting to us, so we won't bother parsing any further. | ||||||
|  | 				# The CA key has the modulus, however... | ||||||
|  | 				ptr = 0 | ||||||
|  |  | ||||||
|  | 				# 'ssh-rsa', 'rsa-sha2-256', etc. | ||||||
|  | 				ca_key_type, ca_key_type_len, ptr = KexDH.__get_bytes(ca_key, ptr) | ||||||
|  |  | ||||||
|  | 				# CA's public key exponent. | ||||||
|  | 				ca_key_e, ca_key_e_len, ptr = KexDH.__get_bytes(ca_key, ptr) | ||||||
|  |  | ||||||
|  | 				# CA's modulus.  Bingo. | ||||||
|  | 				ca_key_n, self.__ca_n_len, ptr = KexDH.__get_bytes(ca_key, ptr) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 	@staticmethod | ||||||
|  | 	def __get_bytes(buf, ptr): | ||||||
|  | 		num_bytes = struct.unpack('>I', buf[ptr:ptr + 4])[0] | ||||||
|  | 		ptr += 4 | ||||||
|  | 		return buf[ptr:ptr + num_bytes], num_bytes, ptr + num_bytes | ||||||
|  |  | ||||||
|  | 	# Converts a modulus length in bytes to its size in bits, after some | ||||||
|  | 	# possible adjustments. | ||||||
|  | 	@staticmethod | ||||||
|  | 	def __adjust_key_size(size): | ||||||
|  | 		size = size * 8 | ||||||
| 		# Actual keys are observed to be about 8 bits bigger than expected | 		# Actual keys are observed to be about 8 bits bigger than expected | ||||||
| 		# (i.e.: 1024-bit keys have a 1032-bit modulus).  Check if this is | 		# (i.e.: 1024-bit keys have a 1032-bit modulus).  Check if this is | ||||||
| 		# the case, and subtract 8 if so.  This simply improves readability | 		# the case, and subtract 8 if so.  This simply improves readability | ||||||
| @@ -2066,8 +2182,16 @@ class KexDH(object):  # pragma: nocover | |||||||
| 			size = size - 8 | 			size = size - 8 | ||||||
| 		return size | 		return size | ||||||
|  |  | ||||||
|  | 	# Returns the size of the hostkey, in bits. | ||||||
|  | 	def get_hostkey_size(self): | ||||||
|  | 		return KexDH.__adjust_key_size(self.__hostkey_n_len) | ||||||
|  |  | ||||||
|  | 	# Returns the size of the CA key, in bits. | ||||||
|  | 	def get_ca_size(self): | ||||||
|  | 		return KexDH.__adjust_key_size(self.__ca_n_len) | ||||||
|  |  | ||||||
| 	# Returns the size of the DH modulus, in bits. | 	# Returns the size of the DH modulus, in bits. | ||||||
| 	def get_modulus_size(self): | 	def get_dh_modulus_size(self): | ||||||
| 		# -2 to account for the '0b' prefix in the string. | 		# -2 to account for the '0b' prefix in the string. | ||||||
| 		return len(bin(self.__p)) - 2 | 		return len(bin(self.__p)) - 2 | ||||||
|  |  | ||||||
| @@ -2301,7 +2425,12 @@ def output_algorithm(alg_db, alg_type, alg_name, alg_max_len=0, alg_sizes=None): | |||||||
| 	# the padding. | 	# the padding. | ||||||
| 	alg_name_with_size = None | 	alg_name_with_size = None | ||||||
| 	if (alg_sizes is not None) and (alg_name in alg_sizes): | 	if (alg_sizes is not None) and (alg_name in alg_sizes): | ||||||
| 		alg_name_with_size = '%s (%d-bit)' % (alg_name, alg_sizes[alg_name]) | 		hostkey_size, ca_size = alg_sizes[alg_name] | ||||||
|  | 		if ca_size > 0: | ||||||
|  | 			alg_name_with_size = '%s (%d-bit cert/%d-bit CA)' % (alg_name, hostkey_size, ca_size) | ||||||
|  | 			padding = padding[0:-15] | ||||||
|  | 		else: | ||||||
|  | 			alg_name_with_size = '%s (%d-bit)' % (alg_name, hostkey_size) | ||||||
| 			padding = padding[0:-11] | 			padding = padding[0:-11] | ||||||
|  |  | ||||||
| 	texts = [] | 	texts = [] | ||||||
| @@ -2439,20 +2568,24 @@ def output_recommendations(algs, software, padlen=0): | |||||||
| 			for alg_type in ['kex', 'key', 'enc', 'mac']: | 			for alg_type in ['kex', 'key', 'enc', 'mac']: | ||||||
| 				if alg_type not in alg_rec[sshv]: | 				if alg_type not in alg_rec[sshv]: | ||||||
| 					continue | 					continue | ||||||
| 				for action in ['del', 'add']: | 				for action in ['del', 'add', 'chg']: | ||||||
| 					if action not in alg_rec[sshv][alg_type]: | 					if action not in alg_rec[sshv][alg_type]: | ||||||
| 						continue | 						continue | ||||||
| 					for name in alg_rec[sshv][alg_type][action]: | 					for name in alg_rec[sshv][alg_type][action]: | ||||||
| 						p = '' if out.batch else ' ' * (padlen - len(name)) | 						p = '' if out.batch else ' ' * (padlen - len(name)) | ||||||
|  | 						chg_additional_info = '' | ||||||
| 						if action == 'del': | 						if action == 'del': | ||||||
| 							an, sg, fn = 'remove', '-', out.warn | 							an, sg, fn = 'remove', '-', out.warn | ||||||
| 							if alg_rec[sshv][alg_type][action][name] >= 10: | 							if alg_rec[sshv][alg_type][action][name] >= 10: | ||||||
| 								fn = out.fail | 								fn = out.fail | ||||||
| 						else: | 						elif action == 'add': | ||||||
| 							an, sg, fn = 'append', '+', out.good | 							an, sg, fn = 'append', '+', out.good | ||||||
|  | 						elif action == 'chg': | ||||||
|  | 							an, sg, fn = 'change', '!', out.fail | ||||||
|  | 							chg_additional_info = ' (increase modulus size to 2048 bits or larger)' | ||||||
| 						b = '(SSH{0})'.format(sshv) if sshv == 1 else '' | 						b = '(SSH{0})'.format(sshv) if sshv == 1 else '' | ||||||
| 						fm = '(rec) {0}{1}{2}-- {3} algorithm to {4} {5}' | 						fm = '(rec) {0}{1}{2}-- {3} algorithm to {4}{5} {6}' | ||||||
| 						fn(fm.format(sg, name, p, alg_type, an, b)) | 						fn(fm.format(sg, name, p, alg_type, an, chg_additional_info, b)) | ||||||
| 	if len(obuf) > 0: | 	if len(obuf) > 0: | ||||||
| 		if software is not None: | 		if software is not None: | ||||||
| 			title = '(for {0})'.format(software.display(False)) | 			title = '(for {0})'.format(software.display(False)) | ||||||
| @@ -2511,7 +2644,7 @@ def output(banner, header, kex=None, pkm=None): | |||||||
| 		title, atype = 'key exchange algorithms', 'kex' | 		title, atype = 'key exchange algorithms', 'kex' | ||||||
| 		output_algorithms(title, adb, atype, kex.kex_algorithms, maxlen, kex.dh_modulus_sizes()) | 		output_algorithms(title, adb, atype, kex.kex_algorithms, maxlen, kex.dh_modulus_sizes()) | ||||||
| 		title, atype = 'host-key algorithms', 'key' | 		title, atype = 'host-key algorithms', 'key' | ||||||
| 		output_algorithms(title, adb, atype, kex.key_algorithms, maxlen, kex.rsa_hostkey_sizes()) | 		output_algorithms(title, adb, atype, kex.key_algorithms, maxlen, kex.rsa_key_sizes()) | ||||||
| 		title, atype = 'encryption algorithms (ciphers)', 'enc' | 		title, atype = 'encryption algorithms (ciphers)', 'enc' | ||||||
| 		output_algorithms(title, adb, atype, kex.server.encryption, maxlen) | 		output_algorithms(title, adb, atype, kex.server.encryption, maxlen) | ||||||
| 		title, atype = 'message authentication code algorithms', 'mac' | 		title, atype = 'message authentication code algorithms', 'mac' | ||||||
| @@ -2687,7 +2820,7 @@ def audit(aconf, sshv=None): | |||||||
| 	elif sshv == 2: | 	elif sshv == 2: | ||||||
| 		kex = SSH2.Kex.parse(payload) | 		kex = SSH2.Kex.parse(payload) | ||||||
| 		SSH2.RSAKeyTest.run(s, kex) | 		SSH2.RSAKeyTest.run(s, kex) | ||||||
| 		SSH2.GEXTest.run(aconf.ipvo, aconf.host, aconf.port, s, kex) | 		SSH2.GEXTest.run(s, kex) | ||||||
| 		output(banner, header, kex=kex) | 		output(banner, header, kex=kex) | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Joe Testa
					Joe Testa