mirror of
https://github.com/jtesta/ssh-audit.git
synced 2024-11-25 12:01:40 +01:00
Added RSA certificate auditing.
This commit is contained in:
parent
33ae2946ea
commit
ee5dde1cde
357
ssh-audit.py
357
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,57 +544,97 @@ 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:
|
|
||||||
|
|
||||||
# Send the server our KEXINIT message, using only our
|
# If the connection is closed, re-open it and get the kex again.
|
||||||
# selected kex and RSA type. Send the server's own
|
if not s.is_connected():
|
||||||
# list of ciphers and MACs back to it (this doesn't
|
s.connect()
|
||||||
# matter, really).
|
unused1, unused2, err = s.get_banner()
|
||||||
client_kex = SSH2.Kex(os.urandom(16), [selected_kex_str], [rsa_type], kex.client, kex.server, 0, 0)
|
if err is not None:
|
||||||
|
s.close()
|
||||||
|
return
|
||||||
|
|
||||||
s.write_byte(SSH.Protocol.MSG_KEXINIT)
|
# Parse the server's initial KEX.
|
||||||
client_kex.write(s)
|
packet_type, payload = s.read_packet()
|
||||||
s.send_packet()
|
SSH2.Kex.parse(payload)
|
||||||
|
|
||||||
# Do the initial DH exchange. The server responds back
|
|
||||||
# with the host key and its length. Bingo.
|
|
||||||
kex_group.send_init(s)
|
|
||||||
kex_group.recv_reply(s)
|
|
||||||
modulus_size = kex_group.get_hostkey_size()
|
|
||||||
|
|
||||||
# We only need to test one RSA type, since the others
|
for rsa_type in rsa_types:
|
||||||
# will all be the same.
|
if rsa_type in server_kex.key_algorithms:
|
||||||
|
ran_test = True
|
||||||
|
|
||||||
|
# Send the server our KEXINIT message, using only our
|
||||||
|
# selected kex and RSA type. Send the server's own
|
||||||
|
# list of ciphers and MACs back to it (this doesn't
|
||||||
|
# matter, really).
|
||||||
|
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)
|
||||||
|
client_kex.write(s)
|
||||||
|
s.send_packet()
|
||||||
|
|
||||||
|
# Do the initial DH exchange. The server responds back
|
||||||
|
# with the host key and its length. Bingo.
|
||||||
|
kex_group.send_init(s)
|
||||||
|
kex_group.recv_reply(s)
|
||||||
|
|
||||||
|
hostkey_modulus_size = kex_group.get_hostkey_size()
|
||||||
|
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:
|
||||||
for rsa_type in SSH2.RSAKeyTest.RSA_TYPES:
|
if ca is False:
|
||||||
|
for rsa_type in SSH2.RSAKeyTest.RSA_TYPES:
|
||||||
|
alg_list = SSH2.KexDB.ALGORITHMS['key'][rsa_type]
|
||||||
|
alg_list.append(['using small %d-bit modulus' % hostkey_modulus_size])
|
||||||
|
else:
|
||||||
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])
|
|
||||||
|
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)
|
reconnect_failed = True
|
||||||
if s is None:
|
break
|
||||||
reconnect_failed = True
|
|
||||||
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
|
||||||
|
|
||||||
|
# Only SSH2_CERT_TYPE_HOST (2) makes sense in this context.
|
||||||
|
if cert_type == 2:
|
||||||
|
|
||||||
|
# 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
|
ptr += 4
|
||||||
self.__hostkey_n = int(binascii.hexlify(hostkey[ptr:ptr + self.__hostkey_n_len]), 16)
|
return buf[ptr:ptr + num_bytes], num_bytes, ptr + num_bytes
|
||||||
|
|
||||||
# Returns the size of the hostkey, in bits.
|
|
||||||
def get_hostkey_size(self):
|
|
||||||
size = self.__hostkey_n_len * 8
|
|
||||||
|
|
||||||
|
# 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,8 +2425,13 @@ 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]
|
||||||
padding = padding[0:-11]
|
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]
|
||||||
|
|
||||||
texts = []
|
texts = []
|
||||||
if len(alg_name.strip()) == 0:
|
if len(alg_name.strip()) == 0:
|
||||||
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user