mirror of
				https://github.com/jtesta/ssh-audit.git
				synced 2025-11-04 11:12:15 +01:00 
			
		
		
		
	Extract software (Dropbear, OpenSSH, HP iLO, Cisco) and OS (NetBSD, FreeBSD) from banner.
This commit is contained in:
		
							
								
								
									
										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']
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user