mirror of
https://github.com/jtesta/ssh-audit.git
synced 2024-11-22 02:21:40 +01:00
Multiple style fixes (protector, veryhigh).
This commit is contained in:
parent
c759e53779
commit
fba6397721
60
ssh-audit.py
60
ssh-audit.py
@ -29,7 +29,8 @@ import os, io, sys, socket, struct, random, errno, getopt
|
|||||||
VERSION = 'v1.0.20160902'
|
VERSION = 'v1.0.20160902'
|
||||||
SSH_BANNER = 'SSH-2.0-OpenSSH_7.3'
|
SSH_BANNER = 'SSH-2.0-OpenSSH_7.3'
|
||||||
|
|
||||||
def usage(err = None):
|
|
||||||
|
def usage(err=None):
|
||||||
p = os.path.basename(sys.argv[0])
|
p = os.path.basename(sys.argv[0])
|
||||||
out.batch = False
|
out.batch = False
|
||||||
out.minlevel = 'info'
|
out.minlevel = 'info'
|
||||||
@ -45,6 +46,7 @@ def usage(err = None):
|
|||||||
out.sep()
|
out.sep()
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
class Output(object):
|
class Output(object):
|
||||||
LEVELS = ['info', 'warn', 'fail']
|
LEVELS = ['info', 'warn', 'fail']
|
||||||
COLORS = {'head': 36, 'good': 32, 'warn': 33, 'fail': 31}
|
COLORS = {'head': 36, 'good': 32, 'warn': 33, 'fail': 31}
|
||||||
@ -58,20 +60,24 @@ class Output(object):
|
|||||||
@property
|
@property
|
||||||
def minlevel(self):
|
def minlevel(self):
|
||||||
return self.__minlevel
|
return self.__minlevel
|
||||||
|
|
||||||
@minlevel.setter
|
@minlevel.setter
|
||||||
def minlevel(self, name):
|
def minlevel(self, name):
|
||||||
self.__minlevel = self.getlevel(name)
|
self.__minlevel = self.getlevel(name)
|
||||||
|
|
||||||
def getlevel(self, name):
|
def getlevel(self, name):
|
||||||
cname = 'info' if name == 'good' else name
|
cname = 'info' if name == 'good' else name
|
||||||
if not cname in self.LEVELS:
|
if cname not in self.LEVELS:
|
||||||
return sys.maxsize
|
return sys.maxsize
|
||||||
return self.LEVELS.index(cname)
|
return self.LEVELS.index(cname)
|
||||||
|
|
||||||
def sep(self):
|
def sep(self):
|
||||||
if not self.batch:
|
if not self.batch:
|
||||||
print()
|
print()
|
||||||
|
|
||||||
def _colorized(self, color):
|
def _colorized(self, color):
|
||||||
return lambda x: print(u'{0}{1}\033[0m'.format(color, x))
|
return lambda x: print(u'{0}{1}\033[0m'.format(color, x))
|
||||||
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
if name == 'head' and self.batch:
|
if name == 'head' and self.batch:
|
||||||
return lambda x: None
|
return lambda x: None
|
||||||
@ -83,25 +89,30 @@ class Output(object):
|
|||||||
else:
|
else:
|
||||||
return lambda x: print(u'{0}'.format(x))
|
return lambda x: print(u'{0}'.format(x))
|
||||||
|
|
||||||
|
|
||||||
class OutputBuffer(list):
|
class OutputBuffer(list):
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
self.__buf = io.StringIO()
|
self.__buf = io.StringIO()
|
||||||
self.__stdout = sys.stdout
|
self.__stdout = sys.stdout
|
||||||
sys.stdout = self.__buf
|
sys.stdout = self.__buf
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def flush(self):
|
def flush(self):
|
||||||
for line in self:
|
for line in self:
|
||||||
print(line)
|
print(line)
|
||||||
|
|
||||||
def __exit__(self, *args):
|
def __exit__(self, *args):
|
||||||
self.extend(self.__buf.getvalue().splitlines())
|
self.extend(self.__buf.getvalue().splitlines())
|
||||||
sys.stdout = self.__stdout
|
sys.stdout = self.__stdout
|
||||||
|
|
||||||
|
|
||||||
class KexParty(object):
|
class KexParty(object):
|
||||||
encryption = []
|
encryption = []
|
||||||
mac = []
|
mac = []
|
||||||
compression = []
|
compression = []
|
||||||
languages = []
|
languages = []
|
||||||
|
|
||||||
|
|
||||||
class Kex(object):
|
class Kex(object):
|
||||||
cookie = None
|
cookie = None
|
||||||
kex_algorithms = []
|
kex_algorithms = []
|
||||||
@ -130,8 +141,9 @@ class Kex(object):
|
|||||||
kex.unused = buf.read_int()
|
kex.unused = buf.read_int()
|
||||||
return kex
|
return kex
|
||||||
|
|
||||||
|
|
||||||
class ReadBuf(object):
|
class ReadBuf(object):
|
||||||
def __init__(self, data = None):
|
def __init__(self, data=None):
|
||||||
super(ReadBuf, self).__init__()
|
super(ReadBuf, self).__init__()
|
||||||
self._buf = io.BytesIO(data) if data else io.BytesIO()
|
self._buf = io.BytesIO(data) if data else io.BytesIO()
|
||||||
self._len = len(data) if data else 0
|
self._len = len(data) if data else 0
|
||||||
@ -156,8 +168,9 @@ class ReadBuf(object):
|
|||||||
list_size = self.read_int()
|
list_size = self.read_int()
|
||||||
return self.read(list_size).decode().split(',')
|
return self.read(list_size).decode().split(',')
|
||||||
|
|
||||||
|
|
||||||
class WriteBuf(object):
|
class WriteBuf(object):
|
||||||
def __init__(self, data = None):
|
def __init__(self, data=None):
|
||||||
super(WriteBuf, self).__init__()
|
super(WriteBuf, self).__init__()
|
||||||
self._wbuf = io.BytesIO(data) if data else io.BytesIO()
|
self._wbuf = io.BytesIO(data) if data else io.BytesIO()
|
||||||
|
|
||||||
@ -185,10 +198,11 @@ class WriteBuf(object):
|
|||||||
|
|
||||||
def write_mpint(self, v):
|
def write_mpint(self, v):
|
||||||
length = v.bit_length() // 8 + 1
|
length = v.bit_length() // 8 + 1
|
||||||
data = [(v >> i*8) & 0xff for i in reversed(range(length))]
|
data = [(v >> i * 8) & 0xff for i in reversed(range(length))]
|
||||||
data = bytes(bytearray(data))
|
data = bytes(bytearray(data))
|
||||||
self.write_string(data)
|
self.write_string(data)
|
||||||
|
|
||||||
|
|
||||||
class SSH(object):
|
class SSH(object):
|
||||||
MSG_KEXINIT = 20
|
MSG_KEXINIT = 20
|
||||||
MSG_NEWKEYS = 21
|
MSG_NEWKEYS = 21
|
||||||
@ -198,7 +212,7 @@ class SSH(object):
|
|||||||
class Socket(ReadBuf, WriteBuf):
|
class Socket(ReadBuf, WriteBuf):
|
||||||
SM_BANNER_SENT = 1
|
SM_BANNER_SENT = 1
|
||||||
|
|
||||||
def __init__(self, host, port, cto = 3.0, rto = 5.0):
|
def __init__(self, host, port, cto=3.0, rto=5.0):
|
||||||
self.__block_size = 8
|
self.__block_size = 8
|
||||||
self.__state = 0
|
self.__state = 0
|
||||||
self.__header = []
|
self.__header = []
|
||||||
@ -231,7 +245,7 @@ class SSH(object):
|
|||||||
self.__header.append(line)
|
self.__header.append(line)
|
||||||
return self.__banner, self.__header
|
return self.__banner, self.__header
|
||||||
|
|
||||||
def recv(self, size = 2048):
|
def recv(self, size=2048):
|
||||||
try:
|
try:
|
||||||
data = self.__sock.recv(size)
|
data = self.__sock.recv(size)
|
||||||
except socket.timeout as e:
|
except socket.timeout as e:
|
||||||
@ -257,7 +271,7 @@ class SSH(object):
|
|||||||
return (-1, e)
|
return (-1, e)
|
||||||
self.__sock.send(data)
|
self.__sock.send(data)
|
||||||
|
|
||||||
def send_banner(self, banner = SSH_BANNER):
|
def send_banner(self, banner=SSH_BANNER):
|
||||||
self.send(banner.encode() + b'\r\n')
|
self.send(banner.encode() + b'\r\n')
|
||||||
if self.__state < self.SM_BANNER_SENT:
|
if self.__state < self.SM_BANNER_SENT:
|
||||||
self.__state = self.SM_BANNER_SENT
|
self.__state = self.SM_BANNER_SENT
|
||||||
@ -314,6 +328,7 @@ class SSH(object):
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class KexDH(object):
|
class KexDH(object):
|
||||||
def __init__(self, alg, g, p):
|
def __init__(self, alg, g, p):
|
||||||
self.__alg = alg
|
self.__alg = alg
|
||||||
@ -330,6 +345,7 @@ class KexDH(object):
|
|||||||
s.write_mpint(self.__e)
|
s.write_mpint(self.__e)
|
||||||
s.send_packet()
|
s.send_packet()
|
||||||
|
|
||||||
|
|
||||||
class KexGroup1(KexDH):
|
class KexGroup1(KexDH):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
# rfc2409: second oakley group
|
# rfc2409: second oakley group
|
||||||
@ -340,6 +356,7 @@ class KexGroup1(KexDH):
|
|||||||
'ffffffffffffffff', 16)
|
'ffffffffffffffff', 16)
|
||||||
super(KexGroup1, self).__init__('sha1', 2, p)
|
super(KexGroup1, self).__init__('sha1', 2, p)
|
||||||
|
|
||||||
|
|
||||||
class KexGroup14(KexDH):
|
class KexGroup14(KexDH):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
# rfc3526: 2048-bit modp group
|
# rfc3526: 2048-bit modp group
|
||||||
@ -354,12 +371,14 @@ class KexGroup14(KexDH):
|
|||||||
'15728e5a8aacaa68ffffffffffffffff', 16)
|
'15728e5a8aacaa68ffffffffffffffff', 16)
|
||||||
super(KexGroup14, self).__init__('sha1', 2, p)
|
super(KexGroup14, self).__init__('sha1', 2, p)
|
||||||
|
|
||||||
|
|
||||||
def get_ssh_version(version_desc):
|
def get_ssh_version(version_desc):
|
||||||
if version_desc.startswith('d'):
|
if version_desc.startswith('d'):
|
||||||
return ('Dropbear SSH', version_desc[1:])
|
return ('Dropbear SSH', version_desc[1:])
|
||||||
else:
|
else:
|
||||||
return ('OpenSSH', version_desc)
|
return ('OpenSSH', version_desc)
|
||||||
|
|
||||||
|
|
||||||
def get_alg_since_text(alg_desc):
|
def get_alg_since_text(alg_desc):
|
||||||
tv = []
|
tv = []
|
||||||
versions = alg_desc[0]
|
versions = alg_desc[0]
|
||||||
@ -368,7 +387,8 @@ def get_alg_since_text(alg_desc):
|
|||||||
tv.append('{0} {1}'.format(ssh_prefix, ssh_version))
|
tv.append('{0} {1}'.format(ssh_prefix, ssh_version))
|
||||||
return 'available since ' + ', '.join(tv).rstrip(', ')
|
return 'available since ' + ', '.join(tv).rstrip(', ')
|
||||||
|
|
||||||
def get_alg_timeframe(alg_desc, result = {}):
|
|
||||||
|
def get_alg_timeframe(alg_desc, result={}):
|
||||||
versions = alg_desc[0]
|
versions = alg_desc[0]
|
||||||
vlen = len(versions)
|
vlen = len(versions)
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
@ -383,7 +403,7 @@ def get_alg_timeframe(alg_desc, result = {}):
|
|||||||
continue
|
continue
|
||||||
for v in cversions.split(','):
|
for v in cversions.split(','):
|
||||||
ssh_prefix, ssh_version = get_ssh_version(v)
|
ssh_prefix, ssh_version = get_ssh_version(v)
|
||||||
if not ssh_prefix in result:
|
if ssh_prefix not in result:
|
||||||
result[ssh_prefix] = [None, None, None]
|
result[ssh_prefix] = [None, None, None]
|
||||||
prev, push = result[ssh_prefix][i], False
|
prev, push = result[ssh_prefix][i], False
|
||||||
if prev is None:
|
if prev is None:
|
||||||
@ -396,9 +416,10 @@ def get_alg_timeframe(alg_desc, result = {}):
|
|||||||
result[ssh_prefix][i] = ssh_version
|
result[ssh_prefix][i] = ssh_version
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def get_ssh_timeframe(kex):
|
def get_ssh_timeframe(kex):
|
||||||
alg_timeframe = {}
|
alg_timeframe = {}
|
||||||
algs = {'kex': kex.kex_algorithms,
|
algs = {'kex': kex.kex_algorithms,
|
||||||
'key': kex.key_algorithms,
|
'key': kex.key_algorithms,
|
||||||
'enc': kex.server.encryption,
|
'enc': kex.server.encryption,
|
||||||
'mac': kex.server.mac}
|
'mac': kex.server.mac}
|
||||||
@ -459,7 +480,7 @@ KEX_DB = {
|
|||||||
'ecdsa-sha2-nistp384': [['5.7,d2013.62'], [TEXT_CURVES_WEAK], [TEXT_RNDSIG_KEY]],
|
'ecdsa-sha2-nistp384': [['5.7,d2013.62'], [TEXT_CURVES_WEAK], [TEXT_RNDSIG_KEY]],
|
||||||
'ecdsa-sha2-nistp521': [['5.7,d2013.62'], [TEXT_CURVES_WEAK], [TEXT_RNDSIG_KEY]],
|
'ecdsa-sha2-nistp521': [['5.7,d2013.62'], [TEXT_CURVES_WEAK], [TEXT_RNDSIG_KEY]],
|
||||||
'ssh-rsa-cert-v00@openssh.com': [['5.4', '6.9'], [FAIL_OPENSSH70_LEGACY], []],
|
'ssh-rsa-cert-v00@openssh.com': [['5.4', '6.9'], [FAIL_OPENSSH70_LEGACY], []],
|
||||||
'ssh-dss-cert-v00@openssh.com': [['5.4', '6.9'], [FAIL_OPENSSH70_LEGACY], [TEXT_MODULUS_SIZE, TEXT_RNDSIG_KEY]],
|
'ssh-dss-cert-v00@openssh.com': [['5.4', '6.9'], [FAIL_OPENSSH70_LEGACY], [TEXT_MODULUS_SIZE, TEXT_RNDSIG_KEY]],
|
||||||
'ssh-rsa-cert-v01@openssh.com': [['5.6']],
|
'ssh-rsa-cert-v01@openssh.com': [['5.6']],
|
||||||
'ssh-dss-cert-v01@openssh.com': [['5.6', '6.9'], [FAIL_OPENSSH70_WEAK], [TEXT_MODULUS_SIZE, TEXT_RNDSIG_KEY]],
|
'ssh-dss-cert-v01@openssh.com': [['5.6', '6.9'], [FAIL_OPENSSH70_WEAK], [TEXT_MODULUS_SIZE, TEXT_RNDSIG_KEY]],
|
||||||
'ecdsa-sha2-nistp256-cert-v01@openssh.com': [['5.7'], [TEXT_CURVES_WEAK], [TEXT_RNDSIG_KEY]],
|
'ecdsa-sha2-nistp256-cert-v01@openssh.com': [['5.7'], [TEXT_CURVES_WEAK], [TEXT_RNDSIG_KEY]],
|
||||||
@ -470,7 +491,7 @@ KEX_DB = {
|
|||||||
'none': [['1.2.2,d2013.56'], [FAIL_PLAINTEXT]],
|
'none': [['1.2.2,d2013.56'], [FAIL_PLAINTEXT]],
|
||||||
'3des-cbc': [['1.2.2,d0.28', '6.6', None], [FAIL_OPENSSH67_UNSAFE], [TEXT_CIPHER_WEAK, TEXT_CIPHER_MODE, TEXT_BLOCK_SIZE]],
|
'3des-cbc': [['1.2.2,d0.28', '6.6', None], [FAIL_OPENSSH67_UNSAFE], [TEXT_CIPHER_WEAK, TEXT_CIPHER_MODE, TEXT_BLOCK_SIZE]],
|
||||||
'3des-ctr': [['d0.52']],
|
'3des-ctr': [['d0.52']],
|
||||||
'blowfish-cbc': [['1.2.2,d0.28', '6.6,d0.52', '7.1,d0.52'], [FAIL_OPENSSH67_UNSAFE, FAIL_DBEAR53_DISABLED], [WARN_OPENSSH72_LEGACY, TEXT_CIPHER_MODE, TEXT_BLOCK_SIZE]],
|
'blowfish-cbc': [['1.2.2,d0.28', '6.6,d0.52', '7.1,d0.52'], [FAIL_OPENSSH67_UNSAFE, FAIL_DBEAR53_DISABLED], [WARN_OPENSSH72_LEGACY, TEXT_CIPHER_MODE, TEXT_BLOCK_SIZE]],
|
||||||
'twofish-cbc': [['d0.28', 'd2014.66'], [FAIL_DBEAR67_DISABLED], [TEXT_CIPHER_MODE]],
|
'twofish-cbc': [['d0.28', 'd2014.66'], [FAIL_DBEAR67_DISABLED], [TEXT_CIPHER_MODE]],
|
||||||
'twofish128-cbc': [['d0.47', 'd2014.66'], [FAIL_DBEAR67_DISABLED], [TEXT_CIPHER_MODE]],
|
'twofish128-cbc': [['d0.47', 'd2014.66'], [FAIL_DBEAR67_DISABLED], [TEXT_CIPHER_MODE]],
|
||||||
'twofish256-cbc': [['d0.47', 'd2014.66'], [FAIL_DBEAR67_DISABLED], [TEXT_CIPHER_MODE]],
|
'twofish256-cbc': [['d0.47', 'd2014.66'], [FAIL_DBEAR67_DISABLED], [TEXT_CIPHER_MODE]],
|
||||||
@ -520,6 +541,7 @@ KEX_DB = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def output_algorithms(title, alg_type, algorithms, maxlen=0):
|
def output_algorithms(title, alg_type, algorithms, maxlen=0):
|
||||||
with OutputBuffer() as obuf:
|
with OutputBuffer() as obuf:
|
||||||
for algorithm in algorithms:
|
for algorithm in algorithms:
|
||||||
@ -529,6 +551,7 @@ def output_algorithms(title, alg_type, algorithms, maxlen=0):
|
|||||||
obuf.flush()
|
obuf.flush()
|
||||||
out.sep()
|
out.sep()
|
||||||
|
|
||||||
|
|
||||||
def output_algorithm(alg_type, alg_name, alg_max_len=0):
|
def output_algorithm(alg_type, alg_name, alg_max_len=0):
|
||||||
prefix = '(' + alg_type + ') '
|
prefix = '(' + alg_type + ') '
|
||||||
if alg_max_len == 0:
|
if alg_max_len == 0:
|
||||||
@ -543,7 +566,7 @@ def output_algorithm(alg_type, alg_name, alg_max_len=0):
|
|||||||
texts.append((level, get_alg_since_text(alg_desc)))
|
texts.append((level, get_alg_since_text(alg_desc)))
|
||||||
idx = idx + 1
|
idx = idx + 1
|
||||||
if ldesc > idx:
|
if ldesc > idx:
|
||||||
for t in alg_desc[idx]:
|
for t in alg_desc[idx]:
|
||||||
texts.append((level, t))
|
texts.append((level, t))
|
||||||
if len(texts) == 0:
|
if len(texts) == 0:
|
||||||
texts.append(('info', ''))
|
texts.append(('info', ''))
|
||||||
@ -556,7 +579,7 @@ def output_algorithm(alg_type, alg_name, alg_max_len=0):
|
|||||||
if first:
|
if first:
|
||||||
if first and level == 'info':
|
if first and level == 'info':
|
||||||
f = out.good
|
f = out.good
|
||||||
f(prefix + alg_name + padding +' -- ' + text)
|
f(prefix + alg_name + padding + ' -- ' + text)
|
||||||
first = False
|
first = False
|
||||||
else:
|
else:
|
||||||
if out.verbose:
|
if out.verbose:
|
||||||
@ -564,6 +587,7 @@ def output_algorithm(alg_type, alg_name, alg_max_len=0):
|
|||||||
else:
|
else:
|
||||||
f(' ' * len(prefix + alg_name) + padding + ' `- ' + text)
|
f(' ' * len(prefix + alg_name) + padding + ' `- ' + text)
|
||||||
|
|
||||||
|
|
||||||
def output_compatibility(kex, client=False):
|
def output_compatibility(kex, client=False):
|
||||||
ssh_timeframe = get_ssh_timeframe(kex)
|
ssh_timeframe = get_ssh_timeframe(kex)
|
||||||
cp = 2 if client else 1
|
cp = 2 if client else 1
|
||||||
@ -582,6 +606,7 @@ def output_compatibility(kex, client=False):
|
|||||||
if len(comp_text) > 0:
|
if len(comp_text) > 0:
|
||||||
out.good('(gen) compatibility: ' + ', '.join(comp_text))
|
out.good('(gen) compatibility: ' + ', '.join(comp_text))
|
||||||
|
|
||||||
|
|
||||||
def output(banner, header, kex):
|
def output(banner, header, kex):
|
||||||
with OutputBuffer() as obuf:
|
with OutputBuffer() as obuf:
|
||||||
if len(header) > 0:
|
if len(header) > 0:
|
||||||
@ -625,6 +650,7 @@ def parse_int(v):
|
|||||||
except:
|
except:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def parse_args():
|
def parse_args():
|
||||||
host, port = None, 22
|
host, port = None, 22
|
||||||
try:
|
try:
|
||||||
@ -642,7 +668,7 @@ def parse_args():
|
|||||||
elif o in ('-n', '--no-colors'):
|
elif o in ('-n', '--no-colors'):
|
||||||
out.colors = False
|
out.colors = False
|
||||||
elif o in ('-v', '--verbose'):
|
elif o in ('-v', '--verbose'):
|
||||||
out.verbose = True
|
out.verbose = True
|
||||||
elif o in ('-l', '--level'):
|
elif o in ('-l', '--level'):
|
||||||
if a not in ('info', 'warn', 'fail'):
|
if a not in ('info', 'warn', 'fail'):
|
||||||
usage('level ' + a + ' is not valid')
|
usage('level ' + a + ' is not valid')
|
||||||
@ -657,6 +683,7 @@ def parse_args():
|
|||||||
usage('port {0} is not valid'.format(port))
|
usage('port {0} is not valid'.format(port))
|
||||||
return host, port
|
return host, port
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
host, port = parse_args()
|
host, port = parse_args()
|
||||||
s = SSH.Socket(host, port)
|
s = SSH.Socket(host, port)
|
||||||
@ -678,6 +705,7 @@ def main():
|
|||||||
kex = Kex.parse(payload)
|
kex = Kex.parse(payload)
|
||||||
output(banner, header, kex)
|
output(banner, header, kex)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
out = Output()
|
out = Output()
|
||||||
main()
|
main()
|
||||||
|
Loading…
Reference in New Issue
Block a user