mirror of https://github.com/jtesta/ssh-audit.git
Initial SSH1 support (packet reading, SMSG_PUBLIC_KEY, CRC32, etc) #6.
This commit is contained in:
parent
d6980242ba
commit
9030e71892
246
ssh-audit.py
246
ssh-audit.py
|
@ -26,8 +26,7 @@
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
import os, io, sys, socket, struct, random, errno, getopt, re
|
import os, io, sys, socket, struct, random, errno, getopt, re
|
||||||
|
|
||||||
VERSION = 'v1.0.20160908'
|
VERSION = 'v1.0.20160915'
|
||||||
SSH_BANNER = 'SSH-2.0-OpenSSH_7.3'
|
|
||||||
|
|
||||||
|
|
||||||
def usage(err=None):
|
def usage(err=None):
|
||||||
|
@ -142,6 +141,121 @@ class Kex(object):
|
||||||
return kex
|
return kex
|
||||||
|
|
||||||
|
|
||||||
|
class SSH1(object):
|
||||||
|
class CRC32(object):
|
||||||
|
def __init__(self):
|
||||||
|
self._table = [0] * 256
|
||||||
|
for i in range(256):
|
||||||
|
crc = 0
|
||||||
|
n = i
|
||||||
|
for j in range(8):
|
||||||
|
x = (crc ^ n) & 1
|
||||||
|
crc = (crc >> 1) ^ (x * 0xedb88320)
|
||||||
|
n = n >> 1
|
||||||
|
self._table[i] = crc
|
||||||
|
|
||||||
|
def calc(self, v):
|
||||||
|
crc, l = 0, len(v)
|
||||||
|
for i in range(l):
|
||||||
|
n = ord(v[i:i + 1])
|
||||||
|
n = n ^ (crc & 0xff)
|
||||||
|
crc = (crc >> 8) ^ self._table[n]
|
||||||
|
return crc
|
||||||
|
|
||||||
|
_crc32 = CRC32()
|
||||||
|
CIPHERS = [None, 'idea', 'des', '3des', 'tss', 'rc4', 'blowfish']
|
||||||
|
AUTHS = [None, 'rhosts', 'rsa', 'password', 'rhosts_rsa', 'tis', 'kerberos']
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def crc32(cls, v):
|
||||||
|
return cls._crc32.calc(v)
|
||||||
|
|
||||||
|
class PublicKeyMessage(object):
|
||||||
|
def __init__(self, cookie, skey, hkey, pflags, cmask, amask):
|
||||||
|
assert len(skey) == 3
|
||||||
|
assert len(hkey) == 3
|
||||||
|
self.__cookie = cookie
|
||||||
|
self.__server_key = skey
|
||||||
|
self.__host_key = hkey
|
||||||
|
self.__protocol_flags = pflags
|
||||||
|
self.__supported_ciphers_mask = cmask
|
||||||
|
self.__supported_authentications_mask = amask
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cookie(self):
|
||||||
|
return self.__cookie
|
||||||
|
|
||||||
|
@property
|
||||||
|
def server_key_bits(self):
|
||||||
|
return self.__server_key[0]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def server_key_public_exponent(self):
|
||||||
|
return self.__server_key[1]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def server_key_public_modulus(self):
|
||||||
|
return self.__server_key[2]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def host_key_bits(self):
|
||||||
|
return self.__host_key[0]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def host_key_public_exponent(self):
|
||||||
|
return self.__host_key[1]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def host_key_public_modulus(self):
|
||||||
|
return self.__host_key[2]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def protocol_flags(self):
|
||||||
|
return self.__protocol_flags
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_ciphers_mask(self):
|
||||||
|
return self.__supported_ciphers_mask
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_ciphers(self):
|
||||||
|
ciphers = []
|
||||||
|
for i in range(len(SSH1.CIPHERS)):
|
||||||
|
if self.__supported_ciphers_mask & (1 << i) != 0:
|
||||||
|
ciphers.append(SSH1.CIPHERS[i])
|
||||||
|
return ciphers
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_authentications_mask(self):
|
||||||
|
return self.__supported_authentications_mask
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_authentications(self):
|
||||||
|
auths = []
|
||||||
|
for i in range(len(SSH1.AUTHS)):
|
||||||
|
if self.__supported_authentications_mask & (1 << i) != 0:
|
||||||
|
auths.append(SSH1.AUTHS[i])
|
||||||
|
return auths
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse(cls, payload):
|
||||||
|
buf = ReadBuf(payload)
|
||||||
|
cookie = buf.read(8)
|
||||||
|
server_key_bits = buf.read_int()
|
||||||
|
server_key_exponent = buf.read_mpint1()
|
||||||
|
server_key_modulus = buf.read_mpint1()
|
||||||
|
skey = (server_key_bits, server_key_exponent, server_key_modulus)
|
||||||
|
host_key_bits = buf.read_int()
|
||||||
|
host_key_exponent = buf.read_mpint1()
|
||||||
|
host_key_modulus = buf.read_mpint1()
|
||||||
|
hkey = (host_key_bits, host_key_exponent, host_key_modulus)
|
||||||
|
pflags = buf.read_int()
|
||||||
|
cmask = buf.read_int()
|
||||||
|
amask = buf.read_int()
|
||||||
|
pkm = cls(cookie, skey, hkey, pflags, cmask, amask)
|
||||||
|
return pkm
|
||||||
|
|
||||||
|
|
||||||
class ReadBuf(object):
|
class ReadBuf(object):
|
||||||
def __init__(self, data=None):
|
def __init__(self, data=None):
|
||||||
super(ReadBuf, self).__init__()
|
super(ReadBuf, self).__init__()
|
||||||
|
@ -269,6 +383,7 @@ class WriteBuf(object):
|
||||||
|
|
||||||
class SSH(object):
|
class SSH(object):
|
||||||
class Protocol(object):
|
class Protocol(object):
|
||||||
|
SMSG_PUBLIC_KEY = 2
|
||||||
MSG_KEXINIT = 20
|
MSG_KEXINIT = 20
|
||||||
MSG_NEWKEYS = 21
|
MSG_NEWKEYS = 21
|
||||||
MSG_KEXDH_INIT = 30
|
MSG_KEXDH_INIT = 30
|
||||||
|
@ -516,6 +631,9 @@ class SSH(object):
|
||||||
}
|
}
|
||||||
|
|
||||||
class Socket(ReadBuf, WriteBuf):
|
class Socket(ReadBuf, WriteBuf):
|
||||||
|
class InsufficientReadException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
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):
|
||||||
|
@ -534,7 +652,8 @@ class SSH(object):
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def get_banner(self):
|
def get_banner(self, sshv=2):
|
||||||
|
banner = 'SSH-{0}-OpenSSH_7.3'.format('1.5' if sshv == 1 else '2.0')
|
||||||
rto = self.__sock.gettimeout()
|
rto = self.__sock.gettimeout()
|
||||||
self.__sock.settimeout(0.7)
|
self.__sock.settimeout(0.7)
|
||||||
s, e = self.recv()
|
s, e = self.recv()
|
||||||
|
@ -542,7 +661,7 @@ class SSH(object):
|
||||||
if s < 0:
|
if s < 0:
|
||||||
return self.__banner, self.__header
|
return self.__banner, self.__header
|
||||||
if self.__state < self.SM_BANNER_SENT:
|
if self.__state < self.SM_BANNER_SENT:
|
||||||
self.send_banner()
|
self.send_banner(banner)
|
||||||
while self.__banner is None:
|
while self.__banner is None:
|
||||||
if not s > 0:
|
if not s > 0:
|
||||||
s, e = self.recv()
|
s, e = self.recv()
|
||||||
|
@ -586,41 +705,67 @@ 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):
|
||||||
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
|
||||||
|
|
||||||
def read_packet(self):
|
def ensure_read(self, size):
|
||||||
while self.unread_len < self.__block_size:
|
while self.unread_len < size:
|
||||||
s, e = self.recv()
|
s, e = self.recv()
|
||||||
if s < 0:
|
if s < 0:
|
||||||
if e is None:
|
raise SSH.Socket.InsufficientReadException(e)
|
||||||
e = self.read(self.unread_len).strip()
|
|
||||||
return -1, e
|
def read_packet(self, sshv=2):
|
||||||
header = self.read(self.__block_size)
|
try:
|
||||||
if len(header) == 0:
|
header = WriteBuf()
|
||||||
out.fail('[exception] empty ssh packet (no data)')
|
self.ensure_read(4)
|
||||||
sys.exit(1)
|
packet_length = self.read_int()
|
||||||
packet_size = struct.unpack('>I', header[:4])[0]
|
header.write_int(packet_length)
|
||||||
rest = header[4:]
|
# XXX: validate length
|
||||||
lrest = len(rest)
|
if sshv == 1:
|
||||||
padding = ord(rest[0:1])
|
padding_length = (8 - packet_length % 8)
|
||||||
packet_type = ord(rest[1:2])
|
self.ensure_read(padding_length)
|
||||||
if (packet_size - lrest) % self.__block_size != 0:
|
padding = self.read(padding_length)
|
||||||
|
header.write(padding)
|
||||||
|
payload_length = packet_length
|
||||||
|
check_size = padding_length + payload_length
|
||||||
|
else:
|
||||||
|
self.ensure_read(1)
|
||||||
|
padding_length = self.read_byte()
|
||||||
|
header.write_byte(padding_length)
|
||||||
|
payload_length = packet_length - padding_length - 1
|
||||||
|
check_size = 4 + 1 + payload_length + padding_length
|
||||||
|
if check_size % self.__block_size != 0:
|
||||||
out.fail('[exception] invalid ssh packet (block size)')
|
out.fail('[exception] invalid ssh packet (block size)')
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
rlen = packet_size - lrest
|
self.ensure_read(payload_length)
|
||||||
while self.unread_len < rlen:
|
if sshv == 1:
|
||||||
s, e = self.recv()
|
payload = self.read(payload_length - 4)
|
||||||
if s < 0:
|
header.write(payload)
|
||||||
if e is None:
|
crc = self.read_int()
|
||||||
e = (header + self.read(self.unread_len)).strip()
|
header.write_int(crc)
|
||||||
return -1, e
|
else:
|
||||||
buf = self.read(rlen)
|
payload = self.read(payload_length)
|
||||||
packet = rest[2:] + buf[0:packet_size - lrest]
|
header.write(payload)
|
||||||
payload = packet[0:packet_size - padding]
|
packet_type = ord(payload[0:1])
|
||||||
|
if sshv == 1:
|
||||||
|
rcrc = SSH1.crc32(padding + payload)
|
||||||
|
if crc != rcrc:
|
||||||
|
out.fail('[exception] packet checksum CRC32 mismatch.')
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
self.ensure_read(padding_length)
|
||||||
|
padding = self.read(padding_length)
|
||||||
|
payload = payload[1:]
|
||||||
return packet_type, payload
|
return packet_type, payload
|
||||||
|
except SSH.Socket.InsufficientReadException as ex:
|
||||||
|
if ex.args[0] is None:
|
||||||
|
header.write(self.read(self.unread_len))
|
||||||
|
e = header.write_flush().strip()
|
||||||
|
else:
|
||||||
|
e = ex.args[0]
|
||||||
|
return (-1, e)
|
||||||
|
|
||||||
def send_packet(self):
|
def send_packet(self):
|
||||||
payload = self.write_flush()
|
payload = self.write_flush()
|
||||||
|
@ -961,13 +1106,14 @@ def output_security(banner, padlen):
|
||||||
out.sep()
|
out.sep()
|
||||||
|
|
||||||
|
|
||||||
def output(banner, header, kex):
|
def output(banner, header, kex=None, pkm=None):
|
||||||
|
sshv = 1 if pkm else 2
|
||||||
with OutputBuffer() as obuf:
|
with OutputBuffer() as obuf:
|
||||||
if len(header) > 0:
|
if len(header) > 0:
|
||||||
out.info('(gen) header: ' + '\n'.join(header))
|
out.info('(gen) header: ' + '\n'.join(header))
|
||||||
if banner is not None:
|
if banner is not None:
|
||||||
out.good('(gen) banner: {0}'.format(banner))
|
out.good('(gen) banner: {0}'.format(banner))
|
||||||
if banner.protocol[0] == 1:
|
if sshv == 1 or banner.protocol[0] == 1:
|
||||||
out.fail('(gen) protocol SSH1 enabled')
|
out.fail('(gen) protocol SSH1 enabled')
|
||||||
software = SSH.Software.parse(banner)
|
software = SSH.Software.parse(banner)
|
||||||
if software is not None:
|
if software is not None:
|
||||||
|
@ -1045,26 +1191,44 @@ def parse_args():
|
||||||
return host, port
|
return host, port
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def audit(host, port, sshv=2):
|
||||||
host, port = parse_args()
|
|
||||||
s = SSH.Socket(host, port)
|
s = SSH.Socket(host, port)
|
||||||
err = None
|
err = None
|
||||||
banner, header = s.get_banner()
|
banner, header = s.get_banner(sshv)
|
||||||
if banner is None:
|
if banner is None:
|
||||||
err = '[exception] did not receive banner.'
|
err = '[exception] did not receive banner.'
|
||||||
if err is None:
|
if err is None:
|
||||||
packet_type, payload = s.read_packet()
|
packet_type, payload = s.read_packet(sshv)
|
||||||
if packet_type < 0:
|
if packet_type < 0:
|
||||||
|
if payload == b'Protocol major versions differ.':
|
||||||
|
if sshv == 2:
|
||||||
|
audit(host, port, 1)
|
||||||
|
return
|
||||||
err = '[exception] error reading packet ({0})'.format(payload)
|
err = '[exception] error reading packet ({0})'.format(payload)
|
||||||
elif packet_type != SSH.Protocol.MSG_KEXINIT:
|
else:
|
||||||
err = '[exception] did not receive MSG_KEXINIT (20), ' + \
|
if sshv == 1 and packet_type != SSH.Protocol.SMSG_PUBLIC_KEY:
|
||||||
'instead received unknown message ({0})'.format(packet_type)
|
err = ('SMSG_PUBLIC_KEY', SSH.Protocol.SMSG_PUBLIC_KEY)
|
||||||
|
elif sshv == 2 and packet_type != SSH.Protocol.MSG_KEXINIT:
|
||||||
|
err = ('MSG_KEXINIT', SSH.Protocol.MSG_KEXINIT)
|
||||||
|
if err is not None:
|
||||||
|
fmt = '[exception] did not receive {0} ({1}), ' + \
|
||||||
|
'instead received unknown message ({2})'
|
||||||
|
err = fmt.format(err[0], err[1], packet_type)
|
||||||
if err:
|
if err:
|
||||||
output(banner, header, None)
|
output(banner, header)
|
||||||
out.fail(err)
|
out.fail(err)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
if sshv == 1:
|
||||||
|
pkm = SSH1.PublicKeyMessage.parse(payload)
|
||||||
|
output(banner, header, pkm=pkm)
|
||||||
|
elif sshv == 2:
|
||||||
kex = Kex.parse(payload)
|
kex = Kex.parse(payload)
|
||||||
output(banner, header, kex)
|
output(banner, header, kex=kex)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
host, port = parse_args()
|
||||||
|
audit(host, port)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
Loading…
Reference in New Issue