mirror of https://github.com/jtesta/ssh-audit.git
Extract software (Dropbear, OpenSSH, HP iLO, Cisco) and OS (NetBSD, FreeBSD) from banner.
This commit is contained in:
parent
d07d5078cb
commit
ac64f87327
111
ssh-audit.py
111
ssh-audit.py
|
@ -209,6 +209,114 @@ class SSH(object):
|
||||||
MSG_KEXDH_INIT = 30
|
MSG_KEXDH_INIT = 30
|
||||||
MSG_KEXDH_REPLY = 32
|
MSG_KEXDH_REPLY = 32
|
||||||
|
|
||||||
|
class Software(object):
|
||||||
|
def __init__(self, vendor, product, version, patch, os):
|
||||||
|
self.__vendor = vendor
|
||||||
|
self.__product = product
|
||||||
|
self.__version = version
|
||||||
|
self.__patch = patch
|
||||||
|
self.__os = os
|
||||||
|
|
||||||
|
@property
|
||||||
|
def vendor(self):
|
||||||
|
return self.__vendor
|
||||||
|
|
||||||
|
@property
|
||||||
|
def product(self):
|
||||||
|
return self.__product
|
||||||
|
|
||||||
|
@property
|
||||||
|
def version(self):
|
||||||
|
return self.__version
|
||||||
|
|
||||||
|
@property
|
||||||
|
def patch(self):
|
||||||
|
return self.__patch
|
||||||
|
|
||||||
|
@property
|
||||||
|
def os(self):
|
||||||
|
return self.__os
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
out = '{0} '.format(self.vendor) if self.vendor else ''
|
||||||
|
out += self.product
|
||||||
|
if self.version:
|
||||||
|
out += ' {0}'.format(self.version)
|
||||||
|
if self.patch:
|
||||||
|
out += ' {0}'.format(self.patch)
|
||||||
|
if self.os:
|
||||||
|
out += ' running on {0}'.format(self.os)
|
||||||
|
return out
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
out = 'vendor={0} '.format(self.vendor) if self.vendor else ''
|
||||||
|
if self.product:
|
||||||
|
out += ', product={0}'.format(self.software)
|
||||||
|
if self.version:
|
||||||
|
out += ', version={0}'.format(self.version)
|
||||||
|
if self.patch:
|
||||||
|
out += ', patch={0}'.format(self.patch)
|
||||||
|
if self.os:
|
||||||
|
out += ', os={0}'.format(self.os)
|
||||||
|
return '<{0}({1})>'.format(self.__class__.__name__, out)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _fix_patch(patch):
|
||||||
|
return re.sub(r'^[-_\.]+', '', patch)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _fix_date(d):
|
||||||
|
if d is not None and len(d) == 8:
|
||||||
|
return '{0}-{1}-{2}'.format(d[:4], d[4:6], d[6:8])
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _extract_os(cls, c):
|
||||||
|
if c is None:
|
||||||
|
return None
|
||||||
|
mx = re.match(r'^NetBSD(?:_Secure_Shell)?(?:[\s-]+(\d{8})(.*))?$', c)
|
||||||
|
if mx:
|
||||||
|
d = cls._fix_date(mx.group(1))
|
||||||
|
return 'NetBSD' if d is None else 'NetBSD ({0})'.format(d)
|
||||||
|
mx = re.match(r'^FreeBSD(?:\slocalisations)?[\s-]+(\d{8})(.*)$', c)
|
||||||
|
if not mx:
|
||||||
|
mx = re.match(r'^[^@]+@FreeBSD\.org[\s-]+(\d{8})(.*)$', c)
|
||||||
|
if mx:
|
||||||
|
d = cls._fix_date(mx.group(1))
|
||||||
|
return 'FreeBSD' if d is None else 'FreeBSD ({0})'.format(d)
|
||||||
|
generic = ['NetBSD', 'FreeBSD']
|
||||||
|
for g in generic:
|
||||||
|
if c.startswith(g) or c.endswith(g):
|
||||||
|
return g
|
||||||
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse(cls, banner):
|
||||||
|
software = str(banner.software)
|
||||||
|
mx = re.match(r'^dropbear_(\d+.\d+)(.*)', software)
|
||||||
|
if mx:
|
||||||
|
patch = cls._fix_patch(mx.group(2))
|
||||||
|
v, p = 'Matt Johnston', 'Dropbear SSH'
|
||||||
|
v = None
|
||||||
|
return cls(v, p, mx.group(1), patch, None)
|
||||||
|
mx = re.match(r'^OpenSSH[_\.-]+([\d\.]+\d+)(.*)', software)
|
||||||
|
if mx:
|
||||||
|
patch = cls._fix_patch(mx.group(2))
|
||||||
|
v, p = 'OpenBSD', 'OpenSSH'
|
||||||
|
v = None
|
||||||
|
os = cls._extract_os(banner.comments)
|
||||||
|
return cls(v, p, mx.group(1), patch, os)
|
||||||
|
mx = re.match(r'^mpSSH_([\d\.]+\d+)', software)
|
||||||
|
if mx:
|
||||||
|
v, p = 'HP', 'iLO (Integrated Lights-Out) sshd'
|
||||||
|
return cls(v, p, mx.group(1), None, None)
|
||||||
|
mx = re.match(r'^Cisco-([\d\.]+\d+)', software)
|
||||||
|
if mx:
|
||||||
|
v, p = 'Cisco', 'IOS/PIX sshd'
|
||||||
|
return cls(v, p, mx.group(1), None, None)
|
||||||
|
return None
|
||||||
|
|
||||||
class Banner(object):
|
class Banner(object):
|
||||||
_RXP, _RXR = r'SSH-\d\.\s*?\d+', r'(-([^\s]*)(?:\s+(.*))?)?'
|
_RXP, _RXR = r'SSH-\d\.\s*?\d+', r'(-([^\s]*)(?:\s+(.*))?)?'
|
||||||
RX_PROTOCOL = re.compile(_RXP.replace('\d', '(\d)'))
|
RX_PROTOCOL = re.compile(_RXP.replace('\d', '(\d)'))
|
||||||
|
@ -679,6 +787,9 @@ def output(banner, header, kex):
|
||||||
out.good('(gen) banner: {0}'.format(banner))
|
out.good('(gen) banner: {0}'.format(banner))
|
||||||
if banner.protocol[0] == 1:
|
if banner.protocol[0] == 1:
|
||||||
out.fail('(gen) protocol SSH1 enabled')
|
out.fail('(gen) protocol SSH1 enabled')
|
||||||
|
software = SSH.Software.parse(banner)
|
||||||
|
if software is not None:
|
||||||
|
out.good('(gen) software: {0}'.format(software))
|
||||||
if kex is not None:
|
if kex is not None:
|
||||||
output_compatibility(kex)
|
output_compatibility(kex)
|
||||||
compressions = [x for x in kex.server.compression if x != 'none']
|
compressions = [x for x in kex.server.compression if x != 'none']
|
||||||
|
|
Loading…
Reference in New Issue