From dd3ca9688e4f0a8d6fc06f6c006dc92617ec9d27 Mon Sep 17 00:00:00 2001 From: Andris Raugulis Date: Wed, 26 Oct 2016 19:14:03 +0300 Subject: [PATCH 001/103] Back to development version. --- ssh-audit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ssh-audit.py b/ssh-audit.py index 8b67387..bf43e95 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -26,7 +26,7 @@ from __future__ import print_function import os, io, sys, socket, struct, random, errno, getopt, re, hashlib, base64 -VERSION = 'v1.7.0' +VERSION = 'v1.7.1.dev' if sys.version_info >= (3,): # pragma: nocover StringIO, BytesIO = io.StringIO, io.BytesIO From 44c1d4827c73cf2e0dbd63a61734977c429ba96f Mon Sep 17 00:00:00 2001 From: Andris Raugulis Date: Wed, 2 Nov 2016 13:00:24 +0200 Subject: [PATCH 002/103] Specify error when couldn't get banner. Test for timeout and retry cases. --- ssh-audit.py | 16 ++++--- test/test_errors.py | 106 ++++++++++++++++++++++++++++---------------- 2 files changed, 78 insertions(+), 44 deletions(-) diff --git a/ssh-audit.py b/ssh-audit.py index bf43e95..0dc8bb1 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -1141,16 +1141,17 @@ class SSH(object): # pylint: disable=too-few-public-methods sys.exit(1) def get_banner(self, sshv=2): - # type: (int) -> Tuple[Optional[SSH.Banner], List[text_type]] + # type: (int) -> Tuple[Optional[SSH.Banner], List[text_type], Optional[str]] banner = 'SSH-{0}-OpenSSH_7.3'.format('1.5' if sshv == 1 else '2.0') rto = self.__sock.gettimeout() self.__sock.settimeout(0.7) s, e = self.recv() self.__sock.settimeout(rto) if s < 0: - return self.__banner, self.__header + return self.__banner, self.__header, e if self.__state < self.SM_BANNER_SENT: self.send_banner(banner) + e = None while self.__banner is None: if not s > 0: s, e = self.recv() @@ -1166,14 +1167,14 @@ class SSH(object): # pylint: disable=too-few-public-methods continue self.__header.append(line) s = 0 - return self.__banner, self.__header + return self.__banner, self.__header, e def recv(self, size=2048): # type: (int) -> Tuple[int, Optional[str]] try: data = self.__sock.recv(size) except socket.timeout: - return (-1, 'timeout') + return (-1, 'timed out') except socket.error as e: if e.args[0] in (errno.EAGAIN, errno.EWOULDBLOCK): return (0, 'retry') @@ -1971,9 +1972,12 @@ def audit(aconf, sshv=None): if sshv is None: sshv = 2 if aconf.ssh2 else 1 err = None - banner, header = s.get_banner(sshv) + banner, header, err = s.get_banner(sshv) if banner is None: - err = '[exception] did not receive banner.' + if err is None: + err = '[exception] did not receive banner.' + else: + err = '[exception] did not receive banner: {0}'.format(err) if err is None: packet_type, payload = s.read_packet(sshv) if packet_type < 0: diff --git a/test/test_errors.py b/test/test_errors.py index ad35a54..e37f60e 100644 --- a/test/test_errors.py +++ b/test/test_errors.py @@ -17,46 +17,91 @@ class TestErrors(object): conf.batch = True return conf + def _audit(self, spy, conf=None, sysexit=True): + if conf is None: + conf = self._conf() + spy.begin() + if sysexit: + with pytest.raises(SystemExit): + self.audit(conf) + else: + self.audit(conf) + lines = spy.flush() + return lines + def test_connection_refused(self, output_spy, virtual_socket): vsocket = virtual_socket vsocket.errors['connect'] = socket.error(61, 'Connection refused') - output_spy.begin() - with pytest.raises(SystemExit): - self.audit(self._conf()) - lines = output_spy.flush() + lines = self._audit(output_spy) assert len(lines) == 1 assert 'Connection refused' in lines[-1] + def test_connection_timeout(self, output_spy, virtual_socket): + vsocket = virtual_socket + vsocket.errors['connect'] = socket.timeout('timed out') + lines = self._audit(output_spy) + assert len(lines) == 1 + assert 'timed out' in lines[-1] + + def test_recv_empty(self, output_spy, virtual_socket): + vsocket = virtual_socket + lines = self._audit(output_spy) + assert len(lines) == 1 + assert 'did not receive banner' in lines[-1] + + def test_recv_timeout(self, output_spy, virtual_socket): + vsocket = virtual_socket + vsocket.rdata.append(socket.timeout('timed out')) + lines = self._audit(output_spy) + assert len(lines) == 1 + assert 'did not receive banner' in lines[-1] + assert 'timed out' in lines[-1] + + def test_recv_retry_till_timeout(self, output_spy, virtual_socket): + vsocket = virtual_socket + vsocket.rdata.append(socket.error(35, 'Resource temporarily unavailable')) + vsocket.rdata.append(socket.error(35, 'Resource temporarily unavailable')) + vsocket.rdata.append(socket.error(35, 'Resource temporarily unavailable')) + vsocket.rdata.append(socket.timeout('timed out')) + lines = self._audit(output_spy) + assert len(lines) == 1 + assert 'did not receive banner' in lines[-1] + assert 'timed out' in lines[-1] + + def test_recv_retry_till_reset(self, output_spy, virtual_socket): + vsocket = virtual_socket + vsocket.rdata.append(socket.error(35, 'Resource temporarily unavailable')) + vsocket.rdata.append(socket.error(35, 'Resource temporarily unavailable')) + vsocket.rdata.append(socket.error(35, 'Resource temporarily unavailable')) + vsocket.rdata.append(socket.error(54, 'Connection reset by peer')) + lines = self._audit(output_spy) + assert len(lines) == 1 + assert 'did not receive banner' in lines[-1] + assert 'reset by peer' in lines[-1] + def test_connection_closed_before_banner(self, output_spy, virtual_socket): vsocket = virtual_socket vsocket.rdata.append(socket.error(54, 'Connection reset by peer')) - output_spy.begin() - with pytest.raises(SystemExit): - self.audit(self._conf()) - lines = output_spy.flush() + lines = self._audit(output_spy) assert len(lines) == 1 assert 'did not receive banner' in lines[-1] + assert 'reset by peer' in lines[-1] def test_connection_closed_after_header(self, output_spy, virtual_socket): vsocket = virtual_socket vsocket.rdata.append(b'header line 1\n') vsocket.rdata.append(b'header line 2\n') vsocket.rdata.append(socket.error(54, 'Connection reset by peer')) - output_spy.begin() - with pytest.raises(SystemExit): - self.audit(self._conf()) - lines = output_spy.flush() + lines = self._audit(output_spy) assert len(lines) == 3 assert 'did not receive banner' in lines[-1] + assert 'reset by peer' in lines[-1] def test_connection_closed_after_banner(self, output_spy, virtual_socket): vsocket = virtual_socket vsocket.rdata.append(b'SSH-2.0-ssh-audit-test\r\n') vsocket.rdata.append(socket.error(54, 'Connection reset by peer')) - output_spy.begin() - with pytest.raises(SystemExit): - self.audit(self._conf()) - lines = output_spy.flush() + lines = self._audit(output_spy) assert len(lines) == 2 assert 'error reading packet' in lines[-1] assert 'reset by peer' in lines[-1] @@ -64,10 +109,7 @@ class TestErrors(object): def test_empty_data_after_banner(self, output_spy, virtual_socket): vsocket = virtual_socket vsocket.rdata.append(b'SSH-2.0-ssh-audit-test\r\n') - output_spy.begin() - with pytest.raises(SystemExit): - self.audit(self._conf()) - lines = output_spy.flush() + lines = self._audit(output_spy) assert len(lines) == 2 assert 'error reading packet' in lines[-1] assert 'empty' in lines[-1] @@ -76,10 +118,7 @@ class TestErrors(object): vsocket = virtual_socket vsocket.rdata.append(b'SSH-2.0-ssh-audit-test\r\n') vsocket.rdata.append(b'xxx\n') - output_spy.begin() - with pytest.raises(SystemExit): - self.audit(self._conf()) - lines = output_spy.flush() + lines = self._audit(output_spy) assert len(lines) == 2 assert 'error reading packet' in lines[-1] assert 'xxx' in lines[-1] @@ -87,10 +126,7 @@ class TestErrors(object): def test_non_ascii_banner(self, output_spy, virtual_socket): vsocket = virtual_socket vsocket.rdata.append(b'SSH-2.0-ssh-audit-test\xc3\xbc\r\n') - output_spy.begin() - with pytest.raises(SystemExit): - self.audit(self._conf()) - lines = output_spy.flush() + lines = self._audit(output_spy) assert len(lines) == 3 assert 'error reading packet' in lines[-1] assert 'ASCII' in lines[-2] @@ -100,10 +136,7 @@ class TestErrors(object): vsocket = virtual_socket vsocket.rdata.append(b'SSH-2.0-ssh-audit-test\r\n') vsocket.rdata.append(b'\x81\xff\n') - output_spy.begin() - with pytest.raises(SystemExit): - self.audit(self._conf()) - lines = output_spy.flush() + lines = self._audit(output_spy) assert len(lines) == 2 assert 'error reading packet' in lines[-1] assert '\\x81\\xff' in lines[-1] @@ -112,12 +145,9 @@ class TestErrors(object): vsocket = virtual_socket vsocket.rdata.append(b'SSH-1.3-ssh-audit-test\r\n') vsocket.rdata.append(b'Protocol major versions differ.\n') - output_spy.begin() - with pytest.raises(SystemExit): - conf = self._conf() - conf.ssh1, conf.ssh2 = True, False - self.audit(conf) - lines = output_spy.flush() + conf = self._conf() + conf.ssh1, conf.ssh2 = True, False + lines = self._audit(output_spy, conf) assert len(lines) == 3 assert 'error reading packet' in lines[-1] assert 'major versions differ' in lines[-1] From b3ed4c7715577851c81971b52083591a0e2d267d Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Wed, 2 Nov 2016 10:45:56 +0000 Subject: [PATCH 003/103] Add LICENSE file (#22) Create LICENSE --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0eb1032 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (C) 2016 Andris Raugulis (moo@arthepsy.eu) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From 11b6155c64e66f53acdcb00349f2b29b46a52fab Mon Sep 17 00:00:00 2001 From: Andris Raugulis Date: Wed, 2 Nov 2016 13:18:03 +0200 Subject: [PATCH 004/103] Use Python defined error numbers. --- test/test_errors.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/test/test_errors.py b/test/test_errors.py index e37f60e..4f3d6cc 100644 --- a/test/test_errors.py +++ b/test/test_errors.py @@ -1,6 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- import socket +import errno import pytest @@ -31,7 +32,7 @@ class TestErrors(object): def test_connection_refused(self, output_spy, virtual_socket): vsocket = virtual_socket - vsocket.errors['connect'] = socket.error(61, 'Connection refused') + vsocket.errors['connect'] = socket.error(errno.ECONNREFUSED, 'Connection refused') lines = self._audit(output_spy) assert len(lines) == 1 assert 'Connection refused' in lines[-1] @@ -59,9 +60,9 @@ class TestErrors(object): def test_recv_retry_till_timeout(self, output_spy, virtual_socket): vsocket = virtual_socket - vsocket.rdata.append(socket.error(35, 'Resource temporarily unavailable')) - vsocket.rdata.append(socket.error(35, 'Resource temporarily unavailable')) - vsocket.rdata.append(socket.error(35, 'Resource temporarily unavailable')) + vsocket.rdata.append(socket.error(errno.EAGAIN, 'Resource temporarily unavailable')) + vsocket.rdata.append(socket.error(errno.EWOULDBLOCK, 'Resource temporarily unavailable')) + vsocket.rdata.append(socket.error(errno.EAGAIN, 'Resource temporarily unavailable')) vsocket.rdata.append(socket.timeout('timed out')) lines = self._audit(output_spy) assert len(lines) == 1 @@ -70,10 +71,10 @@ class TestErrors(object): def test_recv_retry_till_reset(self, output_spy, virtual_socket): vsocket = virtual_socket - vsocket.rdata.append(socket.error(35, 'Resource temporarily unavailable')) - vsocket.rdata.append(socket.error(35, 'Resource temporarily unavailable')) - vsocket.rdata.append(socket.error(35, 'Resource temporarily unavailable')) - vsocket.rdata.append(socket.error(54, 'Connection reset by peer')) + vsocket.rdata.append(socket.error(errno.EAGAIN, 'Resource temporarily unavailable')) + vsocket.rdata.append(socket.error(errno.EWOULDBLOCK, 'Resource temporarily unavailable')) + vsocket.rdata.append(socket.error(errno.EAGAIN, 'Resource temporarily unavailable')) + vsocket.rdata.append(socket.error(errno.ECONNRESET, 'Connection reset by peer')) lines = self._audit(output_spy) assert len(lines) == 1 assert 'did not receive banner' in lines[-1] @@ -81,7 +82,7 @@ class TestErrors(object): def test_connection_closed_before_banner(self, output_spy, virtual_socket): vsocket = virtual_socket - vsocket.rdata.append(socket.error(54, 'Connection reset by peer')) + vsocket.rdata.append(socket.error(errno.ECONNRESET, 'Connection reset by peer')) lines = self._audit(output_spy) assert len(lines) == 1 assert 'did not receive banner' in lines[-1] @@ -91,7 +92,7 @@ class TestErrors(object): vsocket = virtual_socket vsocket.rdata.append(b'header line 1\n') vsocket.rdata.append(b'header line 2\n') - vsocket.rdata.append(socket.error(54, 'Connection reset by peer')) + vsocket.rdata.append(socket.error(errno.ECONNRESET, 'Connection reset by peer')) lines = self._audit(output_spy) assert len(lines) == 3 assert 'did not receive banner' in lines[-1] From 5bb0ae0ceb2acd9a37e8ea9c500e49500afa1d47 Mon Sep 17 00:00:00 2001 From: Andris Raugulis Date: Wed, 2 Nov 2016 18:23:55 +0200 Subject: [PATCH 005/103] Rework is/to ASCII and implement printable ASCII is/to functions. Add Utils tests. --- ssh-audit.py | 64 ++++++++++--- test/test_utils.py | 218 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 269 insertions(+), 13 deletions(-) create mode 100644 test/test_utils.py diff --git a/ssh-audit.py b/ssh-audit.py index 0dc8bb1..9100a31 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -1914,28 +1914,58 @@ class Utils(object): if isinstance(v, str): return v elif isinstance(v, text_type): - return v.encode(enc) + return v.encode(enc) # PY2 only elif isinstance(v, binary_type): - return v.decode(enc) + return v.decode(enc) # PY3 only raise cls._type_err(v, 'native text') + @classmethod + def _is_ascii(cls, v, char_filter=lambda x: x <= 127): + # type: (Union[text_type, str], Callable[[int], bool]) -> bool + r = False + if isinstance(v, (text_type, str)): + for c in v: + i = cls.ctoi(c) + if not char_filter(i): + return r + r = True + return r + + @classmethod + def _to_ascii(cls, v, char_filter=lambda x: x <= 127, errors='replace'): + # type: (Union[text_type, str], Callable[[int], bool], str) -> str + if isinstance(v, (text_type, str)): + r = bytearray() + for c in v: + i = cls.ctoi(c) + if char_filter(i): + r.append(i) + else: + if errors == 'ignore': + continue + r.append(63) + return cls.to_ntext(r.decode('ascii')) + raise cls._type_err(v, 'ascii') + @classmethod def is_ascii(cls, v): # type: (Union[text_type, str]) -> bool - try: - if isinstance(v, (text_type, str)): - v.encode('ascii') - return True - except UnicodeEncodeError: - pass - return False + return cls._is_ascii(v) @classmethod def to_ascii(cls, v, errors='replace'): # type: (Union[text_type, str], str) -> str - if isinstance(v, (text_type, str)): - return cls.to_ntext(v.encode('ascii', errors)) - raise cls._type_err(v, 'ascii') + return cls._to_ascii(v, errors=errors) + + @classmethod + def is_print_ascii(cls, v): + # type: (Union[text_type, str]) -> bool + return cls._is_ascii(v, lambda x: x >= 32 and x <= 126) + + @classmethod + def to_print_ascii(cls, v, errors='replace'): + # type: (Union[text_type, str], str) -> str + return cls._to_ascii(v, lambda x: x >= 32 and x <= 126, errors) @classmethod def unique_seq(cls, seq): @@ -1951,7 +1981,15 @@ class Utils(object): return tuple(x for x in seq if x not in seen and not _seen_add(x)) else: return [x for x in seq if x not in seen and not _seen_add(x)] - + + @classmethod + def ctoi(cls, c): + # type: (Union[text_type, str, int]) -> int + if isinstance(c, (text_type, str)): + return ord(c[0]) + else: + return c + @staticmethod def parse_int(v): # type: (Any) -> int diff --git a/test/test_utils.py b/test/test_utils.py new file mode 100644 index 0000000..64cb07b --- /dev/null +++ b/test/test_utils.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import sys +import pytest + + +# pylint: disable=attribute-defined-outside-init +class TestUtils(object): + @pytest.fixture(autouse=True) + def init(self, ssh_audit): + self.utils = ssh_audit.Utils + self.PY3 = sys.version_info >= (3,) + + def test_to_bytes_py2(self): + if self.PY3: + return + # binary_type (native str, bytes as str) + assert self.utils.to_bytes('fran\xc3\xa7ais') == 'fran\xc3\xa7ais' + assert self.utils.to_bytes(b'fran\xc3\xa7ais') == 'fran\xc3\xa7ais' + # text_type (unicode) + assert self.utils.to_bytes(u'fran\xe7ais') == 'fran\xc3\xa7ais' + # other + with pytest.raises(TypeError): + self.utils.to_bytes(123) + + def test_to_bytes_py3(self): + if not self.PY3: + return + # binary_type (bytes) + assert self.utils.to_bytes(b'fran\xc3\xa7ais') == b'fran\xc3\xa7ais' + # text_type (native str as unicode, unicode) + assert self.utils.to_bytes('fran\xe7ais') == b'fran\xc3\xa7ais' + assert self.utils.to_bytes(u'fran\xe7ais') == b'fran\xc3\xa7ais' + # other + with pytest.raises(TypeError): + self.utils.to_bytes(123) + + def test_to_utext_py2(self): + if self.PY3: + return + # binary_type (native str, bytes as str) + assert self.utils.to_utext('fran\xc3\xa7ais') == u'fran\xe7ais' + assert self.utils.to_utext(b'fran\xc3\xa7ais') == u'fran\xe7ais' + # text_type (unicode) + assert self.utils.to_utext(u'fran\xe7ais') == u'fran\xe7ais' + # other + with pytest.raises(TypeError): + self.utils.to_utext(123) + + def test_to_utext_py3(self): + if not self.PY3: + return + # binary_type (bytes) + assert self.utils.to_utext(b'fran\xc3\xa7ais') == u'fran\xe7ais' + # text_type (native str as unicode, unicode) + assert self.utils.to_utext('fran\xe7ais') == 'fran\xe7ais' + assert self.utils.to_utext(u'fran\xe7ais') == u'fran\xe7ais' + # other + with pytest.raises(TypeError): + self.utils.to_utext(123) + + def test_to_ntext_py2(self): + if self.PY3: + return + # str (native str, bytes as str) + assert self.utils.to_ntext('fran\xc3\xa7ais') == 'fran\xc3\xa7ais' + assert self.utils.to_ntext(b'fran\xc3\xa7ais') == 'fran\xc3\xa7ais' + # text_type (unicode) + assert self.utils.to_ntext(u'fran\xe7ais') == 'fran\xc3\xa7ais' + # other + with pytest.raises(TypeError): + self.utils.to_ntext(123) + + def test_to_ntext_py3(self): + if not self.PY3: + return + # str (native str) + assert self.utils.to_ntext('fran\xc3\xa7ais') == 'fran\xc3\xa7ais' + assert self.utils.to_ntext(u'fran\xe7ais') == 'fran\xe7ais' + # binary_type (bytes) + assert self.utils.to_utext(b'fran\xc3\xa7ais') == 'fran\xe7ais' + # other + with pytest.raises(TypeError): + self.utils.to_ntext(123) + + def test_is_ascii_py2(self): + if self.PY3: + return + # text_type (unicode) + assert self.utils.is_ascii(u'francais') is True + assert self.utils.is_ascii(u'fran\xe7ais') is False + # str + assert self.utils.is_ascii('francais') is True + assert self.utils.is_ascii('fran\xc3\xa7ais') is False + # other + assert self.utils.is_ascii(123) is False + + def test_is_ascii_py3(self): + if not self.PY3: + return + # text_type (str) + assert self.utils.is_ascii('francais') is True + assert self.utils.is_ascii(u'francais') is True + assert self.utils.is_ascii('fran\xe7ais') is False + assert self.utils.is_ascii(u'fran\xe7ais') is False + # other + assert self.utils.is_ascii(123) is False + + def test_to_ascii_py2(self): + if self.PY3: + return + # text_type (unicode) + assert self.utils.to_ascii(u'francais') == 'francais' + assert self.utils.to_ascii(u'fran\xe7ais') == 'fran?ais' + assert self.utils.to_ascii(u'fran\xe7ais', 'ignore') == 'franais' + # str + assert self.utils.to_ascii('francais') == 'francais' + assert self.utils.to_ascii('fran\xc3\xa7ais') == 'fran??ais' + assert self.utils.to_ascii('fran\xc3\xa7ais', 'ignore') == 'franais' + with pytest.raises(TypeError): + self.utils.to_ascii(123) + + def test_to_ascii_py3(self): + if not self.PY3: + return + # text_type (str) + assert self.utils.to_ascii('francais') == 'francais' + assert self.utils.to_ascii(u'francais') == 'francais' + assert self.utils.to_ascii('fran\xe7ais') == 'fran?ais' + assert self.utils.to_ascii('fran\xe7ais', 'ignore') == 'franais' + assert self.utils.to_ascii(u'fran\xe7ais') == 'fran?ais' + assert self.utils.to_ascii(u'fran\xe7ais', 'ignore') == 'franais' + with pytest.raises(TypeError): + self.utils.to_ascii(123) + + def test_is_print_ascii_py2(self): + if self.PY3: + return + # text_type (unicode) + assert self.utils.is_print_ascii(u'francais') is True + assert self.utils.is_print_ascii(u'francais\n') is False + assert self.utils.is_print_ascii(u'fran\xe7ais') is False + assert self.utils.is_print_ascii(u'fran\xe7ais\n') is False + # str + assert self.utils.is_print_ascii('francais') is True + assert self.utils.is_print_ascii('francais\n') is False + assert self.utils.is_print_ascii('fran\xc3\xa7ais') is False + # other + assert self.utils.is_print_ascii(123) is False + + def test_is_print_ascii_py3(self): + if not self.PY3: + return + # text_type (str) + assert self.utils.is_print_ascii('francais') is True + assert self.utils.is_print_ascii('francais\n') is False + assert self.utils.is_print_ascii(u'francais') is True + assert self.utils.is_print_ascii(u'francais\n') is False + assert self.utils.is_print_ascii('fran\xe7ais') is False + assert self.utils.is_print_ascii(u'fran\xe7ais') is False + # other + assert self.utils.is_print_ascii(123) is False + + def test_to_print_ascii_py2(self): + if self.PY3: + return + # text_type (unicode) + assert self.utils.to_print_ascii(u'francais') == 'francais' + assert self.utils.to_print_ascii(u'francais\n') == 'francais?' + assert self.utils.to_print_ascii(u'fran\xe7ais') == 'fran?ais' + assert self.utils.to_print_ascii(u'fran\xe7ais\n') == 'fran?ais?' + assert self.utils.to_print_ascii(u'fran\xe7ais', 'ignore') == 'franais' + assert self.utils.to_print_ascii(u'fran\xe7ais\n', 'ignore') == 'franais' + # str + assert self.utils.to_print_ascii('francais') == 'francais' + assert self.utils.to_print_ascii('francais\n') == 'francais?' + assert self.utils.to_print_ascii('fran\xc3\xa7ais') == 'fran??ais' + assert self.utils.to_print_ascii('fran\xc3\xa7ais\n') == 'fran??ais?' + assert self.utils.to_print_ascii('fran\xc3\xa7ais', 'ignore') == 'franais' + assert self.utils.to_print_ascii('fran\xc3\xa7ais\n', 'ignore') == 'franais' + with pytest.raises(TypeError): + self.utils.to_print_ascii(123) + + def test_to_print_ascii_py3(self): + if not self.PY3: + return + # text_type (str) + assert self.utils.to_print_ascii('francais') == 'francais' + assert self.utils.to_print_ascii('francais\n') == 'francais?' + assert self.utils.to_print_ascii(u'francais') == 'francais' + assert self.utils.to_print_ascii(u'francais\n') == 'francais?' + assert self.utils.to_print_ascii('fran\xe7ais') == 'fran?ais' + assert self.utils.to_print_ascii('fran\xe7ais\n') == 'fran?ais?' + assert self.utils.to_print_ascii('fran\xe7ais', 'ignore') == 'franais' + assert self.utils.to_print_ascii('fran\xe7ais\n', 'ignore') == 'franais' + assert self.utils.to_print_ascii(u'fran\xe7ais') == 'fran?ais' + assert self.utils.to_print_ascii(u'fran\xe7ais\n') == 'fran?ais?' + assert self.utils.to_print_ascii(u'fran\xe7ais', 'ignore') == 'franais' + assert self.utils.to_print_ascii(u'fran\xe7ais\n', 'ignore') == 'franais' + with pytest.raises(TypeError): + self.utils.to_print_ascii(123) + + def test_ctoi(self): + assert self.utils.ctoi(123) == 123 + assert self.utils.ctoi('ABC') == 65 + + def test_parse_int(self): + assert self.utils.parse_int(123) == 123 + assert self.utils.parse_int('123') == 123 + assert self.utils.parse_int(-123) == -123 + assert self.utils.parse_int('-123') == -123 + assert self.utils.parse_int('abc') == 0 + + def test_unique_seq(self): + assert self.utils.unique_seq((1, 2, 2, 3, 3, 3)) == (1, 2, 3) + assert self.utils.unique_seq((3, 3, 3, 2, 2, 1)) == (3, 2, 1) + assert self.utils.unique_seq([1, 2, 2, 3, 3, 3]) == [1, 2, 3] + assert self.utils.unique_seq([3, 3, 3, 2, 2, 1]) == [3, 2, 1] From 6c4b9fcadfe7041be4a0620d1e6e59c3c396e27d Mon Sep 17 00:00:00 2001 From: Andris Raugulis Date: Wed, 2 Nov 2016 18:25:13 +0200 Subject: [PATCH 006/103] Banner should be in printable ASCII, not the whole ASCII space. --- ssh-audit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ssh-audit.py b/ssh-audit.py index 9100a31..958d995 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -1005,8 +1005,8 @@ class SSH(object): # pylint: disable=too-few-public-methods @classmethod def parse(cls, banner): # type: (text_type) -> SSH.Banner - valid_ascii = utils.is_ascii(banner) - ascii_banner = utils.to_ascii(banner) + valid_ascii = utils.is_print_ascii(banner) + ascii_banner = utils.to_print_ascii(banner) mx = cls.RX_BANNER.match(ascii_banner) if mx is None: return None From 6fde896d77a43b40c27894954a394d06a9cc1cc0 Mon Sep 17 00:00:00 2001 From: Andris Raugulis Date: Wed, 2 Nov 2016 19:28:16 +0200 Subject: [PATCH 007/103] Add resolve tests. --- test/conftest.py | 54 ++++++++++++++++++++-------- test/test_errors.py | 8 +++++ test/test_resolve.py | 85 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 133 insertions(+), 14 deletions(-) create mode 100644 test/test_resolve.py diff --git a/test/conftest.py b/test/conftest.py index 524c0fa..0bc4124 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -40,6 +40,41 @@ def output_spy(): return _OutputSpy() +class _VirtualGlobalSocket(object): + def __init__(self, vsocket): + self.vsocket = vsocket + self.addrinfodata = {} + + # pylint: disable=unused-argument + def create_connection(self, address, timeout=0, source_address=None): + # pylint: disable=protected-access + return self.vsocket._connect(address, True) + + # pylint: disable=unused-argument + def socket(self, + family=socket.AF_INET, + socktype=socket.SOCK_STREAM, + proto=0, + fileno=None): + return self.vsocket + + def getaddrinfo(self, host, port, family=0, socktype=0, proto=0, flags=0): + key = '{0}#{1}'.format(host, port) + if key in self.addrinfodata: + data = self.addrinfodata[key] + if isinstance(data, Exception): + raise data + return data + if host == 'localhost': + r = [] + if family in (0, socket.AF_INET): + r.append((socket.AF_INET, 1, 6, '', ('127.0.0.1', port))) + if family in (0, socket.AF_INET6): + r.append((socket.AF_INET6, 1, 6, '', ('::1', port))) + return r + return [] + + class _VirtualSocket(object): def __init__(self): self.sock_address = ('127.0.0.1', 0) @@ -49,6 +84,7 @@ class _VirtualSocket(object): self.rdata = [] self.sdata = [] self.errors = {} + self.gsock = _VirtualGlobalSocket(self) def _check_err(self, method): method_error = self.errors.get(method) @@ -113,18 +149,8 @@ class _VirtualSocket(object): @pytest.fixture() def virtual_socket(monkeypatch): vsocket = _VirtualSocket() - - # pylint: disable=unused-argument - def _socket(family=socket.AF_INET, - socktype=socket.SOCK_STREAM, - proto=0, - fileno=None): - return vsocket - - def _cc(address, timeout=0, source_address=None): - # pylint: disable=protected-access - return vsocket._connect(address, True) - - monkeypatch.setattr(socket, 'create_connection', _cc) - monkeypatch.setattr(socket, 'socket', _socket) + gsock = vsocket.gsock + monkeypatch.setattr(socket, 'create_connection', gsock.create_connection) + monkeypatch.setattr(socket, 'socket', gsock.socket) + monkeypatch.setattr(socket, 'getaddrinfo', gsock.getaddrinfo) return vsocket diff --git a/test/test_errors.py b/test/test_errors.py index 4f3d6cc..abf720e 100644 --- a/test/test_errors.py +++ b/test/test_errors.py @@ -30,6 +30,13 @@ class TestErrors(object): lines = spy.flush() return lines + def test_connection_unresolved(self, output_spy, virtual_socket): + vsocket = virtual_socket + vsocket.gsock.addrinfodata['localhost#22'] = [] + lines = self._audit(output_spy) + assert len(lines) == 1 + assert 'has no DNS records' in lines[-1] + def test_connection_refused(self, output_spy, virtual_socket): vsocket = virtual_socket vsocket.errors['connect'] = socket.error(errno.ECONNREFUSED, 'Connection refused') @@ -91,6 +98,7 @@ class TestErrors(object): def test_connection_closed_after_header(self, output_spy, virtual_socket): vsocket = virtual_socket vsocket.rdata.append(b'header line 1\n') + vsocket.rdata.append(b'\n') vsocket.rdata.append(b'header line 2\n') vsocket.rdata.append(socket.error(errno.ECONNRESET, 'Connection reset by peer')) lines = self._audit(output_spy) diff --git a/test/test_resolve.py b/test/test_resolve.py new file mode 100644 index 0000000..8fcddf6 --- /dev/null +++ b/test/test_resolve.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import socket +import pytest + + +# pylint: disable=attribute-defined-outside-init,protected-access +class TestResolve(object): + @pytest.fixture(autouse=True) + def init(self, ssh_audit): + self.AuditConf = ssh_audit.AuditConf + self.audit = ssh_audit.audit + self.ssh = ssh_audit.SSH + + def _conf(self): + conf = self.AuditConf('localhost', 22) + conf.colors = False + conf.batch = True + return conf + + def test_resolve_error(self, output_spy, virtual_socket): + vsocket = virtual_socket + vsocket.gsock.addrinfodata['localhost#22'] = socket.gaierror(8, 'hostname nor servname provided, or not known') + s = self.ssh.Socket('localhost', 22) + conf = self._conf() + output_spy.begin() + with pytest.raises(SystemExit): + r = list(s._resolve(conf.ipvo)) + lines = output_spy.flush() + assert len(lines) == 1 + assert 'hostname nor servname provided' in lines[-1] + + def test_resolve_hostname_without_records(self, output_spy, virtual_socket): + vsocket = virtual_socket + vsocket.gsock.addrinfodata['localhost#22'] = [] + s = self.ssh.Socket('localhost', 22) + conf = self._conf() + output_spy.begin() + r = list(s._resolve(conf.ipvo)) + assert len(r) == 0 + + def test_resolve_ipv4(self, virtual_socket): + vsocket = virtual_socket + conf = self._conf() + conf.ipv4 = True + s = self.ssh.Socket('localhost', 22) + r = list(s._resolve(conf.ipvo)) + assert len(r) == 1 + assert r[0] == (socket.AF_INET, ('127.0.0.1', 22)) + + def test_resolve_ipv6(self, virtual_socket): + vsocket = virtual_socket + s = self.ssh.Socket('localhost', 22) + conf = self._conf() + conf.ipv6 = True + r = list(s._resolve(conf.ipvo)) + assert len(r) == 1 + assert r[0] == (socket.AF_INET6, ('::1', 22)) + + def test_resolve_ipv46_both(self, virtual_socket): + vsocket = virtual_socket + s = self.ssh.Socket('localhost', 22) + conf = self._conf() + r = list(s._resolve(conf.ipvo)) + assert len(r) == 2 + assert r[0] == (socket.AF_INET, ('127.0.0.1', 22)) + assert r[1] == (socket.AF_INET6, ('::1', 22)) + + def test_resolve_ipv46_order(self, virtual_socket): + vsocket = virtual_socket + s = self.ssh.Socket('localhost', 22) + conf = self._conf() + conf.ipv4 = True + conf.ipv6 = True + r = list(s._resolve(conf.ipvo)) + assert len(r) == 2 + assert r[0] == (socket.AF_INET, ('127.0.0.1', 22)) + assert r[1] == (socket.AF_INET6, ('::1', 22)) + conf = self._conf() + conf.ipv6 = True + conf.ipv4 = True + r = list(s._resolve(conf.ipvo)) + assert len(r) == 2 + assert r[0] == (socket.AF_INET6, ('::1', 22)) + assert r[1] == (socket.AF_INET, ('127.0.0.1', 22)) From 9a409e835ea4c1789a13f9011c2d9fa293e515ad Mon Sep 17 00:00:00 2001 From: Andris Raugulis Date: Thu, 3 Nov 2016 19:10:49 +0200 Subject: [PATCH 008/103] Refactor outer functions within classes. Use mypy strict optional checks and fix them. Use better comparison for compatiblity output. Add initial socket tests. --- ssh-audit.py | 796 ++++++++++++++++++++++++-------------------- test/mypy-py2.sh | 2 +- test/mypy-py3.sh | 2 +- test/test_socket.py | 41 +++ 4 files changed, 473 insertions(+), 368 deletions(-) create mode 100644 test/test_socket.py diff --git a/ssh-audit.py b/ssh-audit.py index 958d995..0d86e03 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -267,6 +267,117 @@ class OutputBuffer(list): class SSH2(object): # pylint: disable=too-few-public-methods + class KexDB(object): # pylint: disable=too-few-public-methods + # pylint: disable=bad-whitespace + WARN_OPENSSH72_LEGACY = 'disabled (in client) since OpenSSH 7.2, legacy algorithm' + FAIL_OPENSSH70_LEGACY = 'removed since OpenSSH 7.0, legacy algorithm' + FAIL_OPENSSH70_WEAK = 'removed (in server) and disabled (in client) since OpenSSH 7.0, weak algorithm' + FAIL_OPENSSH70_LOGJAM = 'disabled (in client) since OpenSSH 7.0, logjam attack' + INFO_OPENSSH69_CHACHA = 'default cipher since OpenSSH 6.9.' + FAIL_OPENSSH67_UNSAFE = 'removed (in server) since OpenSSH 6.7, unsafe algorithm' + FAIL_OPENSSH61_REMOVE = 'removed since OpenSSH 6.1, removed from specification' + FAIL_OPENSSH31_REMOVE = 'removed since OpenSSH 3.1' + FAIL_DBEAR67_DISABLED = 'disabled since Dropbear SSH 2015.67' + FAIL_DBEAR53_DISABLED = 'disabled since Dropbear SSH 0.53' + FAIL_PLAINTEXT = 'no encryption/integrity' + WARN_CURVES_WEAK = 'using weak elliptic curves' + WARN_RNDSIG_KEY = 'using weak random number generator could reveal the key' + WARN_MODULUS_SIZE = 'using small 1024-bit modulus' + WARN_MODULUS_CUSTOM = 'using custom size modulus (possibly weak)' + WARN_HASH_WEAK = 'using weak hashing algorithm' + WARN_CIPHER_MODE = 'using weak cipher mode' + WARN_BLOCK_SIZE = 'using small 64-bit block size' + WARN_CIPHER_WEAK = 'using weak cipher' + WARN_ENCRYPT_AND_MAC = 'using encrypt-and-MAC mode' + WARN_TAG_SIZE = 'using small 64-bit tag size' + + ALGORITHMS = { + 'kex': { + 'diffie-hellman-group1-sha1': [['2.3.0,d0.28,l10.2', '6.6', '6.9'], [FAIL_OPENSSH67_UNSAFE, FAIL_OPENSSH70_LOGJAM], [WARN_MODULUS_SIZE, WARN_HASH_WEAK]], + 'diffie-hellman-group14-sha1': [['3.9,d0.53,l10.6.0'], [], [WARN_HASH_WEAK]], + 'diffie-hellman-group14-sha256': [['7.3,d2016.73']], + 'diffie-hellman-group16-sha512': [['7.3,d2016.73']], + 'diffie-hellman-group18-sha512': [['7.3']], + 'diffie-hellman-group-exchange-sha1': [['2.3.0', '6.6', None], [FAIL_OPENSSH67_UNSAFE], [WARN_HASH_WEAK]], + 'diffie-hellman-group-exchange-sha256': [['4.4'], [], [WARN_MODULUS_CUSTOM]], + 'ecdh-sha2-nistp256': [['5.7,d2013.62,l10.6.0'], [WARN_CURVES_WEAK]], + 'ecdh-sha2-nistp384': [['5.7,d2013.62'], [WARN_CURVES_WEAK]], + 'ecdh-sha2-nistp521': [['5.7,d2013.62'], [WARN_CURVES_WEAK]], + 'curve25519-sha256@libssh.org': [['6.5,d2013.62,l10.6.0']], + 'kexguess2@matt.ucc.asn.au': [['d2013.57']], + }, + 'key': { + 'rsa-sha2-256': [['7.2']], + 'rsa-sha2-512': [['7.2']], + 'ssh-ed25519': [['6.5,l10.7.0']], + 'ssh-ed25519-cert-v01@openssh.com': [['6.5']], + 'ssh-rsa': [['2.5.0,d0.28,l10.2']], + 'ssh-dss': [['2.1.0,d0.28,l10.2', '6.9'], [FAIL_OPENSSH70_WEAK], [WARN_MODULUS_SIZE, WARN_RNDSIG_KEY]], + 'ecdsa-sha2-nistp256': [['5.7,d2013.62,l10.6.4'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]], + 'ecdsa-sha2-nistp384': [['5.7,d2013.62,l10.6.4'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]], + 'ecdsa-sha2-nistp521': [['5.7,d2013.62,l10.6.4'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]], + '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], [WARN_MODULUS_SIZE, WARN_RNDSIG_KEY]], + 'ssh-rsa-cert-v01@openssh.com': [['5.6']], + 'ssh-dss-cert-v01@openssh.com': [['5.6', '6.9'], [FAIL_OPENSSH70_WEAK], [WARN_MODULUS_SIZE, WARN_RNDSIG_KEY]], + 'ecdsa-sha2-nistp256-cert-v01@openssh.com': [['5.7'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]], + 'ecdsa-sha2-nistp384-cert-v01@openssh.com': [['5.7'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]], + 'ecdsa-sha2-nistp521-cert-v01@openssh.com': [['5.7'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]], + }, + 'enc': { + 'none': [['1.2.2,d2013.56,l10.2'], [FAIL_PLAINTEXT]], + '3des-cbc': [['1.2.2,d0.28,l10.2', '6.6', None], [FAIL_OPENSSH67_UNSAFE], [WARN_CIPHER_WEAK, WARN_CIPHER_MODE, WARN_BLOCK_SIZE]], + '3des-ctr': [['d0.52']], + 'blowfish-cbc': [['1.2.2,d0.28,l10.2', '6.6,d0.52', '7.1,d0.52'], [FAIL_OPENSSH67_UNSAFE, FAIL_DBEAR53_DISABLED], [WARN_OPENSSH72_LEGACY, WARN_CIPHER_MODE, WARN_BLOCK_SIZE]], + 'twofish-cbc': [['d0.28', 'd2014.66'], [FAIL_DBEAR67_DISABLED], [WARN_CIPHER_MODE]], + 'twofish128-cbc': [['d0.47', 'd2014.66'], [FAIL_DBEAR67_DISABLED], [WARN_CIPHER_MODE]], + 'twofish256-cbc': [['d0.47', 'd2014.66'], [FAIL_DBEAR67_DISABLED], [WARN_CIPHER_MODE]], + 'twofish128-ctr': [['d2015.68']], + 'twofish256-ctr': [['d2015.68']], + 'cast128-cbc': [['2.1.0', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_CIPHER_MODE, WARN_BLOCK_SIZE]], + 'arcfour': [['2.1.0', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_CIPHER_WEAK]], + 'arcfour128': [['4.2', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_CIPHER_WEAK]], + 'arcfour256': [['4.2', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_CIPHER_WEAK]], + 'aes128-cbc': [['2.3.0,d0.28,l10.2', '6.6', None], [FAIL_OPENSSH67_UNSAFE], [WARN_CIPHER_MODE]], + 'aes192-cbc': [['2.3.0,l10.2', '6.6', None], [FAIL_OPENSSH67_UNSAFE], [WARN_CIPHER_MODE]], + 'aes256-cbc': [['2.3.0,d0.47,l10.2', '6.6', None], [FAIL_OPENSSH67_UNSAFE], [WARN_CIPHER_MODE]], + 'rijndael128-cbc': [['2.3.0', '3.0.2'], [FAIL_OPENSSH31_REMOVE], [WARN_CIPHER_MODE]], + 'rijndael192-cbc': [['2.3.0', '3.0.2'], [FAIL_OPENSSH31_REMOVE], [WARN_CIPHER_MODE]], + 'rijndael256-cbc': [['2.3.0', '3.0.2'], [FAIL_OPENSSH31_REMOVE], [WARN_CIPHER_MODE]], + 'rijndael-cbc@lysator.liu.se': [['2.3.0', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_CIPHER_MODE]], + 'aes128-ctr': [['3.7,d0.52,l10.4.1']], + 'aes192-ctr': [['3.7,l10.4.1']], + 'aes256-ctr': [['3.7,d0.52,l10.4.1']], + 'aes128-gcm@openssh.com': [['6.2']], + 'aes256-gcm@openssh.com': [['6.2']], + 'chacha20-poly1305@openssh.com': [['6.5'], [], [], [INFO_OPENSSH69_CHACHA]], + }, + 'mac': { + 'none': [['d2013.56'], [FAIL_PLAINTEXT]], + 'hmac-sha1': [['2.1.0,d0.28,l10.2'], [], [WARN_ENCRYPT_AND_MAC, WARN_HASH_WEAK]], + 'hmac-sha1-96': [['2.5.0,d0.47', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC, WARN_HASH_WEAK]], + 'hmac-sha2-256': [['5.9,d2013.56,l10.7.0'], [], [WARN_ENCRYPT_AND_MAC]], + 'hmac-sha2-256-96': [['5.9', '6.0'], [FAIL_OPENSSH61_REMOVE], [WARN_ENCRYPT_AND_MAC]], + 'hmac-sha2-512': [['5.9,d2013.56,l10.7.0'], [], [WARN_ENCRYPT_AND_MAC]], + 'hmac-sha2-512-96': [['5.9', '6.0'], [FAIL_OPENSSH61_REMOVE], [WARN_ENCRYPT_AND_MAC]], + 'hmac-md5': [['2.1.0,d0.28', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC, WARN_HASH_WEAK]], + 'hmac-md5-96': [['2.5.0', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC, WARN_HASH_WEAK]], + 'hmac-ripemd160': [['2.5.0', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC]], + 'hmac-ripemd160@openssh.com': [['2.1.0', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC]], + 'umac-64@openssh.com': [['4.7'], [], [WARN_ENCRYPT_AND_MAC, WARN_TAG_SIZE]], + 'umac-128@openssh.com': [['6.2'], [], [WARN_ENCRYPT_AND_MAC]], + 'hmac-sha1-etm@openssh.com': [['6.2'], [], [WARN_HASH_WEAK]], + 'hmac-sha1-96-etm@openssh.com': [['6.2', '6.6', None], [FAIL_OPENSSH67_UNSAFE], [WARN_HASH_WEAK]], + 'hmac-sha2-256-etm@openssh.com': [['6.2']], + 'hmac-sha2-512-etm@openssh.com': [['6.2']], + 'hmac-md5-etm@openssh.com': [['6.2', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_HASH_WEAK]], + 'hmac-md5-96-etm@openssh.com': [['6.2', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_HASH_WEAK]], + 'hmac-ripemd160-etm@openssh.com': [['6.2', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY]], + 'umac-64-etm@openssh.com': [['6.2'], [], [WARN_TAG_SIZE]], + 'umac-128-etm@openssh.com': [['6.2']], + } + } # type: Dict[str, Dict[str, List[List[Optional[str]]]]] + class KexParty(object): def __init__(self, enc, mac, compression, languages): # type: (List[text_type], List[text_type], List[text_type], List[text_type]) -> None @@ -414,7 +525,7 @@ class SSH1(object): _crc32 = None # type: Optional[SSH1.CRC32] CIPHERS = ['none', 'idea', 'des', '3des', 'tss', 'rc4', 'blowfish'] - AUTHS = [None, 'rhosts', 'rsa', 'password', 'rhosts_rsa', 'tis', 'kerberos'] + AUTHS = ['none', 'rhosts', 'rsa', 'password', 'rhosts_rsa', 'tis', 'kerberos'] @classmethod def crc32(cls, v): @@ -452,7 +563,7 @@ class SSH1(object): 'tis': [['1.2.2']], 'kerberos': [['1.2.2', '3.6'], [FAIL_OPENSSH37_REMOVE]], } - } # type: Dict[str, Dict[str, List[List[str]]]] + } # type: Dict[str, Dict[str, List[List[Optional[str]]]]] class PublicKeyMessage(object): def __init__(self, cookie, skey, hkey, pflags, cmask, amask): @@ -887,7 +998,7 @@ class SSH(object): # pylint: disable=too-few-public-methods @classmethod def _extract_os_version(cls, c): - # type: (Optional[str]) -> str + # type: (Optional[str]) -> Optional[str] if c is None: return None mx = re.match(r'^NetBSD(?:_Secure_Shell)?(?:[\s-]+(\d{8})(.*))?$', c) @@ -914,10 +1025,11 @@ class SSH(object): # pylint: disable=too-few-public-methods @classmethod def parse(cls, banner): - # type: (SSH.Banner) -> SSH.Software + # type: (SSH.Banner) -> Optional[SSH.Software] # pylint: disable=too-many-return-statements software = str(banner.software) mx = re.match(r'^dropbear_([\d\.]+\d+)(.*)', software) + v = None # type: Optional[str] if mx: patch = cls._fix_patch(mx.group(2)) v, p = 'Matt Johnston', SSH.Product.DropbearSSH @@ -957,7 +1069,7 @@ class SSH(object): # pylint: disable=too-few-public-methods RX_BANNER = re.compile(r'^({0}(?:(?:-{0})*)){1}$'.format(_RXP, _RXR)) def __init__(self, protocol, software, comments, valid_ascii): - # type: (Tuple[int, int], str, str, bool) -> None + # type: (Tuple[int, int], Optional[str], Optional[str], bool) -> None self.__protocol = protocol self.__software = software self.__comments = comments @@ -970,12 +1082,12 @@ class SSH(object): # pylint: disable=too-few-public-methods @property def software(self): - # type: () -> str + # type: () -> Optional[str] return self.__software @property def comments(self): - # type: () -> str + # type: () -> Optional[str] return self.__comments @property @@ -1039,6 +1151,268 @@ class SSH(object): # pylint: disable=too-few-public-methods r = h.decode('ascii').rstrip('=') return u'SHA256:{0}'.format(r) + class Algorithm(object): + @staticmethod + def get_ssh_version(version_desc): + # type: (str) -> Tuple[str, str] + if version_desc.startswith('d'): + return (SSH.Product.DropbearSSH, version_desc[1:]) + elif version_desc.startswith('l1'): + return (SSH.Product.LibSSH, version_desc[2:]) + else: + return (SSH.Product.OpenSSH, version_desc) + + @classmethod + def get_timeframe(cls, versions, for_server=True, result=None): + # type: (List[Optional[str]], bool, Optional[Dict[str, List[Optional[str]]]]) -> Dict[str, List[Optional[str]]] + result = result or {} + vlen = len(versions) + for i in range(3): + if i > vlen - 1: + if i == 2 and vlen > 1: + cversions = versions[1] + else: + continue + else: + cversions = versions[i] + if cversions is None: + continue + for v in cversions.split(','): + ssh_prefix, ssh_version = cls.get_ssh_version(v) + if not ssh_version: + continue + if ssh_version.endswith('C'): + if for_server: + continue + ssh_version = ssh_version[:-1] + if ssh_prefix not in result: + result[ssh_prefix] = [None, None, None] + prev, push = result[ssh_prefix][i], False + if prev is None: + push = True + elif i == 0 and prev < ssh_version: + push = True + elif i > 0 and prev > ssh_version: + push = True + if push: + result[ssh_prefix][i] = ssh_version + return result + + @classmethod + def get_since_text(cls, versions): + # type: (List[Optional[str]]) -> Optional[text_type] + tv = [] + if len(versions) == 0 or versions[0] is None: + return None + for v in versions[0].split(','): + ssh_prefix, ssh_version = cls.get_ssh_version(v) + if not ssh_version: + continue + if ssh_prefix in [SSH.Product.LibSSH]: + continue + if ssh_version.endswith('C'): + ssh_version = '{0} (client only)'.format(ssh_version[:-1]) + tv.append('{0} {1}'.format(ssh_prefix, ssh_version)) + if len(tv) == 0: + return None + return 'available since ' + ', '.join(tv).rstrip(', ') + + class Algorithms(object): + def __init__(self, pkm, kex): + # type: (Optional[SSH1.PublicKeyMessage], Optional[SSH2.Kex]) -> None + self.__ssh1kex = pkm + self.__ssh2kex = kex + + @property + def ssh1kex(self): + # type: () -> Optional[SSH1.PublicKeyMessage] + return self.__ssh1kex + + @property + def ssh2kex(self): + # type: () -> Optional[SSH2.Kex] + return self.__ssh2kex + + @property + def ssh1(self): + # type: () -> Optional[SSH.Algorithms.Item] + if self.ssh1kex is None: + return None + item = SSH.Algorithms.Item(1, SSH1.KexDB.ALGORITHMS) + item.add('key', [u'ssh-rsa1']) + item.add('enc', self.ssh1kex.supported_ciphers) + item.add('aut', self.ssh1kex.supported_authentications) + return item + + @property + def ssh2(self): + # type: () -> Optional[SSH.Algorithms.Item] + if self.ssh2kex is None: + return None + item = SSH.Algorithms.Item(2, SSH2.KexDB.ALGORITHMS) + item.add('kex', self.ssh2kex.kex_algorithms) + item.add('key', self.ssh2kex.key_algorithms) + item.add('enc', self.ssh2kex.server.encryption) + item.add('mac', self.ssh2kex.server.mac) + return item + + @property + def values(self): + # type: () -> Iterable[SSH.Algorithms.Item] + for item in [self.ssh1, self.ssh2]: + if item is not None: + yield item + + @property + def maxlen(self): + # type: () -> int + ml, maxlen = lambda l: max(len(i) for i in l), 0 + if self.ssh1kex is not None: + maxlen = max(ml(self.ssh1kex.supported_ciphers), + ml(self.ssh1kex.supported_authentications), + maxlen) + if self.ssh2kex is not None: + maxlen = max(ml(self.ssh2kex.kex_algorithms), + ml(self.ssh2kex.key_algorithms), + ml(self.ssh2kex.server.encryption), + ml(self.ssh2kex.server.mac), + maxlen) + return maxlen + + def get_ssh_timeframe(self, for_server=True): + # type: (bool) -> Dict[str, List[Optional[str]]] + r = {} # type: Dict[str, List[Optional[str]]] + for alg_pair in self.values: + alg_db = alg_pair.db + for alg_type, alg_list in alg_pair.items(): + for alg_name in alg_list: + alg_name_native = utils.to_ntext(alg_name) + alg_desc = alg_db[alg_type].get(alg_name_native) + if alg_desc is None: + continue + versions = alg_desc[0] + r = SSH.Algorithm.get_timeframe(versions, for_server, r) + return r + + def get_recommendations(self, software, for_server=True): + # type: (Optional[SSH.Software], bool) -> Tuple[Optional[SSH.Software], Dict[int, Dict[str, Dict[str, Dict[str, int]]]]] + # pylint: disable=too-many-locals,too-many-statements + vproducts = [SSH.Product.OpenSSH, + SSH.Product.DropbearSSH, + SSH.Product.LibSSH] + if software is not None: + if software.product not in vproducts: + software = None + if software is None: + ssh_timeframe = self.get_ssh_timeframe(for_server) + for product in vproducts: + if product not in ssh_timeframe: + continue + version = ssh_timeframe[product][0] + if version is not None: + software = SSH.Software(None, product, version, None, None) + break + rec = {} # type: Dict[int, Dict[str, Dict[str, Dict[str, int]]]] + if software is None: + return software, rec + for alg_pair in self.values: + sshv, alg_db = alg_pair.sshv, alg_pair.db + rec[sshv] = {} + for alg_type, alg_list in alg_pair.items(): + if alg_type == 'aut': + continue + rec[sshv][alg_type] = {'add': {}, 'del': {}} + for n, alg_desc in alg_db[alg_type].items(): + if alg_type == 'key' and '-cert-' in n: + continue + versions = alg_desc[0] + if len(versions) == 0 or versions[0] is None: + continue + matches = False + for v in versions[0].split(','): + ssh_prefix, ssh_version = SSH.Algorithm.get_ssh_version(v) + if not ssh_version: + continue + if ssh_prefix != software.product: + continue + if ssh_version.endswith('C'): + if for_server: + continue + ssh_version = ssh_version[:-1] + if software.compare_version(ssh_version) < 0: + continue + matches = True + break + if not matches: + continue + adl, faults = len(alg_desc), 0 + for i in range(1, 3): + if not adl > i: + continue + fc = len(alg_desc[i]) + if fc > 0: + faults += pow(10, 2 - i) * fc + if n not in alg_list: + if faults > 0: + continue + rec[sshv][alg_type]['add'][n] = 0 + else: + if faults == 0: + continue + if n == 'diffie-hellman-group-exchange-sha256': + if software.compare_version('7.3') < 0: + continue + rec[sshv][alg_type]['del'][n] = faults + add_count = len(rec[sshv][alg_type]['add']) + del_count = len(rec[sshv][alg_type]['del']) + new_alg_count = len(alg_list) + add_count - del_count + if new_alg_count < 1 and del_count > 0: + mf = min(rec[sshv][alg_type]['del'].values()) + new_del = {} + for k, cf in rec[sshv][alg_type]['del'].items(): + if cf != mf: + new_del[k] = cf + if del_count != len(new_del): + rec[sshv][alg_type]['del'] = new_del + new_alg_count += del_count - len(new_del) + if new_alg_count < 1: + del rec[sshv][alg_type] + else: + if add_count == 0: + del rec[sshv][alg_type]['add'] + if del_count == 0: + del rec[sshv][alg_type]['del'] + if len(rec[sshv][alg_type]) == 0: + del rec[sshv][alg_type] + if len(rec[sshv]) == 0: + del rec[sshv] + return software, rec + + class Item(object): + def __init__(self, sshv, db): + # type: (int, Dict[str, Dict[str, List[List[Optional[str]]]]]) -> None + self.__sshv = sshv + self.__db = db + self.__storage = {} # type: Dict[str, List[text_type]] + + @property + def sshv(self): + # type: () -> int + return self.__sshv + + @property + def db(self): + # type: () -> Dict[str, Dict[str, List[List[Optional[str]]]]] + return self.__db + + def add(self, key, value): + # type: (str, List[text_type]) -> None + self.__storage[key] = value + + def items(self): + # type: () -> Iterable[Tuple[str, List[text_type]]] + return self.__storage.items() + class Security(object): # pylint: disable=too-few-public-methods # pylint: disable=bad-whitespace CVE = { @@ -1080,19 +1454,20 @@ class SSH(object): # pylint: disable=too-few-public-methods SM_BANNER_SENT = 1 def __init__(self, host, port): - # type: (str, int) -> None + # type: (Optional[str], int) -> None super(SSH.Socket, self).__init__() + self.__sock = None # type: Optional[socket.socket] self.__block_size = 8 self.__state = 0 self.__header = [] # type: List[text_type] self.__banner = None # type: Optional[SSH.Banner] + if host is None: + raise ValueError('undefined host') + nport = utils.parse_int(port) + if nport < 1 or nport > 65535: + raise ValueError('invalid port: {0}'.format(port)) self.__host = host - self.__port = port - self.__sock = None # type: socket.socket - - def __enter__(self): - # type: () -> SSH.Socket - return self + self.__port = nport def _resolve(self, ipvo): # type: (Sequence[int]) -> Iterable[Tuple[int, Tuple[Any, ...]]] @@ -1142,6 +1517,8 @@ class SSH(object): # pylint: disable=too-few-public-methods def get_banner(self, sshv=2): # type: (int) -> Tuple[Optional[SSH.Banner], List[text_type], Optional[str]] + if self.__sock is None: + return self.__banner, self.__header, 'not connected' banner = 'SSH-{0}-OpenSSH_7.3'.format('1.5' if sshv == 1 else '2.0') rto = self.__sock.gettimeout() self.__sock.settimeout(0.7) @@ -1171,6 +1548,8 @@ class SSH(object): # pylint: disable=too-few-public-methods def recv(self, size=2048): # type: (int) -> Tuple[int, Optional[str]] + if self.__sock is None: + return (-1, 'not connected') try: data = self.__sock.recv(size) except socket.timeout: @@ -1190,6 +1569,8 @@ class SSH(object): # pylint: disable=too-few-public-methods def send(self, data): # type: (binary_type) -> Tuple[int, Optional[str]] + if self.__sock is None: + return (-1, 'not connected') try: self.__sock.send(data) return (0, None) @@ -1278,7 +1659,7 @@ class SSH(object): # pylint: disable=too-few-public-methods try: if s is not None: s.shutdown(socket.SHUT_RDWR) - s.close() + s.close() # pragma: nocover except: # pylint: disable=bare-except pass @@ -1286,24 +1667,20 @@ class SSH(object): # pylint: disable=too-few-public-methods # type: () -> None self.__cleanup() - def __exit__(self, *args): - # type: (*Any) -> None - self.__cleanup() - def __cleanup(self): # type: () -> None self._close_socket(self.__sock) -class KexDH(object): +class KexDH(object): # pragma: nocover def __init__(self, alg, g, p): # type: (str, int, int) -> None self.__alg = alg self.__g = g self.__p = p self.__q = (self.__p - 1) // 2 - self.__x = None # type: Optional[int] - self.__e = None # type: Optional[int] + self.__x = 0 + self.__e = 0 def send_init(self, s): # type: (SSH.Socket) -> None @@ -1315,7 +1692,7 @@ class KexDH(object): s.send_packet() -class KexGroup1(KexDH): +class KexGroup1(KexDH): # pragma: nocover def __init__(self): # type: () -> None # rfc2409: second oakley group @@ -1327,7 +1704,7 @@ class KexGroup1(KexDH): super(KexGroup1, self).__init__('sha1', 2, p) -class KexGroup14(KexDH): +class KexGroup14(KexDH): # pragma: nocover def __init__(self): # type: () -> None # rfc3526: 2048-bit modp group @@ -1343,316 +1720,9 @@ class KexGroup14(KexDH): super(KexGroup14, self).__init__('sha1', 2, p) -class KexDB(object): # pylint: disable=too-few-public-methods - # pylint: disable=bad-whitespace - WARN_OPENSSH72_LEGACY = 'disabled (in client) since OpenSSH 7.2, legacy algorithm' - FAIL_OPENSSH70_LEGACY = 'removed since OpenSSH 7.0, legacy algorithm' - FAIL_OPENSSH70_WEAK = 'removed (in server) and disabled (in client) since OpenSSH 7.0, weak algorithm' - FAIL_OPENSSH70_LOGJAM = 'disabled (in client) since OpenSSH 7.0, logjam attack' - INFO_OPENSSH69_CHACHA = 'default cipher since OpenSSH 6.9.' - FAIL_OPENSSH67_UNSAFE = 'removed (in server) since OpenSSH 6.7, unsafe algorithm' - FAIL_OPENSSH61_REMOVE = 'removed since OpenSSH 6.1, removed from specification' - FAIL_OPENSSH31_REMOVE = 'removed since OpenSSH 3.1' - FAIL_DBEAR67_DISABLED = 'disabled since Dropbear SSH 2015.67' - FAIL_DBEAR53_DISABLED = 'disabled since Dropbear SSH 0.53' - FAIL_PLAINTEXT = 'no encryption/integrity' - WARN_CURVES_WEAK = 'using weak elliptic curves' - WARN_RNDSIG_KEY = 'using weak random number generator could reveal the key' - WARN_MODULUS_SIZE = 'using small 1024-bit modulus' - WARN_MODULUS_CUSTOM = 'using custom size modulus (possibly weak)' - WARN_HASH_WEAK = 'using weak hashing algorithm' - WARN_CIPHER_MODE = 'using weak cipher mode' - WARN_BLOCK_SIZE = 'using small 64-bit block size' - WARN_CIPHER_WEAK = 'using weak cipher' - WARN_ENCRYPT_AND_MAC = 'using encrypt-and-MAC mode' - WARN_TAG_SIZE = 'using small 64-bit tag size' - - ALGORITHMS = { - 'kex': { - 'diffie-hellman-group1-sha1': [['2.3.0,d0.28,l10.2', '6.6', '6.9'], [FAIL_OPENSSH67_UNSAFE, FAIL_OPENSSH70_LOGJAM], [WARN_MODULUS_SIZE, WARN_HASH_WEAK]], - 'diffie-hellman-group14-sha1': [['3.9,d0.53,l10.6.0'], [], [WARN_HASH_WEAK]], - 'diffie-hellman-group14-sha256': [['7.3,d2016.73']], - 'diffie-hellman-group16-sha512': [['7.3,d2016.73']], - 'diffie-hellman-group18-sha512': [['7.3']], - 'diffie-hellman-group-exchange-sha1': [['2.3.0', '6.6', None], [FAIL_OPENSSH67_UNSAFE], [WARN_HASH_WEAK]], - 'diffie-hellman-group-exchange-sha256': [['4.4'], [], [WARN_MODULUS_CUSTOM]], - 'ecdh-sha2-nistp256': [['5.7,d2013.62,l10.6.0'], [WARN_CURVES_WEAK]], - 'ecdh-sha2-nistp384': [['5.7,d2013.62'], [WARN_CURVES_WEAK]], - 'ecdh-sha2-nistp521': [['5.7,d2013.62'], [WARN_CURVES_WEAK]], - 'curve25519-sha256@libssh.org': [['6.5,d2013.62,l10.6.0']], - 'kexguess2@matt.ucc.asn.au': [['d2013.57']], - }, - 'key': { - 'rsa-sha2-256': [['7.2']], - 'rsa-sha2-512': [['7.2']], - 'ssh-ed25519': [['6.5,l10.7.0']], - 'ssh-ed25519-cert-v01@openssh.com': [['6.5']], - 'ssh-rsa': [['2.5.0,d0.28,l10.2']], - 'ssh-dss': [['2.1.0,d0.28,l10.2', '6.9'], [FAIL_OPENSSH70_WEAK], [WARN_MODULUS_SIZE, WARN_RNDSIG_KEY]], - 'ecdsa-sha2-nistp256': [['5.7,d2013.62,l10.6.4'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]], - 'ecdsa-sha2-nistp384': [['5.7,d2013.62,l10.6.4'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]], - 'ecdsa-sha2-nistp521': [['5.7,d2013.62,l10.6.4'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]], - '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], [WARN_MODULUS_SIZE, WARN_RNDSIG_KEY]], - 'ssh-rsa-cert-v01@openssh.com': [['5.6']], - 'ssh-dss-cert-v01@openssh.com': [['5.6', '6.9'], [FAIL_OPENSSH70_WEAK], [WARN_MODULUS_SIZE, WARN_RNDSIG_KEY]], - 'ecdsa-sha2-nistp256-cert-v01@openssh.com': [['5.7'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]], - 'ecdsa-sha2-nistp384-cert-v01@openssh.com': [['5.7'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]], - 'ecdsa-sha2-nistp521-cert-v01@openssh.com': [['5.7'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]], - }, - 'enc': { - 'none': [['1.2.2,d2013.56,l10.2'], [FAIL_PLAINTEXT]], - '3des-cbc': [['1.2.2,d0.28,l10.2', '6.6', None], [FAIL_OPENSSH67_UNSAFE], [WARN_CIPHER_WEAK, WARN_CIPHER_MODE, WARN_BLOCK_SIZE]], - '3des-ctr': [['d0.52']], - 'blowfish-cbc': [['1.2.2,d0.28,l10.2', '6.6,d0.52', '7.1,d0.52'], [FAIL_OPENSSH67_UNSAFE, FAIL_DBEAR53_DISABLED], [WARN_OPENSSH72_LEGACY, WARN_CIPHER_MODE, WARN_BLOCK_SIZE]], - 'twofish-cbc': [['d0.28', 'd2014.66'], [FAIL_DBEAR67_DISABLED], [WARN_CIPHER_MODE]], - 'twofish128-cbc': [['d0.47', 'd2014.66'], [FAIL_DBEAR67_DISABLED], [WARN_CIPHER_MODE]], - 'twofish256-cbc': [['d0.47', 'd2014.66'], [FAIL_DBEAR67_DISABLED], [WARN_CIPHER_MODE]], - 'twofish128-ctr': [['d2015.68']], - 'twofish256-ctr': [['d2015.68']], - 'cast128-cbc': [['2.1.0', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_CIPHER_MODE, WARN_BLOCK_SIZE]], - 'arcfour': [['2.1.0', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_CIPHER_WEAK]], - 'arcfour128': [['4.2', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_CIPHER_WEAK]], - 'arcfour256': [['4.2', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_CIPHER_WEAK]], - 'aes128-cbc': [['2.3.0,d0.28,l10.2', '6.6', None], [FAIL_OPENSSH67_UNSAFE], [WARN_CIPHER_MODE]], - 'aes192-cbc': [['2.3.0,l10.2', '6.6', None], [FAIL_OPENSSH67_UNSAFE], [WARN_CIPHER_MODE]], - 'aes256-cbc': [['2.3.0,d0.47,l10.2', '6.6', None], [FAIL_OPENSSH67_UNSAFE], [WARN_CIPHER_MODE]], - 'rijndael128-cbc': [['2.3.0', '3.0.2'], [FAIL_OPENSSH31_REMOVE], [WARN_CIPHER_MODE]], - 'rijndael192-cbc': [['2.3.0', '3.0.2'], [FAIL_OPENSSH31_REMOVE], [WARN_CIPHER_MODE]], - 'rijndael256-cbc': [['2.3.0', '3.0.2'], [FAIL_OPENSSH31_REMOVE], [WARN_CIPHER_MODE]], - 'rijndael-cbc@lysator.liu.se': [['2.3.0', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_CIPHER_MODE]], - 'aes128-ctr': [['3.7,d0.52,l10.4.1']], - 'aes192-ctr': [['3.7,l10.4.1']], - 'aes256-ctr': [['3.7,d0.52,l10.4.1']], - 'aes128-gcm@openssh.com': [['6.2']], - 'aes256-gcm@openssh.com': [['6.2']], - 'chacha20-poly1305@openssh.com': [['6.5'], [], [], [INFO_OPENSSH69_CHACHA]], - }, - 'mac': { - 'none': [['d2013.56'], [FAIL_PLAINTEXT]], - 'hmac-sha1': [['2.1.0,d0.28,l10.2'], [], [WARN_ENCRYPT_AND_MAC, WARN_HASH_WEAK]], - 'hmac-sha1-96': [['2.5.0,d0.47', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC, WARN_HASH_WEAK]], - 'hmac-sha2-256': [['5.9,d2013.56,l10.7.0'], [], [WARN_ENCRYPT_AND_MAC]], - 'hmac-sha2-256-96': [['5.9', '6.0'], [FAIL_OPENSSH61_REMOVE], [WARN_ENCRYPT_AND_MAC]], - 'hmac-sha2-512': [['5.9,d2013.56,l10.7.0'], [], [WARN_ENCRYPT_AND_MAC]], - 'hmac-sha2-512-96': [['5.9', '6.0'], [FAIL_OPENSSH61_REMOVE], [WARN_ENCRYPT_AND_MAC]], - 'hmac-md5': [['2.1.0,d0.28', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC, WARN_HASH_WEAK]], - 'hmac-md5-96': [['2.5.0', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC, WARN_HASH_WEAK]], - 'hmac-ripemd160': [['2.5.0', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC]], - 'hmac-ripemd160@openssh.com': [['2.1.0', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC]], - 'umac-64@openssh.com': [['4.7'], [], [WARN_ENCRYPT_AND_MAC, WARN_TAG_SIZE]], - 'umac-128@openssh.com': [['6.2'], [], [WARN_ENCRYPT_AND_MAC]], - 'hmac-sha1-etm@openssh.com': [['6.2'], [], [WARN_HASH_WEAK]], - 'hmac-sha1-96-etm@openssh.com': [['6.2', '6.6', None], [FAIL_OPENSSH67_UNSAFE], [WARN_HASH_WEAK]], - 'hmac-sha2-256-etm@openssh.com': [['6.2']], - 'hmac-sha2-512-etm@openssh.com': [['6.2']], - 'hmac-md5-etm@openssh.com': [['6.2', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_HASH_WEAK]], - 'hmac-md5-96-etm@openssh.com': [['6.2', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_HASH_WEAK]], - 'hmac-ripemd160-etm@openssh.com': [['6.2', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY]], - 'umac-64-etm@openssh.com': [['6.2'], [], [WARN_TAG_SIZE]], - 'umac-128-etm@openssh.com': [['6.2']], - } - } # type: Dict[str, Dict[str, List[List[str]]]] - - -def get_ssh_version(version_desc): - # type: (str) -> Tuple[str, str] - if version_desc.startswith('d'): - return (SSH.Product.DropbearSSH, version_desc[1:]) - elif version_desc.startswith('l1'): - return (SSH.Product.LibSSH, version_desc[2:]) - else: - return (SSH.Product.OpenSSH, version_desc) - - -def get_alg_timeframe(versions, for_server=True, result=None): - # type: (List[str], bool, Optional[Dict[str, List[Optional[str]]]]) -> Dict[str, List[Optional[str]]] - result = result or {} - vlen = len(versions) - for i in range(3): - if i > vlen - 1: - if i == 2 and vlen > 1: - cversions = versions[1] - else: - continue - else: - cversions = versions[i] - if cversions is None: - continue - for v in cversions.split(','): - ssh_prefix, ssh_version = get_ssh_version(v) - if not ssh_version: - continue - if ssh_version.endswith('C'): - if for_server: - continue - ssh_version = ssh_version[:-1] - if ssh_prefix not in result: - result[ssh_prefix] = [None, None, None] - prev, push = result[ssh_prefix][i], False - if prev is None: - push = True - elif i == 0 and prev < ssh_version: - push = True - elif i > 0 and prev > ssh_version: - push = True - if push: - result[ssh_prefix][i] = ssh_version - return result - - -def get_ssh_timeframe(alg_pairs, for_server=True): - # type: (List[Tuple[int, Dict[str, Dict[str, List[List[str]]]], List[Tuple[str, List[text_type]]]]], bool) -> Dict[str, List[Optional[str]]] - timeframe = {} # type: Dict[str, List[Optional[str]]] - for alg_pair in alg_pairs: - alg_db = alg_pair[1] - for alg_set in alg_pair[2]: - alg_type, alg_list = alg_set - for alg_name in alg_list: - alg_name_native = utils.to_ntext(alg_name) - alg_desc = alg_db[alg_type].get(alg_name_native) - if alg_desc is None: - continue - versions = alg_desc[0] - timeframe = get_alg_timeframe(versions, for_server, timeframe) - return timeframe - - -def get_alg_since_text(versions): - # type: (List[str]) -> text_type - tv = [] - if len(versions) == 0 or versions[0] is None: - return None - for v in versions[0].split(','): - ssh_prefix, ssh_version = get_ssh_version(v) - if not ssh_version: - continue - if ssh_prefix in [SSH.Product.LibSSH]: - continue - if ssh_version.endswith('C'): - ssh_version = '{0} (client only)'.format(ssh_version[:-1]) - tv.append('{0} {1}'.format(ssh_prefix, ssh_version)) - if len(tv) == 0: - return None - return 'available since ' + ', '.join(tv).rstrip(', ') - - -def get_alg_pairs(kex, pkm): - # type: (Optional[SSH2.Kex], Optional[SSH1.PublicKeyMessage]) -> List[Tuple[int, Dict[str, Dict[str, List[List[str]]]], List[Tuple[str, List[text_type]]]]] - alg_pairs = [] - if pkm is not None: - alg_pairs.append((1, SSH1.KexDB.ALGORITHMS, - [('key', [u'ssh-rsa1']), - ('enc', pkm.supported_ciphers), - ('aut', pkm.supported_authentications)])) - if kex is not None: - alg_pairs.append((2, KexDB.ALGORITHMS, - [('kex', kex.kex_algorithms), - ('key', kex.key_algorithms), - ('enc', kex.server.encryption), - ('mac', kex.server.mac)])) - return alg_pairs - - -def get_alg_recommendations(software, kex, pkm, for_server=True): - # type: (SSH.Software, SSH2.Kex, SSH1.PublicKeyMessage, bool) -> Tuple[SSH.Software, Dict[int, Dict[str, Dict[str, Dict[str, int]]]]] - # pylint: disable=too-many-locals,too-many-statements - alg_pairs = get_alg_pairs(kex, pkm) - vproducts = [SSH.Product.OpenSSH, - SSH.Product.DropbearSSH, - SSH.Product.LibSSH] - if software is not None: - if software.product not in vproducts: - software = None - if software is None: - ssh_timeframe = get_ssh_timeframe(alg_pairs, for_server) - for product in vproducts: - if product not in ssh_timeframe: - continue - version = ssh_timeframe[product][0] - if version is not None: - software = SSH.Software(None, product, version, None, None) - break - rec = {} # type: Dict[int, Dict[str, Dict[str, Dict[str, int]]]] - if software is None: - return software, rec - for alg_pair in alg_pairs: - sshv, alg_db = alg_pair[0], alg_pair[1] - rec[sshv] = {} - for alg_set in alg_pair[2]: - alg_type, alg_list = alg_set - if alg_type == 'aut': - continue - rec[sshv][alg_type] = {'add': {}, 'del': {}} - for n, alg_desc in alg_db[alg_type].items(): - if alg_type == 'key' and '-cert-' in n: - continue - versions = alg_desc[0] - if len(versions) == 0 or versions[0] is None: - continue - matches = False - for v in versions[0].split(','): - ssh_prefix, ssh_version = get_ssh_version(v) - if not ssh_version: - continue - if ssh_prefix != software.product: - continue - if ssh_version.endswith('C'): - if for_server: - continue - ssh_version = ssh_version[:-1] - if software.compare_version(ssh_version) < 0: - continue - matches = True - break - if not matches: - continue - adl, faults = len(alg_desc), 0 - for i in range(1, 3): - if not adl > i: - continue - fc = len(alg_desc[i]) - if fc > 0: - faults += pow(10, 2 - i) * fc - if n not in alg_list: - if faults > 0: - continue - rec[sshv][alg_type]['add'][n] = 0 - else: - if faults == 0: - continue - if n == 'diffie-hellman-group-exchange-sha256': - if software.compare_version('7.3') < 0: - continue - rec[sshv][alg_type]['del'][n] = faults - add_count = len(rec[sshv][alg_type]['add']) - del_count = len(rec[sshv][alg_type]['del']) - new_alg_count = len(alg_list) + add_count - del_count - if new_alg_count < 1 and del_count > 0: - mf = min(rec[sshv][alg_type]['del'].values()) - new_del = {} - for k, cf in rec[sshv][alg_type]['del'].items(): - if cf != mf: - new_del[k] = cf - if del_count != len(new_del): - rec[sshv][alg_type]['del'] = new_del - new_alg_count += del_count - len(new_del) - if new_alg_count < 1: - del rec[sshv][alg_type] - else: - if add_count == 0: - del rec[sshv][alg_type]['add'] - if del_count == 0: - del rec[sshv][alg_type]['del'] - if len(rec[sshv][alg_type]) == 0: - del rec[sshv][alg_type] - if len(rec[sshv]) == 0: - del rec[sshv] - return software, rec - def output_algorithms(title, alg_db, alg_type, algorithms, maxlen=0): - # type: (str, Dict[str, Dict[str, List[List[str]]]], str, List[text_type], int) -> None + # type: (str, Dict[str, Dict[str, List[List[Optional[str]]]]], str, List[text_type], int) -> None with OutputBuffer() as obuf: for algorithm in algorithms: output_algorithm(alg_db, alg_type, algorithm, maxlen) @@ -1663,7 +1733,7 @@ def output_algorithms(title, alg_db, alg_type, algorithms, maxlen=0): def output_algorithm(alg_db, alg_type, alg_name, alg_max_len=0): - # type: (Dict[str, Dict[str, List[List[str]]]], str, text_type, int) -> None + # type: (Dict[str, Dict[str, List[List[Optional[str]]]]], str, text_type, int) -> None prefix = '(' + alg_type + ') ' if alg_max_len == 0: alg_max_len = len(alg_name) @@ -1678,12 +1748,14 @@ def output_algorithm(alg_db, alg_type, alg_name, alg_max_len=0): for idx, level in enumerate(['fail', 'warn', 'info']): if level == 'info': versions = alg_desc[0] - since_text = get_alg_since_text(versions) + since_text = SSH.Algorithm.get_since_text(versions) if since_text: texts.append((level, since_text)) idx = idx + 1 if ldesc > idx: for t in alg_desc[idx]: + if t is None: + continue texts.append((level, t)) if len(texts) == 0: texts.append(('info', '')) @@ -1705,22 +1777,24 @@ def output_algorithm(alg_db, alg_type, alg_name, alg_max_len=0): f(' ' * len(prefix + alg_name) + padding + ' `- ' + text) -def output_compatibility(kex, pkm, for_server=True): - # type: (Optional[SSH2.Kex], Optional[SSH1.PublicKeyMessage], bool) -> None - alg_pairs = get_alg_pairs(kex, pkm) - ssh_timeframe = get_ssh_timeframe(alg_pairs, for_server) +def output_compatibility(algs, for_server=True): + # type: (SSH.Algorithms, bool) -> None + ssh_timeframe = algs.get_ssh_timeframe(for_server) vp = 1 if for_server else 2 comp_text = [] for sshd_name in [SSH.Product.OpenSSH, SSH.Product.DropbearSSH]: if sshd_name not in ssh_timeframe: continue v = ssh_timeframe[sshd_name] + if v[0] is None: + continue if v[vp] is None: comp_text.append('{0} {1}+'.format(sshd_name, v[0])) elif v[0] == v[vp]: comp_text.append('{0} {1}'.format(sshd_name, v[0])) else: - if v[vp] < v[0]: + software = SSH.Software(None, sshd_name, v[0], None, None) + if software.compare_version(v[vp]) > 0: tfmt = '{0} {1}+ (some functionality from {2})' else: tfmt = '{0} {1}-{2}' @@ -1730,7 +1804,7 @@ def output_compatibility(kex, pkm, for_server=True): def output_security_sub(sub, software, padlen): - # type: (str, SSH.Software, int) -> None + # type: (str, Optional[SSH.Software], int) -> None secdb = SSH.Security.CVE if sub == 'cve' else SSH.Security.TXT if software is None or software.product not in secdb: return @@ -1753,9 +1827,9 @@ def output_security_sub(sub, software, padlen): def output_security(banner, padlen): - # type: (SSH.Banner, int) -> None + # type: (Optional[SSH.Banner], int) -> None with OutputBuffer() as obuf: - if banner: + if banner is not None: software = SSH.Software.parse(banner) output_security_sub('cve', software, padlen) output_security_sub('txt', software, padlen) @@ -1765,14 +1839,14 @@ def output_security(banner, padlen): out.sep() -def output_fingerprint(kex, pkm, sha256=True, padlen=0): - # type: (Optional[SSH2.Kex], Optional[SSH1.PublicKeyMessage], bool, int) -> None +def output_fingerprint(algs, sha256=True, padlen=0): + # type: (SSH.Algorithms, bool, int) -> None with OutputBuffer() as obuf: fps = [] - if pkm is not None: + if algs.ssh1kex is not None: name = 'ssh-rsa1' - fp = SSH.Fingerprint(pkm.host_key_fingerprint_data) - bits = pkm.host_key_bits + fp = SSH.Fingerprint(algs.ssh1kex.host_key_fingerprint_data) + bits = algs.ssh1kex.host_key_bits fps.append((name, fp, bits)) for fpp in fps: name, fp, bits = fpp @@ -1785,11 +1859,11 @@ def output_fingerprint(kex, pkm, sha256=True, padlen=0): out.sep() -def output_recommendations(software, kex, pkm, padlen=0): - # type: (SSH.Software, SSH2.Kex, SSH1.PublicKeyMessage, int) -> None +def output_recommendations(algs, software, padlen=0): + # type: (SSH.Algorithms, Optional[SSH.Software], int) -> None for_server = True with OutputBuffer() as obuf: - software, alg_rec = get_alg_recommendations(software, kex, pkm, for_server) + software, alg_rec = algs.get_recommendations(software, for_server) for sshv in range(2, 0, -1): if sshv not in alg_rec: continue @@ -1820,6 +1894,7 @@ def output_recommendations(software, kex, pkm, padlen=0): def output(banner, header, kex=None, pkm=None): # type: (Optional[SSH.Banner], List[text_type], Optional[SSH2.Kex], Optional[SSH1.PublicKeyMessage]) -> None sshv = 1 if pkm else 2 + algs = SSH.Algorithms(pkm, kex) with OutputBuffer() as obuf: if len(header) > 0: out.info('(gen) header: ' + '\n'.join(header)) @@ -1835,7 +1910,7 @@ def output(banner, header, kex=None, pkm=None): out.good('(gen) software: {0}'.format(software)) else: software = None - output_compatibility(kex, pkm) + output_compatibility(algs) if kex is not None: compressions = [x for x in kex.server.compression if x != 'none'] if len(compressions) > 0: @@ -1847,18 +1922,7 @@ def output(banner, header, kex=None, pkm=None): out.head('# general') obuf.flush() out.sep() - ml, maxlen = lambda l: max(len(i) for i in l), 0 - if pkm is not None: - maxlen = max(ml(pkm.supported_ciphers), - ml(pkm.supported_authentications), - maxlen) - if kex is not None: - maxlen = max(ml(kex.kex_algorithms), - ml(kex.key_algorithms), - ml(kex.server.encryption), - ml(kex.server.mac), - maxlen) - maxlen += 1 + maxlen = algs.maxlen + 1 output_security(banner, maxlen) if pkm is not None: adb = SSH1.KexDB.ALGORITHMS @@ -1871,7 +1935,7 @@ def output(banner, header, kex=None, pkm=None): title, atype = 'SSH1 authentication types', 'aut' output_algorithms(title, adb, atype, auths, maxlen) if kex is not None: - adb = KexDB.ALGORITHMS + adb = SSH2.KexDB.ALGORITHMS title, atype = 'key exchange algorithms', 'kex' output_algorithms(title, adb, atype, kex.kex_algorithms, maxlen) title, atype = 'host-key algorithms', 'key' @@ -1880,8 +1944,8 @@ def output(banner, header, kex=None, pkm=None): output_algorithms(title, adb, atype, kex.server.encryption, maxlen) title, atype = 'message authentication code algorithms', 'mac' output_algorithms(title, adb, atype, kex.server.mac, maxlen) - output_recommendations(software, kex, pkm, maxlen) - output_fingerprint(kex, pkm, True, maxlen) + output_recommendations(algs, software, maxlen) + output_fingerprint(algs, True, maxlen) class Utils(object): diff --git a/test/mypy-py2.sh b/test/mypy-py2.sh index f8e9244..766eb59 100755 --- a/test/mypy-py2.sh +++ b/test/mypy-py2.sh @@ -7,4 +7,4 @@ if [ $? -ne 0 ]; then fi _htmldir="${_cdir}/../html/mypy-py2" mkdir -p "${_htmldir}" -mypy --python-version 2.7 --config-file "${_cdir}/mypy.ini" --html-report "${_htmldir}" "${_cdir}/../ssh-audit.py" +mypy --python-version 2.7 --strict-optional --config-file "${_cdir}/mypy.ini" --html-report "${_htmldir}" "${_cdir}/../ssh-audit.py" diff --git a/test/mypy-py3.sh b/test/mypy-py3.sh index 0d2dfe5..c77ca4b 100755 --- a/test/mypy-py3.sh +++ b/test/mypy-py3.sh @@ -7,4 +7,4 @@ if [ $? -ne 0 ]; then fi _htmldir="${_cdir}/../html/mypy-py3" mkdir -p "${_htmldir}" -mypy --python-version 3.5 --config-file "${_cdir}/mypy.ini" --html-report "${_htmldir}" "${_cdir}/../ssh-audit.py" +mypy --python-version 3.5 --strict-optional --config-file "${_cdir}/mypy.ini" --html-report "${_htmldir}" "${_cdir}/../ssh-audit.py" diff --git a/test/test_socket.py b/test/test_socket.py new file mode 100644 index 0000000..d5c27fc --- /dev/null +++ b/test/test_socket.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import socket +import pytest + + +# pylint: disable=attribute-defined-outside-init +class TestSocket(object): + @pytest.fixture(autouse=True) + def init(self, ssh_audit): + self.ssh = ssh_audit.SSH + + def test_invalid_host(self, virtual_socket): + with pytest.raises(ValueError): + s = self.ssh.Socket(None, 22) + + def test_invalid_port(self, virtual_socket): + with pytest.raises(ValueError): + s = self.ssh.Socket('localhost', 'abc') + with pytest.raises(ValueError): + s = self.ssh.Socket('localhost', -1) + with pytest.raises(ValueError): + s = self.ssh.Socket('localhost', 0) + with pytest.raises(ValueError): + s = self.ssh.Socket('localhost', 65536) + + def test_not_connected_socket(self, virtual_socket): + sock = self.ssh.Socket('localhost', 22) + banner, header, err = sock.get_banner() + assert banner is None + assert len(header) == 0 + assert err == 'not connected' + s, e = sock.recv() + assert s == -1 + assert e == 'not connected' + s, e = sock.send('nothing') + assert s == -1 + assert e == 'not connected' + s, e = sock.send_packet() + assert s == -1 + assert e == 'not connected' From ff500ba84b8e008bf2bfb8a2876d96f249f19218 Mon Sep 17 00:00:00 2001 From: bs Date: Mon, 23 Jan 2017 17:45:25 +0200 Subject: [PATCH 009/103] Add OpenSSH CVE list (#25) --- ssh-audit.py | 64 ++++++++++++++++++++++++++++++++++++++++++++--- test/test_ssh1.py | 4 +-- 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/ssh-audit.py b/ssh-audit.py index 0d86e03..274778f 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -1419,10 +1419,10 @@ class SSH(object): # pylint: disable=too-few-public-methods 'Dropbear SSH': [ ['0.44', '2015.71', 1, 'CVE-2016-3116', 5.5, 'bypass command restrictions via xauth command injection'], ['0.28', '2013.58', 1, 'CVE-2013-4434', 5.0, 'discover valid usernames through different time delays'], - ['0.28', '2013.58', 1, 'CVE-2013-4421', 5.0, 'cause DoS (memory consumption) via a compressed packet'], + ['0.28', '2013.58', 1, 'CVE-2013-4421', 5.0, 'cause DoS via a compressed packet (memory consumption)'], ['0.52', '2011.54', 1, 'CVE-2012-0920', 7.1, 'execute arbitrary code or bypass command restrictions'], ['0.40', '0.48.1', 1, 'CVE-2007-1099', 7.5, 'conduct a MitM attack (no warning for hostkey mismatch)'], - ['0.28', '0.47', 1, 'CVE-2006-1206', 7.5, 'cause DoS (slot exhaustion) via large number of connections'], + ['0.28', '0.47', 1, 'CVE-2006-1206', 7.5, 'cause DoS via large number of connections (slot exhaustion)'], ['0.39', '0.47', 1, 'CVE-2006-0225', 4.6, 'execute arbitrary commands via scp with crafted filenames'], ['0.28', '0.46', 1, 'CVE-2005-4178', 6.5, 'execute arbitrary code via buffer overflow vulnerability'], ['0.28', '0.42', 1, 'CVE-2004-2486', 7.5, 'execute arbitrary code via DSS verification code']], @@ -1436,7 +1436,65 @@ class SSH(object): # pylint: disable=too-few-public-methods ['0.4.7', '0.5.2', 1, 'CVE-2012-4562', 7.5, 'cause DoS or execute arbitrary code (overflow check)'], ['0.4.7', '0.5.2', 1, 'CVE-2012-4561', 5.0, 'cause DoS via unspecified vectors (invalid pointer)'], ['0.4.7', '0.5.2', 1, 'CVE-2012-4560', 7.5, 'cause DoS or execute arbitrary code (buffer overflow)'], - ['0.4.7', '0.5.2', 1, 'CVE-2012-4559', 6.8, 'cause DoS or execute arbitrary code (double free)']] + ['0.4.7', '0.5.2', 1, 'CVE-2012-4559', 6.8, 'cause DoS or execute arbitrary code (double free)']], + 'OpenSSH': [ + ['7.2', '7.2p2', 1, 'CVE-2016-6515', 7.8, 'cause DoS via long password string (crypt CPU consumption)'], + ['1.2.2', '7.2', 1, 'CVE-2016-3115', 5.5, 'bypass command restrictions via crafted X11 forwarding data'], + ['5.4', '7.1', 1, 'CVE-2016-1907', 5.0, 'cause DoS via crafted network traffic (out of bounds read)'], + ['5.4', '7.1p1', 2, 'CVE-2016-0778', 4.6, 'cause DoS via requesting many forwardings (heap based buffer overflow)'], + ['5.0', '7.1p1', 2, 'CVE-2016-0777', 4.0, 'leak data via allowing transfer of entire buffer'], + ['6.0', '7.2p2', 5, 'CVE-2015-8325', 7.2, 'privilege escalation via triggering crafted environment'], + ['6.8', '6.9', 5, 'CVE-2015-6565', 7.2, 'cause DoS via writing to a device (terminal disruption)'], + ['5.0', '6.9', 5, 'CVE-2015-6564', 6.9, 'privilege escalation via leveraging sshd uid'], + ['5.0', '6.9', 5, 'CVE-2015-6563', 1.9, 'conduct impersonation attack'], + ['6.9p1', '6.9p1', 1, 'CVE-2015-5600', 8.5, 'cause Dos or aid in conduct brute force attack (CPU consumption)'], + ['6.0', '6.6', 1, 'CVE-2015-5352', 4.3, 'bypass access restrictions via a specific connection'], + ['6.0', '6.6', 2, 'CVE-2014-2653', 5.8, 'bypass SSHFP DNS RR check via unacceptable host certificate'], + ['5.0', '6.5', 1, 'CVE-2014-2532', 5.8, 'bypass environment restrictions via specific string before wildcard'], + ['1.2', '6.4', 1, 'CVE-2014-1692', 7.5, 'cause DoS via triggering error condition (memory corruption)'], + ['6.2', '6.3', 1, 'CVE-2013-4548', 6.0, 'bypass command restrictions via crafted packet data'], + ['1.2', '5.6', 1, 'CVE-2012-0814', 3.5, 'leak data via debug messages'], + ['1.2', '5.8', 1, 'CVE-2011-5000', 3.5, 'cause DoS via large value in certain length field (memory consumption)'], + ['5.6', '5.7', 2, 'CVE-2011-0539', 5.0, 'leak data or conduct hash collision attack'], + ['1.2', '6.1', 1, 'CVE-2010-5107', 5.0, 'cause DoS via large number of connections (slot exhaustion)'], + ['1.2', '5.8', 1, 'CVE-2010-4755', 4.0, 'cause DoS via crafted glob expression (CPU and memory consumption)'], + ['1.2', '5.6', 1, 'CVE-2010-4478', 7.5, 'bypass authentication check via crafted values'], + ['4.3', '4.8', 1, 'CVE-2009-2904', 6.9, 'privilege escalation via hard links to setuid programs'], + ['4.0', '5.1', 1, 'CVE-2008-5161', 2.6, 'recover plaintext data from ciphertext'], + ['1.2', '4.6', 1, 'CVE-2008-4109', 5.0, 'cause DoS via multiple login attempts (slot exhaustion)'], + ['1.2', '4.8', 1, 'CVE-2008-1657', 6.5, 'bypass command restrictions via modifying session file'], + ['1.2.2', '4.9', 1, 'CVE-2008-1483', 6.9, 'hijack forwarded X11 connections'], + ['4.0', '4.6', 1, 'CVE-2007-4752', 7.5, 'privilege escalation via causing an X client to be trusted'], + ['4.3p2', '4.3p2', 1, 'CVE-2007-3102', 4.3, 'allow attacker to write random data to audit log'], + ['1.2', '4.6', 1, 'CVE-2007-2243', 5.0, 'discover valid usernames through different responses'], + ['4.4', '4.4', 1, 'CVE-2006-5794', 7.5, 'bypass authentication'], + ['4.1', '4.1p1', 1, 'CVE-2006-5229', 2.6, 'discover valid usernames through different time delays'], + ['1.2', '4.3p2', 1, 'CVE-2006-5052', 5.0, 'discover valid usernames through different responses'], + ['1.2', '4.3p2', 1, 'CVE-2006-5051', 9.3, 'cause DoS or execute arbitrary code (double free)'], + ['4.5', '4.5', 1, 'CVE-2006-4925', 5.0, 'cause DoS via invalid protocol sequence (crash)'], + ['1.2', '4.3p2', 1, 'CVE-2006-4924', 7.8, 'cause DoS via crafted packet (CPU consumption)'], + ['3.8.1p1', '3.8.1p1', 1, 'CVE-2006-0883', 5.0, 'cause DoS via connecting multiple times (client connection refusal)'], + ['3.0', '4.2p1', 1, 'CVE-2006-0225', 4.6, 'execute arbitrary code'], + ['2.1', '4.1p1', 1, 'CVE-2005-2798', 5.0, 'leak data about authentication credentials'], + ['3.5', '3.5p1', 1, 'CVE-2004-2760', 6.8, 'leak data through different connection states'], + ['2.3', '3.7.1p2', 1, 'CVE-2004-2069', 5.0, 'cause DoS via large number of connections (slot exhaustion)'], + ['3.0', '3.4p1', 1, 'CVE-2004-0175', 4.3, 'leak data through directoy traversal'], + ['1.2', '3.9p1', 1, 'CVE-2003-1562', 7.6, 'leak data about authentication credentials'], + ['3.1p1', '3.7.1p1', 1, 'CVE-2003-0787', 7.5, 'privilege escalation via modifying stack'], + ['3.1p1', '3.7.1p1', 1, 'CVE-2003-0786', 10.0,'privilege escalation via bypassing authentication'], + ['1.0', '3.7.1', 1, 'CVE-2003-0695', 7.5, 'cause DoS or execute arbitrary code'], + ['1.0', '3.7', 1, 'CVE-2003-0693', 10.0,'execute arbitrary code'], + ['3.0', '3.6.1p2', 1, 'CVE-2003-0386', 7.5, 'bypass address restrictions for connection'], + ['3.1p1', '3.6.1p1', 1, 'CVE-2003-0190', 5.0, 'discover valid usernames through different time delays'], + ['3.2.2', '3.2.2', 1, 'CVE-2002-0765', 7.5, 'bypass authentication'], + ['1.2.2', '3.3p1', 1, 'CVE-2002-0640', 10.0,'execute arbitrary code'], + ['1.2.2', '3.3p1', 1, 'CVE-2002-0639', 10.0,'execute arbitrary code'], + ['2.1', '3.2', 1, 'CVE-2002-0575', 7.5, 'privilege escalation'], + ['2.1', '3.0.2p1', 2, 'CVE-2002-0083', 10.0,'privilege escalation'], + ['3.0', '3.0p1', 1, 'CVE-2001-1507', 7.5, 'bypass authentication'], + ['1.2.3', '3.0.1p1', 5, 'CVE-2001-0872', 7.2, 'privilege escalation via crafted environment variables'], + ['1.2.3', '2.1.1', 1, 'CVE-2001-0361', 4.0, 'recover plaintext from ciphertext'], + ['1.2', '2.1', 1, 'CVE-2000-0525', 10.0,'execute arbitrary code (improper privileges)']] } # type: Dict[str, List[List[Any]]] TXT = { 'Dropbear SSH': [ diff --git a/test/test_ssh1.py b/test/test_ssh1.py index 0029845..0f62983 100644 --- a/test/test_ssh1.py +++ b/test/test_ssh1.py @@ -108,7 +108,7 @@ class TestSSH1(object): output_spy.begin() self.audit(self._conf()) lines = output_spy.flush() - assert len(lines) == 10 + assert len(lines) == 13 def test_ssh1_server_invalid_first_packet(self, output_spy, virtual_socket): vsocket = virtual_socket @@ -121,7 +121,7 @@ class TestSSH1(object): with pytest.raises(SystemExit): self.audit(self._conf()) lines = output_spy.flush() - assert len(lines) == 4 + assert len(lines) == 7 assert 'unknown message' in lines[-1] def test_ssh1_server_invalid_checksum(self, output_spy, virtual_socket): From c9443e6e0697208025e6736cf47659ecb3fbb896 Mon Sep 17 00:00:00 2001 From: Andris Raugulis Date: Mon, 23 Jan 2017 19:20:42 +0200 Subject: [PATCH 010/103] Fix pyp3 version for Travis-CI (https://github.com/travis-ci/travis-ci/issues/6277). --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f1ee663..4832d8d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,7 @@ python: - 3.4 - 3.5 - pypy - - pypy3 + - pypy3.3-5.2-alpha1 install: - pip install --upgrade pytest - pip install --upgrade pytest-cov From 54b09605024e8109582cf3e8d38e4b91cd5e50ee Mon Sep 17 00:00:00 2001 From: Andris Raugulis Date: Mon, 23 Jan 2017 19:34:06 +0200 Subject: [PATCH 011/103] Upgrade to Mypy 0.470. Add colorama stub. Fix identation. --- ssh-audit.py | 17 +++++++++-------- test/mypy-py2.sh | 2 +- test/mypy-py3.sh | 2 +- test/mypy.ini | 4 ++-- test/stubs/colorama.pyi | 6 ++++++ 5 files changed, 19 insertions(+), 12 deletions(-) create mode 100644 test/stubs/colorama.pyi diff --git a/ssh-audit.py b/ssh-audit.py index 274778f..e406567 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -1267,16 +1267,17 @@ class SSH(object): # pylint: disable=too-few-public-methods def maxlen(self): # type: () -> int ml, maxlen = lambda l: max(len(i) for i in l), 0 + # type: Callable[[Sequence[text_type]], int], int if self.ssh1kex is not None: maxlen = max(ml(self.ssh1kex.supported_ciphers), - ml(self.ssh1kex.supported_authentications), - maxlen) + ml(self.ssh1kex.supported_authentications), + maxlen) if self.ssh2kex is not None: maxlen = max(ml(self.ssh2kex.kex_algorithms), - ml(self.ssh2kex.key_algorithms), - ml(self.ssh2kex.server.encryption), - ml(self.ssh2kex.server.mac), - maxlen) + ml(self.ssh2kex.key_algorithms), + ml(self.ssh2kex.server.encryption), + ml(self.ssh2kex.server.mac), + maxlen) return maxlen def get_ssh_timeframe(self, for_server=True): @@ -1298,8 +1299,8 @@ class SSH(object): # pylint: disable=too-few-public-methods # type: (Optional[SSH.Software], bool) -> Tuple[Optional[SSH.Software], Dict[int, Dict[str, Dict[str, Dict[str, int]]]]] # pylint: disable=too-many-locals,too-many-statements vproducts = [SSH.Product.OpenSSH, - SSH.Product.DropbearSSH, - SSH.Product.LibSSH] + SSH.Product.DropbearSSH, + SSH.Product.LibSSH] if software is not None: if software.product not in vproducts: software = None diff --git a/test/mypy-py2.sh b/test/mypy-py2.sh index 766eb59..2da29f5 100755 --- a/test/mypy-py2.sh +++ b/test/mypy-py2.sh @@ -7,4 +7,4 @@ if [ $? -ne 0 ]; then fi _htmldir="${_cdir}/../html/mypy-py2" mkdir -p "${_htmldir}" -mypy --python-version 2.7 --strict-optional --config-file "${_cdir}/mypy.ini" --html-report "${_htmldir}" "${_cdir}/../ssh-audit.py" +env MYPYPATH="${_cdir}/stubs/" mypy --python-version 2.7 --strict-optional --config-file "${_cdir}/mypy.ini" --html-report "${_htmldir}" "${_cdir}/../ssh-audit.py" diff --git a/test/mypy-py3.sh b/test/mypy-py3.sh index c77ca4b..0b7f27c 100755 --- a/test/mypy-py3.sh +++ b/test/mypy-py3.sh @@ -7,4 +7,4 @@ if [ $? -ne 0 ]; then fi _htmldir="${_cdir}/../html/mypy-py3" mkdir -p "${_htmldir}" -mypy --python-version 3.5 --strict-optional --config-file "${_cdir}/mypy.ini" --html-report "${_htmldir}" "${_cdir}/../ssh-audit.py" +env MYPYPATH="${_cdir}/stubs/" mypy --python-version 3.5 --strict-optional --config-file "${_cdir}/mypy.ini" --html-report "${_htmldir}" "${_cdir}/../ssh-audit.py" diff --git a/test/mypy.ini b/test/mypy.ini index 9c0a3e0..f4190cd 100644 --- a/test/mypy.ini +++ b/test/mypy.ini @@ -1,9 +1,9 @@ [mypy] -silent_imports = True +ignore_missing_imports = False +follow_imports = error disallow_untyped_calls = True disallow_untyped_defs = True check_untyped_defs = True disallow-subclassing-any = True warn-incomplete-stub = True warn-redundant-casts = True - diff --git a/test/stubs/colorama.pyi b/test/stubs/colorama.pyi new file mode 100644 index 0000000..81d6ef0 --- /dev/null +++ b/test/stubs/colorama.pyi @@ -0,0 +1,6 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from typing import Optional + +def init(autoreset: bool = False, convert: Optional[bool] = None, strip: Optional[bool] = None, wrap: bool = True) -> None: ... + From 9ac03d368a3fcbc1629a239f9d023ae4585b8f14 Mon Sep 17 00:00:00 2001 From: Andris Raugulis Date: Tue, 24 Jan 2017 12:45:53 +0200 Subject: [PATCH 012/103] Add OpenSSH 7.4 changes and use as default banner. --- ssh-audit.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ssh-audit.py b/ssh-audit.py index e406567..6087761 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -269,6 +269,7 @@ class OutputBuffer(list): class SSH2(object): # pylint: disable=too-few-public-methods class KexDB(object): # pylint: disable=too-few-public-methods # pylint: disable=bad-whitespace + WARN_OPENSSH74_UNSAFE = 'disabled (in client) since OpenSSH 7.4, unsafe algorithm' WARN_OPENSSH72_LEGACY = 'disabled (in client) since OpenSSH 7.2, legacy algorithm' FAIL_OPENSSH70_LEGACY = 'removed since OpenSSH 7.0, legacy algorithm' FAIL_OPENSSH70_WEAK = 'removed (in server) and disabled (in client) since OpenSSH 7.0, weak algorithm' @@ -304,6 +305,7 @@ class SSH2(object): # pylint: disable=too-few-public-methods 'ecdh-sha2-nistp384': [['5.7,d2013.62'], [WARN_CURVES_WEAK]], 'ecdh-sha2-nistp521': [['5.7,d2013.62'], [WARN_CURVES_WEAK]], 'curve25519-sha256@libssh.org': [['6.5,d2013.62,l10.6.0']], + 'curve25519-sha256': [['7.4']], 'kexguess2@matt.ucc.asn.au': [['d2013.57']], }, 'key': { @@ -326,7 +328,7 @@ class SSH2(object): # pylint: disable=too-few-public-methods }, 'enc': { 'none': [['1.2.2,d2013.56,l10.2'], [FAIL_PLAINTEXT]], - '3des-cbc': [['1.2.2,d0.28,l10.2', '6.6', None], [FAIL_OPENSSH67_UNSAFE], [WARN_CIPHER_WEAK, WARN_CIPHER_MODE, WARN_BLOCK_SIZE]], + '3des-cbc': [['1.2.2,d0.28,l10.2', '6.6', None], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH74_UNSAFE, WARN_CIPHER_WEAK, WARN_CIPHER_MODE, WARN_BLOCK_SIZE]], '3des-ctr': [['d0.52']], 'blowfish-cbc': [['1.2.2,d0.28,l10.2', '6.6,d0.52', '7.1,d0.52'], [FAIL_OPENSSH67_UNSAFE, FAIL_DBEAR53_DISABLED], [WARN_OPENSSH72_LEGACY, WARN_CIPHER_MODE, WARN_BLOCK_SIZE]], 'twofish-cbc': [['d0.28', 'd2014.66'], [FAIL_DBEAR67_DISABLED], [WARN_CIPHER_MODE]], @@ -1578,7 +1580,7 @@ class SSH(object): # pylint: disable=too-few-public-methods # type: (int) -> Tuple[Optional[SSH.Banner], List[text_type], Optional[str]] if self.__sock is None: return self.__banner, self.__header, 'not connected' - banner = 'SSH-{0}-OpenSSH_7.3'.format('1.5' if sshv == 1 else '2.0') + banner = 'SSH-{0}-OpenSSH_7.4'.format('1.5' if sshv == 1 else '2.0') rto = self.__sock.gettimeout() self.__sock.settimeout(0.7) s, e = self.recv() From 94a74e9cfda22ca9edd2cbb569ad47d7dacbd0c7 Mon Sep 17 00:00:00 2001 From: Andris Raugulis Date: Mon, 13 Feb 2017 13:33:50 +0200 Subject: [PATCH 013/103] Reviewed libssh-0.7.4 changes. --- test/test_software.py | 8 ++++---- test/test_version_compare.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/test/test_software.py b/test/test_software.py index 141ffec..4785041 100644 --- a/test/test_software.py +++ b/test/test_software.py @@ -168,17 +168,17 @@ class TestSoftware(object): assert s.display(True) == str(s) assert s.display(False) == str(s) assert repr(s) == '' - s = ps('SSH-2.0-libssh-0.7.3') + s = ps('SSH-2.0-libssh-0.7.4') assert s.vendor is None assert s.product == 'libssh' - assert s.version == '0.7.3' + assert s.version == '0.7.4' assert s.patch is None assert s.os is None - assert str(s) == 'libssh 0.7.3' + assert str(s) == 'libssh 0.7.4' assert str(s) == s.display() assert s.display(True) == str(s) assert s.display(False) == str(s) - assert repr(s) == '' + assert repr(s) == '' def test_romsshell_software(self): ps = lambda x: self.ssh.Software.parse(self.ssh.Banner.parse(x)) # noqa diff --git a/test/test_version_compare.py b/test/test_version_compare.py index d3f8554..b5c4a1f 100644 --- a/test/test_version_compare.py +++ b/test/test_version_compare.py @@ -200,7 +200,7 @@ class TestVersionCompare(object): versions.append('0.5.{0}'.format(i)) for i in range(0, 6): versions.append('0.6.{0}'.format(i)) - for i in range(0, 4): + for i in range(0, 5): versions.append('0.7.{0}'.format(i)) l = len(versions) for i in range(l): From 65ef250aaee69c916ddba71b24ad4eb48436a722 Mon Sep 17 00:00:00 2001 From: Andris Raugulis Date: Thu, 23 Mar 2017 23:17:35 +0200 Subject: [PATCH 014/103] Upgrade to Mypy 0.501 and fix issues. Add requirements.txt. --- ssh-audit.py | 18 +++++++++++------- test/mypy-py2.sh | 7 ++++++- test/mypy-py3.sh | 6 +++++- test/mypy.ini | 10 +++++++--- test/requirements-py2.txt | 5 +++++ test/requirements-py3.txt | 6 ++++++ 6 files changed, 40 insertions(+), 12 deletions(-) create mode 100644 test/requirements-py2.txt create mode 100644 test/requirements-py3.txt diff --git a/ssh-audit.py b/ssh-audit.py index 6087761..b482467 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -39,7 +39,7 @@ else: # pragma: nocover binary_type = str try: # pragma: nocover # pylint: disable=unused-import - from typing import List, Set, Sequence, Tuple, Iterable + from typing import Dict, List, Set, Sequence, Tuple, Iterable from typing import Callable, Optional, Union, Any except ImportError: # pragma: nocover pass @@ -713,7 +713,8 @@ class ReadBuf(object): def read_byte(self): # type: () -> int - return struct.unpack('B', self.read(1))[0] + v = struct.unpack('B', self.read(1))[0] # type: int + return v def read_bool(self): # type: () -> bool @@ -721,7 +722,8 @@ class ReadBuf(object): def read_int(self): # type: () -> int - return struct.unpack('>I', self.read(4))[0] + v = struct.unpack('>I', self.read(4))[0] # type: int + return v def read_list(self): # type: () -> List[text_type] @@ -1268,8 +1270,10 @@ class SSH(object): # pylint: disable=too-few-public-methods @property def maxlen(self): # type: () -> int - ml, maxlen = lambda l: max(len(i) for i in l), 0 - # type: Callable[[Sequence[text_type]], int], int + maxlen = 0 + def ml(items): + # type: (Sequence[text_type]) -> int + return max(len(i) for i in items) if self.ssh1kex is not None: maxlen = max(ml(self.ssh1kex.supported_ciphers), ml(self.ssh1kex.supported_authentications), @@ -1536,8 +1540,8 @@ class SSH(object): # pylint: disable=too-few-public-methods ipvo_len = len(ipvo) prefer_ipvo = ipvo_len > 0 prefer_ipv4 = prefer_ipvo and ipvo[0] == 4 - if len(ipvo) == 1: - family = {4: socket.AF_INET, 6: socket.AF_INET6}.get(ipvo[0]) + if ipvo_len == 1: + family = socket.AF_INET if ipvo[0] == 4 else socket.AF_INET6 else: family = socket.AF_UNSPEC try: diff --git a/test/mypy-py2.sh b/test/mypy-py2.sh index 2da29f5..17ba99e 100755 --- a/test/mypy-py2.sh +++ b/test/mypy-py2.sh @@ -7,4 +7,9 @@ if [ $? -ne 0 ]; then fi _htmldir="${_cdir}/../html/mypy-py2" mkdir -p "${_htmldir}" -env MYPYPATH="${_cdir}/stubs/" mypy --python-version 2.7 --strict-optional --config-file "${_cdir}/mypy.ini" --html-report "${_htmldir}" "${_cdir}/../ssh-audit.py" +env MYPYPATH="${_cdir}/stubs/" mypy \ +--python-version 2.7 \ +--no-warn-incomplete-stub \ +--show-error-context \ +--config-file "${_cdir}/mypy.ini" \ +--html-report "${_htmldir}" "${_cdir}/../ssh-audit.py" diff --git a/test/mypy-py3.sh b/test/mypy-py3.sh index 0b7f27c..81f2421 100755 --- a/test/mypy-py3.sh +++ b/test/mypy-py3.sh @@ -7,4 +7,8 @@ if [ $? -ne 0 ]; then fi _htmldir="${_cdir}/../html/mypy-py3" mkdir -p "${_htmldir}" -env MYPYPATH="${_cdir}/stubs/" mypy --python-version 3.5 --strict-optional --config-file "${_cdir}/mypy.ini" --html-report "${_htmldir}" "${_cdir}/../ssh-audit.py" +env MYPYPATH="${_cdir}/stubs/" mypy \ +--python-version 3.5 \ +--show-error-context \ +--config-file "${_cdir}/mypy.ini" \ +--html-report "${_htmldir}" "${_cdir}/../ssh-audit.py" diff --git a/test/mypy.ini b/test/mypy.ini index f4190cd..128ac30 100644 --- a/test/mypy.ini +++ b/test/mypy.ini @@ -4,6 +4,10 @@ follow_imports = error disallow_untyped_calls = True disallow_untyped_defs = True check_untyped_defs = True -disallow-subclassing-any = True -warn-incomplete-stub = True -warn-redundant-casts = True +disallow_subclassing_any = True +warn_incomplete_stub = True +warn_redundant_casts = True +warn_return_any = True +warn_unused_ignores = True +strict_optional = True +#strict_boolean = False diff --git a/test/requirements-py2.txt b/test/requirements-py2.txt new file mode 100644 index 0000000..bd50c11 --- /dev/null +++ b/test/requirements-py2.txt @@ -0,0 +1,5 @@ +pytest==3.0.7 +pytest-cov==2.4.0 +lxml==3.7.3 +colorama==0.3.7 +prospector==0.12.4 diff --git a/test/requirements-py3.txt b/test/requirements-py3.txt new file mode 100644 index 0000000..9957de7 --- /dev/null +++ b/test/requirements-py3.txt @@ -0,0 +1,6 @@ +pytest==3.0.7 +pytest-cov==2.4.0 +mypy==0.501 +lxml==3.7.3 +colorama==0.3.7 +prospector==0.12.4 From d3ba5a4e6ffa3aebcac4055a86f08d6f22c67d2b Mon Sep 17 00:00:00 2001 From: Andris Raugulis Date: Sat, 25 Mar 2017 06:00:42 +0200 Subject: [PATCH 015/103] Use tox, use codecov, work around pypy3 issues. --- .gitignore | 4 +- .travis.yml | 62 +++++++++++++++----- tox.ini | 161 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 213 insertions(+), 14 deletions(-) create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore index 481cc4a..8a442a4 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ *.pyc html/ venv/ -.cache/ \ No newline at end of file +.cache/ +.tox +.coverage diff --git a/.travis.yml b/.travis.yml index 4832d8d..0722577 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,18 +1,54 @@ language: python -python: - - 2.6 - - 2.7 - - 3.3 - - 3.4 - - 3.5 - - pypy - - pypy3.3-5.2-alpha1 +matrix: + include: + - python: 2.6 + env: TOXENV=py26 + - python: 2.7 + env: TOXENV=py27 + - python: 3.3 + env: TOXENV=py33 + - python: 3.4 + env: TOXENV=py34 + - python: 3.5 + env: TOXENV=py35 + - python: 3.6 + env: TOXENV=py36 + - python: 3.7-dev + env: TOXENV=py37 + - python: nightly + env: TOXENV=py37 + - python: pypy + env: TOXENV=pypy + - python: pypy3 + env: TOXENV=pypy3 + - python: pypy3.3-5.2-alpha1 + env: TOXENV=pypy3 + - python: pypy3.3-5.5-alpha + env: TOXENV=pypy3 + allow_failures: + - python: pypy3 + - python: 3.7-dev + - python: nightly install: - - pip install --upgrade pytest - - pip install --upgrade pytest-cov - - pip install --upgrade coveralls + - pip install --upgrade tox coveralls codecov script: - - py.test --cov-report= --cov=ssh-audit -v test + - if [ -z "${TOXENV##*py3*}" ]; then + export MYPYBASE=python; + if [ -z "${TOXENV##*pypy3*}" ]; then + _pydir=$(dirname $(which python)); + ln -s -- "${_pydir}/python" "${_pydir}/pypy3"; + export TOXENV=${TOXENV},cov,lint; + else + export TOXENV=${TOXENV},cov,type,lint; + fi + else + export MYPYBASE=python-unknown; + export TOXENV=${TOXENV},cov,lint; + fi + - tox -e $TOXENV after_success: - coveralls - + - codecov +after_failure: + - cat .tox/log/* + - cat .tox/*/log/* diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..b5e5acc --- /dev/null +++ b/tox.ini @@ -0,0 +1,161 @@ +[tox] +envlist = py26,py27,py33,py34,py35,py36,py37,jython,pypy,pypy3,cov,type,lint +skipsdist = true +skip_missing_interpreters = true + +[testenv] +deps = + pytest==3.0.7 + coverage==4.3.4 + colorama==0.3.7 +setenv = + SSHAUDIT = {toxinidir}/ssh-audit.py + COVERAGE_FILE = {toxinidir}/.coverage.{envname} +commands = + coverage run --source ssh-audit -m -- pytest -v {posargs:test} + coverage report --show-missing + coverage html -d {toxinidir}/html/coverage.{envname} + +[testenv:cov] +deps = + coverage==4.3.4 +setenv = + COVERAGE_FILE = {toxinidir}/.coverage +commands = + coverage erase + coverage combine + coverage report --show-missing + coverage html -d {toxinidir}/html/coverage +ignore_outcome = true + +[testenv:mypy] +basepython = + {env:MYPYBASE:python3.5} +deps = + colorama==0.3.7 + mypy==0.501 + lxml==3.7.3 +setenv = + {[testenv]setenv} + MYPYPATH = {toxinidir}/test/stubs + MYPYHTML = {toxinidir}/html/mypy +commands = + mypy \ + --show-error-context \ + --config-file {toxinidir}/tox.ini \ + --html-report {env:MYPYHTML}.py3 \ + {posargs:{env:SSHAUDIT}} + mypy \ + -2 \ + --no-warn-incomplete-stub \ + --show-error-context \ + --config-file {toxinidir}/tox.ini \ + --html-report {env:MYPYHTML}.py2 \ + {posargs:{env:SSHAUDIT}} + +[testenv:pylint] +deps = + mccabe + pylint +commands = + pylint \ + --rcfile tox.ini \ + --load-plugins=pylint.extensions.bad_builtin \ + --load-plugins=pylint.extensions.check_elif \ + --load-plugins=pylint.extensions.mccabe \ + {posargs:{env:SSHAUDIT}} + +[testenv:flake8] +deps = + flake8 +commands = + flake8 {posargs:{env:SSHAUDIT}} + +[testenv:vulture] +deps = vulture +commands = + python -c "import sys; from subprocess import Popen, PIPE; \ + a = ['vulture'] + r'{posargs:{env:SSHAUDIT}}'.split(' '); \ + o = Popen(a, shell=False, stdout=PIPE).communicate()[0]; \ + l = [x for x in o.split('\n') if x and 'Unused import' not in x]; \ + print('\n'.join(l)); \ + sys.exit(1 if len(l) > 0 else 0)" + +[testenv:type] +basepython = + {[testenv:mypy]basepython} +deps = + {[testenv:mypy]deps} +setenv = + {[testenv:mypy]setenv} +commands = + {[testenv:mypy]commands} +ignore_outcome = true + +[testenv:lint] +deps = + {[testenv:pylint]deps} + {[testenv:flake8]deps} + {[testenv:vulture]deps} +commands = + {[testenv:pylint]commands} + {[testenv:flake8]commands} + {[testenv:vulture]commands} +ignore_outcome = true + + +[mypy] +ignore_missing_imports = False +follow_imports = error +disallow_untyped_calls = True +disallow_untyped_defs = True +check_untyped_defs = True +disallow_subclassing_any = True +warn_incomplete_stub = True +warn_redundant_casts = True +warn_return_any = True +warn_unused_ignores = True +strict_optional = True +#strict_boolean = False + +[pylint] +reports = no +#output-format = colorized +indent-string = \t +disable = locally-disabled, bad-continuation, multiple-imports, invalid-name, trailing-whitespace, missing-docstring +max-complexity = 15 +max-args = 8 +max-locals = 20 +max-returns = 6 +max-branches = 15 +max-statements = 60 +max-parents = 7 +max-attributes = 8 +min-public-methods = 1 +max-public-methods = 20 +max-bool-expr = 5 +max-nested-blocks = 6 +max-line-length = 80 +ignore-long-lines = ^\s*(#\s+type:\s+.*|[A-Z0-9_]+\s+=\s+.*|('.*':\s+)?\[.*\],?)$ +max-module-lines = 2500 + +[flake8] +ignore = + # indentation contains tabs + W191, + # blank line contains whitespace + W293, + # indentation contains mixed spaces and tabs + E101, + # multiple spaces before operator + E221, + # multiple spaces after operator + E241, + # multiple imports on one line + E401, + # line too long + E501, + # module imported but unused + F401, + # undefined name + F821 From 8b7659c4d3d58b1446d0b6b368858998626e30da Mon Sep 17 00:00:00 2001 From: Andris Raugulis Date: Sat, 25 Mar 2017 08:02:49 +0200 Subject: [PATCH 016/103] Remove unnecessary files, now that everything is in tox. Add codecov badge. --- .gitignore | 1 + README.md | 1 + test/coverage.sh | 10 ---------- test/mypy-py2.sh | 15 -------------- test/mypy-py3.sh | 14 ------------- test/mypy.ini | 13 ------------ test/prospector.sh | 13 ------------ test/prospector.yml | 42 --------------------------------------- test/requirements-py2.txt | 5 ----- test/requirements-py3.txt | 6 ------ 10 files changed, 2 insertions(+), 118 deletions(-) delete mode 100755 test/coverage.sh delete mode 100755 test/mypy-py2.sh delete mode 100755 test/mypy-py3.sh delete mode 100644 test/mypy.ini delete mode 100755 test/prospector.sh delete mode 100644 test/prospector.yml delete mode 100644 test/requirements-py2.txt delete mode 100644 test/requirements-py3.txt diff --git a/.gitignore b/.gitignore index 8a442a4..0edf4f4 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ venv/ .cache/ .tox .coverage +coverage.xml diff --git a/README.md b/README.md index e9f8f13..c78d779 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # ssh-audit [![build status](https://api.travis-ci.org/arthepsy/ssh-audit.svg)](https://travis-ci.org/arthepsy/ssh-audit) +[![codecov](https://codecov.io/gh/arthepsy/ssh-audit/branch/develop/graph/badge.svg)](https://codecov.io/gh/arthepsy/ssh-audit) [![coverage status](https://coveralls.io/repos/github/arthepsy/ssh-audit/badge.svg)](https://coveralls.io/github/arthepsy/ssh-audit) **ssh-audit** is a tool for ssh server auditing. diff --git a/test/coverage.sh b/test/coverage.sh deleted file mode 100755 index 28f2010..0000000 --- a/test/coverage.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/sh -_cdir=$(cd -- "$(dirname "$0")" && pwd) -type py.test > /dev/null 2>&1 -if [ $? -ne 0 ]; then - echo "err: py.test (Python testing framework) not found." - exit 1 -fi -cd -- "${_cdir}/.." -mkdir -p html -py.test -v --cov-report=html:html/coverage --cov=ssh-audit test diff --git a/test/mypy-py2.sh b/test/mypy-py2.sh deleted file mode 100755 index 17ba99e..0000000 --- a/test/mypy-py2.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/sh -_cdir=$(cd -- "$(dirname "$0")" && pwd) -type mypy > /dev/null 2>&1 -if [ $? -ne 0 ]; then - echo "err: mypy (Optional Static Typing for Python) not found." - exit 1 -fi -_htmldir="${_cdir}/../html/mypy-py2" -mkdir -p "${_htmldir}" -env MYPYPATH="${_cdir}/stubs/" mypy \ ---python-version 2.7 \ ---no-warn-incomplete-stub \ ---show-error-context \ ---config-file "${_cdir}/mypy.ini" \ ---html-report "${_htmldir}" "${_cdir}/../ssh-audit.py" diff --git a/test/mypy-py3.sh b/test/mypy-py3.sh deleted file mode 100755 index 81f2421..0000000 --- a/test/mypy-py3.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/sh -_cdir=$(cd -- "$(dirname "$0")" && pwd) -type mypy > /dev/null 2>&1 -if [ $? -ne 0 ]; then - echo "err: mypy (Optional Static Typing for Python) not found." - exit 1 -fi -_htmldir="${_cdir}/../html/mypy-py3" -mkdir -p "${_htmldir}" -env MYPYPATH="${_cdir}/stubs/" mypy \ ---python-version 3.5 \ ---show-error-context \ ---config-file "${_cdir}/mypy.ini" \ ---html-report "${_htmldir}" "${_cdir}/../ssh-audit.py" diff --git a/test/mypy.ini b/test/mypy.ini deleted file mode 100644 index 128ac30..0000000 --- a/test/mypy.ini +++ /dev/null @@ -1,13 +0,0 @@ -[mypy] -ignore_missing_imports = False -follow_imports = error -disallow_untyped_calls = True -disallow_untyped_defs = True -check_untyped_defs = True -disallow_subclassing_any = True -warn_incomplete_stub = True -warn_redundant_casts = True -warn_return_any = True -warn_unused_ignores = True -strict_optional = True -#strict_boolean = False diff --git a/test/prospector.sh b/test/prospector.sh deleted file mode 100755 index 4398ec7..0000000 --- a/test/prospector.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh -_cdir=$(cd -- "$(dirname "$0")" && pwd) -type prospector > /dev/null 2>&1 -if [ $? -ne 0 ]; then - echo "err: prospector (Python Static Analysis) not found." - exit 1 -fi -if [ X"$1" == X"" ]; then - _file="${_cdir}/../ssh-audit.py" -else - _file="$1" -fi -prospector -E --profile-path "${_cdir}" -P prospector "${_file}" diff --git a/test/prospector.yml b/test/prospector.yml deleted file mode 100644 index 474af15..0000000 --- a/test/prospector.yml +++ /dev/null @@ -1,42 +0,0 @@ -strictness: veryhigh -doc-warnings: false - -pylint: - disable: - - multiple-imports - - invalid-name - - trailing-whitespace - - options: - max-args: 8 # default: 5 - max-locals: 20 # default: 15 - max-returns: 6 - max-branches: 15 # default: 12 - max-statements: 60 # default: 50 - max-parents: 7 - max-attributes: 8 # default: 7 - min-public-methods: 1 # default: 2 - max-public-methods: 20 - max-bool-expr: 5 - max-nested-blocks: 6 # default: 5 - max-line-length: 80 # default: 100 - ignore-long-lines: ^\s*(#\s+type:\s+.*|[A-Z0-9_]+\s+=\s+.*|('.*':\s+)?\[.*\],?)$ - max-module-lines: 2500 # default: 10000 - -pep8: - disable: - - W191 # indentation contains tabs - - W293 # blank line contains whitespace - - E101 # indentation contains mixed spaces and tabs - - E401 # multiple imports on one line - - E501 # line too long - - E221 # multiple spaces before operator - -pyflakes: - disable: - - F401 # module imported but unused - - F821 # undefined name - -mccabe: - options: - max-complexity: 15 diff --git a/test/requirements-py2.txt b/test/requirements-py2.txt deleted file mode 100644 index bd50c11..0000000 --- a/test/requirements-py2.txt +++ /dev/null @@ -1,5 +0,0 @@ -pytest==3.0.7 -pytest-cov==2.4.0 -lxml==3.7.3 -colorama==0.3.7 -prospector==0.12.4 diff --git a/test/requirements-py3.txt b/test/requirements-py3.txt deleted file mode 100644 index 9957de7..0000000 --- a/test/requirements-py3.txt +++ /dev/null @@ -1,6 +0,0 @@ -pytest==3.0.7 -pytest-cov==2.4.0 -mypy==0.501 -lxml==3.7.3 -colorama==0.3.7 -prospector==0.12.4 From cfae0d020ad4e247ec6a9861e5a38bf7185b0182 Mon Sep 17 00:00:00 2001 From: Andris Raugulis Date: Sat, 25 Mar 2017 08:33:16 +0200 Subject: [PATCH 017/103] Fix vulture output for Python 3. --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index b5e5acc..92f8ff7 100644 --- a/tox.ini +++ b/tox.ini @@ -77,8 +77,8 @@ commands = python -c "import sys; from subprocess import Popen, PIPE; \ a = ['vulture'] + r'{posargs:{env:SSHAUDIT}}'.split(' '); \ o = Popen(a, shell=False, stdout=PIPE).communicate()[0]; \ - l = [x for x in o.split('\n') if x and 'Unused import' not in x]; \ - print('\n'.join(l)); \ + l = [x for x in o.split(b'\n') if x and b'Unused import' not in x]; \ + print(b'\n'.join(l).decode('utf-8')); \ sys.exit(1 if len(l) > 0 else 0)" [testenv:type] From 29d9e4270d6f42a0a3c1ebcbc9a89fce71bc8a83 Mon Sep 17 00:00:00 2001 From: Andris Raugulis Date: Sat, 25 Mar 2017 08:44:37 +0200 Subject: [PATCH 018/103] Fix flake8 reported issues. --- ssh-audit.py | 117 +++++++++++++++++++++++++-------------------------- 1 file changed, 58 insertions(+), 59 deletions(-) diff --git a/ssh-audit.py b/ssh-audit.py index b482467..3aaada7 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -1270,10 +1270,10 @@ class SSH(object): # pylint: disable=too-few-public-methods @property def maxlen(self): # type: () -> int - maxlen = 0 def ml(items): # type: (Sequence[text_type]) -> int return max(len(i) for i in items) + maxlen = 0 if self.ssh1kex is not None: maxlen = max(ml(self.ssh1kex.supported_ciphers), ml(self.ssh1kex.supported_authentications), @@ -1445,63 +1445,63 @@ class SSH(object): # pylint: disable=too-few-public-methods ['0.4.7', '0.5.2', 1, 'CVE-2012-4560', 7.5, 'cause DoS or execute arbitrary code (buffer overflow)'], ['0.4.7', '0.5.2', 1, 'CVE-2012-4559', 6.8, 'cause DoS or execute arbitrary code (double free)']], 'OpenSSH': [ - ['7.2', '7.2p2', 1, 'CVE-2016-6515', 7.8, 'cause DoS via long password string (crypt CPU consumption)'], - ['1.2.2', '7.2', 1, 'CVE-2016-3115', 5.5, 'bypass command restrictions via crafted X11 forwarding data'], - ['5.4', '7.1', 1, 'CVE-2016-1907', 5.0, 'cause DoS via crafted network traffic (out of bounds read)'], - ['5.4', '7.1p1', 2, 'CVE-2016-0778', 4.6, 'cause DoS via requesting many forwardings (heap based buffer overflow)'], - ['5.0', '7.1p1', 2, 'CVE-2016-0777', 4.0, 'leak data via allowing transfer of entire buffer'], - ['6.0', '7.2p2', 5, 'CVE-2015-8325', 7.2, 'privilege escalation via triggering crafted environment'], - ['6.8', '6.9', 5, 'CVE-2015-6565', 7.2, 'cause DoS via writing to a device (terminal disruption)'], - ['5.0', '6.9', 5, 'CVE-2015-6564', 6.9, 'privilege escalation via leveraging sshd uid'], - ['5.0', '6.9', 5, 'CVE-2015-6563', 1.9, 'conduct impersonation attack'], - ['6.9p1', '6.9p1', 1, 'CVE-2015-5600', 8.5, 'cause Dos or aid in conduct brute force attack (CPU consumption)'], - ['6.0', '6.6', 1, 'CVE-2015-5352', 4.3, 'bypass access restrictions via a specific connection'], - ['6.0', '6.6', 2, 'CVE-2014-2653', 5.8, 'bypass SSHFP DNS RR check via unacceptable host certificate'], - ['5.0', '6.5', 1, 'CVE-2014-2532', 5.8, 'bypass environment restrictions via specific string before wildcard'], - ['1.2', '6.4', 1, 'CVE-2014-1692', 7.5, 'cause DoS via triggering error condition (memory corruption)'], - ['6.2', '6.3', 1, 'CVE-2013-4548', 6.0, 'bypass command restrictions via crafted packet data'], - ['1.2', '5.6', 1, 'CVE-2012-0814', 3.5, 'leak data via debug messages'], - ['1.2', '5.8', 1, 'CVE-2011-5000', 3.5, 'cause DoS via large value in certain length field (memory consumption)'], - ['5.6', '5.7', 2, 'CVE-2011-0539', 5.0, 'leak data or conduct hash collision attack'], - ['1.2', '6.1', 1, 'CVE-2010-5107', 5.0, 'cause DoS via large number of connections (slot exhaustion)'], - ['1.2', '5.8', 1, 'CVE-2010-4755', 4.0, 'cause DoS via crafted glob expression (CPU and memory consumption)'], - ['1.2', '5.6', 1, 'CVE-2010-4478', 7.5, 'bypass authentication check via crafted values'], - ['4.3', '4.8', 1, 'CVE-2009-2904', 6.9, 'privilege escalation via hard links to setuid programs'], - ['4.0', '5.1', 1, 'CVE-2008-5161', 2.6, 'recover plaintext data from ciphertext'], - ['1.2', '4.6', 1, 'CVE-2008-4109', 5.0, 'cause DoS via multiple login attempts (slot exhaustion)'], - ['1.2', '4.8', 1, 'CVE-2008-1657', 6.5, 'bypass command restrictions via modifying session file'], - ['1.2.2', '4.9', 1, 'CVE-2008-1483', 6.9, 'hijack forwarded X11 connections'], - ['4.0', '4.6', 1, 'CVE-2007-4752', 7.5, 'privilege escalation via causing an X client to be trusted'], - ['4.3p2', '4.3p2', 1, 'CVE-2007-3102', 4.3, 'allow attacker to write random data to audit log'], - ['1.2', '4.6', 1, 'CVE-2007-2243', 5.0, 'discover valid usernames through different responses'], - ['4.4', '4.4', 1, 'CVE-2006-5794', 7.5, 'bypass authentication'], - ['4.1', '4.1p1', 1, 'CVE-2006-5229', 2.6, 'discover valid usernames through different time delays'], - ['1.2', '4.3p2', 1, 'CVE-2006-5052', 5.0, 'discover valid usernames through different responses'], - ['1.2', '4.3p2', 1, 'CVE-2006-5051', 9.3, 'cause DoS or execute arbitrary code (double free)'], - ['4.5', '4.5', 1, 'CVE-2006-4925', 5.0, 'cause DoS via invalid protocol sequence (crash)'], - ['1.2', '4.3p2', 1, 'CVE-2006-4924', 7.8, 'cause DoS via crafted packet (CPU consumption)'], - ['3.8.1p1', '3.8.1p1', 1, 'CVE-2006-0883', 5.0, 'cause DoS via connecting multiple times (client connection refusal)'], - ['3.0', '4.2p1', 1, 'CVE-2006-0225', 4.6, 'execute arbitrary code'], - ['2.1', '4.1p1', 1, 'CVE-2005-2798', 5.0, 'leak data about authentication credentials'], - ['3.5', '3.5p1', 1, 'CVE-2004-2760', 6.8, 'leak data through different connection states'], - ['2.3', '3.7.1p2', 1, 'CVE-2004-2069', 5.0, 'cause DoS via large number of connections (slot exhaustion)'], - ['3.0', '3.4p1', 1, 'CVE-2004-0175', 4.3, 'leak data through directoy traversal'], - ['1.2', '3.9p1', 1, 'CVE-2003-1562', 7.6, 'leak data about authentication credentials'], - ['3.1p1', '3.7.1p1', 1, 'CVE-2003-0787', 7.5, 'privilege escalation via modifying stack'], - ['3.1p1', '3.7.1p1', 1, 'CVE-2003-0786', 10.0,'privilege escalation via bypassing authentication'], - ['1.0', '3.7.1', 1, 'CVE-2003-0695', 7.5, 'cause DoS or execute arbitrary code'], - ['1.0', '3.7', 1, 'CVE-2003-0693', 10.0,'execute arbitrary code'], - ['3.0', '3.6.1p2', 1, 'CVE-2003-0386', 7.5, 'bypass address restrictions for connection'], - ['3.1p1', '3.6.1p1', 1, 'CVE-2003-0190', 5.0, 'discover valid usernames through different time delays'], - ['3.2.2', '3.2.2', 1, 'CVE-2002-0765', 7.5, 'bypass authentication'], - ['1.2.2', '3.3p1', 1, 'CVE-2002-0640', 10.0,'execute arbitrary code'], - ['1.2.2', '3.3p1', 1, 'CVE-2002-0639', 10.0,'execute arbitrary code'], - ['2.1', '3.2', 1, 'CVE-2002-0575', 7.5, 'privilege escalation'], - ['2.1', '3.0.2p1', 2, 'CVE-2002-0083', 10.0,'privilege escalation'], - ['3.0', '3.0p1', 1, 'CVE-2001-1507', 7.5, 'bypass authentication'], - ['1.2.3', '3.0.1p1', 5, 'CVE-2001-0872', 7.2, 'privilege escalation via crafted environment variables'], - ['1.2.3', '2.1.1', 1, 'CVE-2001-0361', 4.0, 'recover plaintext from ciphertext'], - ['1.2', '2.1', 1, 'CVE-2000-0525', 10.0,'execute arbitrary code (improper privileges)']] + ['7.2', '7.2p2', 1, 'CVE-2016-6515', 7.8, 'cause DoS via long password string (crypt CPU consumption)'], + ['1.2.2', '7.2', 1, 'CVE-2016-3115', 5.5, 'bypass command restrictions via crafted X11 forwarding data'], + ['5.4', '7.1', 1, 'CVE-2016-1907', 5.0, 'cause DoS via crafted network traffic (out of bounds read)'], + ['5.4', '7.1p1', 2, 'CVE-2016-0778', 4.6, 'cause DoS via requesting many forwardings (heap based buffer overflow)'], + ['5.0', '7.1p1', 2, 'CVE-2016-0777', 4.0, 'leak data via allowing transfer of entire buffer'], + ['6.0', '7.2p2', 5, 'CVE-2015-8325', 7.2, 'privilege escalation via triggering crafted environment'], + ['6.8', '6.9', 5, 'CVE-2015-6565', 7.2, 'cause DoS via writing to a device (terminal disruption)'], + ['5.0', '6.9', 5, 'CVE-2015-6564', 6.9, 'privilege escalation via leveraging sshd uid'], + ['5.0', '6.9', 5, 'CVE-2015-6563', 1.9, 'conduct impersonation attack'], + ['6.9p1', '6.9p1', 1, 'CVE-2015-5600', 8.5, 'cause Dos or aid in conduct brute force attack (CPU consumption)'], + ['6.0', '6.6', 1, 'CVE-2015-5352', 4.3, 'bypass access restrictions via a specific connection'], + ['6.0', '6.6', 2, 'CVE-2014-2653', 5.8, 'bypass SSHFP DNS RR check via unacceptable host certificate'], + ['5.0', '6.5', 1, 'CVE-2014-2532', 5.8, 'bypass environment restrictions via specific string before wildcard'], + ['1.2', '6.4', 1, 'CVE-2014-1692', 7.5, 'cause DoS via triggering error condition (memory corruption)'], + ['6.2', '6.3', 1, 'CVE-2013-4548', 6.0, 'bypass command restrictions via crafted packet data'], + ['1.2', '5.6', 1, 'CVE-2012-0814', 3.5, 'leak data via debug messages'], + ['1.2', '5.8', 1, 'CVE-2011-5000', 3.5, 'cause DoS via large value in certain length field (memory consumption)'], + ['5.6', '5.7', 2, 'CVE-2011-0539', 5.0, 'leak data or conduct hash collision attack'], + ['1.2', '6.1', 1, 'CVE-2010-5107', 5.0, 'cause DoS via large number of connections (slot exhaustion)'], + ['1.2', '5.8', 1, 'CVE-2010-4755', 4.0, 'cause DoS via crafted glob expression (CPU and memory consumption)'], + ['1.2', '5.6', 1, 'CVE-2010-4478', 7.5, 'bypass authentication check via crafted values'], + ['4.3', '4.8', 1, 'CVE-2009-2904', 6.9, 'privilege escalation via hard links to setuid programs'], + ['4.0', '5.1', 1, 'CVE-2008-5161', 2.6, 'recover plaintext data from ciphertext'], + ['1.2', '4.6', 1, 'CVE-2008-4109', 5.0, 'cause DoS via multiple login attempts (slot exhaustion)'], + ['1.2', '4.8', 1, 'CVE-2008-1657', 6.5, 'bypass command restrictions via modifying session file'], + ['1.2.2', '4.9', 1, 'CVE-2008-1483', 6.9, 'hijack forwarded X11 connections'], + ['4.0', '4.6', 1, 'CVE-2007-4752', 7.5, 'privilege escalation via causing an X client to be trusted'], + ['4.3p2', '4.3p2', 1, 'CVE-2007-3102', 4.3, 'allow attacker to write random data to audit log'], + ['1.2', '4.6', 1, 'CVE-2007-2243', 5.0, 'discover valid usernames through different responses'], + ['4.4', '4.4', 1, 'CVE-2006-5794', 7.5, 'bypass authentication'], + ['4.1', '4.1p1', 1, 'CVE-2006-5229', 2.6, 'discover valid usernames through different time delays'], + ['1.2', '4.3p2', 1, 'CVE-2006-5052', 5.0, 'discover valid usernames through different responses'], + ['1.2', '4.3p2', 1, 'CVE-2006-5051', 9.3, 'cause DoS or execute arbitrary code (double free)'], + ['4.5', '4.5', 1, 'CVE-2006-4925', 5.0, 'cause DoS via invalid protocol sequence (crash)'], + ['1.2', '4.3p2', 1, 'CVE-2006-4924', 7.8, 'cause DoS via crafted packet (CPU consumption)'], + ['3.8.1p1', '3.8.1p1', 1, 'CVE-2006-0883', 5.0, 'cause DoS via connecting multiple times (client connection refusal)'], + ['3.0', '4.2p1', 1, 'CVE-2006-0225', 4.6, 'execute arbitrary code'], + ['2.1', '4.1p1', 1, 'CVE-2005-2798', 5.0, 'leak data about authentication credentials'], + ['3.5', '3.5p1', 1, 'CVE-2004-2760', 6.8, 'leak data through different connection states'], + ['2.3', '3.7.1p2', 1, 'CVE-2004-2069', 5.0, 'cause DoS via large number of connections (slot exhaustion)'], + ['3.0', '3.4p1', 1, 'CVE-2004-0175', 4.3, 'leak data through directoy traversal'], + ['1.2', '3.9p1', 1, 'CVE-2003-1562', 7.6, 'leak data about authentication credentials'], + ['3.1p1', '3.7.1p1', 1, 'CVE-2003-0787', 7.5, 'privilege escalation via modifying stack'], + ['3.1p1', '3.7.1p1', 1, 'CVE-2003-0786', 10.0, 'privilege escalation via bypassing authentication'], + ['1.0', '3.7.1', 1, 'CVE-2003-0695', 7.5, 'cause DoS or execute arbitrary code'], + ['1.0', '3.7', 1, 'CVE-2003-0693', 10.0, 'execute arbitrary code'], + ['3.0', '3.6.1p2', 1, 'CVE-2003-0386', 7.5, 'bypass address restrictions for connection'], + ['3.1p1', '3.6.1p1', 1, 'CVE-2003-0190', 5.0, 'discover valid usernames through different time delays'], + ['3.2.2', '3.2.2', 1, 'CVE-2002-0765', 7.5, 'bypass authentication'], + ['1.2.2', '3.3p1', 1, 'CVE-2002-0640', 10.0, 'execute arbitrary code'], + ['1.2.2', '3.3p1', 1, 'CVE-2002-0639', 10.0, 'execute arbitrary code'], + ['2.1', '3.2', 1, 'CVE-2002-0575', 7.5, 'privilege escalation'], + ['2.1', '3.0.2p1', 2, 'CVE-2002-0083', 10.0, 'privilege escalation'], + ['3.0', '3.0p1', 1, 'CVE-2001-1507', 7.5, 'bypass authentication'], + ['1.2.3', '3.0.1p1', 5, 'CVE-2001-0872', 7.2, 'privilege escalation via crafted environment variables'], + ['1.2.3', '2.1.1', 1, 'CVE-2001-0361', 4.0, 'recover plaintext from ciphertext'], + ['1.2', '2.1', 1, 'CVE-2000-0525', 10.0, 'execute arbitrary code (improper privileges)']] } # type: Dict[str, List[List[Any]]] TXT = { 'Dropbear SSH': [ @@ -1785,7 +1785,6 @@ class KexGroup14(KexDH): # pragma: nocover super(KexGroup14, self).__init__('sha1', 2, p) - def output_algorithms(title, alg_db, alg_type, algorithms, maxlen=0): # type: (str, Dict[str, Dict[str, List[List[Optional[str]]]]], str, List[text_type], int) -> None with OutputBuffer() as obuf: From 6d9f5e6f2ab1ad7a6b1f551e518d0406cd9a1cef Mon Sep 17 00:00:00 2001 From: Andris Raugulis Date: Sun, 26 Mar 2017 05:39:24 +0300 Subject: [PATCH 019/103] Refactor tox.ini to be more versatile. --- .travis.yml | 11 ++++----- tox.ini | 70 ++++++++++++++++++++++------------------------------- 2 files changed, 34 insertions(+), 47 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0722577..8e07e44 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,5 @@ language: python +sudo: false matrix: include: - python: 2.6 @@ -33,19 +34,17 @@ install: - pip install --upgrade tox coveralls codecov script: - if [ -z "${TOXENV##*py3*}" ]; then - export MYPYBASE=python; if [ -z "${TOXENV##*pypy3*}" ]; then _pydir=$(dirname $(which python)); ln -s -- "${_pydir}/python" "${_pydir}/pypy3"; - export TOXENV=${TOXENV},cov,lint; + export TOXENV=${TOXENV}-test,${TOXENV}-lint; else - export TOXENV=${TOXENV},cov,type,lint; + export TOXENV=${TOXENV}-test,${TOXENV}-type,${TOXENV}-lint; fi else - export MYPYBASE=python-unknown; - export TOXENV=${TOXENV},cov,lint; + export TOXENV=${TOXENV}-test,${TOXENV}-lint; fi - - tox -e $TOXENV + - tox -e $TOXENV,cov after_success: - coveralls - codecov diff --git a/tox.ini b/tox.ini index 92f8ff7..16ba685 100644 --- a/tox.ini +++ b/tox.ini @@ -1,20 +1,36 @@ [tox] -envlist = py26,py27,py33,py34,py35,py36,py37,jython,pypy,pypy3,cov,type,lint +envlist = + py{26,27,py,py3}-{test,pylint,flake8,vulture} + py{33,34,35,36,37}-{test,mypy,pylint,flake8,vulture} + cov skipsdist = true skip_missing_interpreters = true [testenv] deps = - pytest==3.0.7 - coverage==4.3.4 - colorama==0.3.7 + test: pytest==3.0.7 + test,cov: {[testenv:cov]deps} + test,py{33,34,35,36,37}-{type,mypy}: colorama==0.3.7 + py{33,34,35,36,37}-{type,mypy}: {[testenv:mypy]deps} + lint,pylint: {[testenv:pylint]deps} + lint,flake8: {[testenv:flake8]deps} + lint,vulture: {[testenv:vulture]deps} setenv = SSHAUDIT = {toxinidir}/ssh-audit.py - COVERAGE_FILE = {toxinidir}/.coverage.{envname} + test: COVERAGE_FILE = {toxinidir}/.coverage.{envname} + type,mypy: MYPYPATH = {toxinidir}/test/stubs + type,mypy: MYPYHTML = {toxinidir}/html/mypy commands = - coverage run --source ssh-audit -m -- pytest -v {posargs:test} - coverage report --show-missing - coverage html -d {toxinidir}/html/coverage.{envname} + test: coverage run --source ssh-audit -m -- pytest -v {posargs:test} + test: coverage report --show-missing + test: - coverage html -d {toxinidir}/html/coverage.{envname} + py{33,34,35,36,37}-{type,mypy}: {[testenv:mypy]commands} + lint,pylint: {[testenv:pylint]commands} + lint,flake8: {[testenv:flake8]commands} + lint,vulture: {[testenv:vulture]commands} +ignore_outcome = + type: true + lint: true [testenv:cov] deps = @@ -25,32 +41,25 @@ commands = coverage erase coverage combine coverage report --show-missing - coverage html -d {toxinidir}/html/coverage -ignore_outcome = true + - coverage html -d {toxinidir}/html/coverage [testenv:mypy] -basepython = - {env:MYPYBASE:python3.5} deps = colorama==0.3.7 mypy==0.501 lxml==3.7.3 -setenv = - {[testenv]setenv} - MYPYPATH = {toxinidir}/test/stubs - MYPYHTML = {toxinidir}/html/mypy commands = mypy \ --show-error-context \ --config-file {toxinidir}/tox.ini \ - --html-report {env:MYPYHTML}.py3 \ + --html-report {env:MYPYHTML}.py3.{envname} \ {posargs:{env:SSHAUDIT}} mypy \ -2 \ --no-warn-incomplete-stub \ --show-error-context \ --config-file {toxinidir}/tox.ini \ - --html-report {env:MYPYHTML}.py2 \ + --html-report {env:MYPYHTML}.py2.{envname} \ {posargs:{env:SSHAUDIT}} [testenv:pylint] @@ -72,7 +81,8 @@ commands = flake8 {posargs:{env:SSHAUDIT}} [testenv:vulture] -deps = vulture +deps = + vulture commands = python -c "import sys; from subprocess import Popen, PIPE; \ a = ['vulture'] + r'{posargs:{env:SSHAUDIT}}'.split(' '); \ @@ -81,28 +91,6 @@ commands = print(b'\n'.join(l).decode('utf-8')); \ sys.exit(1 if len(l) > 0 else 0)" -[testenv:type] -basepython = - {[testenv:mypy]basepython} -deps = - {[testenv:mypy]deps} -setenv = - {[testenv:mypy]setenv} -commands = - {[testenv:mypy]commands} -ignore_outcome = true - -[testenv:lint] -deps = - {[testenv:pylint]deps} - {[testenv:flake8]deps} - {[testenv:vulture]deps} -commands = - {[testenv:pylint]commands} - {[testenv:flake8]commands} - {[testenv:vulture]commands} -ignore_outcome = true - [mypy] ignore_missing_imports = False From 74d1b5c7b513e8f5a6c1a74c433d695ffa4f55f1 Mon Sep 17 00:00:00 2001 From: Andris Raugulis Date: Sun, 26 Mar 2017 05:54:14 +0300 Subject: [PATCH 020/103] Fix pylint's bad-builtin and deprecated-lambda with list comprehension. --- ssh-audit.py | 6 +++--- tox.ini | 8 +++++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/ssh-audit.py b/ssh-audit.py index 3aaada7..d04c567 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -103,12 +103,12 @@ class AuditConf(object): if len(self.ipvo) == 0: value = (6,) if ipv == 4 else (4,) else: - value = tuple(filter(lambda x: x != ipv, self.ipvo)) + value = tuple([x for x in self.ipvo if x != ipv]) self.__setattr__('ipvo', value) elif name == 'ipvo': if isinstance(value, (tuple, list)): uniq_value = utils.unique_seq(value) - value = tuple(filter(lambda x: x in (4, 6), uniq_value)) + value = tuple([x for x in uniq_value if x in (4, 6)]) valid = True ipv_both = len(value) == 0 object.__setattr__(self, 'ipv4', ipv_both or 4 in value) @@ -1536,7 +1536,7 @@ class SSH(object): # pylint: disable=too-few-public-methods def _resolve(self, ipvo): # type: (Sequence[int]) -> Iterable[Tuple[int, Tuple[Any, ...]]] - ipvo = tuple(filter(lambda x: x in (4, 6), utils.unique_seq(ipvo))) + ipvo = tuple([x for x in utils.unique_seq(ipvo) if x in (4, 6)]) ipvo_len = len(ipvo) prefer_ipvo = ipvo_len > 0 prefer_ipv4 = prefer_ipvo and ipvo[0] == 4 diff --git a/tox.ini b/tox.ini index 16ba685..9119720 100644 --- a/tox.ini +++ b/tox.ini @@ -110,7 +110,13 @@ strict_optional = True reports = no #output-format = colorized indent-string = \t -disable = locally-disabled, bad-continuation, multiple-imports, invalid-name, trailing-whitespace, missing-docstring +disable = + locally-disabled, + bad-continuation, + multiple-imports, + invalid-name, + trailing-whitespace, + missing-docstring max-complexity = 15 max-args = 8 max-locals = 20 From 3ebb59108b7d05aa6e4bc9c862eececf961942d6 Mon Sep 17 00:00:00 2001 From: Andris Raugulis Date: Sun, 26 Mar 2017 05:58:39 +0300 Subject: [PATCH 021/103] Ignore pylint's else-if-used in validly used places. --- ssh-audit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ssh-audit.py b/ssh-audit.py index d04c567..72f3d84 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -99,7 +99,7 @@ class AuditConf(object): ipv = 4 if name == 'ipv4' else 6 if value: value = tuple(list(self.ipvo) + [ipv]) - else: + else: # pylint: disable=else-if-used if len(self.ipvo) == 0: value = (6,) if ipv == 4 else (4,) else: @@ -1834,7 +1834,7 @@ def output_algorithm(alg_db, alg_type, alg_name, alg_max_len=0): f = out.good f(prefix + alg_name + padding + ' -- ' + text) first = False - else: + else: # pylint: disable=else-if-used if out.verbose: f(prefix + alg_name + padding + ' -- ' + text) else: From 57a8744d03ab498c0c8b148b10c31ddcfc56e3ba Mon Sep 17 00:00:00 2001 From: Andris Raugulis Date: Sun, 26 Mar 2017 06:24:07 +0300 Subject: [PATCH 022/103] Fix some unused variable warnings. --- ssh-audit.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ssh-audit.py b/ssh-audit.py index 72f3d84..358be10 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -1550,7 +1550,7 @@ class SSH(object): # pylint: disable=too-few-public-methods if prefer_ipvo: r = sorted(r, key=lambda x: x[0], reverse=not prefer_ipv4) check = any(stype == rline[2] for rline in r) - for (af, socktype, proto, canonname, addr) in r: + for (af, socktype, _proto, _canonname, addr) in r: if not check or socktype == socket.SOCK_STREAM: yield (af, addr) except socket.error as e: @@ -1877,8 +1877,9 @@ def output_security_sub(sub, software, padlen): if not software.between_versions(vfrom, vtill): continue target, name = line[2:4] # type: int, str - is_server, is_client = target & 1 == 1, target & 2 == 2 - is_local = target & 4 == 4 + is_server = target & 1 == 1 + # is_client = target & 2 == 2 + # is_local = target & 4 == 4 if not is_server: continue p = '' if out.batch else ' ' * (padlen - len(name)) From 76849540be26b6302e3256b186b43a79f5af1109 Mon Sep 17 00:00:00 2001 From: Andris Raugulis Date: Sun, 26 Mar 2017 06:31:06 +0300 Subject: [PATCH 023/103] It's 2017 already. --- LICENSE | 2 +- ssh-audit.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index 0eb1032..a778a9a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (C) 2016 Andris Raugulis (moo@arthepsy.eu) +Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/ssh-audit.py b/ssh-audit.py index 358be10..e1684f7 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -3,7 +3,7 @@ """ The MIT License (MIT) - Copyright (C) 2016 Andris Raugulis (moo@arthepsy.eu) + Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 0ffb15dd54b9d4663b3109c45bb07ae8b3020779 Mon Sep 17 00:00:00 2001 From: Andris Raugulis Date: Sun, 26 Mar 2017 06:47:31 +0300 Subject: [PATCH 024/103] Pylint and flake8 is not supported on Python 2.6. --- .travis.yml | 13 +++++++------ tox.ini | 11 ++++++----- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8e07e44..b5ef195 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,16 +33,17 @@ matrix: install: - pip install --upgrade tox coveralls codecov script: - - if [ -z "${TOXENV##*py3*}" ]; then + - | + if [ -z "${TOXENV##*py3*}" ]; then if [ -z "${TOXENV##*pypy3*}" ]; then - _pydir=$(dirname $(which python)); - ln -s -- "${_pydir}/python" "${_pydir}/pypy3"; - export TOXENV=${TOXENV}-test,${TOXENV}-lint; + _pydir=$(dirname $(which python)) + ln -s -- "${_pydir}/python" "${_pydir}/pypy3" + export TOXENV=${TOXENV}-test,${TOXENV}-lint else - export TOXENV=${TOXENV}-test,${TOXENV}-type,${TOXENV}-lint; + export TOXENV=${TOXENV}-test,${TOXENV}-type,${TOXENV}-lint fi else - export TOXENV=${TOXENV}-test,${TOXENV}-lint; + export TOXENV=${TOXENV}-test,${TOXENV}-lint fi - tox -e $TOXENV,cov after_success: diff --git a/tox.ini b/tox.ini index 9119720..19c2274 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,7 @@ [tox] envlist = - py{26,27,py,py3}-{test,pylint,flake8,vulture} + py26-{test,vulture} + py{27,py,py3}-{test,pylint,flake8,vulture} py{33,34,35,36,37}-{test,mypy,pylint,flake8,vulture} cov skipsdist = true @@ -12,8 +13,8 @@ deps = test,cov: {[testenv:cov]deps} test,py{33,34,35,36,37}-{type,mypy}: colorama==0.3.7 py{33,34,35,36,37}-{type,mypy}: {[testenv:mypy]deps} - lint,pylint: {[testenv:pylint]deps} - lint,flake8: {[testenv:flake8]deps} + py{27,py,py3,33,34,35,36,37}-{lint,pylint}: {[testenv:pylint]deps} + py{27,py,py3,33,34,35,36,37}-{lint,flake8}: {[testenv:flake8]deps} lint,vulture: {[testenv:vulture]deps} setenv = SSHAUDIT = {toxinidir}/ssh-audit.py @@ -25,8 +26,8 @@ commands = test: coverage report --show-missing test: - coverage html -d {toxinidir}/html/coverage.{envname} py{33,34,35,36,37}-{type,mypy}: {[testenv:mypy]commands} - lint,pylint: {[testenv:pylint]commands} - lint,flake8: {[testenv:flake8]commands} + py{27,py,py3,33,34,35,36,37}-{lint,pylint}: {[testenv:pylint]commands} + py{27,py,py3,33,34,35,36,37}-{lint,flake8}: {[testenv:flake8]commands} lint,vulture: {[testenv:vulture]commands} ignore_outcome = type: true From 95ba7d11ce8db24df614f306d75ac42f46e1b9e8 Mon Sep 17 00:00:00 2001 From: Andris Raugulis Date: Sun, 26 Mar 2017 07:22:47 +0300 Subject: [PATCH 025/103] Test on Ubuntu 12.04/14.04 and Mac OS X 10.10-10.12. --- .travis.yml | 267 ++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 248 insertions(+), 19 deletions(-) diff --git a/.travis.yml b/.travis.yml index b5ef195..813934a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,54 +1,283 @@ language: python -sudo: false matrix: include: - - python: 2.6 + # Ubuntu 12.04 + - os: linux + dist: precise + python: 2.6 env: TOXENV=py26 - - python: 2.7 + - os: linux + dist: precise + python: 2.7 env: TOXENV=py27 - - python: 3.3 + - os: linux + dist: precise + python: 3.3 env: TOXENV=py33 - - python: 3.4 + - os: linux + dist: precise + python: 3.4 env: TOXENV=py34 - - python: 3.5 + - os: linux + dist: precise + python: 3.5 env: TOXENV=py35 - - python: 3.6 + - os: linux + dist: precise + python: 3.6 env: TOXENV=py36 - - python: 3.7-dev + - os: linux + dist: precise + python: 3.7-dev env: TOXENV=py37 - - python: nightly + - os: linux + dist: precise + python: nightly env: TOXENV=py37 - - python: pypy + - os: linux + dist: precise + python: pypy + env: TOXENV=pypy PYORIGIN=pyenv + - os: linux + dist: precise + python: pypy3 + env: TOXENV=pypy3 PYORIGIN=pyenv + # Ubuntu 14.04 + - os: linux + dist: trusty + python: 2.6 + env: TOXENV=py26 + - os: linux + dist: trusty + python: 2.7 + env: TOXENV=py27 + - os: linux + dist: trusty + python: 3.3 + env: TOXENV=py33 + - os: linux + dist: trusty + python: 3.4 + env: TOXENV=py34 + - os: linux + dist: trusty + python: 3.5 + env: TOXENV=py35 + - os: linux + dist: trusty + python: 3.6 + env: TOXENV=py36 + - os: linux + dist: trusty + python: 3.7-dev + env: TOXENV=py37 + - os: linux + dist: trusty + python: nightly + env: TOXENV=py37 + - os: linux + dist: trusty + python: 2.7 # NOTE: workaround for trusty + env: TOXENV=pypy PYORIGIN=pyenv + - os: linux + dist: trusty + python: 3.5 # NOTE: workaround for trusty + env: TOXENV=pypy3 PYORIGIN=pyenv + # Mac OS X 10.10 Yosemite + - os: osx + osx_image: xcode6.4 + language: generic + env: TOXENV=py26 + - os: osx + osx_image: xcode6.4 + language: generic + env: TOXENV=py27 + - os: osx + osx_image: xcode6.4 + language: generic + env: TOXENV=py33 + - os: osx + osx_image: xcode6.4 + language: generic + env: TOXENV=py34 + - os: osx + osx_image: xcode6.4 + language: generic + env: TOXENV=py35 + - os: osx + osx_image: xcode6.4 + language: generic + env: TOXENV=py36 + - os: osx + osx_image: xcode6.4 + language: generic + env: TOXENV=py37 + - os: osx + osx_image: xcode6.4 + language: generic env: TOXENV=pypy - - python: pypy3 + - os: osx + osx_image: xcode6.4 + language: generic env: TOXENV=pypy3 - - python: pypy3.3-5.2-alpha1 + # Mac OS X 10.11 El Capitan + - os: osx + osx_image: xcode7.3 + language: generic + env: TOXENV=py26 + - os: osx + osx_image: xcode7.3 + language: generic + env: TOXENV=py27 + - os: osx + osx_image: xcode7.3 + language: generic + env: TOXENV=py33 + - os: osx + osx_image: xcode7.3 + language: generic + env: TOXENV=py34 + - os: osx + osx_image: xcode7.3 + language: generic + env: TOXENV=py35 + - os: osx + osx_image: xcode7.3 + language: generic + env: TOXENV=py36 + - os: osx + osx_image: xcode7.3 + language: generic + env: TOXENV=py37 + - os: osx + osx_image: xcode7.3 + language: generic + env: TOXENV=pypy + - os: osx + osx_image: xcode7.3 + language: generic env: TOXENV=pypy3 - - python: pypy3.3-5.5-alpha + # macOS 10.12 Sierra + - os: osx + osx_image: xcode8.2 + language: generic + env: TOXENV=py26 + - os: osx + osx_image: xcode8.2 + language: generic + env: TOXENV=py27 + - os: osx + osx_image: xcode8.2 + language: generic + env: TOXENV=py33 + - os: osx + osx_image: xcode8.2 + language: generic + env: TOXENV=py34 + - os: osx + osx_image: xcode8.2 + language: generic + env: TOXENV=py35 + - os: osx + osx_image: xcode8.2 + language: generic + env: TOXENV=py36 + - os: osx + osx_image: xcode8.2 + language: generic + env: TOXENV=py37 + - os: osx + osx_image: xcode8.2 + language: generic + env: TOXENV=pypy + - os: osx + osx_image: xcode8.2 + language: generic env: TOXENV=pypy3 allow_failures: - - python: pypy3 - - python: 3.7-dev - - python: nightly + - env: TOXENV=3.7-dev + - env: TOXENV=nightly +sudo: false + +before_install: + - | + if [ X"$(uname -s)" == X"Darwin" ]; then + sw_vers + brew update || brew update + brew install autoconf pkg-config openssl readline xz + brew upgrade autoconf pkg-config openssl readline xz + PYORIGIN=pyenv + fi + - | + case "${PYORIGIN}" in + pyenv) + rm -rf ~/.pyenv + git clone --depth 1 https://github.com/yyuu/pyenv.git ~/.pyenv + PATH="$HOME/.pyenv/bin:$PATH" + eval "$(pyenv init -)" + pyenv -v + if [ X"$(uname -s)" == X"Darwin" ]; then + [ -z "${PYPY2VERSION}" ] && PYPY2VERSION=pypy2-5.7.0 + # NOTE: latest binary for osx is pypy3.3-5.5-alpha + [ -z "${PYPY3VERSION}" ] && PYPY3VERSION=pypy3.3-5.5-alpha + else + [ -z "${PYPY2VERSION}" ] && PYPY2VERSION=pypy-portable-5.7.0 + [ -z "${PYPY3VERSION}" ] && PYPY3VERSION=pypy3-portable-5.7.0 + fi + case "${TOXENV}" in + py26) pyenv install 2.6.9 || exit 1; pyenv global 2.6.9 ;; + py33) pyenv install 3.3.6 || exit 1; pyenv global 3.3.6 ;; + py34) pyenv install 3.4.6 || exit 1; pyenv global 3.4.6 ;; + py35) pyenv install 3.5.3 || exit 1; pyenv global 3.5.3 ;; + py36) pyenv install 3.6.1 || exit 1; pyenv global 3.6.1 ;; + py37) pyenv install 3.7-dev || exit 1; pyenv global 3.7-dev ;; + pypy) pyenv install ${PYPY2VERSION} || exit 1; pyenv global ${PYPY2VERSION} ;; + pypy3) pyenv install ${PYPY3VERSION} || exit 1; pyenv global ${PYPY3VERSION} ;; + esac + pyenv rehash + ;; + esac + - | + PIPOPT=$(python -c 'import sys; print("" if hasattr(sys, "real_prefix") else "--user")') + if [ -z "${TOXENV##py2*}" ]; then + curl -O https://bootstrap.pypa.io/get-pip.py + python get-pip.py ${PIPOPT} + fi + if [ X"${TOXENV}" == X"py26" ]; then + python -c 'import pip; pip.main();' install ${PIPOPT} -U pip virtualenv + python -c 'import virtualenv; virtualenv.main();' ~/.venv + else + python -m pip install ${PIPOPT} -U pip virtualenv + python -m virtualenv ~/.venv + fi + source ~/.venv/bin/activate + install: - pip install --upgrade tox coveralls codecov + script: - | if [ -z "${TOXENV##*py3*}" ]; then if [ -z "${TOXENV##*pypy3*}" ]; then + # NOTE: workaround for travis environment _pydir=$(dirname $(which python)) ln -s -- "${_pydir}/python" "${_pydir}/pypy3" - export TOXENV=${TOXENV}-test,${TOXENV}-lint + # NOTE: do not lint, as it hangs when flake8 is run + # NOTE: do not type, as it can't install dependencies + TOXENV=${TOXENV}-test else - export TOXENV=${TOXENV}-test,${TOXENV}-type,${TOXENV}-lint + TOXENV=${TOXENV}-test,${TOXENV}-type,${TOXENV}-lint fi else - export TOXENV=${TOXENV}-test,${TOXENV}-lint + # NOTE: do not type, as it isn't supported on py2x + TOXENV=${TOXENV}-test,${TOXENV}-lint fi - tox -e $TOXENV,cov + after_success: - coveralls - codecov + after_failure: - cat .tox/log/* - cat .tox/*/log/* From e91bbb5e307c304d4e7b6f5f35a7c1e71615fe9d Mon Sep 17 00:00:00 2001 From: Andris Raugulis Date: Tue, 28 Mar 2017 05:53:47 +0300 Subject: [PATCH 026/103] Better testing environment. --- .travis.yml | 296 ++++++----------------------------------- test/tools/ci-linux.sh | 225 +++++++++++++++++++++++++++++++ 2 files changed, 267 insertions(+), 254 deletions(-) create mode 100755 test/tools/ci-linux.sh diff --git a/.travis.yml b/.travis.yml index 813934a..30191d0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,283 +1,71 @@ language: python +sudo: false matrix: include: + # (default) + - os: linux + python: 2.6 + - os: linux + python: 2.7 + - os: linux + python: 3.3 + - os: linux + python: 3.4 + - os: linux + python: 3.5 + - os: linux + python: 3.6 + - os: linux + python: 3.7-dev # Ubuntu 12.04 - os: linux dist: precise - python: 2.6 - env: TOXENV=py26 - - os: linux - dist: precise - python: 2.7 - env: TOXENV=py27 - - os: linux - dist: precise - python: 3.3 - env: TOXENV=py33 - - os: linux - dist: precise - python: 3.4 - env: TOXENV=py34 - - os: linux - dist: precise - python: 3.5 - env: TOXENV=py35 - - os: linux - dist: precise - python: 3.6 - env: TOXENV=py36 - - os: linux - dist: precise - python: 3.7-dev - env: TOXENV=py37 - - os: linux - dist: precise - python: nightly - env: TOXENV=py37 - - os: linux - dist: precise - python: pypy - env: TOXENV=pypy PYORIGIN=pyenv - - os: linux - dist: precise - python: pypy3 - env: TOXENV=pypy3 PYORIGIN=pyenv + language: generic + env: PY_VER=py26,py27,py33,py34,py35,py36 PY_ORIGIN=pyenv # Ubuntu 14.04 - os: linux dist: trusty - python: 2.6 - env: TOXENV=py26 - - os: linux - dist: trusty - python: 2.7 - env: TOXENV=py27 - - os: linux - dist: trusty - python: 3.3 - env: TOXENV=py33 - - os: linux - dist: trusty - python: 3.4 - env: TOXENV=py34 - - os: linux - dist: trusty - python: 3.5 - env: TOXENV=py35 - - os: linux - dist: trusty - python: 3.6 - env: TOXENV=py36 - - os: linux - dist: trusty - python: 3.7-dev - env: TOXENV=py37 - - os: linux - dist: trusty - python: nightly - env: TOXENV=py37 - - os: linux - dist: trusty - python: 2.7 # NOTE: workaround for trusty - env: TOXENV=pypy PYORIGIN=pyenv - - os: linux - dist: trusty - python: 3.5 # NOTE: workaround for trusty - env: TOXENV=pypy3 PYORIGIN=pyenv - # Mac OS X 10.10 Yosemite - - os: osx - osx_image: xcode6.4 language: generic - env: TOXENV=py26 - - os: osx - osx_image: xcode6.4 - language: generic - env: TOXENV=py27 - - os: osx - osx_image: xcode6.4 - language: generic - env: TOXENV=py33 - - os: osx - osx_image: xcode6.4 - language: generic - env: TOXENV=py34 - - os: osx - osx_image: xcode6.4 - language: generic - env: TOXENV=py35 - - os: osx - osx_image: xcode6.4 - language: generic - env: TOXENV=py36 - - os: osx - osx_image: xcode6.4 - language: generic - env: TOXENV=py37 - - os: osx - osx_image: xcode6.4 - language: generic - env: TOXENV=pypy - - os: osx - osx_image: xcode6.4 - language: generic - env: TOXENV=pypy3 - # Mac OS X 10.11 El Capitan - - os: osx - osx_image: xcode7.3 - language: generic - env: TOXENV=py26 - - os: osx - osx_image: xcode7.3 - language: generic - env: TOXENV=py27 - - os: osx - osx_image: xcode7.3 - language: generic - env: TOXENV=py33 - - os: osx - osx_image: xcode7.3 - language: generic - env: TOXENV=py34 - - os: osx - osx_image: xcode7.3 - language: generic - env: TOXENV=py35 - - os: osx - osx_image: xcode7.3 - language: generic - env: TOXENV=py36 - - os: osx - osx_image: xcode7.3 - language: generic - env: TOXENV=py37 - - os: osx - osx_image: xcode7.3 - language: generic - env: TOXENV=pypy - - os: osx - osx_image: xcode7.3 - language: generic - env: TOXENV=pypy3 + env: PY_VER=py26,py27,py33,py34,py35,py36 PY_ORIGIN=pyenv # macOS 10.12 Sierra - os: osx osx_image: xcode8.2 language: generic - env: TOXENV=py26 + env: PY_VER=py26,py27,py33,py34,py35,py36 + # Mac OS X 10.11 El Capitan - os: osx - osx_image: xcode8.2 + osx_image: xcode7.3 language: generic - env: TOXENV=py27 + env: PY_VER=py26,py27,py33,py34,py35,py36 + # Mac OS X 10.10 Yosemite - os: osx - osx_image: xcode8.2 + osx_image: xcode6.4 language: generic - env: TOXENV=py33 - - os: osx - osx_image: xcode8.2 - language: generic - env: TOXENV=py34 - - os: osx - osx_image: xcode8.2 - language: generic - env: TOXENV=py35 - - os: osx - osx_image: xcode8.2 - language: generic - env: TOXENV=py36 - - os: osx - osx_image: xcode8.2 - language: generic - env: TOXENV=py37 - - os: osx - osx_image: xcode8.2 - language: generic - env: TOXENV=pypy - - os: osx - osx_image: xcode8.2 - language: generic - env: TOXENV=pypy3 + env: PY_VER=py26,py27,py33,py34,py35,py36 allow_failures: - - env: TOXENV=3.7-dev - - env: TOXENV=nightly -sudo: false + - python: 3.7-dev + - env: PY_VER=py37 + - env: PY_VER=py37/pyenv + - env: PY_VER=py37 PY_ORIGIN=pyenv + fast_finish: true + +cache: + - pip + - directories: + - $HOME/.pyenv.cache before_install: - - | - if [ X"$(uname -s)" == X"Darwin" ]; then - sw_vers - brew update || brew update - brew install autoconf pkg-config openssl readline xz - brew upgrade autoconf pkg-config openssl readline xz - PYORIGIN=pyenv - fi - - | - case "${PYORIGIN}" in - pyenv) - rm -rf ~/.pyenv - git clone --depth 1 https://github.com/yyuu/pyenv.git ~/.pyenv - PATH="$HOME/.pyenv/bin:$PATH" - eval "$(pyenv init -)" - pyenv -v - if [ X"$(uname -s)" == X"Darwin" ]; then - [ -z "${PYPY2VERSION}" ] && PYPY2VERSION=pypy2-5.7.0 - # NOTE: latest binary for osx is pypy3.3-5.5-alpha - [ -z "${PYPY3VERSION}" ] && PYPY3VERSION=pypy3.3-5.5-alpha - else - [ -z "${PYPY2VERSION}" ] && PYPY2VERSION=pypy-portable-5.7.0 - [ -z "${PYPY3VERSION}" ] && PYPY3VERSION=pypy3-portable-5.7.0 - fi - case "${TOXENV}" in - py26) pyenv install 2.6.9 || exit 1; pyenv global 2.6.9 ;; - py33) pyenv install 3.3.6 || exit 1; pyenv global 3.3.6 ;; - py34) pyenv install 3.4.6 || exit 1; pyenv global 3.4.6 ;; - py35) pyenv install 3.5.3 || exit 1; pyenv global 3.5.3 ;; - py36) pyenv install 3.6.1 || exit 1; pyenv global 3.6.1 ;; - py37) pyenv install 3.7-dev || exit 1; pyenv global 3.7-dev ;; - pypy) pyenv install ${PYPY2VERSION} || exit 1; pyenv global ${PYPY2VERSION} ;; - pypy3) pyenv install ${PYPY3VERSION} || exit 1; pyenv global ${PYPY3VERSION} ;; - esac - pyenv rehash - ;; - esac - - | - PIPOPT=$(python -c 'import sys; print("" if hasattr(sys, "real_prefix") else "--user")') - if [ -z "${TOXENV##py2*}" ]; then - curl -O https://bootstrap.pypa.io/get-pip.py - python get-pip.py ${PIPOPT} - fi - if [ X"${TOXENV}" == X"py26" ]; then - python -c 'import pip; pip.main();' install ${PIPOPT} -U pip virtualenv - python -c 'import virtualenv; virtualenv.main();' ~/.venv - else - python -m pip install ${PIPOPT} -U pip virtualenv - python -m virtualenv ~/.venv - fi - source ~/.venv/bin/activate + - source test/tools/ci-linux.sh + - ci_step_before_install install: - - pip install --upgrade tox coveralls codecov + - ci_step_install script: - - | - if [ -z "${TOXENV##*py3*}" ]; then - if [ -z "${TOXENV##*pypy3*}" ]; then - # NOTE: workaround for travis environment - _pydir=$(dirname $(which python)) - ln -s -- "${_pydir}/python" "${_pydir}/pypy3" - # NOTE: do not lint, as it hangs when flake8 is run - # NOTE: do not type, as it can't install dependencies - TOXENV=${TOXENV}-test - else - TOXENV=${TOXENV}-test,${TOXENV}-type,${TOXENV}-lint - fi - else - # NOTE: do not type, as it isn't supported on py2x - TOXENV=${TOXENV}-test,${TOXENV}-lint - fi - - tox -e $TOXENV,cov + - ci_step_script after_success: - - coveralls - - codecov + - ci_step_success after_failure: - - cat .tox/log/* - - cat .tox/*/log/* + - ci_step_failure diff --git a/test/tools/ci-linux.sh b/test/tools/ci-linux.sh new file mode 100755 index 0000000..c95f389 --- /dev/null +++ b/test/tools/ci-linux.sh @@ -0,0 +1,225 @@ +#!/bin/sh + +CI_VERBOSE=1 + +ci_err() { [ $1 -ne 0 ] && echo "err: $2" >&2 && exit 1; } +ci_is_osx() { [ X"$(uname -s)" == X"Darwin" ]; } + +ci_get_py_ver() { + local _v + case "$1" in + py26) _v=2.6.9 ;; + py27) _v=2.7.13 ;; + py33) _v=3.3.6 ;; + py34) _v=3.4.6 ;; + py35) _v=3.5.3 ;; + py36) _v=3.6.1 ;; + py37) _v=3.7-dev ;; + pypy) ci_is_osx && _v=pypy2-5.7.0 || _v=pypy-portable-5.7.0 ;; + pypy3) ci_is_osx && _v=pypy3.3-5.5-alpha || _v=pypy3-portable-5.7.0 ;; + *) [ -z "$1" ] && _v=$(python -V 2>&1 | cut -d ' ' -f 2) || _v="$1" ;; + esac + echo "${_v}" +} + +ci_get_py_env() { + if [ -z "$1" ]; then + set -- "$(python -V 2>&1)" + fi + case "$1" in + pypy|pypy2|pypy-*|pypy2-*) echo "pypy" ;; + pypy3|pypy3*) echo "pypy3" ;; + *) + local _v=$(echo "$1" | head -1 | sed -e 's/[^0-9]//g' | cut -c1-2;) + echo "$1" | tail -1 | grep -qi pypy + if [ $? -eq 0 ]; then + case "${_ver}" in + 2*) echo "pypy" ;; + *) echo "pypy3" ;; + esac + else + echo "py${_v}" + fi + esac +} + +ci_pyenv_setup() { + rm -rf ~/.pyenv + git clone --depth 1 https://github.com/yyuu/pyenv.git ~/.pyenv + PYENV_ROOT=$HOME/.pyenv + PATH="$HOME/.pyenv/bin:$PATH" + eval "$(pyenv init -)" + [ ${CI_VERBOSE} -gt 0 ] && echo "[ci] $(pyenv -v)" +} + +ci_pyenv_install() { + CI_PYENV_CACHE=~/.pyenv.cache + type pyenv > /dev/null 2>&1 + ci_err $? "pyenv not found" + local _py_ver=$(ci_get_py_ver "$1") + local _py_env=$(ci_get_py_env "${_py_ver}") + local _nocache + case "${_py_env}" in + py37) _nocache=1 ;; + esac + [ -z "${PYENV_ROOT}" ] && PYENV_ROOT="$HOME/.pyenv" + local _py_ver_dir="${PYENV_ROOT}/versions/${_py_ver}" + local _py_ver_cached_dir="${CI_PYENV_CACHE}/${_py_ver}" + if [ -z "${_nocache}" ]; then + if [ ! -d "${_py_ver_dir}" ]; then + if [ -d "${_py_ver_cached_dir}" ]; then + [ ${CI_VERBOSE} -gt 0 ] && echo "[ci] using cached pyenv ${_py_ver}" + ln -s "${_py_ver_cached_dir}" "${_py_ver_dir}" + fi + fi + fi + if [ ! -d "${_py_ver_dir}" ]; then + [ ${CI_VERBOSE} -gt 0 ] && echo "[ci] pyenv install ${_py_ver}" + pyenv install -s "${_py_ver}" + ci_err $? "pyenv failed to install ${_py_ver}" + if [ -z "${_nocache}" ]; then + [ ${CI_VERBOSE} -gt 0 ] && echo "[ci] pyenv cache ${_py_ver}" + rm -rf -- "${_py_ver_cached_dir}" + mkdir -p -- "${CI_PYENV_CACHE}" + mv "${_py_ver_dir}" "${_py_ver_cached_dir}" + ln -s "${_py_ver_cached_dir}" "${_py_ver_dir}" + fi + fi + pyenv rehash +} + +ci_pyenv_use() { + type pyenv > /dev/null 2>&1 + ci_err $? "pyenv not found" + local _py_ver=$(ci_get_py_ver "$1") + pyenv shell "${_py_ver}" + ci_err $? "pyenv could not use ${_py_ver}" + [ ${CI_VERBOSE} -gt 0 ] && echo "[ci] using python version: $(python -V 2>&1)" +} + +ci_pip_setup() { + local _py_ver=$(ci_get_py_ver "$1") + local _py_env=$(ci_get_py_env "${_py_ver}") + PIPOPT=$(python -c 'import sys; print("" if hasattr(sys, "real_prefix") else "--user")') + if [ -z "${_py_env##py2*}" ]; then + curl -O https://bootstrap.pypa.io/get-pip.py + python get-pip.py ${PIPOPT} + ci_err $? "failed to install pip" + fi + if [ X"${_py_env}" == X"py26" ]; then + python -c 'import pip; pip.main();' install ${PIPOPT} -U pip virtualenv + else + python -m pip install ${PIPOPT} -U pip virtualenv + fi +} + +ci_venv_setup() { + local _py_ver=$(ci_get_py_ver "$1") + local _py_env=$(ci_get_py_env "${_py_ver}") + local VENV_DIR=~/.venv/${_py_ver} + mkdir -p -- ~/.venv + rm -rf -- "${VENV_DIR}" + if [ X"${_py_env}" == X"py26" ]; then + python -c 'import virtualenv; virtualenv.main();' "${VENV_DIR}" + else + python -m virtualenv "${VENV_DIR}" + fi +} + +ci_venv_use() { + local _py_ver=$(ci_get_py_ver "$1") + local _py_env=$(ci_get_py_env "${_py_ver}") + local VENV_DIR=~/.venv/${_py_ver} + . "${VENV_DIR}/bin/activate" + [ ${CI_VERBOSE} -gt 0 ] && echo "[ci] virtualenv python path: $(which python)" +} + +ci_run_wrapped() { + local _versions=$(echo "${PY_VER}" | sed -e 's/,/ /g') + [ -z "${_versions}" ] && eval "$1" + for _i in ${_versions}; do + local _v=$(echo "$_i" | cut -d '/' -f 1) + local _o=$(echo "$_i" | cut -d '/' -sf 2) + [ -z "${_o}" ] && _o="${PY_ORIGIN}" + eval "$1" "${_v}" "${_o}" + done +} + +ci_step_before_install_wrapped() { + local _py_ver="$1" + local _py_ori="$2" + case "${_py_ori}" in + pyenv) + if [ "${CI_PYENV_SETUP}" -eq 0 ]; then + ci_pyenv_setup + CI_PYENV_SETUP=1 + fi + ci_pyenv_install "${_py_ver}" + ci_pyenv_use "${_py_ver}" + ;; + esac + ci_pip_setup "${_py_ver}" + ci_venv_setup "${_py_ver}" +} + +ci_step_before_install() { + if ci_is_osx; then + [ ${CI_VERBOSE} -gt 0 ] && sw_vers + brew update || brew update + brew install autoconf pkg-config openssl readline xz + brew upgrade autoconf pkg-config openssl readline xz + PY_ORIGIN=pyenv + fi + CI_PYENV_SETUP=0 + ci_run_wrapped "ci_step_before_install_wrapped" + if [ "${CI_PYENV_SETUP}" -eq 1 ]; then + pyenv shell --unset + [ ${CI_VERBOSE} -gt 0 ] && pyenv versions + fi +} + +ci_step_install_wrapped() { + local _py_ver="$1" + ci_venv_use "${_py_ver}" + pip install -U tox coveralls codecov +} + +ci_step_script_wrapped() { + local _py_ver="$1" + local _py_ori="$2" + local _py_env=$(ci_get_py_env "${_py_ver}") + ci_venv_use "${_py_ver}" + if [ -z "${_py_env##*py3*}" ]; then + if [ -z "${_py_env##*pypy3*}" ]; then + # NOTE: workaround for travis environment + _pydir=$(dirname $(which python)) + ln -s -- "${_pydir}/python" "${_pydir}/pypy3" + # NOTE: do not lint, as it hangs when flake8 is run + # NOTE: do not type, as it can't install dependencies + TOXENV=${_py_env}-test + else + TOXENV=${_py_env}-test,${_py_env}-type,${_py_env}-lint + fi + else + # NOTE: do not type, as it isn't supported on py2x + TOXENV=${_py_env}-test,${_py_env}-lint + fi + tox -e $TOXENV,cov +} + +ci_step_success_wrapped() { + local _py_ver="$1" + local _py_ori="$2" + ci_venv_use "${_py_ver}" + coveralls + codecov +} + +ci_step_failure() { + cat .tox/log/* + cat .tox/*/log/* +} + +ci_step_install() { ci_run_wrapped "ci_step_install_wrapped"; } +ci_step_script() { ci_run_wrapped "ci_step_script_wrapped"; } +ci_step_success() { ci_run_wrapped "ci_step_success_wrapped"; } From 2f7c64d896fc21d5c87860666939d223ddf20886 Mon Sep 17 00:00:00 2001 From: Andris Raugulis Date: Tue, 28 Mar 2017 10:25:55 +0300 Subject: [PATCH 027/103] Report python version in CI. --- test/tools/ci-linux.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/tools/ci-linux.sh b/test/tools/ci-linux.sh index c95f389..543b213 100755 --- a/test/tools/ci-linux.sh +++ b/test/tools/ci-linux.sh @@ -94,7 +94,7 @@ ci_pyenv_use() { local _py_ver=$(ci_get_py_ver "$1") pyenv shell "${_py_ver}" ci_err $? "pyenv could not use ${_py_ver}" - [ ${CI_VERBOSE} -gt 0 ] && echo "[ci] using python version: $(python -V 2>&1)" + [ ${CI_VERBOSE} -gt 0 ] && echo "[ci] using python: $(python -V 2>&1)" } ci_pip_setup() { @@ -131,7 +131,7 @@ ci_venv_use() { local _py_env=$(ci_get_py_env "${_py_ver}") local VENV_DIR=~/.venv/${_py_ver} . "${VENV_DIR}/bin/activate" - [ ${CI_VERBOSE} -gt 0 ] && echo "[ci] virtualenv python path: $(which python)" + [ ${CI_VERBOSE} -gt 0 ] && echo "[ci] using python: $(python -V 2>&1)" } ci_run_wrapped() { From 041805f6088479540a27bc0a841f0bc86ffdb032 Mon Sep 17 00:00:00 2001 From: Andris Raugulis Date: Tue, 28 Mar 2017 17:08:36 +0300 Subject: [PATCH 028/103] Test with AppVeyor environment. --- .appveyor.yml | 37 ++++++++++++ README.md | 3 +- test/tools/ci-win.cmd | 131 ++++++++++++++++++++++++++++++++++++++++++ tox.ini | 2 +- 4 files changed, 171 insertions(+), 2 deletions(-) create mode 100644 .appveyor.yml create mode 100644 test/tools/ci-win.cmd diff --git a/.appveyor.yml b/.appveyor.yml new file mode 100644 index 0000000..a367a30 --- /dev/null +++ b/.appveyor.yml @@ -0,0 +1,37 @@ +version: '1.7.1.dev.{build}' + +build: off +branches: + only: + - master + - develop + +environment: + matrix: + - PYTHON: "C:\\Python26" + - PYTHON: "C:\\Python26-x64" + - PYTHON: "C:\\Python27" + - PYTHON: "C:\\Python27-x64" + - PYTHON: "C:\\Python33" + - PYTHON: "C:\\Python33-x64" + - PYTHON: "C:\\Python34" + - PYTHON: "C:\\Python34-x64" + - PYTHON: "C:\\Python35" + - PYTHON: "C:\\Python35-x64" + - PYTHON: "C:\\Python36" + - PYTHON: "C:\\Python36-x64" +matrix: + fast_finish: true + +cache: + - '%LOCALAPPDATA%\pip\Cache' + - .downloads -> .appveyor.yml + +install: + - "cmd /c .\\test\\tools\\ci-win.cmd install" + +test_script: + - "cmd /c .\\test\\tools\\ci-win.cmd test" + +on_failure: + - ps: get-content .tox\*\log\* diff --git a/README.md b/README.md index c78d779..d709a1a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # ssh-audit -[![build status](https://api.travis-ci.org/arthepsy/ssh-audit.svg)](https://travis-ci.org/arthepsy/ssh-audit) +[![travis build status](https://api.travis-ci.org/arthepsy/ssh-audit.svg?branch=develop)](https://travis-ci.org/arthepsy/ssh-audit) +[![appveyor build status](https://ci.appveyor.com/api/projects/status/4m5r73m0r023edil/branch/develop?svg=true)](https://ci.appveyor.com/project/arthepsy/ssh-audit) [![codecov](https://codecov.io/gh/arthepsy/ssh-audit/branch/develop/graph/badge.svg)](https://codecov.io/gh/arthepsy/ssh-audit) [![coverage status](https://coveralls.io/repos/github/arthepsy/ssh-audit/badge.svg)](https://coveralls.io/github/arthepsy/ssh-audit) **ssh-audit** is a tool for ssh server auditing. diff --git a/test/tools/ci-win.cmd b/test/tools/ci-win.cmd new file mode 100644 index 0000000..103036c --- /dev/null +++ b/test/tools/ci-win.cmd @@ -0,0 +1,131 @@ +@ECHO OFF + +IF "%PYTHON%" == "" ( + ECHO PYTHON environment variable not set + EXIT 1 +) +SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" +FOR /F %%i IN ('python -c "import platform; print(platform.python_version());"') DO ( + SET PYTHON_VERSION=%%i +) +SET PYTHON_VERSION_MAJOR=%PYTHON_VERSION:~0,1% +IF "%PYTHON_VERSION:~3,1%" == "." ( + SET PYTHON_VERSION_MINOR=%PYTHON_VERSION:~2,1% +) ELSE ( + SET PYTHON_VERSION_MINOR=%PYTHON_VERSION:~2,2% +) +FOR /F %%i IN ('python -c "import struct; print(struct.calcsize(\"P\")*8)"') DO ( + SET PYTHON_ARCH=%%i +) +CALL :devenv + +IF /I "%1"=="" ( + SET target=test +) ELSE ( + SET target=%1 +) + +echo [CI] TARGET=%target% +GOTO %target% + +:devenv +SET WIN_SDK_ROOT=C:\Program Files\Microsoft SDKs\Windows +SET VS2015_ROOT=C:\Program Files (x86)\Microsoft Visual Studio 14.0 +IF %PYTHON_VERSION_MAJOR% == 2 ( + SET WINDOWS_SDK_VERSION="v7.0" +) ELSE IF %PYTHON_VERSION_MAJOR% == 3 ( + IF %PYTHON_VERSION_MAJOR% LEQ 4 ( + SET WINDOWS_SDK_VERSION="v7.1" + ) ELSE ( + SET WINDOWS_SDK_VERSION="2015" + ) +) ELSE ( + ECHO Unsupported Python version: "%PYTHON_VERSION%" + EXIT 1 +) +SETLOCAL ENABLEDELAYEDEXPANSION +IF %PYTHON_ARCH% == 32 (SET PYTHON_ARCHX=x86) ELSE (SET PYTHON_ARCHX=x64) +IF %WINDOWS_SDK_VERSION% == "2015" ( + "%VS2015_ROOT%\VC\vcvarsall.bat" %PYTHON_ARCHX% +) ELSE ( + SET DISTUTILS_USE_SDK=1 + SET MSSdk=1 + "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Setup\WindowsSdkVer.exe" -q -version:%WINDOWS_SDK_VERSION% + "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Bin\SetEnv.cmd" /%PYTHON_ARCHX% /release +) +GOTO :eof + +:install +pip install --user --upgrade pip virtualenv +SET VENV_DIR=.venv\%PYTHON_VERSION% +rmdir /s /q %VENV_DIR% > nul 2>nul +mkdir .venv > nul 2>nul +IF "%PYTHON_VERSION_MAJOR%%PYTHON_VERSION_MINOR%" == "26" ( + python -c "import virtualenv; virtualenv.main();" %VENV_DIR% +) ELSE ( + python -m virtualenv %VENV_DIR% +) +CALL %VENV_DIR%\Scripts\activate +python -V +pip install tox +deactivate +GOTO :eof + +:install_deps +SET LXML_FILE= +SET LXML_URL= +IF %PYTHON_VERSION_MAJOR% == 3 ( + IF %PYTHON_VERSION_MINOR% == 3 ( + IF %PYTHON_ARCH% == 32 ( + SET LXML_FILE=lxml-3.7.3.win32-py3.3.exe + SET LXML_URL=https://pypi.python.org/packages/66/fd/b82a54e7a15e91184efeef4b659379d0581a73cf78239d70feb0f0877841/lxml-3.7.3.win32-py3.3.exe + ) ELSE ( + SET LXML_FILE=lxml-3.7.3.win-amd64-py3.3.exe + SET LXML_URL=https://pypi.python.org/packages/dc/bc/4742b84793fa1fd991b5d2c6f2e5d32695659d6cfedf5c66aef9274a8723/lxml-3.7.3.win-amd64-py3.3.exe + ) + ) ELSE IF %PYTHON_VERSION_MINOR% == 4 ( + IF %PYTHON_ARCH% == 32 ( + SET LXML_FILE=lxml-3.7.3.win32-py3.4.exe + SET LXML_URL=https://pypi.python.org/packages/88/33/265459d68d465ddc707621e6471989f5c2cb0d43f230f516800ffd629af7/lxml-3.7.3.win32-py3.4.exe + ) ELSE ( + SET LXML_FILE=lxml-3.7.3.win-amd64-py3.4.exe + SET LXML_URL=https://pypi.python.org/packages/2d/65/e47db7f36a69a1b59b4f661e42d699d6c43e663b8fd91035e6f7681d017e/lxml-3.7.3.win-amd64-py3.4.exe + ) + ) +) +IF NOT "%LXML_FILE%" == "" ( + CALL :download %LXML_URL% .downloads\%LXML_FILE% + easy_install --user .downloads\%LXML_FILE% +) +GOTO :eof + +:test + SET VENV_DIR=.venv\%PYTHON_VERSION% + CALL %VENV_DIR%\Scripts\activate + IF "%TOXENV%" == "" ( + SET TOXENV=py%PYTHON_VERSION_MAJOR%%PYTHON_VERSION_MINOR% + ) + IF "%PYTHON_VERSION_MAJOR%%PYTHON_VERSION_MINOR%" == "26" ( + SET TOX=python -c "from tox import cmdline; cmdline()" + ) ELSE ( + SET TOX=python -m tox + ) + IF %PYTHON_VERSION_MAJOR% == 3 ( + IF %PYTHON_VERSION_MINOR% LEQ 4 ( + :: Python 3.3 and 3.4 does not support typed-ast (mypy dependency) + %TOX% --sitepackages -e %TOXENV%-test,%TOXENV%-lint,cov || EXIT 1 + ) ELSE ( + %TOX% --sitepackages -e %TOXENV%-test,%TOXENV%-type,%TOXENV%-lint,cov || EXIT 1 + ) + ) ELSE ( + %TOX% --sitepackages -e %TOXENV%-test,%TOXENV%-lint,cov || EXIT 1 + ) +GOTO :eof + +:download +IF NOT EXIST %2 ( + IF NOT EXIST .downloads\ mkdir .downloads + powershell -command "(new-object net.webclient).DownloadFile('%1', '%2')" || EXIT 1 + +) +GOTO :eof diff --git a/tox.ini b/tox.ini index 19c2274..9e5e3be 100644 --- a/tox.ini +++ b/tox.ini @@ -47,8 +47,8 @@ commands = [testenv:mypy] deps = colorama==0.3.7 - mypy==0.501 lxml==3.7.3 + mypy==0.501 commands = mypy \ --show-error-context \ From cab83f837aeb811ea70474997cb686b3ee66270e Mon Sep 17 00:00:00 2001 From: Andris Raugulis Date: Fri, 31 Mar 2017 02:48:51 +0300 Subject: [PATCH 029/103] Update to Xcode 8.3. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 30191d0..5e39fa0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,7 +29,7 @@ matrix: env: PY_VER=py26,py27,py33,py34,py35,py36 PY_ORIGIN=pyenv # macOS 10.12 Sierra - os: osx - osx_image: xcode8.2 + osx_image: xcode8.3 language: generic env: PY_VER=py26,py27,py33,py34,py35,py36 # Mac OS X 10.11 El Capitan From f33060827825d491e235de045d6b79773f934d70 Mon Sep 17 00:00:00 2001 From: Andris Raugulis Date: Fri, 31 Mar 2017 13:51:05 +0300 Subject: [PATCH 030/103] Test with pypy and pypy3 environments. --- .travis.yml | 17 +++++--- test/tools/ci-linux.sh | 91 +++++++++++++++++++++++++++++------------- 2 files changed, 76 insertions(+), 32 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5e39fa0..b0cb261 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,34 +15,41 @@ matrix: python: 3.5 - os: linux python: 3.6 + - os: linux + python: pypy + - os: linux + python: pypy3 - os: linux python: 3.7-dev # Ubuntu 12.04 - os: linux dist: precise language: generic - env: PY_VER=py26,py27,py33,py34,py35,py36 PY_ORIGIN=pyenv + env: PY_VER=py26,py27,py33,py34,py35,py36,pypy,pypy3 PY_ORIGIN=pyenv # Ubuntu 14.04 - os: linux dist: trusty language: generic - env: PY_VER=py26,py27,py33,py34,py35,py36 PY_ORIGIN=pyenv + env: PY_VER=py26,py27,py33,py34,py35,py36,pypy,pypy3 PY_ORIGIN=pyenv # macOS 10.12 Sierra - os: osx osx_image: xcode8.3 language: generic - env: PY_VER=py26,py27,py33,py34,py35,py36 + env: PY_VER=py26,py27,py33,py34,py35,py36,pypy,pypy3 # Mac OS X 10.11 El Capitan - os: osx osx_image: xcode7.3 language: generic - env: PY_VER=py26,py27,py33,py34,py35,py36 + env: PY_VER=py26,py27,py33,py34,py35,py36,pypy,pypy3 # Mac OS X 10.10 Yosemite - os: osx osx_image: xcode6.4 language: generic - env: PY_VER=py26,py27,py33,py34,py35,py36 + env: PY_VER=py26,py27,py33,py34,py35,py36,pypy,pypy3 allow_failures: + # PyPy3 on Travis CI is out of date + - python: pypy3 + # Python nightly could fail - python: 3.7-dev - env: PY_VER=py37 - env: PY_VER=py37/pyenv diff --git a/test/tools/ci-linux.sh b/test/tools/ci-linux.sh index 543b213..9453adc 100755 --- a/test/tools/ci-linux.sh +++ b/test/tools/ci-linux.sh @@ -5,6 +5,31 @@ CI_VERBOSE=1 ci_err() { [ $1 -ne 0 ] && echo "err: $2" >&2 && exit 1; } ci_is_osx() { [ X"$(uname -s)" == X"Darwin" ]; } +ci_get_pypy_ver() { + local _v="$1" + [ -z "$_v" ] && _v=$(python -V 2>&1) + case "$_v" in + pypy-*|pypy2-*|pypy3-*|pypy3.*) echo "$_v"; return 0 ;; + pypy|pypy2|pypy3) echo "$_v-unknown"; return 0 ;; + esac + echo "$_v" | tail -1 | grep -qi pypy + if [ $? -eq 0 ]; then + local _py_ver=$(echo "$_v" | head -1 | cut -d ' ' -sf 2) + local _pypy_ver=$(echo "$_v" | tail -1 | cut -d ' ' -sf 2) + [ -z "${_py_ver} " ] && _py_ver=2 + [ -z "${_pypy_ver}" ] && _pypy_ver="unknown" + case "${_py_ver}" in + 2*) echo "pypy-${_pypy_ver}" ;; + 3.3*) echo "pypy3.3-${_pypy_ver}" ;; + 3.5*) echo "pypy3.5-${_pypy_ver}" ;; + *) echo "pypy3-${_pypy_ver}" ;; + esac + return 0 + else + return 1 + fi +} + ci_get_py_ver() { local _v case "$1" in @@ -17,39 +42,38 @@ ci_get_py_ver() { py37) _v=3.7-dev ;; pypy) ci_is_osx && _v=pypy2-5.7.0 || _v=pypy-portable-5.7.0 ;; pypy3) ci_is_osx && _v=pypy3.3-5.5-alpha || _v=pypy3-portable-5.7.0 ;; - *) [ -z "$1" ] && _v=$(python -V 2>&1 | cut -d ' ' -f 2) || _v="$1" ;; + *) + [ -z "$1" ] && set -- "$(python -V 2>&1)" + _v=$(ci_get_pypy_ver "$1") + [ -z "$_v" ] && _v=$(echo "$_v" | head -1 | cut -d ' ' -sf 2) + ;; esac echo "${_v}" + return 0 } ci_get_py_env() { - if [ -z "$1" ]; then - set -- "$(python -V 2>&1)" - fi - case "$1" in + [ -z "$1" ] && set -- "$(python -V 2>&1)" + case "$(ci_get_pypy_ver "$1")" in pypy|pypy2|pypy-*|pypy2-*) echo "pypy" ;; pypy3|pypy3*) echo "pypy3" ;; *) local _v=$(echo "$1" | head -1 | sed -e 's/[^0-9]//g' | cut -c1-2;) - echo "$1" | tail -1 | grep -qi pypy - if [ $? -eq 0 ]; then - case "${_ver}" in - 2*) echo "pypy" ;; - *) echo "pypy3" ;; - esac - else - echo "py${_v}" - fi + echo "py${_v}" esac + return 0 } ci_pyenv_setup() { + [ ${CI_VERBOSE} -gt 0 ] && echo "[ci] install pyenv" rm -rf ~/.pyenv git clone --depth 1 https://github.com/yyuu/pyenv.git ~/.pyenv PYENV_ROOT=$HOME/.pyenv PATH="$HOME/.pyenv/bin:$PATH" eval "$(pyenv init -)" - [ ${CI_VERBOSE} -gt 0 ] && echo "[ci] $(pyenv -v)" + ci_err $? "failed to init pyenv" + [ ${CI_VERBOSE} -gt 0 ] && echo "[ci] pyenv init: $(pyenv -v 2>&1)" + return 0 } ci_pyenv_install() { @@ -62,19 +86,19 @@ ci_pyenv_install() { case "${_py_env}" in py37) _nocache=1 ;; esac + [ ${CI_VERBOSE} -gt 0 ] && echo "[ci] pyenv install: ${_py_env}/${_py_ver}" [ -z "${PYENV_ROOT}" ] && PYENV_ROOT="$HOME/.pyenv" local _py_ver_dir="${PYENV_ROOT}/versions/${_py_ver}" local _py_ver_cached_dir="${CI_PYENV_CACHE}/${_py_ver}" if [ -z "${_nocache}" ]; then if [ ! -d "${_py_ver_dir}" ]; then if [ -d "${_py_ver_cached_dir}" ]; then - [ ${CI_VERBOSE} -gt 0 ] && echo "[ci] using cached pyenv ${_py_ver}" + [ ${CI_VERBOSE} -gt 0 ] && echo "[ci] pyenv reuse ${_py_ver}" ln -s "${_py_ver_cached_dir}" "${_py_ver_dir}" fi fi fi if [ ! -d "${_py_ver_dir}" ]; then - [ ${CI_VERBOSE} -gt 0 ] && echo "[ci] pyenv install ${_py_ver}" pyenv install -s "${_py_ver}" ci_err $? "pyenv failed to install ${_py_ver}" if [ -z "${_nocache}" ]; then @@ -86,6 +110,7 @@ ci_pyenv_install() { fi fi pyenv rehash + return 0 } ci_pyenv_use() { @@ -94,12 +119,14 @@ ci_pyenv_use() { local _py_ver=$(ci_get_py_ver "$1") pyenv shell "${_py_ver}" ci_err $? "pyenv could not use ${_py_ver}" - [ ${CI_VERBOSE} -gt 0 ] && echo "[ci] using python: $(python -V 2>&1)" + [ ${CI_VERBOSE} -gt 0 ] && echo "[ci] pyenv using python: $(python -V 2>&1)" + return 0 } ci_pip_setup() { local _py_ver=$(ci_get_py_ver "$1") local _py_env=$(ci_get_py_env "${_py_ver}") + [ ${CI_VERBOSE} -gt 0 ] && echo "[ci] install pip/venv for ${_py_env}/${_py_ver}" PIPOPT=$(python -c 'import sys; print("" if hasattr(sys, "real_prefix") else "--user")') if [ -z "${_py_env##py2*}" ]; then curl -O https://bootstrap.pypa.io/get-pip.py @@ -111,11 +138,13 @@ ci_pip_setup() { else python -m pip install ${PIPOPT} -U pip virtualenv fi + ci_err $? "failed to upgrade pip/venv" || return 0 } ci_venv_setup() { local _py_ver=$(ci_get_py_ver "$1") local _py_env=$(ci_get_py_env "${_py_ver}") + [ ${CI_VERBOSE} -gt 0 ] && echo "[ci] create venv for ${_py_env}/${_py_ver}" local VENV_DIR=~/.venv/${_py_ver} mkdir -p -- ~/.venv rm -rf -- "${VENV_DIR}" @@ -124,6 +153,7 @@ ci_venv_setup() { else python -m virtualenv "${VENV_DIR}" fi + ci_err $? "failed to create venv" || return 0 } ci_venv_use() { @@ -131,7 +161,9 @@ ci_venv_use() { local _py_env=$(ci_get_py_env "${_py_ver}") local VENV_DIR=~/.venv/${_py_ver} . "${VENV_DIR}/bin/activate" - [ ${CI_VERBOSE} -gt 0 ] && echo "[ci] using python: $(python -V 2>&1)" + ci_err $? "could not actiavte virtualenv" + [ ${CI_VERBOSE} -gt 0 ] && echo "[ci] venv using python: $(python -V 2>&1)" + return 0 } ci_run_wrapped() { @@ -141,8 +173,9 @@ ci_run_wrapped() { local _v=$(echo "$_i" | cut -d '/' -f 1) local _o=$(echo "$_i" | cut -d '/' -sf 2) [ -z "${_o}" ] && _o="${PY_ORIGIN}" - eval "$1" "${_v}" "${_o}" + eval "$1" "${_v}" "${_o}" || return 1 done + return 0 } ci_step_before_install_wrapped() { @@ -154,12 +187,13 @@ ci_step_before_install_wrapped() { ci_pyenv_setup CI_PYENV_SETUP=1 fi - ci_pyenv_install "${_py_ver}" - ci_pyenv_use "${_py_ver}" + ci_pyenv_install "${_py_ver}" || return 1 + ci_pyenv_use "${_py_ver}" || return 1 ;; esac - ci_pip_setup "${_py_ver}" - ci_venv_setup "${_py_ver}" + ci_pip_setup "${_py_ver}" || return 1 + ci_venv_setup "${_py_ver}" || return 1 + return 0 } ci_step_before_install() { @@ -171,24 +205,26 @@ ci_step_before_install() { PY_ORIGIN=pyenv fi CI_PYENV_SETUP=0 - ci_run_wrapped "ci_step_before_install_wrapped" + ci_run_wrapped "ci_step_before_install_wrapped" || return 1 if [ "${CI_PYENV_SETUP}" -eq 1 ]; then pyenv shell --unset [ ${CI_VERBOSE} -gt 0 ] && pyenv versions fi + return 0 } ci_step_install_wrapped() { local _py_ver="$1" ci_venv_use "${_py_ver}" pip install -U tox coveralls codecov + ci_err $? "failed to install dependencies" || return 0 } ci_step_script_wrapped() { local _py_ver="$1" local _py_ori="$2" local _py_env=$(ci_get_py_env "${_py_ver}") - ci_venv_use "${_py_ver}" + ci_venv_use "${_py_ver}" || return 1 if [ -z "${_py_env##*py3*}" ]; then if [ -z "${_py_env##*pypy3*}" ]; then # NOTE: workaround for travis environment @@ -205,12 +241,13 @@ ci_step_script_wrapped() { TOXENV=${_py_env}-test,${_py_env}-lint fi tox -e $TOXENV,cov + ci_err $? "tox failed" || return 0 } ci_step_success_wrapped() { local _py_ver="$1" local _py_ori="$2" - ci_venv_use "${_py_ver}" + ci_venv_use "${_py_ver}" || return 1 coveralls codecov } From 9fe69841eb729fe7e8357bd39a12949e0f02bb5b Mon Sep 17 00:00:00 2001 From: Andris Raugulis Date: Wed, 5 Apr 2017 00:56:17 +0300 Subject: [PATCH 031/103] Integrate SonarQube analysis. --- .gitignore | 8 +-- .travis.yml | 2 + test/tools/ci-linux.sh | 154 ++++++++++++++++++++++++++++++++++++++++- tox.ini | 10 +-- 4 files changed, 164 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 0edf4f4..9dc68e2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,8 @@ *~ *.pyc -html/ -venv/ +venv*/ .cache/ .tox -.coverage -coverage.xml +.coverage* +reports/ +.scannerwork/ diff --git a/.travis.yml b/.travis.yml index b0cb261..08daa94 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,7 @@ matrix: python: 2.6 - os: linux python: 2.7 + env: SQ=1 - os: linux python: 3.3 - os: linux @@ -60,6 +61,7 @@ cache: - pip - directories: - $HOME/.pyenv.cache + - $HOME/.bin before_install: - source test/tools/ci-linux.sh diff --git a/test/tools/ci-linux.sh b/test/tools/ci-linux.sh index 9453adc..ba423e5 100755 --- a/test/tools/ci-linux.sh +++ b/test/tools/ci-linux.sh @@ -2,7 +2,8 @@ CI_VERBOSE=1 -ci_err() { [ $1 -ne 0 ] && echo "err: $2" >&2 && exit 1; } +ci_err_msg() { echo "[ci] error: $1" >&2; } +ci_err() { [ $1 -ne 0 ] && ci_err_msg "$2" && exit 1; } ci_is_osx() { [ X"$(uname -s)" == X"Darwin" ]; } ci_get_pypy_ver() { @@ -58,7 +59,7 @@ ci_get_py_env() { pypy|pypy2|pypy-*|pypy2-*) echo "pypy" ;; pypy3|pypy3*) echo "pypy3" ;; *) - local _v=$(echo "$1" | head -1 | sed -e 's/[^0-9]//g' | cut -c1-2;) + local _v=$(echo "$1" | head -1 | sed -e 's/[^0-9]//g' | cut -c1-2) echo "py${_v}" esac return 0 @@ -166,6 +167,152 @@ ci_venv_use() { return 0 } +ci_get_filedir() { + local _sdir=$(cd -- "$(dirname "$0")" && pwd) + local _pdir=$(pwd) + if [ -z "${_pdir##${_sdir}*}" ]; then + _sdir="${_pdir}" + fi + local _first=1 + while [ X"${_sdir}" != X"/" ]; do + if [ ${_first} -eq 1 ]; then + _first=0 + local _f=$(find "${_sdir}" -name "$1" | head -1) + if [ -n "${_f}" ]; then + echo $(dirname -- "${_f}") + return 0 + fi + else + _f=$(find "${_sdir}" -mindepth 1 -maxdepth 1 -name "$1" | head -1) + fi + [ -n "${_f}" ] && echo "${_sdir}" && return 0 + _sdir=$(cd -- "${_sdir}/.." && pwd) + done + return 1 +} + +ci_sq_ensure_java() { + type java >/dev/null 2>&1 + if [ $? -ne 0 ]; then + ci_err_msg "java not found" + return 1 + fi + local _java_ver=$(java -version 2>&1 | head -1 | sed -e 's/[^0-9\._]//g') + if [ -z "${_java_ver##1.8*}" ]; then + return 0 + fi + ci_err_msg "unsupported java version: ${_java_ver}" + return 1 +} + +ci_sq_ensure_scanner() { + local _cli_version="3.0.0.702" + local _cli_basedir="$HOME/.bin" + local _cli_postfix="" + case "$(uname -s)" in + Linux) + [ X"$(uname -m)" = X"x86_64" ] && _cli_postfix="-linux" + [ X"$(uname -m)" = X"amd64" ] && _cli_postfix="-linux" + ;; + Darwin) _cli_postfix="-macosx" ;; + esac + if [ X"${_cli_postfix}" = X"" ]; then + ci_sq_ensure_java || return 1 + fi + if [ X"${SONAR_SCANNER_PATH}" != X"" ]; then + if [ -e "${SONAR_SCANNER_PATH}" ]; then + return 0 + fi + fi + local _cli_fname="sonar-scanner-cli-${_cli_version}${_cli_postfix}" + [ ${CI_VERBOSE} -gt 0 ] && echo "[ci] ensure scanner ${_cli_fname}" + local _cli_dname="sonar-scanner-${_cli_version}${_cli_postfix}" + local _cli_archive="${_cli_basedir}/${_cli_fname}.zip" + local _cli_dir="${_cli_basedir}/${_cli_dname}" + local _cli_url="https://sonarsource.bintray.com/Distribution/sonar-scanner-cli/${_cli_fname}.zip" + if [ ! -e "${_cli_archive}" ]; then + mkdir -p -- "${_cli_basedir}" > /dev/null 2>&1 + if [ $? -ne 0 ]; then + ci_err_msg "could not create ${_cli_basedir}" + return 1 + fi + [ ${CI_VERBOSE} -gt 0 ] && echo "[ci] downloading ${_cli_fname}" + curl -kL -o "${_cli_archive}" "${_cli_url}" + [ $? -ne 0 ] && ci_err_msg "download failed" && return 1 + [ ! -e "${_cli_archive}" ] && ci_err_msg "download verify" && return 1 + fi + if [ ! -d "${_cli_dir}" ]; then + [ ${CI_VERBOSE} -gt 0 ] && echo "[ci] extracting ${_cli_fname}" + unzip -od "${_cli_basedir}" "${_cli_archive}" + [ $? -ne 0 ] && ci_err_msg "extract failed" && return 1 + [ ! -d "${_cli_dir}" ] && ci_err_msg "extract verify" && return 1 + fi + if [ ! -e "${_cli_dir}/bin/sonar-scanner" ]; then + ci_err_msg "sonar-scanner binary not found." + return 1 + fi + SONAR_SCANNER_PATH="${_cli_dir}/bin/sonar-scanner" + return 0 +} + +ci_sq_run() { + if [ X"${SONAR_SCANNER_PATH}" = X"" ]; then + ci_err_msg "environment variable SONAR_SCANNER_PATH not set" + return 1 + fi + if [ X"${SONAR_HOST_URL}" = X"" ]; then + ci_err_msg "environment variable SONAR_HOST_URL not set" + return 1 + fi + if [ X"${SONAR_AUTH_TOKEN}" = X"" ]; then + ci_err_msg "environment variable SONAR_AUTH_TOKEN not set" + return 1 + fi + local _pdir=$(ci_get_filedir "ssh-audit.py") + if [ -z "${_pdir}" ]; then + ci_err_msg "failed to find project directory" + return 1 + fi + local _odir=$(pwd) + cd -- "${_pdir}" + local _branch=$(git name-rev --name-only HEAD | cut -d '~' -f 1) + case "${_branch}" in + master) ;; + develop) ;; + *) ci_err_msg "unknown branch: ${_branch}"; return 1 ;; + esac + local _junit=$(cd -- "${_pdir}" && ls -1 reports/junit.*.xml | sort -r | head -1) + if [ X"${_junit}" = X"" ]; then + ci_err_msg "no junit.xml found" + return 1 + fi + local _project_ver=$(grep VERSION ssh-audit.py | head -1 | cut -d "'" -f 2) + if [ -z "${_project_ver}" ]; then + ci_err_msg "failed to get project version" + return 1 + fi + if [ -z "${_project_ver##*dev}" ]; then + local _git_rc=$(git rev-list --count `git rev-parse HEAD`) + _project_ver="${_project_ver}.${_git_rc}" + fi + [ ${CI_VERBOSE} -gt 0 ] && echo "[ci] run sonar-scanner for ${_project_ver}" + "${SONAR_SCANNER_PATH}" -X \ + -Dsonar.projectKey=arthepsy-github:ssh-audit \ + -Dsonar.sources=ssh-audit.py \ + -Dsonar.tests=test \ + -Dsonar.test.inclusions=test/*.py \ + -Dsonar.host.url="${SONAR_HOST_URL}" \ + -Dsonar.projectName=ssh-audit \ + -Dsonar.projectVersion="${_project_ver}" \ + -Dsonar.branch="${_branch}" \ + -Dsonar.python.coverage.overallReportPath=reports/coverage.xml \ + -Dsonar.python.xunit.reportPath="${_junit}" \ + -Dsonar.organization=arthepsy-github \ + -Dsonar.login="${SONAR_AUTH_TOKEN}" + cd -- "${_odir}" + return 0 +} + ci_run_wrapped() { local _versions=$(echo "${PY_VER}" | sed -e 's/,/ /g') [ -z "${_versions}" ] && eval "$1" @@ -247,6 +394,9 @@ ci_step_script_wrapped() { ci_step_success_wrapped() { local _py_ver="$1" local _py_ori="$2" + if [ X"${SQ}" = X"1" ]; then + ci_sq_ensure_scanner && ci_sq_run + fi ci_venv_use "${_py_ver}" || return 1 coveralls codecov diff --git a/tox.ini b/tox.ini index 9e5e3be..4bb7ef5 100644 --- a/tox.ini +++ b/tox.ini @@ -20,11 +20,12 @@ setenv = SSHAUDIT = {toxinidir}/ssh-audit.py test: COVERAGE_FILE = {toxinidir}/.coverage.{envname} type,mypy: MYPYPATH = {toxinidir}/test/stubs - type,mypy: MYPYHTML = {toxinidir}/html/mypy + type,mypy: MYPYHTML = {toxinidir}/reports/html/mypy commands = - test: coverage run --source ssh-audit -m -- pytest -v {posargs:test} + test: coverage run --source ssh-audit -m -- \ + test: pytest -v --junitxml={toxinidir}/reports/junit.{envname}.xml {posargs:test} test: coverage report --show-missing - test: - coverage html -d {toxinidir}/html/coverage.{envname} + test: coverage html -d {toxinidir}/reports/html/coverage.{envname} py{33,34,35,36,37}-{type,mypy}: {[testenv:mypy]commands} py{27,py,py3,33,34,35,36,37}-{lint,pylint}: {[testenv:pylint]commands} py{27,py,py3,33,34,35,36,37}-{lint,flake8}: {[testenv:flake8]commands} @@ -42,7 +43,8 @@ commands = coverage erase coverage combine coverage report --show-missing - - coverage html -d {toxinidir}/html/coverage + coverage xml -i -o {toxinidir}/reports/coverage.xml + coverage html -d {toxinidir}/reports/html/coverage [testenv:mypy] deps = From 464bb154f30f4f7b8f8d465295c1d6665dde47bd Mon Sep 17 00:00:00 2001 From: Andris Raugulis Date: Wed, 5 Apr 2017 04:25:01 +0300 Subject: [PATCH 032/103] Use git commit as dev version suffix. Add badge. --- README.md | 2 +- test/tools/ci-linux.sh | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d709a1a..65281c2 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![travis build status](https://api.travis-ci.org/arthepsy/ssh-audit.svg?branch=develop)](https://travis-ci.org/arthepsy/ssh-audit) [![appveyor build status](https://ci.appveyor.com/api/projects/status/4m5r73m0r023edil/branch/develop?svg=true)](https://ci.appveyor.com/project/arthepsy/ssh-audit) [![codecov](https://codecov.io/gh/arthepsy/ssh-audit/branch/develop/graph/badge.svg)](https://codecov.io/gh/arthepsy/ssh-audit) -[![coverage status](https://coveralls.io/repos/github/arthepsy/ssh-audit/badge.svg)](https://coveralls.io/github/arthepsy/ssh-audit) +[![Quality Gate](https://sonarqube.com/api/badges/gate?key=arthepsy-github%3Assh-audit%3Adevelop&template=ROUNDED)](https://sq.evolutiongaming.com/dashboard?id=arthepsy-github%3Assh-audit%3Adevelop) **ssh-audit** is a tool for ssh server auditing. ## Features diff --git a/test/tools/ci-linux.sh b/test/tools/ci-linux.sh index ba423e5..0bb0253 100755 --- a/test/tools/ci-linux.sh +++ b/test/tools/ci-linux.sh @@ -292,8 +292,8 @@ ci_sq_run() { return 1 fi if [ -z "${_project_ver##*dev}" ]; then - local _git_rc=$(git rev-list --count `git rev-parse HEAD`) - _project_ver="${_project_ver}.${_git_rc}" + local _git_commit=$(git rev-parse --short=8 HEAD) + _project_ver="${_project_ver}.${_git_commit}" fi [ ${CI_VERBOSE} -gt 0 ] && echo "[ci] run sonar-scanner for ${_project_ver}" "${SONAR_SCANNER_PATH}" -X \ From 09c2e7b2d59b10ddf0d6ab20ead2c40ee52a4250 Mon Sep 17 00:00:00 2001 From: Andris Raugulis Date: Wed, 5 Apr 2017 04:27:39 +0300 Subject: [PATCH 033/103] Fix SonarQube python:S1871. --- ssh-audit.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/ssh-audit.py b/ssh-audit.py index e1684f7..9fcd04c 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -1192,11 +1192,9 @@ class SSH(object): # pylint: disable=too-few-public-methods if ssh_prefix not in result: result[ssh_prefix] = [None, None, None] prev, push = result[ssh_prefix][i], False - if prev is None: - push = True - elif i == 0 and prev < ssh_version: - push = True - elif i > 0 and prev > ssh_version: + if ((prev is None) or + (prev < ssh_version and i == 0) or + (prev > ssh_version and i > 0)): push = True if push: result[ssh_prefix][i] = ssh_version From bb122ffe139ced0825cbb3965ee1cf5166ea4b29 Mon Sep 17 00:00:00 2001 From: Andris Raugulis Date: Wed, 5 Apr 2017 16:02:40 +0300 Subject: [PATCH 034/103] Replace assertions with exceptions. --- ssh-audit.py | 12 +++++++----- test/test_ssh1.py | 41 +++++++++++++++++++++++++++++------------ 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/ssh-audit.py b/ssh-audit.py index 9fcd04c..50a7d0b 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -570,8 +570,10 @@ class SSH1(object): class PublicKeyMessage(object): def __init__(self, cookie, skey, hkey, pflags, cmask, amask): # type: (binary_type, Tuple[int, int, int], Tuple[int, int, int], int, int, int) -> None - assert len(skey) == 3 - assert len(hkey) == 3 + if len(skey) != 3: + raise ValueError('invalid server key pair: {0}'.format(skey)) + if len(hkey) != 3: + raise ValueError('invalid host key pair: {0}'.format(hkey)) self.__cookie = cookie self.__server_key = skey self.__host_key = hkey @@ -1192,9 +1194,9 @@ class SSH(object): # pylint: disable=too-few-public-methods if ssh_prefix not in result: result[ssh_prefix] = [None, None, None] prev, push = result[ssh_prefix][i], False - if ((prev is None) or - (prev < ssh_version and i == 0) or - (prev > ssh_version and i > 0)): + if (prev is None or + (prev < ssh_version and i == 0) or + (prev > ssh_version and i > 0)): push = True if push: result[ssh_prefix][i] = ssh_version diff --git a/test/test_ssh1.py b/test/test_ssh1.py index 0f62983..f18e4be 100644 --- a/test/test_ssh1.py +++ b/test/test_ssh1.py @@ -66,34 +66,51 @@ class TestSSH1(object): assert fp.md5 == 'MD5:9d:26:f8:39:fc:20:9d:9b:ca:cc:4a:0f:e1:93:f5:96' assert fp.sha256 == 'SHA256:vZdx3mhzbvVJmn08t/ruv8WDhJ9jfKYsCTuSzot+QIs' - def test_pkm_read(self): - pkm = self.ssh1.PublicKeyMessage.parse(self._pkm_payload()) - assert pkm is not None - assert pkm.cookie == b'\x88\x99\xaa\xbb\xcc\xdd\xee\xff' - b, e, m = self._server_key() + def _assert_pkm_keys(self, pkm, skey, hkey): + b, e, m = skey assert pkm.server_key_bits == b assert pkm.server_key_public_exponent == e assert pkm.server_key_public_modulus == m - b, e, m = self._host_key() + b, e, m = hkey assert pkm.host_key_bits == b assert pkm.host_key_public_exponent == e assert pkm.host_key_public_modulus == m - fp = self.ssh.Fingerprint(pkm.host_key_fingerprint_data) + + def _assert_pkm_fields(self, pkm, skey, hkey): + assert pkm is not None + assert pkm.cookie == b'\x88\x99\xaa\xbb\xcc\xdd\xee\xff' + self._assert_pkm_keys(pkm, skey, hkey) assert pkm.protocol_flags == 2 assert pkm.supported_ciphers_mask == 72 assert pkm.supported_ciphers == ['3des', 'blowfish'] assert pkm.supported_authentications_mask == 36 assert pkm.supported_authentications == ['rsa', 'tis'] + fp = self.ssh.Fingerprint(pkm.host_key_fingerprint_data) assert fp.md5 == 'MD5:9d:26:f8:39:fc:20:9d:9b:ca:cc:4a:0f:e1:93:f5:96' assert fp.sha256 == 'SHA256:vZdx3mhzbvVJmn08t/ruv8WDhJ9jfKYsCTuSzot+QIs' + def test_pkm_init(self): + cookie = b'\x88\x99\xaa\xbb\xcc\xdd\xee\xff' + pflags, cmask, amask = 2, 72, 36 + skey, hkey = self._server_key(), self._host_key() + pkm = self.ssh1.PublicKeyMessage(cookie, skey, hkey, pflags, cmask, amask) + self._assert_pkm_fields(pkm, skey, hkey) + for skey2 in ([], [0], [0,1], [0,1,2,3]): + with pytest.raises(ValueError): + pkm = self.ssh1.PublicKeyMessage(cookie, skey2, hkey, pflags, cmask, amask) + for hkey2 in ([], [0], [0,1], [0,1,2,3]): + with pytest.raises(ValueError): + print(hkey2) + pkm = self.ssh1.PublicKeyMessage(cookie, skey, hkey2, pflags, cmask, amask) + + def test_pkm_read(self): + pkm = self.ssh1.PublicKeyMessage.parse(self._pkm_payload()) + self._assert_pkm_fields(pkm, self._server_key(), self._host_key()) + def test_pkm_payload(self): cookie = b'\x88\x99\xaa\xbb\xcc\xdd\xee\xff' - skey = self._server_key() - hkey = self._host_key() - pflags = 2 - cmask = 72 - amask = 36 + skey, hkey = self._server_key(), self._host_key() + pflags, cmask, amask = 2, 72, 36 pkm1 = self.ssh1.PublicKeyMessage(cookie, skey, hkey, pflags, cmask, amask) pkm2 = self.ssh1.PublicKeyMessage.parse(self._pkm_payload()) assert pkm1.payload == pkm2.payload From c132c62b96e9909b34f665da0002e29c7cd224b0 Mon Sep 17 00:00:00 2001 From: Andris Raugulis Date: Wed, 5 Apr 2017 16:13:35 +0300 Subject: [PATCH 035/103] Remove useless parentheses. --- ssh-audit.py | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/ssh-audit.py b/ssh-audit.py index 50a7d0b..8ccd3ff 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -819,7 +819,7 @@ class WriteBuf(object): ql = (length + 7) // 8 fmt, v2 = '>{0}Q'.format(ql), [0] * ql for i in range(ql): - v2[ql - i - 1] = (n & 0xffffffffffffffff) + v2[ql - i - 1] = n & 0xffffffffffffffff n >>= 64 data = bytes(struct.pack(fmt, *v2)[-length:]) if not signed: @@ -1162,11 +1162,11 @@ class SSH(object): # pylint: disable=too-few-public-methods def get_ssh_version(version_desc): # type: (str) -> Tuple[str, str] if version_desc.startswith('d'): - return (SSH.Product.DropbearSSH, version_desc[1:]) + return SSH.Product.DropbearSSH, version_desc[1:] elif version_desc.startswith('l1'): - return (SSH.Product.LibSSH, version_desc[2:]) + return SSH.Product.LibSSH, version_desc[2:] else: - return (SSH.Product.OpenSSH, version_desc) + return SSH.Product.OpenSSH, version_desc @classmethod def get_timeframe(cls, versions, for_server=True, result=None): @@ -1550,9 +1550,9 @@ class SSH(object): # pylint: disable=too-few-public-methods if prefer_ipvo: r = sorted(r, key=lambda x: x[0], reverse=not prefer_ipv4) check = any(stype == rline[2] for rline in r) - for (af, socktype, _proto, _canonname, addr) in r: + for af, socktype, _proto, _canonname, addr in r: if not check or socktype == socket.SOCK_STREAM: - yield (af, addr) + yield af, addr except socket.error as e: out.fail('[exception] {0}'.format(e)) sys.exit(1) @@ -1560,7 +1560,7 @@ class SSH(object): # pylint: disable=too-few-public-methods def connect(self, ipvo=(), cto=3.0, rto=5.0): # type: (Sequence[int], float, float) -> None err = None - for (af, addr) in self._resolve(ipvo): + for af, addr in self._resolve(ipvo): s = None try: s = socket.socket(af, socket.SOCK_STREAM) @@ -1614,33 +1614,33 @@ class SSH(object): # pylint: disable=too-few-public-methods def recv(self, size=2048): # type: (int) -> Tuple[int, Optional[str]] if self.__sock is None: - return (-1, 'not connected') + return -1, 'not connected' try: data = self.__sock.recv(size) except socket.timeout: - return (-1, 'timed out') + return -1, 'timed out' except socket.error as e: if e.args[0] in (errno.EAGAIN, errno.EWOULDBLOCK): - return (0, 'retry') - return (-1, str(e.args[-1])) + return 0, 'retry' + return -1, str(e.args[-1]) if len(data) == 0: - return (-1, None) + return -1, None pos = self._buf.tell() self._buf.seek(0, 2) self._buf.write(data) self._len += len(data) self._buf.seek(pos, 0) - return (len(data), None) + return len(data), None def send(self, data): # type: (binary_type) -> Tuple[int, Optional[str]] if self.__sock is None: - return (-1, 'not connected') + return -1, 'not connected' try: self.__sock.send(data) - return (0, None) + return 0, None except socket.error as e: - return (-1, str(e.args[-1])) + return -1, str(e.args[-1]) self.__sock.send(data) def send_banner(self, banner): @@ -1665,7 +1665,7 @@ class SSH(object): # pylint: disable=too-few-public-methods header.write_int(packet_length) # XXX: validate length if sshv == 1: - padding_length = (8 - packet_length % 8) + padding_length = 8 - packet_length % 8 self.ensure_read(padding_length) padding = self.read(padding_length) header.write(padding) @@ -1706,7 +1706,7 @@ class SSH(object): # pylint: disable=too-few-public-methods e = header.write_flush().strip() else: e = ex.args[0].encode('utf-8') - return (-1, e) + return -1, e def send_packet(self): # type: () -> Tuple[int, Optional[str]] @@ -1826,7 +1826,7 @@ def output_algorithm(alg_db, alg_type, alg_name, alg_max_len=0): else: texts.append(('warn', 'unknown algorithm')) first = True - for (level, text) in texts: + for level, text in texts: f = getattr(out, level) text = '[' + level + '] ' + text if first: From e4bdabb8916f8e8a932eae808a2e64cca64ce5e4 Mon Sep 17 00:00:00 2001 From: Andris Raugulis Date: Wed, 5 Apr 2017 16:31:43 +0300 Subject: [PATCH 036/103] Fix method type and naming. --- ssh-audit.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/ssh-audit.py b/ssh-audit.py index 8ccd3ff..d238925 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -198,6 +198,7 @@ class Output(object): self.colors = True self.verbose = False self.__minlevel = 0 + self.__colsupport = 'colorama' in sys.modules or os.name == 'posix' @property def minlevel(self): @@ -226,7 +227,7 @@ class Output(object): @property def colors_supported(self): # type: () -> bool - return 'colorama' in sys.modules or os.name == 'posix' + return self.__colsupport @staticmethod def _colorized(color): @@ -1270,19 +1271,19 @@ class SSH(object): # pylint: disable=too-few-public-methods @property def maxlen(self): # type: () -> int - def ml(items): + def _ml(items): # type: (Sequence[text_type]) -> int return max(len(i) for i in items) maxlen = 0 if self.ssh1kex is not None: - maxlen = max(ml(self.ssh1kex.supported_ciphers), - ml(self.ssh1kex.supported_authentications), + maxlen = max(_ml(self.ssh1kex.supported_ciphers), + _ml(self.ssh1kex.supported_authentications), maxlen) if self.ssh2kex is not None: - maxlen = max(ml(self.ssh2kex.kex_algorithms), - ml(self.ssh2kex.key_algorithms), - ml(self.ssh2kex.server.encryption), - ml(self.ssh2kex.server.mac), + maxlen = max(_ml(self.ssh2kex.kex_algorithms), + _ml(self.ssh2kex.key_algorithms), + _ml(self.ssh2kex.server.encryption), + _ml(self.ssh2kex.server.mac), maxlen) return maxlen From 0d555d43b327a8e2332fc659b46ad134693841a8 Mon Sep 17 00:00:00 2001 From: Andris Raugulis Date: Wed, 5 Apr 2017 18:12:26 +0300 Subject: [PATCH 037/103] Condition must be a boolean fixes. --- ssh-audit.py | 46 +++++++++++++++++++++++----------------------- tox.ini | 2 +- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/ssh-audit.py b/ssh-audit.py index d238925..9fd4246 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -702,8 +702,8 @@ class ReadBuf(object): def __init__(self, data=None): # type: (Optional[binary_type]) -> None super(ReadBuf, self).__init__() - self._buf = BytesIO(data) if data else BytesIO() - self._len = len(data) if data else 0 + self._buf = BytesIO(data) if data is not None else BytesIO() + self._len = len(data) if data is not None else 0 @property def unread_len(self): @@ -739,13 +739,13 @@ class ReadBuf(object): return self.read(n) @classmethod - def _parse_mpint(cls, v, pad, sf): + def _parse_mpint(cls, v, pad, f): # type: (binary_type, binary_type, str) -> int r = 0 - if len(v) % 4: + if len(v) % 4 != 0: v = pad * (4 - (len(v) % 4)) + v for i in range(0, len(v), 4): - r = (r << 32) | struct.unpack(sf, v[i:i + 4])[0] + r = (r << 32) | struct.unpack(f, v[i:i + 4])[0] return r def read_mpint1(self): @@ -761,8 +761,8 @@ class ReadBuf(object): v = self.read_string() if len(v) == 0: return 0 - pad, sf = (b'\xff', '>i') if ord(v[0:1]) & 0x80 else (b'\x00', '>I') - return self._parse_mpint(v, pad, sf) + pad, f = (b'\xff', '>i') if ord(v[0:1]) & 0x80 != 0 else (b'\x00', '>I') + return self._parse_mpint(v, pad, f) def read_line(self): # type: () -> text_type @@ -773,7 +773,7 @@ class WriteBuf(object): def __init__(self, data=None): # type: (Optional[binary_type]) -> None super(WriteBuf, self).__init__() - self._wbuf = BytesIO(data) if data else BytesIO() + self._wbuf = BytesIO(data) if data is not None else BytesIO() def write(self, data): # type: (binary_type) -> WriteBuf @@ -916,7 +916,7 @@ class SSH(object): # pylint: disable=too-few-public-methods else: other = str(other) mx = re.match(r'^([\d\.]+\d+)(.*)$', other) - if mx: + if mx is not None: oversion, opatch = mx.group(1), mx.group(2).strip() else: oversion, opatch = other, '' @@ -934,9 +934,9 @@ class SSH(object): # pylint: disable=too-few-public-methods mx1 = re.match(r'^p\d(.*)', opatch) mx2 = re.match(r'^p\d(.*)', spatch) if not (mx1 and mx2): - if mx1: + if mx1 is not None: opatch = mx1.group(1) - if mx2: + if mx2 is not None: spatch = mx2.group(1) if spatch < opatch: return -1 @@ -1009,19 +1009,19 @@ class SSH(object): # pylint: disable=too-few-public-methods if c is None: return None mx = re.match(r'^NetBSD(?:_Secure_Shell)?(?:[\s-]+(\d{8})(.*))?$', c) - if mx: + if mx is not None: 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: + if mx is None: mx = re.match(r'^[^@]+@FreeBSD\.org[\s-]+(\d{8})(.*)$', c) - if mx: + if mx is not None: d = cls._fix_date(mx.group(1)) return 'FreeBSD' if d is None else 'FreeBSD ({0})'.format(d) w = ['RemotelyAnywhere', 'DesktopAuthority', 'RemoteSupportManager'] for win_soft in w: mx = re.match(r'^in ' + win_soft + r' ([\d\.]+\d)$', c) - if mx: + if mx is not None: ver = mx.group(1) return 'Microsoft Windows ({0} {1})'.format(win_soft, ver) generic = ['NetBSD', 'FreeBSD'] @@ -1037,35 +1037,35 @@ class SSH(object): # pylint: disable=too-few-public-methods software = str(banner.software) mx = re.match(r'^dropbear_([\d\.]+\d+)(.*)', software) v = None # type: Optional[str] - if mx: + if mx is not None: patch = cls._fix_patch(mx.group(2)) v, p = 'Matt Johnston', SSH.Product.DropbearSSH v = None return cls(v, p, mx.group(1), patch, None) mx = re.match(r'^OpenSSH[_\.-]+([\d\.]+\d+)(.*)', software) - if mx: + if mx is not None: patch = cls._fix_patch(mx.group(2)) v, p = 'OpenBSD', SSH.Product.OpenSSH v = None os_version = cls._extract_os_version(banner.comments) return cls(v, p, mx.group(1), patch, os_version) mx = re.match(r'^libssh-([\d\.]+\d+)(.*)', software) - if mx: + if mx is not None: patch = cls._fix_patch(mx.group(2)) v, p = None, SSH.Product.LibSSH os_version = cls._extract_os_version(banner.comments) return cls(v, p, mx.group(1), patch, os_version) mx = re.match(r'^RomSShell_([\d\.]+\d+)(.*)', software) - if mx: + if mx is not None: patch = cls._fix_patch(mx.group(2)) v, p = 'Allegro Software', 'RomSShell' return cls(v, p, mx.group(1), patch, None) mx = re.match(r'^mpSSH_([\d\.]+\d+)', software) - if mx: + if mx is not None: 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: + if mx is not None: v, p = 'Cisco', 'IOS/PIX sshd' return cls(v, p, mx.group(1), None, None) return None @@ -1959,7 +1959,7 @@ def output_recommendations(algs, software, padlen=0): def output(banner, header, kex=None, pkm=None): # type: (Optional[SSH.Banner], List[text_type], Optional[SSH2.Kex], Optional[SSH1.PublicKeyMessage]) -> None - sshv = 1 if pkm else 2 + sshv = 1 if pkm is not None else 2 algs = SSH.Algorithms(pkm, kex) with OutputBuffer() as obuf: if len(header) > 0: @@ -2168,7 +2168,7 @@ def audit(aconf, sshv=None): fmt = '[exception] did not receive {0} ({1}), ' + \ 'instead received unknown message ({2})' err = fmt.format(err_pair[0], err_pair[1], packet_type) - if err: + if err is not None: output(banner, header) out.fail(err) sys.exit(1) diff --git a/tox.ini b/tox.ini index 4bb7ef5..5e9a864 100644 --- a/tox.ini +++ b/tox.ini @@ -107,7 +107,7 @@ warn_redundant_casts = True warn_return_any = True warn_unused_ignores = True strict_optional = True -#strict_boolean = False +#strict_boolean = True [pylint] reports = no From 21a93cbd66e42c72383d7afc3ae022e3d44c3a7d Mon Sep 17 00:00:00 2001 From: Andris Raugulis Date: Thu, 6 Apr 2017 05:27:13 +0300 Subject: [PATCH 038/103] Condition must be a boolean fixes. --- ssh-audit.py | 83 +++++++++++++++++++++++++++------------------------- tox.ini | 2 +- 2 files changed, 44 insertions(+), 41 deletions(-) diff --git a/ssh-audit.py b/ssh-audit.py index 9fd4246..45cbd9c 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -55,7 +55,7 @@ def usage(err=None): uout = Output() p = os.path.basename(sys.argv[0]) uout.head('# {0} {1}, moo@arthepsy.eu\n'.format(p, VERSION)) - if err is not None: + if err is not None and len(err) > 0: uout.fail('\n' + err) uout.info('usage: {0} [-1246pbnvl] \n'.format(p)) uout.info(' -h, --help print this help') @@ -92,10 +92,10 @@ class AuditConf(object): # type: (str, Union[str, int, bool, Sequence[int]]) -> None valid = False if name in ['ssh1', 'ssh2', 'batch', 'colors', 'verbose']: - valid, value = True, True if value else False + valid, value = True, True if bool(value) else False elif name in ['ipv4', 'ipv6']: valid = False - value = True if value else False + value = True if bool(value) else False ipv = 4 if name == 'ipv4' else 6 if value: value = tuple(list(self.ipvo) + [ipv]) @@ -916,7 +916,7 @@ class SSH(object): # pylint: disable=too-few-public-methods else: other = str(other) mx = re.match(r'^([\d\.]+\d+)(.*)$', other) - if mx is not None: + if bool(mx): oversion, opatch = mx.group(1), mx.group(2).strip() else: oversion, opatch = other, '' @@ -933,10 +933,10 @@ class SSH(object): # pylint: disable=too-few-public-methods elif self.product == SSH.Product.OpenSSH: mx1 = re.match(r'^p\d(.*)', opatch) mx2 = re.match(r'^p\d(.*)', spatch) - if not (mx1 and mx2): - if mx1 is not None: + if not (bool(mx1) and bool(mx2)): + if bool(mx1): opatch = mx1.group(1) - if mx2 is not None: + if bool(mx2): spatch = mx2.group(1) if spatch < opatch: return -1 @@ -946,28 +946,28 @@ class SSH(object): # pylint: disable=too-few-public-methods def between_versions(self, vfrom, vtill): # type: (str, str) -> bool - if vfrom and self.compare_version(vfrom) < 0: + if bool(vfrom) and self.compare_version(vfrom) < 0: return False - if vtill and self.compare_version(vtill) > 0: + if bool(vtill) and self.compare_version(vtill) > 0: return False return True def display(self, full=True): # type: (bool) -> str - r = '{0} '.format(self.vendor) if self.vendor else '' + r = '{0} '.format(self.vendor) if bool(self.vendor) else '' r += self.product - if self.version: + if bool(self.version): r += ' {0}'.format(self.version) if full: patch = self.patch or '' if self.product == SSH.Product.OpenSSH: mx = re.match(r'^(p\d)(.*)$', patch) - if mx is not None: + if bool(mx): r += mx.group(1) patch = mx.group(2).strip() - if patch: + if bool(patch): r += ' ({0})'.format(patch) - if self.os: + if bool(self.os): r += ' running on {0}'.format(self.os) return r @@ -977,16 +977,13 @@ class SSH(object): # pylint: disable=too-few-public-methods def __repr__(self): # type: () -> str - r = 'vendor={0}'.format(self.vendor) if self.vendor else '' - if self.product: - if self.vendor: - r += ', ' - r += 'product={0}'.format(self.product) - if self.version: + r = 'vendor={0}, '.format(self.vendor) if bool(self.vendor) else '' + r += 'product={0}'.format(self.product) + if bool(self.version): r += ', version={0}'.format(self.version) - if self.patch: + if bool(self.patch): r += ', patch={0}'.format(self.patch) - if self.os: + if bool(self.os): r += ', os={0}'.format(self.os) return '<{0}({1})>'.format(self.__class__.__name__, r) @@ -1009,19 +1006,19 @@ class SSH(object): # pylint: disable=too-few-public-methods if c is None: return None mx = re.match(r'^NetBSD(?:_Secure_Shell)?(?:[\s-]+(\d{8})(.*))?$', c) - if mx is not None: + if bool(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 mx is None: + if not bool(mx): mx = re.match(r'^[^@]+@FreeBSD\.org[\s-]+(\d{8})(.*)$', c) - if mx is not None: + if bool(mx): d = cls._fix_date(mx.group(1)) return 'FreeBSD' if d is None else 'FreeBSD ({0})'.format(d) w = ['RemotelyAnywhere', 'DesktopAuthority', 'RemoteSupportManager'] for win_soft in w: mx = re.match(r'^in ' + win_soft + r' ([\d\.]+\d)$', c) - if mx is not None: + if bool(mx): ver = mx.group(1) return 'Microsoft Windows ({0} {1})'.format(win_soft, ver) generic = ['NetBSD', 'FreeBSD'] @@ -1037,35 +1034,35 @@ class SSH(object): # pylint: disable=too-few-public-methods software = str(banner.software) mx = re.match(r'^dropbear_([\d\.]+\d+)(.*)', software) v = None # type: Optional[str] - if mx is not None: + if bool(mx): patch = cls._fix_patch(mx.group(2)) v, p = 'Matt Johnston', SSH.Product.DropbearSSH v = None return cls(v, p, mx.group(1), patch, None) mx = re.match(r'^OpenSSH[_\.-]+([\d\.]+\d+)(.*)', software) - if mx is not None: + if bool(mx): patch = cls._fix_patch(mx.group(2)) v, p = 'OpenBSD', SSH.Product.OpenSSH v = None os_version = cls._extract_os_version(banner.comments) return cls(v, p, mx.group(1), patch, os_version) mx = re.match(r'^libssh-([\d\.]+\d+)(.*)', software) - if mx is not None: + if bool(mx): patch = cls._fix_patch(mx.group(2)) v, p = None, SSH.Product.LibSSH os_version = cls._extract_os_version(banner.comments) return cls(v, p, mx.group(1), patch, os_version) mx = re.match(r'^RomSShell_([\d\.]+\d+)(.*)', software) - if mx is not None: + if bool(mx): patch = cls._fix_patch(mx.group(2)) v, p = 'Allegro Software', 'RomSShell' return cls(v, p, mx.group(1), patch, None) mx = re.match(r'^mpSSH_([\d\.]+\d+)', software) - if mx is not None: + if bool(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 is not None: + if bool(mx): v, p = 'Cisco', 'IOS/PIX sshd' return cls(v, p, mx.group(1), None, None) return None @@ -1107,7 +1104,7 @@ class SSH(object): # pylint: disable=too-few-public-methods r = 'SSH-{0}.{1}'.format(self.protocol[0], self.protocol[1]) if self.software is not None: r += '-{0}'.format(self.software) - if self.comments: + if bool(self.comments): r += ' {0}'.format(self.comments) return r @@ -1115,19 +1112,19 @@ class SSH(object): # pylint: disable=too-few-public-methods # type: () -> str p = '{0}.{1}'.format(self.protocol[0], self.protocol[1]) r = 'protocol={0}'.format(p) - if self.software: + if self.software is not None: r += ', software={0}'.format(self.software) - if self.comments: + if bool(self.comments): r += ', comments={0}'.format(self.comments) return '<{0}({1})>'.format(self.__class__.__name__, r) @classmethod def parse(cls, banner): - # type: (text_type) -> SSH.Banner + # type: (text_type) -> Optional[SSH.Banner] valid_ascii = utils.is_print_ascii(banner) ascii_banner = utils.to_print_ascii(banner) mx = cls.RX_BANNER.match(ascii_banner) - if mx is None: + if not bool(mx): return None protocol = min(re.findall(cls.RX_PROTOCOL, mx.group(1))) protocol = (int(protocol[0]), int(protocol[1])) @@ -1814,7 +1811,7 @@ def output_algorithm(alg_db, alg_type, alg_name, alg_max_len=0): if level == 'info': versions = alg_desc[0] since_text = SSH.Algorithm.get_since_text(versions) - if since_text: + if since_text is not None and len(since_text) > 0: texts.append((level, since_text)) idx = idx + 1 if ldesc > idx: @@ -1951,7 +1948,10 @@ def output_recommendations(algs, software, padlen=0): fm = '(rec) {0}{1}{2}-- {3} algorithm to {4} {5}' fn(fm.format(sg, name, p, alg_type, an, b)) if len(obuf) > 0: - title = '(for {0})'.format(software.display(False)) if software else '' + if software is not None: + title = '(for {0})'.format(software.display(False)) + else: + title = '' out.head('# algorithm recommendations {0}'.format(title)) obuf.flush() out.sep() @@ -2150,7 +2150,10 @@ def audit(aconf, sshv=None): packet_type, payload = s.read_packet(sshv) if packet_type < 0: try: - payload_txt = payload.decode('utf-8') if payload else u'empty' + if payload is not None and len(payload) > 0: + payload_txt = payload.decode('utf-8') + else: + payload_txt = u'empty' except UnicodeDecodeError: payload_txt = u'"{0}"'.format(repr(payload).lstrip('b')[1:-1]) if payload_txt == u'Protocol major versions differ.': diff --git a/tox.ini b/tox.ini index 5e9a864..add92a2 100644 --- a/tox.ini +++ b/tox.ini @@ -107,7 +107,7 @@ warn_redundant_casts = True warn_return_any = True warn_unused_ignores = True strict_optional = True -#strict_boolean = True +strict_boolean = True [pylint] reports = no From 6c8173d409b8e016d8cc788930579c80ed4a46c8 Mon Sep 17 00:00:00 2001 From: Andris Raugulis Date: Thu, 6 Apr 2017 05:27:40 +0300 Subject: [PATCH 039/103] Fix to_ntext test. --- test/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_utils.py b/test/test_utils.py index 64cb07b..2a83bd8 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -78,7 +78,7 @@ class TestUtils(object): assert self.utils.to_ntext('fran\xc3\xa7ais') == 'fran\xc3\xa7ais' assert self.utils.to_ntext(u'fran\xe7ais') == 'fran\xe7ais' # binary_type (bytes) - assert self.utils.to_utext(b'fran\xc3\xa7ais') == 'fran\xe7ais' + assert self.utils.to_ntext(b'fran\xc3\xa7ais') == 'fran\xe7ais' # other with pytest.raises(TypeError): self.utils.to_ntext(123) From 774d1c1fe479eedf32d604d3c0fdf69efcd7af3e Mon Sep 17 00:00:00 2001 From: Andris Raugulis Date: Mon, 10 Apr 2017 13:20:02 +0300 Subject: [PATCH 040/103] Ignore linting long assertion lines. --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index add92a2..0655e65 100644 --- a/tox.ini +++ b/tox.ini @@ -133,7 +133,7 @@ max-public-methods = 20 max-bool-expr = 5 max-nested-blocks = 6 max-line-length = 80 -ignore-long-lines = ^\s*(#\s+type:\s+.*|[A-Z0-9_]+\s+=\s+.*|('.*':\s+)?\[.*\],?)$ +ignore-long-lines = ^\s*(#\s+type:\s+.*|[A-Z0-9_]+\s+=\s+.*|('.*':\s+)?\[.*\],?|assert\s+.*)$ max-module-lines = 2500 [flake8] From 72a6b9eeafdea6c821c69d62f9bccd4ce671d77a Mon Sep 17 00:00:00 2001 From: Andris Raugulis Date: Mon, 10 Apr 2017 13:20:32 +0300 Subject: [PATCH 041/103] Refactor and test SSH.Algorithm. --- ssh-audit.py | 162 ++++++++++++++++++++++--------------- test/test_ssh_algorithm.py | 157 +++++++++++++++++++++++++++++++++++ 2 files changed, 252 insertions(+), 67 deletions(-) create mode 100644 test/test_ssh_algorithm.py diff --git a/ssh-audit.py b/ssh-audit.py index 45cbd9c..30d8ee1 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -1156,49 +1156,79 @@ class SSH(object): # pylint: disable=too-few-public-methods return u'SHA256:{0}'.format(r) class Algorithm(object): + class Timeframe(object): + def __init__(self): + # type: () -> None + self.__storage = {} # type: Dict[str, List[Optional[str]]] + + def __contains__(self, product): + # type: (str) -> bool + return product in self.__storage + + def __getitem__(self, product): + # type: (str) -> Sequence[Optional[str]] + return tuple(self.__storage.get(product, [None]*4)) + + def __str__(self): + # type: () -> str + return self.__storage.__str__() + + def __repr__(self): + # type: () -> str + return self.__str__() + + def get_from(self, product, for_server=True): + # type: (str, bool) -> Optional[str] + return self[product][0 if bool(for_server) else 2] + + def get_till(self, product, for_server=True): + # type: (str, bool) -> Optional[str] + return self[product][1 if bool(for_server) else 3] + + def _update(self, versions, pos): + # type: (Optional[str], int) -> None + ssh_versions = {} # type: Dict[str, str] + for_srv, for_cli = pos < 2, pos > 1 + for v in (versions or '').split(','): + ssh_prod, ssh_ver, is_cli = SSH.Algorithm.get_ssh_version(v) + if (not ssh_ver or + (is_cli and for_srv) or + (not is_cli and for_cli and ssh_prod in ssh_versions)): + continue + ssh_versions[ssh_prod] = ssh_ver + for ssh_product, ssh_version in ssh_versions.items(): + if ssh_product not in self.__storage: + self.__storage[ssh_product] = [None]*4 + prev = self[ssh_product][pos] + if (prev is None or + (prev < ssh_version and pos % 2 == 0) or + (prev > ssh_version and pos % 2 == 1)): + self.__storage[ssh_product][pos] = ssh_version + + def update(self, versions, for_server=None): + # type: (List[Optional[str]], Optional[bool]) -> SSH.Algorithm.Timeframe + for_cli = for_server is None or for_server is False + for_srv = for_server is None or for_server is True + vlen = len(versions) + for i in range(min(3, vlen)): + if for_srv and i < 2: + self._update(versions[i], i) + if for_cli and (i % 2 == 0 or vlen == 2): + self._update(versions[i], 3 - 0**i) + return self + @staticmethod def get_ssh_version(version_desc): - # type: (str) -> Tuple[str, str] + # type: (str) -> Tuple[str, str, bool] + is_client = version_desc.endswith('C') + if is_client: + version_desc = version_desc[:-1] if version_desc.startswith('d'): - return SSH.Product.DropbearSSH, version_desc[1:] + return SSH.Product.DropbearSSH, version_desc[1:], is_client elif version_desc.startswith('l1'): - return SSH.Product.LibSSH, version_desc[2:] + return SSH.Product.LibSSH, version_desc[2:], is_client else: - return SSH.Product.OpenSSH, version_desc - - @classmethod - def get_timeframe(cls, versions, for_server=True, result=None): - # type: (List[Optional[str]], bool, Optional[Dict[str, List[Optional[str]]]]) -> Dict[str, List[Optional[str]]] - result = result or {} - vlen = len(versions) - for i in range(3): - if i > vlen - 1: - if i == 2 and vlen > 1: - cversions = versions[1] - else: - continue - else: - cversions = versions[i] - if cversions is None: - continue - for v in cversions.split(','): - ssh_prefix, ssh_version = cls.get_ssh_version(v) - if not ssh_version: - continue - if ssh_version.endswith('C'): - if for_server: - continue - ssh_version = ssh_version[:-1] - if ssh_prefix not in result: - result[ssh_prefix] = [None, None, None] - prev, push = result[ssh_prefix][i], False - if (prev is None or - (prev < ssh_version and i == 0) or - (prev > ssh_version and i > 0)): - push = True - if push: - result[ssh_prefix][i] = ssh_version - return result + return SSH.Product.OpenSSH, version_desc, is_client @classmethod def get_since_text(cls, versions): @@ -1207,14 +1237,14 @@ class SSH(object): # pylint: disable=too-few-public-methods if len(versions) == 0 or versions[0] is None: return None for v in versions[0].split(','): - ssh_prefix, ssh_version = cls.get_ssh_version(v) - if not ssh_version: + ssh_prod, ssh_ver, is_cli = cls.get_ssh_version(v) + if not ssh_ver: continue - if ssh_prefix in [SSH.Product.LibSSH]: + if ssh_prod in [SSH.Product.LibSSH]: continue - if ssh_version.endswith('C'): - ssh_version = '{0} (client only)'.format(ssh_version[:-1]) - tv.append('{0} {1}'.format(ssh_prefix, ssh_version)) + if is_cli: + ssh_ver = '{0} (client only)'.format(ssh_ver) + tv.append('{0} {1}'.format(ssh_prod, ssh_ver)) if len(tv) == 0: return None return 'available since ' + ', '.join(tv).rstrip(', ') @@ -1284,9 +1314,9 @@ class SSH(object): # pylint: disable=too-few-public-methods maxlen) return maxlen - def get_ssh_timeframe(self, for_server=True): - # type: (bool) -> Dict[str, List[Optional[str]]] - r = {} # type: Dict[str, List[Optional[str]]] + def get_ssh_timeframe(self, for_server=None): + # type: (Optional[bool]) -> SSH.Algorithm.Timeframe + timeframe = SSH.Algorithm.Timeframe() for alg_pair in self.values: alg_db = alg_pair.db for alg_type, alg_list in alg_pair.items(): @@ -1296,8 +1326,8 @@ class SSH(object): # pylint: disable=too-few-public-methods if alg_desc is None: continue versions = alg_desc[0] - r = SSH.Algorithm.get_timeframe(versions, for_server, r) - return r + timeframe.update(versions, for_server) + return timeframe def get_recommendations(self, software, for_server=True): # type: (Optional[SSH.Software], bool) -> Tuple[Optional[SSH.Software], Dict[int, Dict[str, Dict[str, Dict[str, int]]]]] @@ -1313,7 +1343,7 @@ class SSH(object): # pylint: disable=too-few-public-methods for product in vproducts: if product not in ssh_timeframe: continue - version = ssh_timeframe[product][0] + version = ssh_timeframe.get_from(product, for_server) if version is not None: software = SSH.Software(None, product, version, None, None) break @@ -1335,15 +1365,13 @@ class SSH(object): # pylint: disable=too-few-public-methods continue matches = False for v in versions[0].split(','): - ssh_prefix, ssh_version = SSH.Algorithm.get_ssh_version(v) + ssh_prefix, ssh_version, is_cli = SSH.Algorithm.get_ssh_version(v) if not ssh_version: continue if ssh_prefix != software.product: continue - if ssh_version.endswith('C'): - if for_server: - continue - ssh_version = ssh_version[:-1] + if is_cli and for_server: + continue if software.compare_version(ssh_version) < 0: continue matches = True @@ -1842,25 +1870,25 @@ def output_algorithm(alg_db, alg_type, alg_name, alg_max_len=0): def output_compatibility(algs, for_server=True): # type: (SSH.Algorithms, bool) -> None ssh_timeframe = algs.get_ssh_timeframe(for_server) - vp = 1 if for_server else 2 comp_text = [] - for sshd_name in [SSH.Product.OpenSSH, SSH.Product.DropbearSSH]: - if sshd_name not in ssh_timeframe: + for ssh_prod in [SSH.Product.OpenSSH, SSH.Product.DropbearSSH]: + if ssh_prod not in ssh_timeframe: continue - v = ssh_timeframe[sshd_name] - if v[0] is None: + v_from = ssh_timeframe.get_from(ssh_prod, for_server) + v_till = ssh_timeframe.get_till(ssh_prod, for_server) + if v_from is None: continue - if v[vp] is None: - comp_text.append('{0} {1}+'.format(sshd_name, v[0])) - elif v[0] == v[vp]: - comp_text.append('{0} {1}'.format(sshd_name, v[0])) + if v_till is None: + comp_text.append('{0} {1}+'.format(ssh_prod, v_from)) + elif v_from == v_till: + comp_text.append('{0} {1}'.format(ssh_prod, v_from)) else: - software = SSH.Software(None, sshd_name, v[0], None, None) - if software.compare_version(v[vp]) > 0: + software = SSH.Software(None, ssh_prod, v_from, None, None) + if software.compare_version(v_till) > 0: tfmt = '{0} {1}+ (some functionality from {2})' else: tfmt = '{0} {1}-{2}' - comp_text.append(tfmt.format(sshd_name, v[0], v[vp])) + comp_text.append(tfmt.format(ssh_prod, v_from, v_till)) if len(comp_text) > 0: out.good('(gen) compatibility: ' + ', '.join(comp_text)) diff --git a/test/test_ssh_algorithm.py b/test/test_ssh_algorithm.py new file mode 100644 index 0000000..5ba88e5 --- /dev/null +++ b/test/test_ssh_algorithm.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import pytest + + +# pylint: disable=attribute-defined-outside-init +class TestSSHAlgorithm(object): + @pytest.fixture(autouse=True) + def init(self, ssh_audit): + self.ssh = ssh_audit.SSH + + def _tf(self, v, s=None): + return self.ssh.Algorithm.Timeframe().update(v, s) + + def test_get_ssh_version(self): + def ver(v): + return self.ssh.Algorithm.get_ssh_version(v) + + assert ver('7.5') == ('OpenSSH', '7.5', False) + assert ver('7.5C') == ('OpenSSH', '7.5', True) + assert ver('d2016.74') == ('Dropbear SSH', '2016.74', False) + assert ver('l10.7.4') == ('libssh', '0.7.4', False) + assert ver('')[1] == '' + + def test_get_since_text(self): + def gst(v): + return self.ssh.Algorithm.get_since_text(v) + + assert gst(['7.5']) == 'available since OpenSSH 7.5' + assert gst(['7.5C']) == 'available since OpenSSH 7.5 (client only)' + assert gst(['7.5,']) == 'available since OpenSSH 7.5' + assert gst(['d2016.73']) == 'available since Dropbear SSH 2016.73' + assert gst(['7.5,d2016.73']) == 'available since OpenSSH 7.5, Dropbear SSH 2016.73' + assert gst(['l10.7.4']) is None + assert gst([]) is None + + def test_timeframe_creation(self): + # pylint: disable=line-too-long,too-many-statements + def cmp_tf(v, s, r): + assert str(self._tf(v, s)) == str(r) + + cmp_tf(['6.2'], None, {'OpenSSH': ['6.2', None, '6.2', None]}) + cmp_tf(['6.2'], True, {'OpenSSH': ['6.2', None, None, None]}) + cmp_tf(['6.2'], False, {'OpenSSH': [None, None, '6.2', None]}) + cmp_tf(['6.2C'], None, {'OpenSSH': [None, None, '6.2', None]}) + cmp_tf(['6.2C'], True, {}) + cmp_tf(['6.2C'], False, {'OpenSSH': [None, None, '6.2', None]}) + cmp_tf(['6.1,6.2C'], None, {'OpenSSH': ['6.1', None, '6.2', None]}) + cmp_tf(['6.1,6.2C'], True, {'OpenSSH': ['6.1', None, None, None]}) + cmp_tf(['6.1,6.2C'], False, {'OpenSSH': [None, None, '6.2', None]}) + cmp_tf(['6.2C,6.1'], None, {'OpenSSH': ['6.1', None, '6.2', None]}) + cmp_tf(['6.2C,6.1'], True, {'OpenSSH': ['6.1', None, None, None]}) + cmp_tf(['6.2C,6.1'], False, {'OpenSSH': [None, None, '6.2', None]}) + cmp_tf(['6.3,6.2C'], None, {'OpenSSH': ['6.3', None, '6.2', None]}) + cmp_tf(['6.3,6.2C'], True, {'OpenSSH': ['6.3', None, None, None]}) + cmp_tf(['6.3,6.2C'], False, {'OpenSSH': [None, None, '6.2', None]}) + cmp_tf(['6.2C,6.3'], None, {'OpenSSH': ['6.3', None, '6.2', None]}) + cmp_tf(['6.2C,6.3'], True, {'OpenSSH': ['6.3', None, None, None]}) + cmp_tf(['6.2C,6.3'], False, {'OpenSSH': [None, None, '6.2', None]}) + + cmp_tf(['6.2', '6.6'], None, {'OpenSSH': ['6.2', '6.6', '6.2', '6.6']}) + cmp_tf(['6.2', '6.6'], True, {'OpenSSH': ['6.2', '6.6', None, None]}) + cmp_tf(['6.2', '6.6'], False, {'OpenSSH': [None, None, '6.2', '6.6']}) + cmp_tf(['6.2C', '6.6'], None, {'OpenSSH': [None, '6.6', '6.2', '6.6']}) + cmp_tf(['6.2C', '6.6'], True, {'OpenSSH': [None, '6.6', None, None]}) + cmp_tf(['6.2C', '6.6'], False, {'OpenSSH': [None, None, '6.2', '6.6']}) + cmp_tf(['6.1,6.2C', '6.6'], None, {'OpenSSH': ['6.1', '6.6', '6.2', '6.6']}) + cmp_tf(['6.1,6.2C', '6.6'], True, {'OpenSSH': ['6.1', '6.6', None, None]}) + cmp_tf(['6.1,6.2C', '6.6'], False, {'OpenSSH': [None, None, '6.2', '6.6']}) + cmp_tf(['6.2C,6.1', '6.6'], None, {'OpenSSH': ['6.1', '6.6', '6.2', '6.6']}) + cmp_tf(['6.2C,6.1', '6.6'], True, {'OpenSSH': ['6.1', '6.6', None, None]}) + cmp_tf(['6.2C,6.1', '6.6'], False, {'OpenSSH': [None, None, '6.2', '6.6']}) + cmp_tf(['6.3,6.2C', '6.6'], None, {'OpenSSH': ['6.3', '6.6', '6.2', '6.6']}) + cmp_tf(['6.3,6.2C', '6.6'], True, {'OpenSSH': ['6.3', '6.6', None, None]}) + cmp_tf(['6.3,6.2C', '6.6'], False, {'OpenSSH': [None, None, '6.2', '6.6']}) + cmp_tf(['6.2C,6.3', '6.6'], None, {'OpenSSH': ['6.3', '6.6', '6.2', '6.6']}) + cmp_tf(['6.2C,6.3', '6.6'], True, {'OpenSSH': ['6.3', '6.6', None, None]}) + cmp_tf(['6.2C,6.3', '6.6'], False, {'OpenSSH': [None, None, '6.2', '6.6']}) + + cmp_tf(['6.2', '6.6', None], None, {'OpenSSH': ['6.2', '6.6', '6.2', None]}) + cmp_tf(['6.2', '6.6', None], True, {'OpenSSH': ['6.2', '6.6', None, None]}) + cmp_tf(['6.2', '6.6', None], False, {'OpenSSH': [None, None, '6.2', None]}) + cmp_tf(['6.2C', '6.6', None], None, {'OpenSSH': [None, '6.6', '6.2', None]}) + cmp_tf(['6.2C', '6.6', None], True, {'OpenSSH': [None, '6.6', None, None]}) + cmp_tf(['6.2C', '6.6', None], False, {'OpenSSH': [None, None, '6.2', None]}) + cmp_tf(['6.1,6.2C', '6.6', None], None, {'OpenSSH': ['6.1', '6.6', '6.2', None]}) + cmp_tf(['6.1,6.2C', '6.6', None], True, {'OpenSSH': ['6.1', '6.6', None, None]}) + cmp_tf(['6.1,6.2C', '6.6', None], False, {'OpenSSH': [None, None, '6.2', None]}) + cmp_tf(['6.2C,6.1', '6.6', None], None, {'OpenSSH': ['6.1', '6.6', '6.2', None]}) + cmp_tf(['6.2C,6.1', '6.6', None], True, {'OpenSSH': ['6.1', '6.6', None, None]}) + cmp_tf(['6.2C,6.1', '6.6', None], False, {'OpenSSH': [None, None, '6.2', None]}) + cmp_tf(['6.2,6.3C', '6.6', None], None, {'OpenSSH': ['6.2', '6.6', '6.3', None]}) + cmp_tf(['6.2,6.3C', '6.6', None], True, {'OpenSSH': ['6.2', '6.6', None, None]}) + cmp_tf(['6.2,6.3C', '6.6', None], False, {'OpenSSH': [None, None, '6.3', None]}) + cmp_tf(['6.3C,6.2', '6.6', None], None, {'OpenSSH': ['6.2', '6.6', '6.3', None]}) + cmp_tf(['6.3C,6.2', '6.6', None], True, {'OpenSSH': ['6.2', '6.6', None, None]}) + cmp_tf(['6.3C,6.2', '6.6', None], False, {'OpenSSH': [None, None, '6.3', None]}) + + cmp_tf(['6.2', '6.6', '7.1'], None, {'OpenSSH': ['6.2', '6.6', '6.2', '7.1']}) + cmp_tf(['6.2', '6.6', '7.1'], True, {'OpenSSH': ['6.2', '6.6', None, None]}) + cmp_tf(['6.2', '6.6', '7.1'], False, {'OpenSSH': [None, None, '6.2', '7.1']}) + cmp_tf(['6.1,6.2C', '6.6', '7.1'], None, {'OpenSSH': ['6.1', '6.6', '6.2', '7.1']}) + cmp_tf(['6.1,6.2C', '6.6', '7.1'], True, {'OpenSSH': ['6.1', '6.6', None, None]}) + cmp_tf(['6.1,6.2C', '6.6', '7.1'], False, {'OpenSSH': [None, None, '6.2', '7.1']}) + cmp_tf(['6.2C,6.1', '6.6', '7.1'], None, {'OpenSSH': ['6.1', '6.6', '6.2', '7.1']}) + cmp_tf(['6.2C,6.1', '6.6', '7.1'], True, {'OpenSSH': ['6.1', '6.6', None, None]}) + cmp_tf(['6.2C,6.1', '6.6', '7.1'], False, {'OpenSSH': [None, None, '6.2', '7.1']}) + cmp_tf(['6.2,6.3C', '6.6', '7.1'], None, {'OpenSSH': ['6.2', '6.6', '6.3', '7.1']}) + cmp_tf(['6.2,6.3C', '6.6', '7.1'], True, {'OpenSSH': ['6.2', '6.6', None, None]}) + cmp_tf(['6.2,6.3C', '6.6', '7.1'], False, {'OpenSSH': [None, None, '6.3', '7.1']}) + cmp_tf(['6.3C,6.2', '6.6', '7.1'], None, {'OpenSSH': ['6.2', '6.6', '6.3', '7.1']}) + cmp_tf(['6.3C,6.2', '6.6', '7.1'], True, {'OpenSSH': ['6.2', '6.6', None, None]}) + cmp_tf(['6.3C,6.2', '6.6', '7.1'], False, {'OpenSSH': [None, None, '6.3', '7.1']}) + + tf1 = self._tf(['6.1,d2016.72,6.2C', '6.6,d2016.73', '7.1,d2016.74']) + tf2 = self._tf(['d2016.72,6.2C,6.1', 'd2016.73,6.6', 'd2016.74,7.1']) + tf3 = self._tf(['d2016.72,6.2C,6.1', '6.6,d2016.73', '7.1,d2016.74']) + # check without caring for output order + ov = "'OpenSSH': ['6.1', '6.6', '6.2', '7.1']" + dv = "'Dropbear SSH': ['2016.72', '2016.73', '2016.72', '2016.74']" + assert len(str(tf1)) == len(str(tf2)) == len(str(tf3)) + assert ov in str(tf1) and dv in str(tf1) + assert ov in str(tf2) and dv in str(tf3) + assert ov in str(tf2) and dv in str(tf3) + + def test_timeframe_object(self): + tf = self._tf(['6.1,6.2C', '6.6', '7.1']) + assert 'OpenSSH' in tf + assert 'Dropbear SSH' not in tf + assert 'libssh' not in tf + assert 'unknown' not in tf + assert tf['OpenSSH'] == ('6.1', '6.6', '6.2', '7.1') + assert tf['Dropbear SSH'] == (None, None, None, None) + assert tf['libssh'] == (None, None, None, None) + assert tf['unknown'] == (None, None, None, None) + assert tf.get_from('OpenSSH', True) == '6.1' + assert tf.get_till('OpenSSH', True) == '6.6' + assert tf.get_from('OpenSSH', False) == '6.2' + assert tf.get_till('OpenSSH', False) == '7.1' + + tf = self._tf(['6.1,d2016.72,6.2C', '6.6,d2016.73', '7.1,d2016.74']) + assert 'OpenSSH' in tf + assert 'Dropbear SSH' in tf + assert 'libssh' not in tf + assert 'unknown' not in tf + assert tf['OpenSSH'] == ('6.1', '6.6', '6.2', '7.1') + assert tf['Dropbear SSH'] == ('2016.72', '2016.73', '2016.72', '2016.74') + assert tf['libssh'] == (None, None, None, None) + assert tf['unknown'] == (None, None, None, None) + assert tf.get_from('OpenSSH', True) == '6.1' + assert tf.get_till('OpenSSH', True) == '6.6' + assert tf.get_from('OpenSSH', False) == '6.2' + assert tf.get_till('OpenSSH', False) == '7.1' + assert tf.get_from('Dropbear SSH', True) == '2016.72' + assert tf.get_till('Dropbear SSH', True) == '2016.73' + assert tf.get_from('Dropbear SSH', False) == '2016.72' + assert tf.get_till('Dropbear SSH', False) == '2016.74' From 1d1f842bed78b23a9399663dd215dd66d17b3e08 Mon Sep 17 00:00:00 2001 From: Andris Raugulis Date: Mon, 10 Apr 2017 19:11:12 +0300 Subject: [PATCH 042/103] Refactor output level/colors, fix python:S1845. --- ssh-audit.py | 34 +++++++++++++------------- test/test_auditconf.py | 22 ++++++++--------- test/test_output.py | 54 +++++++++++++++++++++--------------------- 3 files changed, 55 insertions(+), 55 deletions(-) diff --git a/ssh-audit.py b/ssh-audit.py index 30d8ee1..8cb5b06 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -83,7 +83,7 @@ class AuditConf(object): self.batch = False self.colors = True self.verbose = False - self.minlevel = 'info' + self.level = 'info' self.ipvo = () # type: Sequence[int] self.ipv4 = False self.ipv6 = False @@ -118,7 +118,7 @@ class AuditConf(object): if port < 1 or port > 65535: raise ValueError('invalid port: {0}'.format(value)) value = port - elif name in ['minlevel']: + elif name in ['level']: if value not in ('info', 'warn', 'fail'): raise ValueError('invalid level: {0}'.format(value)) valid = True @@ -164,7 +164,7 @@ class AuditConf(object): elif o in ('-l', '--level'): if a not in ('info', 'warn', 'fail'): usage_cb('level {0} is not valid'.format(a)) - aconf.minlevel = a + aconf.level = a if len(args) == 0: usage_cb() if oport is not None: @@ -189,30 +189,30 @@ class AuditConf(object): class Output(object): - LEVELS = ['info', 'warn', 'fail'] + LEVELS = ('info', 'warn', 'fail') # type: Sequence[str] COLORS = {'head': 36, 'good': 32, 'warn': 33, 'fail': 31} def __init__(self): # type: () -> None self.batch = False - self.colors = True self.verbose = False - self.__minlevel = 0 + self.use_colors = True + self.__level = 0 self.__colsupport = 'colorama' in sys.modules or os.name == 'posix' @property - def minlevel(self): + def level(self): # type: () -> str - if self.__minlevel < len(self.LEVELS): - return self.LEVELS[self.__minlevel] + if self.__level < len(self.LEVELS): + return self.LEVELS[self.__level] return 'unknown' - @minlevel.setter - def minlevel(self, name): + @level.setter + def level(self, name): # type: (str) -> None - self.__minlevel = self.getlevel(name) + self.__level = self.get_level(name) - def getlevel(self, name): + def get_level(self, name): # type: (str) -> int cname = 'info' if name == 'good' else name if cname not in self.LEVELS: @@ -238,9 +238,9 @@ class Output(object): # type: (str) -> Callable[[text_type], None] if name == 'head' and self.batch: return lambda x: None - if not self.getlevel(name) >= self.__minlevel: + if not self.get_level(name) >= self.__level: return lambda x: None - if self.colors and self.colors_supported and name in self.COLORS: + if self.use_colors and self.colors_supported and name in self.COLORS: color = '\033[0;{0}m'.format(self.COLORS[name]) return self._colorized(color) else: @@ -2160,9 +2160,9 @@ class Utils(object): def audit(aconf, sshv=None): # type: (AuditConf, Optional[int]) -> None out.batch = aconf.batch - out.colors = aconf.colors out.verbose = aconf.verbose - out.minlevel = aconf.minlevel + out.level = aconf.level + out.use_colors = aconf.colors s = SSH.Socket(aconf.host, aconf.port) s.connect(aconf.ipvo) if sshv is None: diff --git a/test/test_auditconf.py b/test/test_auditconf.py index 3472c42..259a881 100644 --- a/test/test_auditconf.py +++ b/test/test_auditconf.py @@ -10,8 +10,8 @@ class TestAuditConf(object): self.AuditConf = ssh_audit.AuditConf self.usage = ssh_audit.usage - @classmethod - def _test_conf(cls, conf, **kwargs): + @staticmethod + def _test_conf(conf, **kwargs): options = { 'host': None, 'port': 22, @@ -20,7 +20,7 @@ class TestAuditConf(object): 'batch': False, 'colors': True, 'verbose': False, - 'minlevel': 'info', + 'level': 'info', 'ipv4': True, 'ipv6': True, 'ipvo': () @@ -34,7 +34,7 @@ class TestAuditConf(object): assert conf.batch is options['batch'] assert conf.colors is options['colors'] assert conf.verbose is options['verbose'] - assert conf.minlevel == options['minlevel'] + assert conf.level == options['level'] assert conf.ipv4 == options['ipv4'] assert conf.ipv6 == options['ipv6'] assert conf.ipvo == options['ipvo'] @@ -115,14 +115,14 @@ class TestAuditConf(object): conf.ipvo = (4, 4, 4, 6, 6) assert conf.ipvo == (4, 6) - def test_audit_conf_minlevel(self): + def test_audit_conf_level(self): conf = self.AuditConf() for level in ['info', 'warn', 'fail']: - conf.minlevel = level - assert conf.minlevel == level + conf.level = level + assert conf.level == level for level in ['head', 'good', 'unknown', None]: with pytest.raises(ValueError) as excinfo: - conf.minlevel = level + conf.level = level excinfo.match(r'.*invalid level.*') def test_audit_conf_cmdline(self): @@ -183,10 +183,10 @@ class TestAuditConf(object): conf = c('-v localhost') self._test_conf(conf, host='localhost', verbose=True) conf = c('-l info localhost') - self._test_conf(conf, host='localhost', minlevel='info') + self._test_conf(conf, host='localhost', level='info') conf = c('-l warn localhost') - self._test_conf(conf, host='localhost', minlevel='warn') + self._test_conf(conf, host='localhost', level='warn') conf = c('-l fail localhost') - self._test_conf(conf, host='localhost', minlevel='fail') + self._test_conf(conf, host='localhost', level='fail') with pytest.raises(SystemExit): conf = c('-l something localhost') diff --git a/test/test_output.py b/test/test_output.py index 74b2c19..3ac6f06 100644 --- a/test/test_output.py +++ b/test/test_output.py @@ -41,13 +41,13 @@ class TestOutput(object): out = self.Output() # default: on assert out.batch is False - assert out.colors is True - assert out.minlevel == 'info' + assert out.use_colors is True + assert out.level == 'info' def test_output_colors(self, output_spy): out = self.Output() # test without colors - out.colors = False + out.use_colors = False output_spy.begin() out.info('info color') assert output_spy.flush() == [u'info color'] @@ -66,7 +66,7 @@ class TestOutput(object): if not out.colors_supported: return # test with colors - out.colors = True + out.use_colors = True output_spy.begin() out.info('info color') assert output_spy.flush() == [u'info color'] @@ -93,29 +93,29 @@ class TestOutput(object): def test_output_levels(self): out = self.Output() - assert out.getlevel('info') == 0 - assert out.getlevel('good') == 0 - assert out.getlevel('warn') == 1 - assert out.getlevel('fail') == 2 - assert out.getlevel('unknown') > 2 + assert out.get_level('info') == 0 + assert out.get_level('good') == 0 + assert out.get_level('warn') == 1 + assert out.get_level('fail') == 2 + assert out.get_level('unknown') > 2 - def test_output_minlevel_property(self): + def test_output_level_property(self): out = self.Output() - out.minlevel = 'info' - assert out.minlevel == 'info' - out.minlevel = 'good' - assert out.minlevel == 'info' - out.minlevel = 'warn' - assert out.minlevel == 'warn' - out.minlevel = 'fail' - assert out.minlevel == 'fail' - out.minlevel = 'invalid level' - assert out.minlevel == 'unknown' + out.level = 'info' + assert out.level == 'info' + out.level = 'good' + assert out.level == 'info' + out.level = 'warn' + assert out.level == 'warn' + out.level = 'fail' + assert out.level == 'fail' + out.level = 'invalid level' + assert out.level == 'unknown' - def test_output_minlevel(self, output_spy): + def test_output_level(self, output_spy): out = self.Output() # visible: all - out.minlevel = 'info' + out.level = 'info' output_spy.begin() out.info('info color') out.head('head color') @@ -124,7 +124,7 @@ class TestOutput(object): out.fail('fail color') assert len(output_spy.flush()) == 5 # visible: head, warn, fail - out.minlevel = 'warn' + out.level = 'warn' output_spy.begin() out.info('info color') out.head('head color') @@ -133,7 +133,7 @@ class TestOutput(object): out.fail('fail color') assert len(output_spy.flush()) == 3 # visible: head, fail - out.minlevel = 'fail' + out.level = 'fail' output_spy.begin() out.info('info color') out.head('head color') @@ -142,7 +142,7 @@ class TestOutput(object): out.fail('fail color') assert len(output_spy.flush()) == 2 # visible: head - out.minlevel = 'invalid level' + out.level = 'invalid level' output_spy.begin() out.info('info color') out.head('head color') @@ -155,7 +155,7 @@ class TestOutput(object): out = self.Output() # visible: all output_spy.begin() - out.minlevel = 'info' + out.level = 'info' out.batch = False out.info('info color') out.head('head color') @@ -165,7 +165,7 @@ class TestOutput(object): assert len(output_spy.flush()) == 5 # visible: all except head output_spy.begin() - out.minlevel = 'info' + out.level = 'info' out.batch = True out.info('info color') out.head('head color') From 9c463b4e061987f2de593f4ed68a523449527a1e Mon Sep 17 00:00:00 2001 From: Andris Raugulis Date: Mon, 10 Apr 2017 19:32:40 +0300 Subject: [PATCH 043/103] Fix lint tox environment. --- tox.ini | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tox.ini b/tox.ini index 0655e65..7f61a11 100644 --- a/tox.ini +++ b/tox.ini @@ -13,9 +13,9 @@ deps = test,cov: {[testenv:cov]deps} test,py{33,34,35,36,37}-{type,mypy}: colorama==0.3.7 py{33,34,35,36,37}-{type,mypy}: {[testenv:mypy]deps} - py{27,py,py3,33,34,35,36,37}-{lint,pylint}: {[testenv:pylint]deps} - py{27,py,py3,33,34,35,36,37}-{lint,flake8}: {[testenv:flake8]deps} - lint,vulture: {[testenv:vulture]deps} + py{27,py,py3,33,34,35,36,37}-{lint,pylint},lint: {[testenv:pylint]deps} + py{27,py,py3,33,34,35,36,37}-{lint,flake8},lint: {[testenv:flake8]deps} + py{27,py,py3,33,34,35,36,37}-{lint,vulture},lint: {[testenv:vulture]deps} setenv = SSHAUDIT = {toxinidir}/ssh-audit.py test: COVERAGE_FILE = {toxinidir}/.coverage.{envname} @@ -27,9 +27,9 @@ commands = test: coverage report --show-missing test: coverage html -d {toxinidir}/reports/html/coverage.{envname} py{33,34,35,36,37}-{type,mypy}: {[testenv:mypy]commands} - py{27,py,py3,33,34,35,36,37}-{lint,pylint}: {[testenv:pylint]commands} - py{27,py,py3,33,34,35,36,37}-{lint,flake8}: {[testenv:flake8]commands} - lint,vulture: {[testenv:vulture]commands} + py{27,py,py3,33,34,35,36,37}-{lint,pylint},lint: {[testenv:pylint]commands} + py{27,py,py3,33,34,35,36,37}-{lint,flake8},lint: {[testenv:flake8]commands} + py{27,py,py3,33,34,35,36,37}-{lint,vulture},lint: {[testenv:vulture]commands} ignore_outcome = type: true lint: true From 96d442ec6291f9ba4b21514e05648bbd6e7a05c7 Mon Sep 17 00:00:00 2001 From: Andris Raugulis Date: Tue, 11 Apr 2017 13:32:38 +0300 Subject: [PATCH 044/103] Test Timeframe repr(). --- test/test_ssh_algorithm.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/test/test_ssh_algorithm.py b/test/test_ssh_algorithm.py index 5ba88e5..5e03529 100644 --- a/test/test_ssh_algorithm.py +++ b/test/test_ssh_algorithm.py @@ -119,9 +119,10 @@ class TestSSHAlgorithm(object): ov = "'OpenSSH': ['6.1', '6.6', '6.2', '7.1']" dv = "'Dropbear SSH': ['2016.72', '2016.73', '2016.72', '2016.74']" assert len(str(tf1)) == len(str(tf2)) == len(str(tf3)) - assert ov in str(tf1) and dv in str(tf1) - assert ov in str(tf2) and dv in str(tf3) - assert ov in str(tf2) and dv in str(tf3) + assert ov in str(tf1) and ov in str(tf2) and ov in str(tf3) + assert dv in str(tf1) and dv in str(tf2) and dv in str(tf3) + assert ov in repr(tf1) and ov in repr(tf2) and ov in repr(tf3) + assert dv in repr(tf1) and dv in repr(tf2) and dv in repr(tf3) def test_timeframe_object(self): tf = self._tf(['6.1,6.2C', '6.6', '7.1']) @@ -155,3 +156,9 @@ class TestSSHAlgorithm(object): assert tf.get_till('Dropbear SSH', True) == '2016.73' assert tf.get_from('Dropbear SSH', False) == '2016.72' assert tf.get_till('Dropbear SSH', False) == '2016.74' + ov = "'OpenSSH': ['6.1', '6.6', '6.2', '7.1']" + dv = "'Dropbear SSH': ['2016.72', '2016.73', '2016.72', '2016.74']" + assert ov in str(tf) + assert dv in str(tf) + assert ov in repr(tf) + assert dv in repr(tf) From d8eb46d7669aa92ad08ad4e714cdab88ff14496f Mon Sep 17 00:00:00 2001 From: Andris Raugulis Date: Fri, 5 May 2017 14:12:45 +0300 Subject: [PATCH 045/103] Correct IPv6 parsing in command-line. Fixes #26. --- ssh-audit.py | 15 +++++++++------ test/test_auditconf.py | 8 ++++++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/ssh-audit.py b/ssh-audit.py index 8cb5b06..461b953 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -169,14 +169,17 @@ class AuditConf(object): usage_cb() if oport is not None: host = args[0] - port = utils.parse_int(oport) else: - s = args[0].split(':') - host = s[0].strip() - if len(s) == 2: - oport, port = s[1], utils.parse_int(s[1]) + mx = re.match(r'^\[([^\]]+)\](?::(.*))?$', args[0]) + if bool(mx): + host, oport = mx.group(1), mx.group(2) else: - oport, port = '22', 22 + s = args[0].split(':') + if len(s) > 2: + host, oport = args[0], '22' + else: + host, oport = s[0], s[1] if len(s) > 1 else '22' + port = utils.parse_int(oport) if not host: usage_cb('host is empty') if port <= 0 or port > 65535: diff --git a/test/test_auditconf.py b/test/test_auditconf.py index 259a881..a901299 100644 --- a/test/test_auditconf.py +++ b/test/test_auditconf.py @@ -148,6 +148,14 @@ class TestAuditConf(object): self._test_conf(conf, host='localhost', port=2222) conf = c('-p 2222 localhost') self._test_conf(conf, host='localhost', port=2222) + conf = c('2001:4860:4860::8888') + self._test_conf(conf, host='2001:4860:4860::8888') + conf = c('[2001:4860:4860::8888]:22') + self._test_conf(conf, host='2001:4860:4860::8888') + conf = c('[2001:4860:4860::8888]:2222') + self._test_conf(conf, host='2001:4860:4860::8888', port=2222) + conf = c('-p 2222 2001:4860:4860::8888') + self._test_conf(conf, host='2001:4860:4860::8888', port=2222) with pytest.raises(SystemExit): conf = c('localhost:') with pytest.raises(SystemExit): From 7c919b093bd40b158eb19e1aceae9033ea27a064 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Thu, 21 Sep 2017 22:44:34 -0400 Subject: [PATCH 046/103] Added RSA & DH modulus size auditing. --- ssh-audit.py | 517 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 497 insertions(+), 20 deletions(-) diff --git a/ssh-audit.py b/ssh-audit.py index 461b953..d26289e 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -24,7 +24,7 @@ THE SOFTWARE. """ from __future__ import print_function -import os, io, sys, socket, struct, random, errno, getopt, re, hashlib, base64 +import binascii, os, io, sys, socket, struct, random, errno, getopt, re, hashlib, base64 VERSION = 'v1.7.1.dev' @@ -288,7 +288,6 @@ class SSH2(object): # pylint: disable=too-few-public-methods WARN_CURVES_WEAK = 'using weak elliptic curves' WARN_RNDSIG_KEY = 'using weak random number generator could reveal the key' WARN_MODULUS_SIZE = 'using small 1024-bit modulus' - WARN_MODULUS_CUSTOM = 'using custom size modulus (possibly weak)' WARN_HASH_WEAK = 'using weak hashing algorithm' WARN_CIPHER_MODE = 'using weak cipher mode' WARN_BLOCK_SIZE = 'using small 64-bit block size' @@ -304,7 +303,7 @@ class SSH2(object): # pylint: disable=too-few-public-methods 'diffie-hellman-group16-sha512': [['7.3,d2016.73']], 'diffie-hellman-group18-sha512': [['7.3']], 'diffie-hellman-group-exchange-sha1': [['2.3.0', '6.6', None], [FAIL_OPENSSH67_UNSAFE], [WARN_HASH_WEAK]], - 'diffie-hellman-group-exchange-sha256': [['4.4'], [], [WARN_MODULUS_CUSTOM]], + 'diffie-hellman-group-exchange-sha256': [['4.4']], 'ecdh-sha2-nistp256': [['5.7,d2013.62,l10.6.0'], [WARN_CURVES_WEAK]], 'ecdh-sha2-nistp384': [['5.7,d2013.62'], [WARN_CURVES_WEAK]], 'ecdh-sha2-nistp521': [['5.7,d2013.62'], [WARN_CURVES_WEAK]], @@ -422,7 +421,10 @@ class SSH2(object): # pylint: disable=too-few-public-methods self.__server = srv self.__follows = follows self.__unused = unused - + + self.__rsa_key_sizes = {} + self.__dh_modulus_sizes = {} + @property def cookie(self): # type: () -> binary_type @@ -459,7 +461,19 @@ class SSH2(object): # pylint: disable=too-few-public-methods def unused(self): # type: () -> int return self.__unused - + + def set_rsa_hostkey_size(self, rsa_type, rsa_hostkey_size): + self.__rsa_key_sizes[rsa_type] = rsa_hostkey_size; + + def rsa_hostkey_sizes(self): + return self.__rsa_key_sizes + + def set_dh_modulus_size(self, gex_alg, modulus_size): + self.__dh_modulus_sizes[gex_alg] = modulus_size + + def dh_modulus_sizes(self): + return self.__dh_modulus_sizes + def write(self, wbuf): # type: (WriteBuf) -> None wbuf.write(self.cookie) @@ -505,6 +519,196 @@ class SSH2(object): # pylint: disable=too-few-public-methods kex = cls(cookie, kex_algs, key_algs, cli, srv, follows, unused) return kex + # Obtains RSA host keys and checks their size. + class RSAKeyTest(object): + RSA_TYPES = ['ssh-rsa', 'rsa-sha2-256', 'rsa-sha2-512'] + + @staticmethod + def run(s, kex): + KEX_TO_DHGROUP = { + 'diffie-hellman-group1-sha1': KexGroup1, + 'diffie-hellman-group14-sha1': KexGroup14_SHA1, + 'diffie-hellman-group14-sha256': KexGroup14_SHA256, + 'curve25519-sha256': KexCurve25519_SHA256, + 'curve25519-sha256@libssh.org': KexCurve25519_SHA256, + 'diffie-hellman-group16-sha512': KexGroup16_SHA512, + 'diffie-hellman-group18-sha512': KexGroup18_SHA512, + 'diffie-hellman-group-exchange-sha1': KexGroupExchange_SHA1, + 'diffie-hellman-group-exchange-sha256': KexGroupExchange_SHA256, + 'ecdh-sha2-nistp256': KexNISTP256, + 'ecdh-sha2-nistp384': KexNISTP384, + 'ecdh-sha2-nistp521': KexNISTP521, + #'kexguess2@matt.ucc.asn.au': ??? + } + + # Pick the first kex algorithm that the server supports, which we + # happen to support as well. + selected_kex_str = None + kex_group = None + for server_kex_alg in kex.kex_algorithms: + if server_kex_alg in KEX_TO_DHGROUP: + selected_kex_str = server_kex_alg + kex_group = KEX_TO_DHGROUP[server_kex_alg]() + break + + # If the server supports one of the RSA types, extract its key size. + modulus_size = 0 + if selected_kex_str is not None: + for rsa_type in SSH2.RSAKeyTest.RSA_TYPES: + if rsa_type in kex.key_algorithms: + + # 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), [selected_kex_str], [rsa_type], kex.client, 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) + modulus_size = kex_group.get_hostkey_size() + + # We only need to test one RSA type, since the others + # will all be the same. + break + + if modulus_size > 0: + + # Set the hostkey size for all RSA key types since 'ssh-rsa', + # 'rsa-sha2-256', etc. are all using the same host key. + for rsa_type in SSH2.RSAKeyTest.RSA_TYPES: + kex.set_rsa_hostkey_size(rsa_type, modulus_size) + + # Keys smaller than 2048 result in a failure. + fail = False + if modulus_size < 2048: + fail = True + + # If this is a bad key size, update the database accordingly. + if fail: + for rsa_type in SSH2.RSAKeyTest.RSA_TYPES: + alg_list = SSH2.KexDB.ALGORITHMS['key'][rsa_type] + alg_list.append(['using small %d-bit modulus' % modulus_size]) + + # Performs DH group exchanges to find what moduli are supported, and checks + # their size. + class GEXTest(object): + + # Creates a new connection to the server. Returns an SSH.Socket, or + # None on failure. + @staticmethod + def reconnect(ipvo, host, port, gex_alg): + s = SSH.Socket(host, port) + s.connect(ipvo) + unused, unused, err = s.get_banner() + if err is not None: + s.close() + return None + + # Parse the server's initial KEX. + packet_type, payload = s.read_packet(2) + kex = SSH2.Kex.parse(payload) + + # Send our KEX using the specified group-exchange and most of the + # server's own values. + client_kex = SSH2.Kex(os.urandom(16), [gex_alg], kex.key_algorithms, kex.client, kex.server, 0, 0) + s.write_byte(SSH.Protocol.MSG_KEXINIT) + client_kex.write(s) + s.send_packet() + return s + + # Runs the DH moduli test against the specified target. + @staticmethod + def run(ipvo, host, port, s, kex): + GEX_ALGS = { + 'diffie-hellman-group-exchange-sha1': KexGroupExchange_SHA1, + 'diffie-hellman-group-exchange-sha256': KexGroupExchange_SHA256, + } + + # Check if the server supports any of the group-exchange + # algorithms. If so, test each one. + for gex_alg in GEX_ALGS: + if gex_alg in kex.kex_algorithms: + + # 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 not None: + s.close() + s = None + + s = SSH2.GEXTest.reconnect(ipvo, host, port, gex_alg) + if s is None: + break + + kex_group = GEX_ALGS[gex_alg]() + smallest_modulus = -1 + + # First try a range of weak sizes. + try: + kex_group.send_init(s, 512, 1024, 1536) + kex_group.recv_reply(s) + + # Its been observed that servers will return a group + # larger than the requested max. So just because we + # got here, doesn't mean the server is vulnerable... + smallest_modulus = kex_group.get_modulus_size() + except Exception as e: + pass + finally: + s.close() + s = None + + # Try an array of specific modulus sizes... one at a time. + reconnect_failed = False + for bits in [512, 768, 1024, 1536, 2048, 3072, 4096]: + + # If we found one modulus size already, but we're about + # to test a larger one, don't bother. + if smallest_modulus > 0 and bits >= smallest_modulus: + break + + if s is None: + s = SSH2.GEXTest.reconnect(ipvo, host, port, gex_alg) + if s is None: + reconnect_failed = True + break + + try: + kex_group.send_init(s, bits, bits, bits) + kex_group.recv_reply(s) + smallest_modulus = kex_group.get_modulus_size() + except Exception as e: + pass + finally: + s.close() + s = None + + if smallest_modulus > 0: + kex.set_dh_modulus_size(gex_alg, smallest_modulus) + + # We flag moduli smaller than 2048 as a failure. + if smallest_modulus < 2048: + text = 'using small %d-bit modulus' % smallest_modulus + lst = SSH2.KexDB.ALGORITHMS['kex'][gex_alg] + # For 'diffie-hellman-group-exchange-sha256', add + # a failure reason. + if len(lst) == 1: + lst.append(text) + # For 'diffie-hellman-group-exchange-sha1', delete + # the existing failure reason (which is vague), and + # insert our own. + else: + del lst[1] + lst.insert(1, [text]) + + if reconnect_failed: + break class SSH1(object): class CRC32(object): @@ -868,7 +1072,11 @@ class SSH(object): # pylint: disable=too-few-public-methods MSG_KEXINIT = 20 MSG_NEWKEYS = 21 MSG_KEXDH_INIT = 30 - MSG_KEXDH_REPLY = 32 + MSG_KEXDH_REPLY = 31 + MSG_KEXDH_GEX_REQUEST = 34 + MSG_KEXDH_GEX_GROUP = 31 + MSG_KEXDH_GEX_INIT = 32 + MSG_KEXDH_GEX_REPLY = 33 class Product(object): # pylint: disable=too-few-public-methods OpenSSH = 'OpenSSH' @@ -1747,7 +1955,10 @@ class SSH(object): # pylint: disable=too-few-public-methods pad_bytes = b'\x00' * padding data = struct.pack('>Ib', plen, padding) + payload + pad_bytes return self.send(data) - + + def close(self): + self.__cleanup() + def _close_socket(self, s): # type: (Optional[socket.socket]) -> None try: @@ -1767,24 +1978,99 @@ class SSH(object): # pylint: disable=too-few-public-methods class KexDH(object): # pragma: nocover - def __init__(self, alg, g, p): + def __init__(self, kex_name, hash_alg, g, p): # type: (str, int, int) -> None - self.__alg = alg + self.__kex_name = kex_name + self.__hash_alg = hash_alg + self.set_params(g, p) + + self.__ed25519_pubkey = 0 + self.__hostkey_type = None + self.__hostkey_e = 0 + self.__hostkey_n = 0 + self.__hostkey_n_len = 0 # This is the length of the host key modulus. + self.__f = 0 + self.__h_sig = 0 + + def set_params(self, g, p): self.__g = g self.__p = p self.__q = (self.__p - 1) // 2 self.__x = 0 self.__e = 0 - - def send_init(self, s): + + + def send_init(self, s, init_msg=SSH.Protocol.MSG_KEXDH_INIT): # type: (SSH.Socket) -> None r = random.SystemRandom() self.__x = r.randrange(2, self.__q) self.__e = pow(self.__g, self.__x, self.__p) - s.write_byte(SSH.Protocol.MSG_KEXDH_INIT) + s.write_byte(init_msg) s.write_mpint2(self.__e) s.send_packet() + # Parse a KEXDH_REPLY or KEXDH_GEX_REPLY message from the server. This + # Contains the host key, among other things. + def recv_reply(self, s): + packet_type, payload = s.read_packet(2) + if packet_type not in [SSH.Protocol.MSG_KEXDH_REPLY, SSH.Protocol.MSG_KEXDH_GEX_REPLY]: + # 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)) + + host_key_len = struct.unpack('>I', payload[0:4])[0] + ptr = 4 + + hostkey = payload[ptr:ptr + host_key_len] + ptr += host_key_len + + 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. + hostkey_type_len = struct.unpack('>I', hostkey[0:4])[0] + ptr = 4 + + # Get the host key type (i.e.: 'ssh-rsa', 'ssh-ed25519', etc). + self.__hostkey_type = hostkey[ptr:ptr + hostkey_type_len] + ptr += hostkey_type_len + + hostkey_e_len = struct.unpack('>I', hostkey[ptr:ptr + 4])[0] + ptr += 4 + + self.__hostkey_e = int(binascii.hexlify(hostkey[ptr:ptr + hostkey_e_len]), 16) + ptr += hostkey_e_len + + # 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] + ptr += 4 + self.__hostkey_n = int(binascii.hexlify(hostkey[ptr:ptr + self.__hostkey_n_len]), 16) + + # Returns the size of the hostkey, in bits. + def get_hostkey_size(self): + size = self.__hostkey_n_len * 8 + + # 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 + # the case, and subtract 8 if so. This simply improves readability + # in the UI. + if (size >> 3) % 2 != 0: + size = size - 8 + return size + + # Returns the size of the DH modulus, in bits. + def get_modulus_size(self): + # -2 to account for the '0b' prefix in the string. + return len(bin(self.__p)) - 2 + class KexGroup1(KexDH): # pragma: nocover def __init__(self): @@ -1795,11 +2081,11 @@ class KexGroup1(KexDH): # pragma: nocover 'f25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff' '5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece65381' 'ffffffffffffffff', 16) - super(KexGroup1, self).__init__('sha1', 2, p) + super(KexGroup1, self).__init__('KexGroup1', 'sha1', 2, p) class KexGroup14(KexDH): # pragma: nocover - def __init__(self): + def __init__(self, hash_alg): # type: () -> None # rfc3526: 2048-bit modp group p = int('ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67' @@ -1811,26 +2097,213 @@ class KexGroup14(KexDH): # pragma: nocover 'ca18217c32905e462e36ce3be39e772c180e86039b2783a2ec07a28fb5c5' '5df06f4c52c9de2bcbf6955817183995497cea956ae515d2261898fa0510' '15728e5a8aacaa68ffffffffffffffff', 16) - super(KexGroup14, self).__init__('sha1', 2, p) + super(KexGroup14, self).__init__('KexGroup14', hash_alg, 2, p) -def output_algorithms(title, alg_db, alg_type, algorithms, maxlen=0): +class KexGroup14_SHA1(KexGroup14): + def __init__(self): + super(KexGroup14_SHA1, self).__init__('sha1') + + +class KexGroup14_SHA256(KexGroup14): + def __init__(self): + super(KexGroup14_SHA256, self).__init__('sha256') + + +class KexGroup16_SHA512(KexDH): + def __init__(self): + # rfc3526: 4096-bit modp group + p = int('ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67' + 'cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6d' + 'f25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff' + '5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3d' + 'c2007cb8a163bf0598da48361c55d39a69163fa8fd24cf5f83655d23dca3' + 'ad961c62f356208552bb9ed529077096966d670c354e4abc9804f1746c08' + 'ca18217c32905e462e36ce3be39e772c180e86039b2783a2ec07a28fb5c5' + '5df06f4c52c9de2bcbf6955817183995497cea956ae515d2261898fa0510' + '15728e5a8aaac42dad33170d04507a33a85521abdf1cba64ecfb850458db' + 'ef0a8aea71575d060c7db3970f85a6e1e4c7abf5ae8cdb0933d71e8c94e0' + '4a25619dcee3d2261ad2ee6bf12ffa06d98a0864d87602733ec86a64521f' + '2b18177b200cbbe117577a615d6c770988c0bad946e208e24fa074e5ab31' + '43db5bfce0fd108e4b82d120a92108011a723c12a787e6d788719a10bdba' + '5b2699c327186af4e23c1a946834b6150bda2583e9ca2ad44ce8dbbbc2db' + '04de8ef92e8efc141fbecaa6287c59474e6bc05d99b2964fa090c3a2233b' + 'a186515be7ed1f612970cee2d7afb81bdd762170481cd0069127d5b05aa9' + '93b4ea988d8fddc186ffb7dc90a6c08f4df435c934063199ffffffffffff' + 'ffff', 16) + super(KexGroup16_SHA512, self).__init__('KexGroup16_SHA512', 'sha512', 2, p) + + +class KexGroup18_SHA512(KexDH): + def __init__(self): + # rfc3526: 8192-bit modp group + p = int('ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67' + 'cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6d' + 'f25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff' + '5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3d' + 'c2007cb8a163bf0598da48361c55d39a69163fa8fd24cf5f83655d23dca3' + 'ad961c62f356208552bb9ed529077096966d670c354e4abc9804f1746c08' + 'ca18217c32905e462e36ce3be39e772c180e86039b2783a2ec07a28fb5c5' + '5df06f4c52c9de2bcbf6955817183995497cea956ae515d2261898fa0510' + '15728e5a8aaac42dad33170d04507a33a85521abdf1cba64ecfb850458db' + 'ef0a8aea71575d060c7db3970f85a6e1e4c7abf5ae8cdb0933d71e8c94e0' + '4a25619dcee3d2261ad2ee6bf12ffa06d98a0864d87602733ec86a64521f' + '2b18177b200cbbe117577a615d6c770988c0bad946e208e24fa074e5ab31' + '43db5bfce0fd108e4b82d120a92108011a723c12a787e6d788719a10bdba' + '5b2699c327186af4e23c1a946834b6150bda2583e9ca2ad44ce8dbbbc2db' + '04de8ef92e8efc141fbecaa6287c59474e6bc05d99b2964fa090c3a2233b' + 'a186515be7ed1f612970cee2d7afb81bdd762170481cd0069127d5b05aa9' + '93b4ea988d8fddc186ffb7dc90a6c08f4df435c93402849236c3fab4d27c' + '7026c1d4dcb2602646dec9751e763dba37bdf8ff9406ad9e530ee5db382f' + '413001aeb06a53ed9027d831179727b0865a8918da3edbebcf9b14ed44ce' + '6cbaced4bb1bdb7f1447e6cc254b332051512bd7af426fb8f401378cd2bf' + '5983ca01c64b92ecf032ea15d1721d03f482d7ce6e74fef6d55e702f4698' + '0c82b5a84031900b1c9e59e7c97fbec7e8f323a97a7e36cc88be0f1d45b7' + 'ff585ac54bd407b22b4154aacc8f6d7ebf48e1d814cc5ed20f8037e0a797' + '15eef29be32806a1d58bb7c5da76f550aa3d8a1fbff0eb19ccb1a313d55c' + 'da56c9ec2ef29632387fe8d76e3c0468043e8f663f4860ee12bf2d5b0b74' + '74d6e694f91e6dbe115974a3926f12fee5e438777cb6a932df8cd8bec4d0' + '73b931ba3bc832b68d9dd300741fa7bf8afc47ed2576f6936ba424663aab' + '639c5ae4f5683423b4742bf1c978238f16cbe39d652de3fdb8befc848ad9' + '22222e04a4037c0713eb57a81a23f0c73473fc646cea306b4bcbc8862f83' + '85ddfa9d4b7fa2c087e879683303ed5bdd3a062b3cf5b3a278a66d2a13f8' + '3f44f82ddf310ee074ab6a364597e899a0255dc164f31cc50846851df9ab' + '48195ded7ea1b1d510bd7ee74d73faf36bc31ecfa268359046f4eb879f92' + '4009438b481c6cd7889a002ed5ee382bc9190da6fc026e479558e4475677' + 'e9aa9e3050e2765694dfc81f56e880b96e7160c980dd98edd3dfffffffff' + 'ffffffff', 16) + super(KexGroup18_SHA512, self).__init__('KexGroup18_SHA512', 'sha512', 2, p) + + +class KexCurve25519_SHA256(KexDH): + def __init__(self): + super(KexCurve25519_SHA256, self).__init__('KexCurve25519_SHA256', 'sha256', 0, 0) + + # To start an ED25519 kex, we simply send a random 256-bit number as the + # public key. + def send_init(self, s): + self.__ed25519_pubkey = os.urandom(32) + s.write_byte(SSH.Protocol.MSG_KEXDH_INIT) + s.write_string(self.__ed25519_pubkey) + s.send_packet() + + +class KexNISTP256(KexDH): + def __init__(self): + super(KexNISTP256, self).__init__('KexNISTP256', 'sha256', 0, 0) + + # Because the server checks that the value sent here is valid (i.e.: it lies + # on the curve, among other things), we would have to write a lot of code + # or import an elliptic curve library in order to randomly generate a + # valid elliptic point each time. Hence, we will simply send a static + # value, which is enough for us to extract the server's host key. + def send_init(self, s): + s.write_byte(SSH.Protocol.MSG_KEXDH_INIT) + s.write_string(b'\x04\x0b\x60\x44\x9f\x8a\x11\x9e\xc7\x81\x0c\xa9\x98\xfc\xb7\x90\xaa\x6b\x26\x8c\x12\x4a\xc0\x09\xbb\xdf\xc4\x2c\x4c\x2c\x99\xb6\xe1\x71\xa0\xd4\xb3\x62\x47\x74\xb3\x39\x0c\xf2\x88\x4a\x84\x6b\x3b\x15\x77\xa5\x77\xd2\xa9\xc9\x94\xf9\xd5\x66\x19\xcd\x02\x34\xd1') + s.send_packet() + + +class KexNISTP384(KexDH): + def __init__(self): + super(KexNISTP384, self).__init__('KexNISTP384', 'sha256', 0, 0) + + # See comment for KexNISTP256.send_init(). + def send_init(self, s): + s.write_byte(SSH.Protocol.MSG_KEXDH_INIT) + s.write_string(b'\x04\xe2\x9b\x84\xce\xa1\x39\x50\xfe\x1e\xa3\x18\x70\x1c\xe2\x7a\xe4\xb5\x6f\xdf\x93\x9f\xd4\xf4\x08\xcc\x9b\x02\x10\xa4\xca\x77\x9c\x2e\x51\x44\x1d\x50\x7a\x65\x4e\x7e\x2f\x10\x2d\x2d\x4a\x32\xc9\x8e\x18\x75\x90\x6c\x19\x10\xda\xcc\xa8\xe9\xf4\xc4\x3a\x53\x80\x35\xf4\x97\x9c\x04\x16\xf9\x5a\xdc\xcc\x05\x94\x29\xfa\xc4\xd6\x87\x4e\x13\x21\xdb\x3d\x12\xac\xbd\x20\x3b\x60\xff\xe6\x58\x42') + s.send_packet() + + +class KexNISTP521(KexDH): + def __init__(self): + super(KexNISTP521, self).__init__('KexNISTP521', 'sha256', 0, 0) + + # See comment for KexNISTP256.send_init(). + def send_init(self, s): + s.write_byte(SSH.Protocol.MSG_KEXDH_INIT) + s.write_string(b'\x04\x01\x02\x90\x29\xe9\x8f\xa8\x04\xaf\x1c\x00\xf9\xc6\x29\xc0\x39\x74\x8e\xea\x47\x7e\x7c\xf7\x15\x6e\x43\x3b\x59\x13\x53\x43\xb0\xae\x0b\xe7\xe6\x7c\x55\x73\x52\xa5\x2a\xc1\x42\xde\xfc\xf4\x1f\x8b\x5a\x8d\xfa\xcd\x0a\x65\x77\xa8\xce\x68\xd2\xc6\x26\xb5\x3f\xee\x4b\x01\x7b\xd2\x96\x23\x69\x53\xc7\x01\xe1\x0d\x39\xe9\x87\x49\x3b\xc8\xec\xda\x0c\xf9\xca\xad\x89\x42\x36\x6f\x93\x78\x78\x31\x55\x51\x09\x51\xc0\x96\xd7\xea\x61\xbf\xc2\x44\x08\x80\x43\xed\xc6\xbb\xfb\x94\xbd\xf8\xdf\x2b\xd8\x0b\x2e\x29\x1b\x8c\xc4\x8a\x04\x2d\x3a') + s.send_packet() + + +class KexGroupExchange(KexDH): + def __init__(self, classname, hash_alg): + super(KexGroupExchange, self).__init__(classname, hash_alg, 0, 0) + + # The group exchange starts with sending a message to the server with + # the minimum, maximum, and preferred number of bits are for the DH group. + # The server responds with a generator and prime modulus that matches that, + # then the handshake continues on like a normal DH handshake (except the + # SSH message types differ). + def send_init(self, s, minbits=1024, prefbits=2048, maxbits=8192): + + # Send the initial group exchange request. Tell the server what range + # of modulus sizes we will accept, along with our preference. + s.write_byte(SSH.Protocol.MSG_KEXDH_GEX_REQUEST) + s.write_int(minbits) + s.write_int(prefbits) + s.write_int(maxbits) + s.send_packet() + + packet_type, payload = s.read_packet(2) + if packet_type != SSH.Protocol.MSG_KEXDH_GEX_GROUP: + # TODO: replace with a better exception type. + raise Exception('Expected MSG_KEXDH_GEX_REPLY (%d), but got %d instead.' % (SSH.Protocol.MSG_KEXDH_GEX_REPLY, packet_type)) + + # Parse the modulus (p) and generator (g) values from the server. + ptr = 0 + p_len = struct.unpack('>I', payload[ptr:ptr + 4])[0] + ptr += 4 + + p = int(binascii.hexlify(payload[ptr:ptr + p_len]), 16) + ptr += p_len + + g_len = struct.unpack('>I', payload[ptr:ptr + 4])[0] + ptr += 4 + + g = int(binascii.hexlify(payload[ptr:ptr + g_len]), 16) + ptr += g_len + + # Now that we got the generator and modulus, perform the DH exchange + # like usual. + super().set_params(g, p) + super().send_init(s, SSH.Protocol.MSG_KEXDH_GEX_INIT) + + +class KexGroupExchange_SHA1(KexGroupExchange): + def __init__(self): + super(KexGroupExchange_SHA1, self).__init__('KexGroupExchange_SHA1', 'sha1') + + +class KexGroupExchange_SHA256(KexGroupExchange): + def __init__(self): + super(KexGroupExchange_SHA256, self).__init__('KexGroupExchange_SHA256', 'sha256') + + +def output_algorithms(title, alg_db, alg_type, algorithms, maxlen=0, alg_sizes=None): # type: (str, Dict[str, Dict[str, List[List[Optional[str]]]]], str, List[text_type], int) -> None with OutputBuffer() as obuf: for algorithm in algorithms: - output_algorithm(alg_db, alg_type, algorithm, maxlen) + output_algorithm(alg_db, alg_type, algorithm, maxlen, alg_sizes) if len(obuf) > 0: out.head('# ' + title) obuf.flush() out.sep() -def output_algorithm(alg_db, alg_type, alg_name, alg_max_len=0): +def output_algorithm(alg_db, alg_type, alg_name, alg_max_len=0, alg_sizes=None): # type: (Dict[str, Dict[str, List[List[Optional[str]]]]], str, text_type, int) -> None prefix = '(' + alg_type + ') ' if alg_max_len == 0: alg_max_len = len(alg_name) padding = '' if out.batch else ' ' * (alg_max_len - len(alg_name)) + + # If this is an RSA host key or DH GEX, append the size to its name and fix + # the padding. + alg_name_with_size = None + 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]) + padding = padding[0:-11] + texts = [] if len(alg_name.strip()) == 0: return @@ -1854,6 +2327,8 @@ def output_algorithm(alg_db, alg_type, alg_name, alg_max_len=0): texts.append(('info', '')) else: texts.append(('warn', 'unknown algorithm')) + + alg_name = alg_name_with_size if alg_name_with_size is not None else alg_name first = True for level, text in texts: f = getattr(out, level) @@ -2034,9 +2509,9 @@ def output(banner, header, kex=None, pkm=None): if kex is not None: adb = SSH2.KexDB.ALGORITHMS title, atype = 'key exchange algorithms', 'kex' - output_algorithms(title, adb, atype, kex.kex_algorithms, maxlen) + output_algorithms(title, adb, atype, kex.kex_algorithms, maxlen, kex.dh_modulus_sizes()) title, atype = 'host-key algorithms', 'key' - output_algorithms(title, adb, atype, kex.key_algorithms, maxlen) + output_algorithms(title, adb, atype, kex.key_algorithms, maxlen, kex.rsa_hostkey_sizes()) title, atype = 'encryption algorithms (ciphers)', 'enc' output_algorithms(title, adb, atype, kex.server.encryption, maxlen) title, atype = 'message authentication code algorithms', 'mac' @@ -2211,6 +2686,8 @@ def audit(aconf, sshv=None): output(banner, header, pkm=pkm) elif sshv == 2: kex = SSH2.Kex.parse(payload) + SSH2.RSAKeyTest.run(s, kex) + SSH2.GEXTest.run(aconf.ipvo, aconf.host, aconf.port, s, kex) output(banner, header, kex=kex) From 33ae2946ea13705e215533944c32a6b924ef336f Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Fri, 22 Sep 2017 15:01:51 -0400 Subject: [PATCH 047/103] Syntax fix for Python2. --- ssh-audit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ssh-audit.py b/ssh-audit.py index d26289e..a750269 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -2265,8 +2265,8 @@ class KexGroupExchange(KexDH): # Now that we got the generator and modulus, perform the DH exchange # like usual. - super().set_params(g, p) - super().send_init(s, SSH.Protocol.MSG_KEXDH_GEX_INIT) + super(KexGroupExchange, self).set_params(g, p) + super(KexGroupExchange, self).send_init(s, SSH.Protocol.MSG_KEXDH_GEX_INIT) class KexGroupExchange_SHA1(KexGroupExchange): From ee5dde1cde30ed7dd94b8199cc19a71b5bd8b7ec Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Tue, 26 Sep 2017 20:46:00 -0400 Subject: [PATCH 048/103] Added RSA certificate auditing. --- ssh-audit.py | 357 +++++++++++++++++++++++++++++++++++---------------- 1 file changed, 245 insertions(+), 112 deletions(-) diff --git a/ssh-audit.py b/ssh-audit.py index a750269..ee12fb6 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -462,14 +462,14 @@ class SSH2(object): # pylint: disable=too-few-public-methods # type: () -> int return self.__unused - def set_rsa_hostkey_size(self, rsa_type, rsa_hostkey_size): - self.__rsa_key_sizes[rsa_type] = rsa_hostkey_size; + def set_rsa_key_size(self, rsa_type, hostkey_size, ca_size=-1): + 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 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): 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. class RSAKeyTest(object): RSA_TYPES = ['ssh-rsa', 'rsa-sha2-256', 'rsa-sha2-512'] + RSA_CA_TYPES = ['ssh-rsa-cert-v01@openssh.com'] @staticmethod - def run(s, kex): + def run(s, server_kex): KEX_TO_DHGROUP = { 'diffie-hellman-group1-sha1': KexGroup1, '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 # happen to support as well. - selected_kex_str = None + kex_str = 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: - selected_kex_str = server_kex_alg - kex_group = KEX_TO_DHGROUP[server_kex_alg]() + kex_str = server_kex_alg + kex_group = KEX_TO_DHGROUP[kex_str]() 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. - modulus_size = 0 - if selected_kex_str is not None: - for rsa_type in SSH2.RSAKeyTest.RSA_TYPES: - if rsa_type in kex.key_algorithms: + hostkey_modulus_size = 0 + ca_modulus_size = 0 + ran_test = False - # 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), [selected_kex_str], [rsa_type], kex.client, kex.server, 0, 0) + # If the connection is closed, re-open it and get the kex again. + if not s.is_connected(): + s.connect() + unused1, unused2, err = s.get_banner() + if err is not None: + s.close() + return - s.write_byte(SSH.Protocol.MSG_KEXINIT) - client_kex.write(s) - s.send_packet() + # Parse the server's initial KEX. + packet_type, payload = s.read_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 - # will all be the same. + for rsa_type in rsa_types: + 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 - 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', # 'rsa-sha2-256', etc. are all using the same host key. - for rsa_type in SSH2.RSAKeyTest.RSA_TYPES: - kex.set_rsa_hostkey_size(rsa_type, modulus_size) + # Note, however, that this may change in the future. + 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. fail = False - if modulus_size < 2048: + if hostkey_modulus_size < 2048 or (ca_modulus_size < 2048 and ca_modulus_size > 0): fail = True # If this is a bad key size, update the database accordingly. 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.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 # 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 # None on failure. @staticmethod - def reconnect(ipvo, host, port, gex_alg): - s = SSH.Socket(host, port) - s.connect(ipvo) + def reconnect(s, gex_alg): + if s.is_connected(): + return + + s.connect() unused, unused, err = s.get_banner() if err is not None: s.close() - return None + return False # Parse the server's initial KEX. 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) client_kex.write(s) s.send_packet() - return s + return True # Runs the DH moduli test against the specified target. @staticmethod - def run(ipvo, host, port, s, kex): + def run(s, kex): GEX_ALGS = { 'diffie-hellman-group-exchange-sha1': KexGroupExchange_SHA1, '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 # algorithms. If so, test each one. for gex_alg in GEX_ALGS: if gex_alg in kex.kex_algorithms: - # 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 not None: - s.close() - s = None - - s = SSH2.GEXTest.reconnect(ipvo, host, port, gex_alg) - if s is None: + if SSH2.GEXTest.reconnect(s, gex_alg) is False: break + kex_group = GEX_ALGS[gex_alg]() 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 # larger than the requested max. So just because we # 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: pass finally: s.close() - s = None # Try an array of specific modulus sizes... one at a time. reconnect_failed = False @@ -673,21 +714,22 @@ class SSH2(object): # pylint: disable=too-few-public-methods if smallest_modulus > 0 and bits >= smallest_modulus: break - if s is None: - s = SSH2.GEXTest.reconnect(ipvo, host, port, gex_alg) - if s is None: - reconnect_failed = True - break + if SSH2.GEXTest.reconnect(s, gex_alg) is False: + reconnect_failed = True + break try: kex_group.send_init(s, bits, bits, bits) kex_group.recv_reply(s) - smallest_modulus = kex_group.get_modulus_size() + smallest_modulus = kex_group.get_dh_modulus_size() except Exception as e: pass 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 = None + if smallest_modulus > 0: 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 # a failure reason. if len(lst) == 1: - lst.append(text) + lst.append([text]) # For 'diffie-hellman-group-exchange-sha1', delete # the existing failure reason (which is vague), and # insert our own. @@ -975,6 +1017,10 @@ class ReadBuf(object): # type: () -> text_type 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): def __init__(self, data=None): @@ -1064,6 +1110,9 @@ class WriteBuf(object): self._wbuf.seek(0) return payload + def reset(self): + self._wbuf = BytesIO() + class SSH(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(): if alg_type == 'aut': continue - rec[sshv][alg_type] = {'add': {}, 'del': {}} + rec[sshv][alg_type] = {'add': {}, 'del': {}, 'chg': {}} for n, alg_desc in alg_db[alg_type].items(): - if alg_type == 'key' and '-cert-' in n: - continue versions = alg_desc[0] if len(versions) == 0 or versions[0] is None: continue @@ -1597,18 +1644,19 @@ class SSH(object): # pylint: disable=too-few-public-methods if fc > 0: faults += pow(10, 2 - i) * fc if n not in alg_list: - if faults > 0: + if faults > 0 or (alg_type == 'key' and '-cert-' in n): continue rec[sshv][alg_type]['add'][n] = 0 else: if faults == 0: continue - if n == 'diffie-hellman-group-exchange-sha256': - if software.compare_version('7.3') < 0: - continue - rec[sshv][alg_type]['del'][n] = faults + if n in ['diffie-hellman-group-exchange-sha256', 'ssh-rsa', 'rsa-sha2-256', 'rsa-sha2-512', 'ssh-rsa-cert-v01@openssh.com']: + rec[sshv][alg_type]['chg'][n] = faults + else: + rec[sshv][alg_type]['del'][n] = faults add_count = len(rec[sshv][alg_type]['add']) 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 if new_alg_count < 1 and del_count > 0: 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'] if del_count == 0: del rec[sshv][alg_type]['del'] + if chg_count == 0: + del rec[sshv][alg_type]['chg'] if len(rec[sshv][alg_type]) == 0: del rec[sshv][alg_type] 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)) self.__host = host self.__port = nport + self.__ipvo = () def _resolve(self, ipvo): # 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)) 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 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 try: 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 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): 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): # type: (Optional[socket.socket]) -> None @@ -1975,6 +2039,7 @@ class SSH(object): # pylint: disable=too-few-public-methods def __cleanup(self): # type: () -> None self._close_socket(self.__sock) + self.__sock = None class KexDH(object): # pragma: nocover @@ -1988,7 +2053,8 @@ class KexDH(object): # pragma: nocover self.__hostkey_type = None self.__hostkey_e = 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.__h_sig = 0 @@ -2017,47 +2083,97 @@ class KexDH(object): # pragma: nocover # 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)) - host_key_len = struct.unpack('>I', payload[0:4])[0] - ptr = 4 - - hostkey = payload[ptr:ptr + host_key_len] - ptr += host_key_len - - 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 + # Get the host key blob, F, and signature. + ptr = 0 + hostkey, hostkey_len, ptr = KexDH.__get_bytes(payload, ptr) + self.__f, f_len, ptr = KexDH.__get_bytes(payload, ptr) + self.__h_sig, h_sig_len, ptr = KexDH.__get_bytes(payload, ptr) # 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). - self.__hostkey_type = hostkey[ptr:ptr + hostkey_type_len] - ptr += hostkey_type_len + ptr = 0 + self.__hostkey_type, hostkey_type_len, ptr = KexDH.__get_bytes(hostkey, ptr) - hostkey_e_len = struct.unpack('>I', hostkey[ptr:ptr + 4])[0] - ptr += 4 + # If this is an RSA certificate, skip over the nonce. + 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) - ptr += hostkey_e_len + # The public key exponent. + 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. - 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 - self.__hostkey_n = int(binascii.hexlify(hostkey[ptr:ptr + self.__hostkey_n_len]), 16) - - # Returns the size of the hostkey, in bits. - def get_hostkey_size(self): - size = self.__hostkey_n_len * 8 + return buf[ptr:ptr + num_bytes], num_bytes, ptr + num_bytes + # 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 # (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 @@ -2066,8 +2182,16 @@ class KexDH(object): # pragma: nocover size = size - 8 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. - def get_modulus_size(self): + def get_dh_modulus_size(self): # -2 to account for the '0b' prefix in the string. 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. alg_name_with_size = None 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]) - padding = padding[0:-11] + hostkey_size, ca_size = alg_sizes[alg_name] + 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 = [] 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']: if alg_type not in alg_rec[sshv]: continue - for action in ['del', 'add']: + for action in ['del', 'add', 'chg']: if action not in alg_rec[sshv][alg_type]: continue for name in alg_rec[sshv][alg_type][action]: p = '' if out.batch else ' ' * (padlen - len(name)) + chg_additional_info = '' if action == 'del': an, sg, fn = 'remove', '-', out.warn if alg_rec[sshv][alg_type][action][name] >= 10: fn = out.fail - else: + elif action == 'add': 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 '' - fm = '(rec) {0}{1}{2}-- {3} algorithm to {4} {5}' - fn(fm.format(sg, name, p, alg_type, an, b)) + fm = '(rec) {0}{1}{2}-- {3} algorithm to {4}{5} {6}' + fn(fm.format(sg, name, p, alg_type, an, chg_additional_info, b)) if len(obuf) > 0: if software is not None: 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' output_algorithms(title, adb, atype, kex.kex_algorithms, maxlen, kex.dh_modulus_sizes()) 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' output_algorithms(title, adb, atype, kex.server.encryption, maxlen) title, atype = 'message authentication code algorithms', 'mac' @@ -2687,7 +2820,7 @@ def audit(aconf, sshv=None): elif sshv == 2: kex = SSH2.Kex.parse(payload) 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) From b2775c9cf94950f4a14f5b35cda7eb5bc7eccb04 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Tue, 26 Sep 2017 20:51:10 -0400 Subject: [PATCH 049/103] Python3 fixes. --- ssh-audit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ssh-audit.py b/ssh-audit.py index ee12fb6..71fb48c 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -2095,7 +2095,7 @@ class KexDH(object): # pragma: nocover self.__hostkey_type, hostkey_type_len, ptr = KexDH.__get_bytes(hostkey, ptr) # If this is an RSA certificate, skip over the nonce. - if self.__hostkey_type.startswith('ssh-rsa-cert-v0'): + if self.__hostkey_type.startswith(b'ssh-rsa-cert-v0'): nonce, nonce_len, ptr = KexDH.__get_bytes(hostkey, ptr) # The public key exponent. @@ -2108,7 +2108,7 @@ class KexDH(object): # pragma: nocover # If this is an RSA certificate, continue parsing to extract the CA # key. - if self.__hostkey_type.startswith('ssh-rsa-cert-v0'): + if self.__hostkey_type.startswith(b'ssh-rsa-cert-v0'): # Skip over the serial number. ptr += 8 From 4f6e23e5681fa6ffe140fb1837e66f94df6b331d Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Wed, 27 Sep 2017 21:27:08 -0400 Subject: [PATCH 050/103] Fixed send_init() inheritance problems. Now kex failures will try to continue on instead of terminating the program. --- ssh-audit.py | 41 +++++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/ssh-audit.py b/ssh-audit.py index 71fb48c..67b7950 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -693,14 +693,14 @@ class SSH2(object): # pylint: disable=too-few-public-methods # First try a range of weak sizes. try: - kex_group.send_init(s, 512, 1024, 1536) + kex_group.send_init_gex(s, 512, 1024, 1536) kex_group.recv_reply(s) # Its been observed that servers will return a group # larger than the requested max. So just because we # got here, doesn't mean the server is vulnerable... smallest_modulus = kex_group.get_dh_modulus_size() - except Exception as e: + except Exception as e: # pylint: disable=bare-except pass finally: s.close() @@ -719,10 +719,10 @@ class SSH2(object): # pylint: disable=too-few-public-methods break try: - kex_group.send_init(s, bits, bits, bits) + kex_group.send_init_gex(s, bits, bits, bits) kex_group.recv_reply(s) smallest_modulus = kex_group.get_dh_modulus_size() - except Exception as e: + except Exception as e: # pylint: disable=bare-except pass finally: # The server is in a state that is not re-testable, @@ -2047,6 +2047,11 @@ class KexDH(object): # pragma: nocover # type: (str, int, int) -> None self.__kex_name = kex_name self.__hash_alg = hash_alg + self.__g = 0 + self.__p = 0 + self.__q = 0 + self.__x = 0 + self.__e = 0 self.set_params(g, p) self.__ed25519_pubkey = 0 @@ -2079,9 +2084,14 @@ class KexDH(object): # pragma: nocover # Contains the host key, among other things. def recv_reply(self, s): packet_type, payload = s.read_packet(2) - if packet_type not in [SSH.Protocol.MSG_KEXDH_REPLY, SSH.Protocol.MSG_KEXDH_GEX_REPLY]: + if packet_type != -1 and packet_type not in [SSH.Protocol.MSG_KEXDH_REPLY, SSH.Protocol.MSG_KEXDH_GEX_REPLY]: # 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)) + elif packet_type == -1: + # A connection error occurred. We can't parse anything, so just + # return. The host key modulus (and perhaps certificate modulus) + # will remain at length 0. + return # Get the host key blob, F, and signature. ptr = 0 @@ -2305,9 +2315,9 @@ class KexCurve25519_SHA256(KexDH): # To start an ED25519 kex, we simply send a random 256-bit number as the # public key. - def send_init(self, s): + def send_init(self, s, init_msg=SSH.Protocol.MSG_KEXDH_INIT): self.__ed25519_pubkey = os.urandom(32) - s.write_byte(SSH.Protocol.MSG_KEXDH_INIT) + s.write_byte(init_msg) s.write_string(self.__ed25519_pubkey) s.send_packet() @@ -2321,8 +2331,8 @@ class KexNISTP256(KexDH): # or import an elliptic curve library in order to randomly generate a # valid elliptic point each time. Hence, we will simply send a static # value, which is enough for us to extract the server's host key. - def send_init(self, s): - s.write_byte(SSH.Protocol.MSG_KEXDH_INIT) + def send_init(self, s, init_msg=SSH.Protocol.MSG_KEXDH_INIT): + s.write_byte(init_msg) s.write_string(b'\x04\x0b\x60\x44\x9f\x8a\x11\x9e\xc7\x81\x0c\xa9\x98\xfc\xb7\x90\xaa\x6b\x26\x8c\x12\x4a\xc0\x09\xbb\xdf\xc4\x2c\x4c\x2c\x99\xb6\xe1\x71\xa0\xd4\xb3\x62\x47\x74\xb3\x39\x0c\xf2\x88\x4a\x84\x6b\x3b\x15\x77\xa5\x77\xd2\xa9\xc9\x94\xf9\xd5\x66\x19\xcd\x02\x34\xd1') s.send_packet() @@ -2332,8 +2342,8 @@ class KexNISTP384(KexDH): super(KexNISTP384, self).__init__('KexNISTP384', 'sha256', 0, 0) # See comment for KexNISTP256.send_init(). - def send_init(self, s): - s.write_byte(SSH.Protocol.MSG_KEXDH_INIT) + def send_init(self, s, init_msg=SSH.Protocol.MSG_KEXDH_INIT): + s.write_byte(init_msg) s.write_string(b'\x04\xe2\x9b\x84\xce\xa1\x39\x50\xfe\x1e\xa3\x18\x70\x1c\xe2\x7a\xe4\xb5\x6f\xdf\x93\x9f\xd4\xf4\x08\xcc\x9b\x02\x10\xa4\xca\x77\x9c\x2e\x51\x44\x1d\x50\x7a\x65\x4e\x7e\x2f\x10\x2d\x2d\x4a\x32\xc9\x8e\x18\x75\x90\x6c\x19\x10\xda\xcc\xa8\xe9\xf4\xc4\x3a\x53\x80\x35\xf4\x97\x9c\x04\x16\xf9\x5a\xdc\xcc\x05\x94\x29\xfa\xc4\xd6\x87\x4e\x13\x21\xdb\x3d\x12\xac\xbd\x20\x3b\x60\xff\xe6\x58\x42') s.send_packet() @@ -2343,8 +2353,8 @@ class KexNISTP521(KexDH): super(KexNISTP521, self).__init__('KexNISTP521', 'sha256', 0, 0) # See comment for KexNISTP256.send_init(). - def send_init(self, s): - s.write_byte(SSH.Protocol.MSG_KEXDH_INIT) + def send_init(self, s, init_msg=SSH.Protocol.MSG_KEXDH_INIT): + s.write_byte(init_msg) s.write_string(b'\x04\x01\x02\x90\x29\xe9\x8f\xa8\x04\xaf\x1c\x00\xf9\xc6\x29\xc0\x39\x74\x8e\xea\x47\x7e\x7c\xf7\x15\x6e\x43\x3b\x59\x13\x53\x43\xb0\xae\x0b\xe7\xe6\x7c\x55\x73\x52\xa5\x2a\xc1\x42\xde\xfc\xf4\x1f\x8b\x5a\x8d\xfa\xcd\x0a\x65\x77\xa8\xce\x68\xd2\xc6\x26\xb5\x3f\xee\x4b\x01\x7b\xd2\x96\x23\x69\x53\xc7\x01\xe1\x0d\x39\xe9\x87\x49\x3b\xc8\xec\xda\x0c\xf9\xca\xad\x89\x42\x36\x6f\x93\x78\x78\x31\x55\x51\x09\x51\xc0\x96\xd7\xea\x61\xbf\xc2\x44\x08\x80\x43\xed\xc6\xbb\xfb\x94\xbd\xf8\xdf\x2b\xd8\x0b\x2e\x29\x1b\x8c\xc4\x8a\x04\x2d\x3a') s.send_packet() @@ -2353,12 +2363,15 @@ class KexGroupExchange(KexDH): def __init__(self, classname, hash_alg): super(KexGroupExchange, self).__init__(classname, hash_alg, 0, 0) + def send_init(self, s, init_msg=SSH.Protocol.MSG_KEXDH_GEX_REQUEST): + self.send_init_gex(s) + # The group exchange starts with sending a message to the server with # the minimum, maximum, and preferred number of bits are for the DH group. # The server responds with a generator and prime modulus that matches that, # then the handshake continues on like a normal DH handshake (except the # SSH message types differ). - def send_init(self, s, minbits=1024, prefbits=2048, maxbits=8192): + def send_init_gex(self, s, minbits=1024, prefbits=2048, maxbits=8192): # Send the initial group exchange request. Tell the server what range # of modulus sizes we will accept, along with our preference. From a3c6d16500d37cffccd80e6ac9b4d523e601dbe7 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Wed, 27 Sep 2017 22:14:48 -0400 Subject: [PATCH 051/103] Suppressing pylint warnings on unused variables. --- ssh-audit.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/ssh-audit.py b/ssh-audit.py index 67b7950..59b8cc1 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -2093,6 +2093,19 @@ class KexDH(object): # pragma: nocover # will remain at length 0. return + hostkey_len = f_len = h_sig_len = 0 # pylint: disable=unused-variable + hostkey_type_len = hostkey_e_len = 0 # pylint: disable=unused-variable + key_id_len = principles_len = 0 # pylint: disable=unused-variable + critical_options_len = extensions_len = 0 # pylint: disable=unused-variable + nonce_len = ca_key_len = ca_key_type_len = 0 # pylint: disable=unused-variable + ca_key_len = ca_key_type_len = ca_key_e_len = 0 # pylint: disable=unused-variable + + key_id = principles = None # pylint: disable=unused-variable + critical_options = extensions = None # pylint: disable=unused-variable + valid_after = valid_before = None # pylint: disable=unused-variable + nonce = ca_key = ca_key_type = None # pylint: disable=unused-variable + ca_key_e = ca_key_n = None # pylint: disable=unused-variable + # Get the host key blob, F, and signature. ptr = 0 hostkey, hostkey_len, ptr = KexDH.__get_bytes(payload, ptr) @@ -2134,7 +2147,7 @@ class KexDH(object): # pragma: nocover 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) + principles, prinicples_len, ptr = KexDH.__get_bytes(hostkey, ptr) # The timestamp that this certificate is valid after. valid_after = hostkey[ptr:ptr + 8] From b7bf8ab38a9b768868f44bddbccc6d96dab21faa Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Wed, 27 Sep 2017 22:22:42 -0400 Subject: [PATCH 052/103] Suppressed more unused variables warnings. --- ssh-audit.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/ssh-audit.py b/ssh-audit.py index 59b8cc1..5fe4b7a 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -566,7 +566,8 @@ class SSH2(object): # pylint: disable=too-few-public-methods # If the connection is closed, re-open it and get the kex again. if not s.is_connected(): s.connect() - unused1, unused2, err = s.get_banner() + unused = None # pylint: disable=unused-variable + unused, unused, err = s.get_banner() if err is not None: s.close() return @@ -648,12 +649,14 @@ class SSH2(object): # pylint: disable=too-few-public-methods return s.connect() + unused = None # pylint: disable=unused-variable unused, unused, err = s.get_banner() if err is not None: s.close() return False # Parse the server's initial KEX. + packet_type = 0 # pylint: disable=unused-variable packet_type, payload = s.read_packet(2) kex = SSH2.Kex.parse(payload) @@ -700,7 +703,7 @@ class SSH2(object): # pylint: disable=too-few-public-methods # larger than the requested max. So just because we # got here, doesn't mean the server is vulnerable... smallest_modulus = kex_group.get_dh_modulus_size() - except Exception as e: # pylint: disable=bare-except + except Exception: # pylint: disable=bare-except pass finally: s.close() @@ -722,7 +725,7 @@ class SSH2(object): # pylint: disable=too-few-public-methods kex_group.send_init_gex(s, bits, bits, bits) kex_group.recv_reply(s) smallest_modulus = kex_group.get_dh_modulus_size() - except Exception as e: # pylint: disable=bare-except + except Exception: # pylint: disable=bare-except pass finally: # The server is in a state that is not re-testable, @@ -2147,7 +2150,7 @@ class KexDH(object): # pragma: nocover key_id, key_id_len, ptr = KexDH.__get_bytes(hostkey, ptr) # The principles, which are... I don't know what. - principles, prinicples_len, ptr = KexDH.__get_bytes(hostkey, ptr) + principles, princicples_len, ptr = KexDH.__get_bytes(hostkey, ptr) # The timestamp that this certificate is valid after. valid_after = hostkey[ptr:ptr + 8] From cd80917c625d658745dca9f385ce1081c24486b5 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Wed, 27 Sep 2017 22:36:23 -0400 Subject: [PATCH 053/103] Fixed more warnings. --- ssh-audit.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ssh-audit.py b/ssh-audit.py index 5fe4b7a..a57858d 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -573,6 +573,7 @@ class SSH2(object): # pylint: disable=too-few-public-methods return # Parse the server's initial KEX. + packet_type = 0 # pylint: disable=unused-variable packet_type, payload = s.read_packet() SSH2.Kex.parse(payload) @@ -704,6 +705,7 @@ class SSH2(object): # pylint: disable=too-few-public-methods # got here, doesn't mean the server is vulnerable... smallest_modulus = kex_group.get_dh_modulus_size() except Exception: # pylint: disable=bare-except + x = 1 # pylint: disable=unused-variable pass finally: s.close() @@ -726,6 +728,7 @@ class SSH2(object): # pylint: disable=too-few-public-methods kex_group.recv_reply(s) smallest_modulus = kex_group.get_dh_modulus_size() except Exception: # pylint: disable=bare-except + x = 1 # pylint: disable=unused-variable pass finally: # The server is in a state that is not re-testable, @@ -2150,7 +2153,7 @@ class KexDH(object): # pragma: nocover key_id, key_id_len, ptr = KexDH.__get_bytes(hostkey, ptr) # The principles, which are... I don't know what. - principles, princicples_len, ptr = KexDH.__get_bytes(hostkey, ptr) + principles, principles_len, ptr = KexDH.__get_bytes(hostkey, ptr) # The timestamp that this certificate is valid after. valid_after = hostkey[ptr:ptr + 8] From c1d0540d1e1b34336bdbca3acd251ab594c88b9d Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Wed, 27 Sep 2017 22:42:49 -0400 Subject: [PATCH 054/103] Fixed one more warning. --- ssh-audit.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/ssh-audit.py b/ssh-audit.py index a57858d..aedabc8 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -706,7 +706,6 @@ class SSH2(object): # pylint: disable=too-few-public-methods smallest_modulus = kex_group.get_dh_modulus_size() except Exception: # pylint: disable=bare-except x = 1 # pylint: disable=unused-variable - pass finally: s.close() @@ -729,7 +728,6 @@ class SSH2(object): # pylint: disable=too-few-public-methods smallest_modulus = kex_group.get_dh_modulus_size() except Exception: # pylint: disable=bare-except x = 1 # pylint: disable=unused-variable - pass finally: # The server is in a state that is not re-testable, # so there's nothing else to do with this open From 1bb5490e01566095b81f7bea1bdf9597d6393b2e Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Wed, 11 Oct 2017 15:13:58 -0400 Subject: [PATCH 055/103] Added new algorithms (some as per RFC4344). --- ssh-audit.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/ssh-audit.py b/ssh-audit.py index 461b953..79a0a1b 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -284,6 +284,7 @@ class SSH2(object): # pylint: disable=too-few-public-methods FAIL_OPENSSH31_REMOVE = 'removed since OpenSSH 3.1' FAIL_DBEAR67_DISABLED = 'disabled since Dropbear SSH 2015.67' FAIL_DBEAR53_DISABLED = 'disabled since Dropbear SSH 0.53' + FAIL_DEPRECATED_CIPHER = 'deprecated cipher' FAIL_PLAINTEXT = 'no encryption/integrity' WARN_CURVES_WEAK = 'using weak elliptic curves' WARN_RNDSIG_KEY = 'using weak random number generator could reveal the key' @@ -301,6 +302,7 @@ class SSH2(object): # pylint: disable=too-few-public-methods 'diffie-hellman-group1-sha1': [['2.3.0,d0.28,l10.2', '6.6', '6.9'], [FAIL_OPENSSH67_UNSAFE, FAIL_OPENSSH70_LOGJAM], [WARN_MODULUS_SIZE, WARN_HASH_WEAK]], 'diffie-hellman-group14-sha1': [['3.9,d0.53,l10.6.0'], [], [WARN_HASH_WEAK]], 'diffie-hellman-group14-sha256': [['7.3,d2016.73']], + 'diffie-hellman-group15-sha512': [[]], 'diffie-hellman-group16-sha512': [['7.3,d2016.73']], 'diffie-hellman-group18-sha512': [['7.3']], 'diffie-hellman-group-exchange-sha1': [['2.3.0', '6.6', None], [FAIL_OPENSSH67_UNSAFE], [WARN_HASH_WEAK]], @@ -329,6 +331,7 @@ class SSH2(object): # pylint: disable=too-few-public-methods 'ecdsa-sha2-nistp256-cert-v01@openssh.com': [['5.7'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]], 'ecdsa-sha2-nistp384-cert-v01@openssh.com': [['5.7'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]], 'ecdsa-sha2-nistp521-cert-v01@openssh.com': [['5.7'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]], + 'ssh-rsa-sha256@ssh.com': [[]], }, 'enc': { 'none': [['1.2.2,d2013.56,l10.2'], [FAIL_PLAINTEXT]], @@ -337,9 +340,16 @@ class SSH2(object): # pylint: disable=too-few-public-methods 'blowfish-cbc': [['1.2.2,d0.28,l10.2', '6.6,d0.52', '7.1,d0.52'], [FAIL_OPENSSH67_UNSAFE, FAIL_DBEAR53_DISABLED], [WARN_OPENSSH72_LEGACY, WARN_CIPHER_MODE, WARN_BLOCK_SIZE]], 'twofish-cbc': [['d0.28', 'd2014.66'], [FAIL_DBEAR67_DISABLED], [WARN_CIPHER_MODE]], 'twofish128-cbc': [['d0.47', 'd2014.66'], [FAIL_DBEAR67_DISABLED], [WARN_CIPHER_MODE]], + 'twofish192-cbc': [[], [], [WARN_CIPHER_MODE]], 'twofish256-cbc': [['d0.47', 'd2014.66'], [FAIL_DBEAR67_DISABLED], [WARN_CIPHER_MODE]], 'twofish128-ctr': [['d2015.68']], + 'twofish192-ctr': [[]], 'twofish256-ctr': [['d2015.68']], + 'serpent128-ctr': [[], [FAIL_DEPRECATED_CIPHER]], + 'serpent192-ctr': [[], [FAIL_DEPRECATED_CIPHER]], + 'serpent256-ctr': [[], [FAIL_DEPRECATED_CIPHER]], + 'idea-ctr': [[], [FAIL_DEPRECATED_CIPHER]], + 'cast128-ctr': [[], [FAIL_DEPRECATED_CIPHER]], 'cast128-cbc': [['2.1.0', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_CIPHER_MODE, WARN_BLOCK_SIZE]], 'arcfour': [['2.1.0', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_CIPHER_WEAK]], 'arcfour128': [['4.2', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_CIPHER_WEAK]], @@ -1857,17 +1867,18 @@ def output_algorithm(alg_db, alg_type, alg_name, alg_max_len=0): first = True for level, text in texts: f = getattr(out, level) - text = '[' + level + '] ' + text + comment = (padding + ' -- [' + level + '] ' + text) if text != '' else '' if first: if first and level == 'info': f = out.good - f(prefix + alg_name + padding + ' -- ' + text) + f(prefix + alg_name + comment) first = False else: # pylint: disable=else-if-used if out.verbose: - f(prefix + alg_name + padding + ' -- ' + text) - else: - f(' ' * len(prefix + alg_name) + padding + ' `- ' + text) + f(prefix + alg_name + comment) + elif text != '': + comment = (padding + ' `- [' + level + '] ' + text) + f(' ' * len(prefix + alg_name) + comment) def output_compatibility(algs, for_server=True): From a3f126a1dd0755d127b19489b3858d358d778644 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Wed, 11 Oct 2017 15:47:01 -0400 Subject: [PATCH 056/103] Added missing algorithms from RFC4250 and RFC4432. --- ssh-audit.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ssh-audit.py b/ssh-audit.py index 79a0a1b..475ef6f 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -285,6 +285,7 @@ class SSH2(object): # pylint: disable=too-few-public-methods FAIL_DBEAR67_DISABLED = 'disabled since Dropbear SSH 2015.67' FAIL_DBEAR53_DISABLED = 'disabled since Dropbear SSH 0.53' FAIL_DEPRECATED_CIPHER = 'deprecated cipher' + FAIL_WEAK_CIPHER = 'using weak cipher' FAIL_PLAINTEXT = 'no encryption/integrity' WARN_CURVES_WEAK = 'using weak elliptic curves' WARN_RNDSIG_KEY = 'using weak random number generator could reveal the key' @@ -313,6 +314,8 @@ class SSH2(object): # pylint: disable=too-few-public-methods 'curve25519-sha256@libssh.org': [['6.5,d2013.62,l10.6.0']], 'curve25519-sha256': [['7.4']], 'kexguess2@matt.ucc.asn.au': [['d2013.57']], + 'rsa1024-sha1': [[], [], [WARN_MODULUS_SIZE, WARN_HASH_WEAK]], + 'rsa2048-sha256': [[]], }, 'key': { 'rsa-sha2-256': [['7.2']], @@ -335,6 +338,7 @@ class SSH2(object): # pylint: disable=too-few-public-methods }, 'enc': { 'none': [['1.2.2,d2013.56,l10.2'], [FAIL_PLAINTEXT]], + 'des-cbc': [[], [FAIL_WEAK_CIPHER], [WARN_CIPHER_MODE, WARN_BLOCK_SIZE]], '3des-cbc': [['1.2.2,d0.28,l10.2', '6.6', None], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH74_UNSAFE, WARN_CIPHER_WEAK, WARN_CIPHER_MODE, WARN_BLOCK_SIZE]], '3des-ctr': [['d0.52']], 'blowfish-cbc': [['1.2.2,d0.28,l10.2', '6.6,d0.52', '7.1,d0.52'], [FAIL_OPENSSH67_UNSAFE, FAIL_DBEAR53_DISABLED], [WARN_OPENSSH72_LEGACY, WARN_CIPHER_MODE, WARN_BLOCK_SIZE]], From 04973df2afdfad2d49d6564c5a889e16b8606b36 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Sun, 29 Oct 2017 17:48:04 -0400 Subject: [PATCH 057/103] Added command-line option to modify connection/read timeout. --- ssh-audit.py | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/ssh-audit.py b/ssh-audit.py index 461b953..3609da6 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -68,6 +68,7 @@ def usage(err=None): uout.info(' -n, --no-colors disable colors') uout.info(' -v, --verbose verbose output') uout.info(' -l, --level= minimum output level (info|warn|fail)') + uout.info(' -t, --timeout= timeout (in seconds) for connection and reading\n (default: 5)') uout.sep() sys.exit(1) @@ -87,7 +88,8 @@ class AuditConf(object): self.ipvo = () # type: Sequence[int] self.ipv4 = False self.ipv6 = False - + self.timeout = 5.0 + def __setattr__(self, name, value): # type: (str, Union[str, int, bool, Sequence[int]]) -> None valid = False @@ -124,6 +126,11 @@ class AuditConf(object): valid = True elif name == 'host': valid = True + elif name == 'timeout': + value = utils.parse_float(value) + if value == -1.0: + raise ValueError('invalid timeout: {0}'.format(value)) + valid = True if valid: object.__setattr__(self, name, value) @@ -133,9 +140,9 @@ class AuditConf(object): # pylint: disable=too-many-branches aconf = cls() try: - sopts = 'h1246p:bnvl:' + sopts = 'h1246p:bnvl:t:' lopts = ['help', 'ssh1', 'ssh2', 'ipv4', 'ipv6', 'port', - 'batch', 'no-colors', 'verbose', 'level='] + 'batch', 'no-colors', 'verbose', 'level=', 'timeout='] opts, args = getopt.getopt(args, sopts, lopts) except getopt.GetoptError as err: usage_cb(str(err)) @@ -165,6 +172,8 @@ class AuditConf(object): if a not in ('info', 'warn', 'fail'): usage_cb('level {0} is not valid'.format(a)) aconf.level = a + elif o in ('-t', '--timeout'): + aconf.timeout = float(a) if len(args) == 0: usage_cb() if oport is not None: @@ -1586,16 +1595,15 @@ class SSH(object): # pylint: disable=too-few-public-methods out.fail('[exception] {0}'.format(e)) sys.exit(1) - def connect(self, ipvo=(), cto=3.0, rto=5.0): - # type: (Sequence[int], float, float) -> None + def connect(self, ipvo, timeout): + # type: (Sequence[int], float) -> None err = None for af, addr in self._resolve(ipvo): s = None try: s = socket.socket(af, socket.SOCK_STREAM) - s.settimeout(cto) + s.settimeout(timeout) s.connect(addr) - s.settimeout(rto) self.__sock = s return except socket.error as e: @@ -2159,6 +2167,14 @@ class Utils(object): except: # pylint: disable=bare-except return 0 + @staticmethod + def parse_float(v): + # type: (Any) -> float + try: + return float(v) + except: # pylint: disable=bare-except + return -1.0 + def audit(aconf, sshv=None): # type: (AuditConf, Optional[int]) -> None @@ -2167,7 +2183,7 @@ def audit(aconf, sshv=None): out.level = aconf.level out.use_colors = aconf.colors s = SSH.Socket(aconf.host, aconf.port) - s.connect(aconf.ipvo) + s.connect(aconf.ipvo, aconf.timeout) if sshv is None: sshv = 2 if aconf.ssh2 else 1 err = None From f44663bfc4a7c70f9a61a8304fd6f16d945403d8 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Tue, 31 Oct 2017 16:49:19 -0400 Subject: [PATCH 058/103] Fixed Socket.connect() method arguments. --- ssh-audit.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/ssh-audit.py b/ssh-audit.py index c6f71ae..1f915f4 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -1832,7 +1832,7 @@ class SSH(object): # pylint: disable=too-few-public-methods SM_BANNER_SENT = 1 - def __init__(self, host, port): + def __init__(self, host, port, ipvo, timeout): # type: (Optional[str], int) -> None super(SSH.Socket, self).__init__() self.__sock = None # type: Optional[socket.socket] @@ -1847,7 +1847,12 @@ class SSH(object): # pylint: disable=too-few-public-methods raise ValueError('invalid port: {0}'.format(port)) self.__host = host self.__port = nport - self.__ipvo = () + if ipvo is not None: + self.__ipvo = ipvo + else: + self.__ipvo = () + self.__timeout = timeout + def _resolve(self, ipvo): # type: (Sequence[int]) -> Iterable[Tuple[int, Tuple[Any, ...]]] @@ -1872,16 +1877,14 @@ class SSH(object): # pylint: disable=too-few-public-methods out.fail('[exception] {0}'.format(e)) sys.exit(1) - def connect(self, ipvo, timeout): - # type: (Sequence[int], float) -> None + def connect(self): + # type: () -> None err = None - if ipvo is not None: - self.__ipvo = ipvo for af, addr in self._resolve(self.__ipvo): s = None try: s = socket.socket(af, socket.SOCK_STREAM) - s.settimeout(timeout) + s.settimeout(self.__timeout) s.connect(addr) self.__sock = s return @@ -2835,8 +2838,8 @@ def audit(aconf, sshv=None): out.verbose = aconf.verbose out.level = aconf.level out.use_colors = aconf.colors - s = SSH.Socket(aconf.host, aconf.port) - s.connect(aconf.ipvo, aconf.timeout) + s = SSH.Socket(aconf.host, aconf.port, aconf.ipvo, aconf.timeout) + s.connect() if sshv is None: sshv = 2 if aconf.ssh2 else 1 err = None From a9933f92119464887844f9049bdb6d0318a7df38 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Fri, 16 Aug 2019 08:56:50 -0400 Subject: [PATCH 059/103] Added myself to copyright header in license. --- LICENSE | 2 ++ 1 file changed, 2 insertions(+) diff --git a/LICENSE b/LICENSE index a778a9a..4c9f264 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,8 @@ The MIT License (MIT) Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu) +Copyright (C) 2017-2019 Joe Testa (jtesta@positronsecurity.com) + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 41d396f5513848916ea8379e36545583d6a01b2c Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Sat, 17 Aug 2019 20:59:23 -0400 Subject: [PATCH 060/103] Updated version, copyright header, URL, and added Python 2 warning. --- ssh-audit.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ssh-audit.py b/ssh-audit.py index 1f915f4..3084f6e 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -3,6 +3,7 @@ """ The MIT License (MIT) + Copyright (C) 2017-2019 Joe Testa (jtesta@positronsecurity.com) Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu) Permission is hereby granted, free of charge, to any person obtaining a copy @@ -26,7 +27,10 @@ from __future__ import print_function import binascii, os, io, sys, socket, struct, random, errno, getopt, re, hashlib, base64 -VERSION = 'v1.7.1.dev' +VERSION = 'v2.0.0-dev' + +if sys.version_info.major < 3: + print("\n!!!! NOTE: Python 2 is being considered for deprecation. If you have a good reason to need continued Python 2 support, please e-mail jtesta@positronsecurity.com with your rationale.\n\n") if sys.version_info >= (3,): # pragma: nocover StringIO, BytesIO = io.StringIO, io.BytesIO @@ -54,7 +58,7 @@ def usage(err=None): # type: (Optional[str]) -> None uout = Output() p = os.path.basename(sys.argv[0]) - uout.head('# {0} {1}, moo@arthepsy.eu\n'.format(p, VERSION)) + uout.head('# {0} {1}, https://github.com/jtesta/ssh-audit\n'.format(p, VERSION)) if err is not None and len(err) > 0: uout.fail('\n' + err) uout.info('usage: {0} [-1246pbnvl] \n'.format(p)) From 7155efeb4a756151f52522db9fdfc5635d277981 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Sat, 17 Aug 2019 23:11:03 -0400 Subject: [PATCH 061/103] Added CVEs for Dropbear & libssh. Fixed libssh CVE parsing. Now prints CVEs in red when score is >= 8.0, otherwise they are printed in orange. --- ssh-audit.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/ssh-audit.py b/ssh-audit.py index 3084f6e..68b1017 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -1338,6 +1338,12 @@ class SSH(object): # pylint: disable=too-few-public-methods os_version = cls._extract_os_version(banner.comments) return cls(v, p, mx.group(1), patch, os_version) mx = re.match(r'^libssh-([\d\.]+\d+)(.*)', software) + if bool(mx): + patch = cls._fix_patch(mx.group(2)) + v, p = None, SSH.Product.LibSSH + os_version = cls._extract_os_version(banner.comments) + return cls(v, p, mx.group(1), patch, os_version) + mx = re.match(r'^libssh_([\d\.]+\d+)(.*)', software) if bool(mx): patch = cls._fix_patch(mx.group(2)) v, p = None, SSH.Product.LibSSH @@ -1739,9 +1745,21 @@ class SSH(object): # pylint: disable=too-few-public-methods return self.__storage.items() class Security(object): # pylint: disable=too-few-public-methods + # Format: [starting_vuln_version, last_vuln_version, affected, CVE_ID, CVSSv2, description] + # affected: 1 = server, 2 = client, 4 = local + # Example: if it affects servers, both remote & local, then affected + # = 1. If it affects servers, but is a local issue only, + # then affected = 1 + 4 = 5. # pylint: disable=bad-whitespace CVE = { 'Dropbear SSH': [ + ['0.0', '2018.76', 1, 'CVE-2018-15599', 5.0, 'remote users may enumerate users on the system'], + ['0.0', '2017.74', 5, 'CVE-2017-9079', 4.7, 'local users can read certain files as root'], + ['0.0', '2017.74', 5, 'CVE-2017-9078', 9.3, 'local users may elevate privileges to root under certain conditions'], + ['0.0', '2016.73', 5, 'CVE-2016-7409', 2.1, 'local users can read process memory under limited conditions'], + ['0.0', '2016.73', 1, 'CVE-2016-7408', 6.5, 'remote users can execute arbitrary code'], + ['0.0', '2016.73', 5, 'CVE-2016-7407', 10.0, 'local users can execute arbitrary code'], + ['0.0', '2016.73', 1, 'CVE-2016-7406', 10.0, 'remote users can execute arbitrary code'], ['0.44', '2015.71', 1, 'CVE-2016-3116', 5.5, 'bypass command restrictions via xauth command injection'], ['0.28', '2013.58', 1, 'CVE-2013-4434', 5.0, 'discover valid usernames through different time delays'], ['0.28', '2013.58', 1, 'CVE-2013-4421', 5.0, 'cause DoS via a compressed packet (memory consumption)'], @@ -1752,6 +1770,9 @@ class SSH(object): # pylint: disable=too-few-public-methods ['0.28', '0.46', 1, 'CVE-2005-4178', 6.5, 'execute arbitrary code via buffer overflow vulnerability'], ['0.28', '0.42', 1, 'CVE-2004-2486', 7.5, 'execute arbitrary code via DSS verification code']], 'libssh': [ + ['0.6.4', '0.6.4', 1, 'CVE-2018-10933', 6.4, 'authentication bypass'], + ['0.7.0', '0.7.5', 1, 'CVE-2018-10933', 6.4, 'authentication bypass'], + ['0.8.0', '0.8.3', 1, 'CVE-2018-10933', 6.4, 'authentication bypass'], ['0.1', '0.7.2', 1, 'CVE-2016-0739', 4.3, 'conduct a MitM attack (weakness in DH key generation)'], ['0.5.1', '0.6.4', 1, 'CVE-2015-3146', 5.0, 'cause DoS via kex packets (null pointer dereference)'], ['0.5.1', '0.6.3', 1, 'CVE-2014-8132', 5.0, 'cause DoS via kex init packet (dangling pointer)'], @@ -2578,7 +2599,12 @@ def output_security_sub(sub, software, padlen): p = '' if out.batch else ' ' * (padlen - len(name)) if sub == 'cve': cvss, descr = line[4:6] # type: float, str - out.fail('(cve) {0}{1} -- ({2}) {3}'.format(name, p, cvss, descr)) + + # Critical CVSS scores (>= 8.0) are printed as a fail, otherwise they are printed as a warning. + out_func = out.warn + if cvss >= 8.0: + out_func = out.fail + out_func('(cve) {0}{1} -- (CVSSv2: {2}) {3}'.format(name, p, cvss, descr)) else: descr = line[4] out.fail('(sec) {0}{1} -- {2}'.format(name, p, descr)) From 76a475093453d49528f18563e832ff4d1756f032 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Sun, 18 Aug 2019 00:09:40 -0400 Subject: [PATCH 062/103] Added support for kex sntrup4591761x25519-sha512@tinyssh.org, introduced in OpenSSH 8.0. --- ssh-audit.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ssh-audit.py b/ssh-audit.py index 68b1017..e9bdf95 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -328,6 +328,7 @@ class SSH2(object): # pylint: disable=too-few-public-methods 'kexguess2@matt.ucc.asn.au': [['d2013.57']], 'rsa1024-sha1': [[], [], [WARN_MODULUS_SIZE, WARN_HASH_WEAK]], 'rsa2048-sha256': [[]], + 'sntrup4591761x25519-sha512@tinyssh.org': [['8.0']] }, 'key': { 'rsa-sha2-256': [['7.2']], From f8fcd119e26f4a357588da53b2841ddb3af63afa Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Sun, 18 Aug 2019 00:16:42 -0400 Subject: [PATCH 063/103] Tagged sntrup4591761x25519-sha512@tinyssh.org as experimental, just as the OpenSSH 8.0 release notes say. --- ssh-audit.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ssh-audit.py b/ssh-audit.py index e9bdf95..faa4339 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -309,6 +309,7 @@ class SSH2(object): # pylint: disable=too-few-public-methods WARN_CIPHER_WEAK = 'using weak cipher' WARN_ENCRYPT_AND_MAC = 'using encrypt-and-MAC mode' WARN_TAG_SIZE = 'using small 64-bit tag size' + WARN_EXPERIMENTAL = 'using experimental algorithm' ALGORITHMS = { 'kex': { @@ -328,7 +329,7 @@ class SSH2(object): # pylint: disable=too-few-public-methods 'kexguess2@matt.ucc.asn.au': [['d2013.57']], 'rsa1024-sha1': [[], [], [WARN_MODULUS_SIZE, WARN_HASH_WEAK]], 'rsa2048-sha256': [[]], - 'sntrup4591761x25519-sha512@tinyssh.org': [['8.0']] + 'sntrup4591761x25519-sha512@tinyssh.org': [['8.0'], [], [WARN_EXPERIMENTAL]], }, 'key': { 'rsa-sha2-256': [['7.2']], From 8527d13343590550b65a34b5a82baa6a0735049b Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Sun, 18 Aug 2019 00:32:59 -0400 Subject: [PATCH 064/103] Added documentation on ALGORITHMS structure. --- ssh-audit.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ssh-audit.py b/ssh-audit.py index faa4339..81e21a6 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -312,6 +312,7 @@ class SSH2(object): # pylint: disable=too-few-public-methods WARN_EXPERIMENTAL = 'using experimental algorithm' ALGORITHMS = { + # Format: 'algorithm_name': [['version_first_appeared_in'], [reason_for_failure1, reason_for_failure2, ...], [warning1, warning2, ...]] 'kex': { 'diffie-hellman-group1-sha1': [['2.3.0,d0.28,l10.2', '6.6', '6.9'], [FAIL_OPENSSH67_UNSAFE, FAIL_OPENSSH70_LOGJAM], [WARN_MODULUS_SIZE, WARN_HASH_WEAK]], 'diffie-hellman-group14-sha1': [['3.9,d0.53,l10.6.0'], [], [WARN_HASH_WEAK]], From f9a51d410840d02f0ced8b563763553946981914 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Sun, 18 Aug 2019 00:34:03 -0400 Subject: [PATCH 065/103] Default interpreter changed to python3. --- ssh-audit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ssh-audit.py b/ssh-audit.py index 81e21a6..8d76339 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ The MIT License (MIT) From 99ac875542e392cf5feb17894a95f6d411c36370 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Sun, 18 Aug 2019 10:03:03 -0400 Subject: [PATCH 066/103] Added timeout argument. --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 65281c2..8798191 100644 --- a/README.md +++ b/README.md @@ -29,13 +29,14 @@ usage: ssh-audit.py [-1246pbnvl] -n, --no-colors disable colors -v, --verbose verbose output -l, --level= minimum output level (info|warn|fail) - + -t, --timeout= timeout (in seconds) for connection and reading + (default: 5) ``` * if both IPv4 and IPv6 are used, order of precedence can be set by using either `-46` or `-64`. * batch flag `-b` will output sections without header and without empty lines (implies verbose flag). * verbose flag `-v` will prefix each line with section type and algorithm name. -### example +### Example ![screenshot](https://cloud.githubusercontent.com/assets/7356025/19233757/3e09b168-8ef0-11e6-91b4-e880bacd0b8a.png) ## ChangeLog From 64656b5228d581a38b9cd3edbdf854bb09feccc6 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Sun, 18 Aug 2019 10:03:44 -0400 Subject: [PATCH 067/103] Added timeout option to usage message. --- ssh-audit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ssh-audit.py b/ssh-audit.py index 8d76339..67dff69 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -61,7 +61,7 @@ def usage(err=None): uout.head('# {0} {1}, https://github.com/jtesta/ssh-audit\n'.format(p, VERSION)) if err is not None and len(err) > 0: uout.fail('\n' + err) - uout.info('usage: {0} [-1246pbnvl] \n'.format(p)) + uout.info('usage: {0} [-1246pbnvlt] \n'.format(p)) uout.info(' -h, --help print this help') uout.info(' -1, --ssh1 force ssh version 1 only') uout.info(' -2, --ssh2 force ssh version 2 only') From afa73d2dd242b122dae622631d10f216710f1264 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Sun, 18 Aug 2019 14:38:39 -0400 Subject: [PATCH 068/103] Added 1 kex (diffie-hellman-group-exchange-sha256@ssh.com), 3 encryption algs (des-cbc-ssh1, blowfish-ctr, twofish-ctr), and 8 macs (hmac-sha2-56, hmac-sha2-224, hmac-sha2-384, hmac-sha3-256, hmac-sha3-384, hmac-sha3-512, hmac-sha256, hmac-sha256@ssh.com). --- ssh-audit.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/ssh-audit.py b/ssh-audit.py index 67dff69..254ffc6 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -322,6 +322,7 @@ class SSH2(object): # pylint: disable=too-few-public-methods 'diffie-hellman-group18-sha512': [['7.3']], 'diffie-hellman-group-exchange-sha1': [['2.3.0', '6.6', None], [FAIL_OPENSSH67_UNSAFE], [WARN_HASH_WEAK]], 'diffie-hellman-group-exchange-sha256': [['4.4']], + 'diffie-hellman-group-exchange-sha256@ssh.com': [['4.4']], 'ecdh-sha2-nistp256': [['5.7,d2013.62,l10.6.0'], [WARN_CURVES_WEAK]], 'ecdh-sha2-nistp384': [['5.7,d2013.62'], [WARN_CURVES_WEAK]], 'ecdh-sha2-nistp521': [['5.7,d2013.62'], [WARN_CURVES_WEAK]], @@ -354,13 +355,16 @@ class SSH2(object): # pylint: disable=too-few-public-methods 'enc': { 'none': [['1.2.2,d2013.56,l10.2'], [FAIL_PLAINTEXT]], 'des-cbc': [[], [FAIL_WEAK_CIPHER], [WARN_CIPHER_MODE, WARN_BLOCK_SIZE]], + 'des-cbc-ssh1': [[], [FAIL_WEAK_CIPHER], [WARN_CIPHER_MODE, WARN_BLOCK_SIZE]], '3des-cbc': [['1.2.2,d0.28,l10.2', '6.6', None], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH74_UNSAFE, WARN_CIPHER_WEAK, WARN_CIPHER_MODE, WARN_BLOCK_SIZE]], '3des-ctr': [['d0.52']], 'blowfish-cbc': [['1.2.2,d0.28,l10.2', '6.6,d0.52', '7.1,d0.52'], [FAIL_OPENSSH67_UNSAFE, FAIL_DBEAR53_DISABLED], [WARN_OPENSSH72_LEGACY, WARN_CIPHER_MODE, WARN_BLOCK_SIZE]], + 'blowfish-ctr': [[], [FAIL_OPENSSH67_UNSAFE, FAIL_DBEAR53_DISABLED], [WARN_OPENSSH72_LEGACY, WARN_CIPHER_MODE, WARN_BLOCK_SIZE]], 'twofish-cbc': [['d0.28', 'd2014.66'], [FAIL_DBEAR67_DISABLED], [WARN_CIPHER_MODE]], 'twofish128-cbc': [['d0.47', 'd2014.66'], [FAIL_DBEAR67_DISABLED], [WARN_CIPHER_MODE]], 'twofish192-cbc': [[], [], [WARN_CIPHER_MODE]], 'twofish256-cbc': [['d0.47', 'd2014.66'], [FAIL_DBEAR67_DISABLED], [WARN_CIPHER_MODE]], + 'twofish-ctr': [[]], 'twofish128-ctr': [['d2015.68']], 'twofish192-ctr': [[]], 'twofish256-ctr': [['d2015.68']], @@ -391,10 +395,18 @@ class SSH2(object): # pylint: disable=too-few-public-methods 'none': [['d2013.56'], [FAIL_PLAINTEXT]], 'hmac-sha1': [['2.1.0,d0.28,l10.2'], [], [WARN_ENCRYPT_AND_MAC, WARN_HASH_WEAK]], 'hmac-sha1-96': [['2.5.0,d0.47', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC, WARN_HASH_WEAK]], + 'hmac-sha2-56': [[], [], [WARN_TAG_SIZE, WARN_ENCRYPT_AND_MAC]], + 'hmac-sha2-224': [[], [], [WARN_TAG_SIZE, WARN_ENCRYPT_AND_MAC]], 'hmac-sha2-256': [['5.9,d2013.56,l10.7.0'], [], [WARN_ENCRYPT_AND_MAC]], 'hmac-sha2-256-96': [['5.9', '6.0'], [FAIL_OPENSSH61_REMOVE], [WARN_ENCRYPT_AND_MAC]], + 'hmac-sha2-384': [[], [], [WARN_ENCRYPT_AND_MAC]], 'hmac-sha2-512': [['5.9,d2013.56,l10.7.0'], [], [WARN_ENCRYPT_AND_MAC]], 'hmac-sha2-512-96': [['5.9', '6.0'], [FAIL_OPENSSH61_REMOVE], [WARN_ENCRYPT_AND_MAC]], + 'hmac-sha3-256': [[], [], [WARN_ENCRYPT_AND_MAC]], + 'hmac-sha3-384': [[], [], [WARN_ENCRYPT_AND_MAC]], + 'hmac-sha3-512': [[], [], [WARN_ENCRYPT_AND_MAC]], + 'hmac-sha256': [[], [], [WARN_ENCRYPT_AND_MAC]], + 'hmac-sha256@ssh.com': [[], [], [WARN_ENCRYPT_AND_MAC]], 'hmac-md5': [['2.1.0,d0.28', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC, WARN_HASH_WEAK]], 'hmac-md5-96': [['2.5.0', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC, WARN_HASH_WEAK]], 'hmac-ripemd160': [['2.5.0', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC]], From ed11fc135beead1635dadf10f8e894218da42991 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Sun, 18 Aug 2019 15:20:16 -0400 Subject: [PATCH 069/103] When unknown algorithms are encountered, ask the user to report them. --- ssh-audit.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/ssh-audit.py b/ssh-audit.py index 254ffc6..9be04dc 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -2498,18 +2498,18 @@ class KexGroupExchange_SHA256(KexGroupExchange): super(KexGroupExchange_SHA256, self).__init__('KexGroupExchange_SHA256', 'sha256') -def output_algorithms(title, alg_db, alg_type, algorithms, maxlen=0, alg_sizes=None): +def output_algorithms(title, alg_db, alg_type, algorithms, unknown_algs, maxlen=0, alg_sizes=None): # type: (str, Dict[str, Dict[str, List[List[Optional[str]]]]], str, List[text_type], int) -> None with OutputBuffer() as obuf: for algorithm in algorithms: - output_algorithm(alg_db, alg_type, algorithm, maxlen, alg_sizes) + output_algorithm(alg_db, alg_type, algorithm, unknown_algs, maxlen, alg_sizes) if len(obuf) > 0: out.head('# ' + title) obuf.flush() out.sep() -def output_algorithm(alg_db, alg_type, alg_name, alg_max_len=0, alg_sizes=None): +def output_algorithm(alg_db, alg_type, alg_name, unknown_algs, alg_max_len=0, alg_sizes=None): # type: (Dict[str, Dict[str, List[List[Optional[str]]]]], str, text_type, int) -> None prefix = '(' + alg_type + ') ' if alg_max_len == 0: @@ -2551,6 +2551,7 @@ def output_algorithm(alg_db, alg_type, alg_name, alg_max_len=0, alg_sizes=None): texts.append(('info', '')) else: texts.append(('warn', 'unknown algorithm')) + unknown_algs.append(alg_name) alg_name = alg_name_with_size if alg_name_with_size is not None else alg_name first = True @@ -2730,29 +2731,33 @@ def output(banner, header, kex=None, pkm=None): out.sep() maxlen = algs.maxlen + 1 output_security(banner, maxlen) + unknown_algorithms = [] # Filled in by output_algorithms() with unidentified algs. if pkm is not None: adb = SSH1.KexDB.ALGORITHMS ciphers = pkm.supported_ciphers auths = pkm.supported_authentications title, atype = 'SSH1 host-key algorithms', 'key' - output_algorithms(title, adb, atype, ['ssh-rsa1'], maxlen) + output_algorithms(title, adb, atype, ['ssh-rsa1'], unknown_algorithms, maxlen) title, atype = 'SSH1 encryption algorithms (ciphers)', 'enc' - output_algorithms(title, adb, atype, ciphers, maxlen) + output_algorithms(title, adb, atype, ciphers, unknown_algorithms, maxlen) title, atype = 'SSH1 authentication types', 'aut' - output_algorithms(title, adb, atype, auths, maxlen) + output_algorithms(title, adb, atype, auths, unknown_algorithms, maxlen) if kex is not None: adb = SSH2.KexDB.ALGORITHMS 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, unknown_algorithms, maxlen, kex.dh_modulus_sizes()) title, atype = 'host-key algorithms', 'key' - output_algorithms(title, adb, atype, kex.key_algorithms, maxlen, kex.rsa_key_sizes()) + output_algorithms(title, adb, atype, kex.key_algorithms, unknown_algorithms, maxlen, kex.rsa_key_sizes()) title, atype = 'encryption algorithms (ciphers)', 'enc' - output_algorithms(title, adb, atype, kex.server.encryption, maxlen) + output_algorithms(title, adb, atype, kex.server.encryption, unknown_algorithms, maxlen) title, atype = 'message authentication code algorithms', 'mac' - output_algorithms(title, adb, atype, kex.server.mac, maxlen) + output_algorithms(title, adb, atype, kex.server.mac, unknown_algorithms, maxlen) output_recommendations(algs, software, maxlen) output_fingerprint(algs, True, maxlen) + # If we encountered any unknown algorithms, ask the user to report them. + if len(unknown_algorithms) > 0: + out.warn("\n\n!!! WARNING: unknown algorithm(s) found!: %s. Please email the full output above to the maintainer (jtesta@positronsecurity.com), or create a Github issue at .\n" % ','.join(unknown_algorithms)) class Utils(object): @classmethod From af663da83811ad7c40bcd5ca411ad27ceafbc134 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Thu, 22 Aug 2019 15:47:37 -0400 Subject: [PATCH 070/103] Now SHA256 fingerprints are displayed for RSA and ED25519 host keys. Fixes #2. --- ssh-audit.py | 209 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 128 insertions(+), 81 deletions(-) diff --git a/ssh-audit.py b/ssh-audit.py index 9be04dc..5f138e0 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -272,7 +272,10 @@ class OutputBuffer(list): sys.stdout = self.__buf return self - def flush(self): + def flush(self, sort_lines=False): + # Lines must be sorted in some cases to ensure consistent testing. + if sort_lines: + self.sort() # type: () -> None for line in self: print(line) @@ -322,7 +325,7 @@ class SSH2(object): # pylint: disable=too-few-public-methods 'diffie-hellman-group18-sha512': [['7.3']], 'diffie-hellman-group-exchange-sha1': [['2.3.0', '6.6', None], [FAIL_OPENSSH67_UNSAFE], [WARN_HASH_WEAK]], 'diffie-hellman-group-exchange-sha256': [['4.4']], - 'diffie-hellman-group-exchange-sha256@ssh.com': [['4.4']], + 'diffie-hellman-group-exchange-sha256@ssh.com': [[]], 'ecdh-sha2-nistp256': [['5.7,d2013.62,l10.6.0'], [WARN_CURVES_WEAK]], 'ecdh-sha2-nistp384': [['5.7,d2013.62'], [WARN_CURVES_WEAK]], 'ecdh-sha2-nistp521': [['5.7,d2013.62'], [WARN_CURVES_WEAK]], @@ -466,6 +469,7 @@ class SSH2(object): # pylint: disable=too-few-public-methods self.__rsa_key_sizes = {} self.__dh_modulus_sizes = {} + self.__host_keys = {} @property def cookie(self): @@ -516,6 +520,12 @@ class SSH2(object): # pylint: disable=too-few-public-methods def dh_modulus_sizes(self): return self.__dh_modulus_sizes + def set_host_key(self, key_type, hostkey): + self.__host_keys[key_type] = hostkey + + def host_keys(self): + return self.__host_keys + def write(self, wbuf): # type: (WriteBuf) -> None wbuf.write(self.cookie) @@ -561,10 +571,22 @@ class SSH2(object): # pylint: disable=too-few-public-methods kex = cls(cookie, kex_algs, key_algs, cli, srv, follows, unused) return kex - # Obtains RSA host keys and checks their size. - class RSAKeyTest(object): - RSA_TYPES = ['ssh-rsa', 'rsa-sha2-256', 'rsa-sha2-512'] - RSA_CA_TYPES = ['ssh-rsa-cert-v01@openssh.com'] + # Obtains host keys, checks their size, and derives their fingerprints. + class HostKeyTest(object): + # Tracks the RSA host key types. As of this writing, testing one in this family yields valid results for the rest. + RSA_FAMILY = ['ssh-rsa', 'rsa-sha2-256', 'rsa-sha2-512'] + + # Dict holding the host key types we should extract & parse. 'cert' is True to denote that a host key type handles certificates (thus requires additional parsing). 'variable_key_len' is True for host key types that can have variable sizes (True only for RSA types, as the rest are of fixed-size). After the host key type is fully parsed, the key 'parsed' is added with a value of True. + HOST_KEY_TYPES = { + 'ssh-rsa': {'cert': False, 'variable_key_len': True}, + 'rsa-sha2-256': {'cert': False, 'variable_key_len': True}, + 'rsa-sha2-512': {'cert': False, 'variable_key_len': True}, + + 'ssh-rsa-cert-v01@openssh.com': {'cert': True, 'variable_key_len': True}, + + 'ssh-ed25519': {'cert': False, 'variable_key_len': False}, + 'ssh-ed25519-cert-v01@openssh.com': {'cert': True, 'variable_key_len': False}, + } @staticmethod def run(s, server_kex): @@ -595,90 +617,90 @@ class SSH2(object): # pylint: disable=too-few-public-methods 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) + SSH2.HostKeyTest.__test(s, server_kex, kex_str, kex_group, SSH2.HostKeyTest.HOST_KEY_TYPES) @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. + def __test(s, server_kex, kex_str, kex_group, host_key_types): hostkey_modulus_size = 0 ca_modulus_size = 0 - ran_test = False - # If the connection is closed, re-open it and get the kex again. - if not s.is_connected(): - s.connect() - unused = None # pylint: disable=unused-variable - unused, unused, err = s.get_banner() - if err is not None: - s.close() - return + # For each host key type... + for host_key_type in host_key_types: + # Skip those already handled (i.e.: those in the RSA family, as testing one tests them all). + if 'parsed' in host_key_types[host_key_type] and host_key_types[host_key_type]['parsed']: + continue - # Parse the server's initial KEX. - packet_type = 0 # pylint: disable=unused-variable - packet_type, payload = s.read_packet() - SSH2.Kex.parse(payload) + # If this host key type is supported by the server, we test it. + if host_key_type in server_kex.key_algorithms: + cert = host_key_types[host_key_type]['cert'] + variable_key_len = host_key_types[host_key_type]['variable_key_len'] + # If the connection is closed, re-open it and get the kex again. + if not s.is_connected(): + s.connect() + unused = None # pylint: disable=unused-variable + unused, unused, err = s.get_banner() + if err is not None: + s.close() + return - for rsa_type in rsa_types: - if rsa_type in server_kex.key_algorithms: - ran_test = True + # Parse the server's initial KEX. + packet_type = 0 # pylint: disable=unused-variable + packet_type, payload = s.read_packet() + SSH2.Kex.parse(payload) # Send the server our KEXINIT message, using only our - # selected kex and RSA type. Send the server's own + # selected kex and host key 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) + client_kex = SSH2.Kex(os.urandom(16), [kex_str], [host_key_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. + # with the host key and its length. Bingo. We also get back the host key fingerprint. kex_group.send_init(s) - kex_group.recv_reply(s) + host_key = kex_group.recv_reply(s, variable_key_len) + server_kex.set_host_key(host_key_type, host_key) 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 + # Close the socket, as the connection has + # been put in a state that later tests can't use. + s.close() - if hostkey_modulus_size > 0 or ca_modulus_size > 0: - # Set the hostkey size for all RSA key types since 'ssh-rsa', - # 'rsa-sha2-256', etc. are all using the same host key. - # Note, however, that this may change in the future. - 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) + # If the host key modulus or CA modulus was successfully parsed, check to see that its a safe size. + if hostkey_modulus_size > 0 or ca_modulus_size > 0: + # Set the hostkey size for all RSA key types since 'ssh-rsa', + # 'rsa-sha2-256', etc. are all using the same host key. + # Note, however, that this may change in the future. + if cert is False and host_key_type in SSH2.HostKeyTest.RSA_FAMILY: + for rsa_type in SSH2.HostKeyTest.RSA_FAMILY: + server_kex.set_rsa_key_size(rsa_type, hostkey_modulus_size) + elif cert is True: + server_kex.set_rsa_key_size(host_key_type, hostkey_modulus_size, ca_modulus_size) - # Keys smaller than 2048 result in a failure. - fail = False - if hostkey_modulus_size < 2048 or (ca_modulus_size < 2048 and ca_modulus_size > 0): - fail = True + # Keys smaller than 2048 result in a failure. Update the database accordingly. + if (cert is False) and (hostkey_modulus_size < 2048): + for rsa_type in SSH2.HostKeyTest.RSA_FAMILY: + alg_list = SSH2.KexDB.ALGORITHMS['key'][rsa_type] + alg_list.append(['using small %d-bit modulus' % hostkey_modulus_size]) + elif (cert is True) and ((hostkey_modulus_size < 2048) or (ca_modulus_size > 0 and ca_modulus_size < 2048)): + alg_list = SSH2.KexDB.ALGORITHMS['key'][host_key_type] + 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 this is a bad key size, update the database accordingly. - if fail: - 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]) + # If this host key type is in the RSA family, then mark them all as parsed (since results in one are valid for them all). + if host_key_type in SSH2.HostKeyTest.RSA_FAMILY: + for rsa_type in SSH2.HostKeyTest.RSA_FAMILY: + host_key_types[rsa_type]['parsed'] = True else: - alg_list = SSH2.KexDB.ALGORITHMS['key'][rsa_type] + host_key_types[host_key_type]['parsed'] = True - 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 # their size. @@ -733,21 +755,21 @@ class SSH2(object): # pylint: disable=too-few-public-methods if SSH2.GEXTest.reconnect(s, gex_alg) is False: break - kex_group = GEX_ALGS[gex_alg]() smallest_modulus = -1 # First try a range of weak sizes. try: kex_group.send_init_gex(s, 512, 1024, 1536) - kex_group.recv_reply(s) + kex_group.recv_reply(s, False) # Its been observed that servers will return a group # larger than the requested max. So just because we # got here, doesn't mean the server is vulnerable... smallest_modulus = kex_group.get_dh_modulus_size() - except Exception: # pylint: disable=bare-except - x = 1 # pylint: disable=unused-variable + + except Exception as e: # pylint: disable=bare-except + pass finally: s.close() @@ -766,10 +788,12 @@ class SSH2(object): # pylint: disable=too-few-public-methods try: kex_group.send_init_gex(s, bits, bits, bits) - kex_group.recv_reply(s) + kex_group.recv_reply(s, False) smallest_modulus = kex_group.get_dh_modulus_size() - except Exception: # pylint: disable=bare-except - x = 1 # pylint: disable=unused-variable + except Exception as e: # pylint: disable=bare-except + #import traceback + #print(traceback.format_exc()) + pass finally: # The server is in a state that is not re-testable, # so there's nothing else to do with this open @@ -2132,6 +2156,7 @@ class KexDH(object): # pragma: nocover self.__f = 0 self.__h_sig = 0 + def set_params(self, g, p): self.__g = g self.__p = p @@ -2150,8 +2175,9 @@ class KexDH(object): # pragma: nocover s.send_packet() # Parse a KEXDH_REPLY or KEXDH_GEX_REPLY message from the server. This - # Contains the host key, among other things. - def recv_reply(self, s): + # contains the host key, among other things. Function returns the host + # key blob (from which the fingerprint can be calculated). + def recv_reply(self, s, parse_host_key_size=True): packet_type, payload = s.read_packet(2) if packet_type != -1 and packet_type not in [SSH.Protocol.MSG_KEXDH_REPLY, SSH.Protocol.MSG_KEXDH_GEX_REPLY]: # TODO: change Exception to something more specific. @@ -2160,7 +2186,7 @@ class KexDH(object): # pragma: nocover # A connection error occurred. We can't parse anything, so just # return. The host key modulus (and perhaps certificate modulus) # will remain at length 0. - return + return None hostkey_len = f_len = h_sig_len = 0 # pylint: disable=unused-variable hostkey_type_len = hostkey_e_len = 0 # pylint: disable=unused-variable @@ -2178,6 +2204,11 @@ class KexDH(object): # pragma: nocover # Get the host key blob, F, and signature. ptr = 0 hostkey, hostkey_len, ptr = KexDH.__get_bytes(payload, ptr) + + # If we are not supposed to parse the host key size (i.e.: it is a type that is of fixed size such as ed25519), then stop here. + if not parse_host_key_size: + return hostkey + self.__f, f_len, ptr = KexDH.__get_bytes(payload, ptr) self.__h_sig, h_sig_len, ptr = KexDH.__get_bytes(payload, ptr) @@ -2254,6 +2285,7 @@ class KexDH(object): # pragma: nocover # CA's modulus. Bingo. ca_key_n, self.__ca_n_len, ptr = KexDH.__get_bytes(ca_key, ptr) + return hostkey @staticmethod def __get_bytes(buf, ptr): @@ -2639,20 +2671,35 @@ def output_security(banner, padlen): out.sep() -def output_fingerprint(algs, sha256=True, padlen=0): +def output_fingerprints(algs, sha256=True): # type: (SSH.Algorithms, bool, int) -> None with OutputBuffer() as obuf: fps = [] if algs.ssh1kex is not None: name = 'ssh-rsa1' fp = SSH.Fingerprint(algs.ssh1kex.host_key_fingerprint_data) - bits = algs.ssh1kex.host_key_bits - fps.append((name, fp, bits)) + #bits = algs.ssh1kex.host_key_bits + fps.append((name, fp)) + if algs.ssh2kex is not None: + host_keys = algs.ssh2kex.host_keys() + for host_key_type in algs.ssh2kex.host_keys(): + fp = SSH.Fingerprint(host_keys[host_key_type]) + + # Workaround for Python's order-indifference in dicts. We might get a random RSA type (ssh-rsa, rsa-sha2-256, or rsa-sha2-512), so running the tool against the same server three times may give three different host key types here. So if we have any RSA type, we will simply hard-code it to 'ssh-rsa'. + if host_key_type in SSH2.HostKeyTest.RSA_FAMILY: + host_key_type = 'ssh-rsa' + + # Skip over certificate host types (or we would return invalid fingerprints). + if '-cert-' not in host_key_type: + fps.append((host_key_type, fp)) + # Similarly, the host keys can be processed in random order due to Python's order-indifference in dicts. So we sort this list before printing; this makes automated testing possible. + fps = sorted(fps) for fpp in fps: - name, fp, bits = fpp + name, fp = fpp fpo = fp.sha256 if sha256 else fp.md5 - p = '' if out.batch else ' ' * (padlen - len(name)) - out.good('(fin) {0}{1} -- {2} {3}'.format(name, p, bits, fpo)) + #p = '' if out.batch else ' ' * (padlen - len(name)) + #out.good('(fin) {0}{1} -- {2} {3}'.format(name, p, bits, fpo)) + out.good('(fin) {0}: {1}'.format(name, fpo)) if len(obuf) > 0: out.head('# fingerprints') obuf.flush() @@ -2694,7 +2741,7 @@ def output_recommendations(algs, software, padlen=0): else: title = '' out.head('# algorithm recommendations {0}'.format(title)) - obuf.flush() + obuf.flush(True) # Sort the output so that it is always stable (needed for repeatable testing). out.sep() @@ -2752,8 +2799,8 @@ def output(banner, header, kex=None, pkm=None): output_algorithms(title, adb, atype, kex.server.encryption, unknown_algorithms, maxlen) title, atype = 'message authentication code algorithms', 'mac' output_algorithms(title, adb, atype, kex.server.mac, unknown_algorithms, maxlen) + output_fingerprints(algs, True) output_recommendations(algs, software, maxlen) - output_fingerprint(algs, True, maxlen) # If we encountered any unknown algorithms, ask the user to report them. if len(unknown_algorithms) > 0: @@ -2933,7 +2980,7 @@ def audit(aconf, sshv=None): output(banner, header, pkm=pkm) elif sshv == 2: kex = SSH2.Kex.parse(payload) - SSH2.RSAKeyTest.run(s, kex) + SSH2.HostKeyTest.run(s, kex) SSH2.GEXTest.run(s, kex) output(banner, header, kex=kex) From 6baff0f8fe50b1cabb2aa19d13f7cddf76db8226 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Thu, 22 Aug 2019 15:49:10 -0400 Subject: [PATCH 071/103] Updated changelog for v2.0.0. --- README.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8798191..3b7e471 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ ## Usage ``` -usage: ssh-audit.py [-1246pbnvl] +usage: ssh-audit.py [-1246pbnvlt] -1, --ssh1 force ssh version 1 only -2, --ssh2 force ssh version 2 only @@ -40,6 +40,18 @@ usage: ssh-audit.py [-1246pbnvl] ![screenshot](https://cloud.githubusercontent.com/assets/7356025/19233757/3e09b168-8ef0-11e6-91b4-e880bacd0b8a.png) ## ChangeLog +### v2.0.0-dev (???) + - Forked from https://github.com/arthepsy/ssh-audit (development was stalled, and developer went MIA). + - Added RSA host key length test. + - Added RSA certificate key length test. + - Added Diffie-Hellman modulus size test. + - Now outputs host key fingerprints for RSA and ED25519. + - Added 2 new key exchanges: sntrup4591761x25519-sha512@tinyssh.org, diffie-hellman-group-exchange-sha256@ssh.com. + - Added 3 new encryption algorithms: des-cbc-ssh1, blowfish-ctr, twofish-ctr. + - Added 8 new MACs: hmac-sha2-56, hmac-sha2-224, hmac-sha2-384, hmac-sha3-256, hmac-sha3-384, hmac-sha3-512, hmac-sha256, hmac-sha256@ssh.com. + - Added command line argument (-t / --timeout) for connection & reading timeouts. + - Updated CVEs for libssh & Dropbear. + ### v1.7.0 (2016-10-26) - implement options to allow specify IPv4/IPv6 usage and order of precedence - implement option to specify remote port (old behavior kept for compatibility) From 7a06b872f9342e4ba8d78270297f27bc7fe32880 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Thu, 22 Aug 2019 15:54:14 -0400 Subject: [PATCH 072/103] Fixed automatic links in changelog. --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3b7e471..7495ad0 100644 --- a/README.md +++ b/README.md @@ -46,9 +46,9 @@ usage: ssh-audit.py [-1246pbnvlt] - Added RSA certificate key length test. - Added Diffie-Hellman modulus size test. - Now outputs host key fingerprints for RSA and ED25519. - - Added 2 new key exchanges: sntrup4591761x25519-sha512@tinyssh.org, diffie-hellman-group-exchange-sha256@ssh.com. - - Added 3 new encryption algorithms: des-cbc-ssh1, blowfish-ctr, twofish-ctr. - - Added 8 new MACs: hmac-sha2-56, hmac-sha2-224, hmac-sha2-384, hmac-sha3-256, hmac-sha3-384, hmac-sha3-512, hmac-sha256, hmac-sha256@ssh.com. + - Added 2 new key exchanges: `sntrup4591761x25519-sha512@tinyssh.org`, `diffie-hellman-group-exchange-sha256@ssh.com`. + - Added 3 new encryption algorithms: `des-cbc-ssh1`, `blowfish-ctr`, `twofish-ctr`. + - Added 8 new MACs: `hmac-sha2-56`, `hmac-sha2-224`, `hmac-sha2-384`, `hmac-sha3-256`, `hmac-sha3-384`, `hmac-sha3-512`, `hmac-sha256`, `hmac-sha256@ssh.com`. - Added command line argument (-t / --timeout) for connection & reading timeouts. - Updated CVEs for libssh & Dropbear. From 4f138d7f82f9754beed0d7e6427d54d26f2fb879 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Thu, 22 Aug 2019 16:04:46 -0400 Subject: [PATCH 073/103] Added docker testing framework. --- docker_test.sh | 264 ++++++++++++++++++ test/docker/Dockerfile | 18 ++ test/docker/debug.sh | 9 + .../expected_results/openssh_5.6p1_test1.txt | 145 ++++++++++ .../expected_results/openssh_5.6p1_test2.txt | 143 ++++++++++ .../expected_results/openssh_5.6p1_test3.txt | 143 ++++++++++ .../expected_results/openssh_5.6p1_test4.txt | 141 ++++++++++ .../expected_results/openssh_5.6p1_test5.txt | 139 +++++++++ .../expected_results/openssh_8.0p1_test1.txt | 79 ++++++ .../expected_results/openssh_8.0p1_test2.txt | 75 +++++ .../expected_results/openssh_8.0p1_test3.txt | 39 +++ test/docker/host_ca_ed25519 | 7 + test/docker/host_ca_ed25519.pub | 1 + test/docker/host_ca_rsa_1024 | 15 + test/docker/host_ca_rsa_1024.pub | 1 + test/docker/host_ca_rsa_3072 | 39 +++ test/docker/host_ca_rsa_3072.pub | 1 + test/docker/moduli_1024 | 44 +++ test/docker/ssh_host_dsa_key | 12 + test/docker/ssh_host_dsa_key.pub | 1 + test/docker/ssh_host_ecdsa_key | 5 + test/docker/ssh_host_ecdsa_key.pub | 1 + test/docker/ssh_host_ed25519_key | 7 + test/docker/ssh_host_ed25519_key-cert.pub | 1 + test/docker/ssh_host_ed25519_key.pub | 1 + test/docker/ssh_host_rsa_key_1024 | 15 + .../ssh_host_rsa_key_1024-cert_1024.pub | 1 + .../ssh_host_rsa_key_1024-cert_3072.pub | 1 + test/docker/ssh_host_rsa_key_1024.pub | 1 + test/docker/ssh_host_rsa_key_3072 | 39 +++ .../ssh_host_rsa_key_3072-cert_1024.pub | 1 + .../ssh_host_rsa_key_3072-cert_3072.pub | 1 + test/docker/ssh_host_rsa_key_3072.pub | 1 + 33 files changed, 1391 insertions(+) create mode 100755 docker_test.sh create mode 100644 test/docker/Dockerfile create mode 100755 test/docker/debug.sh create mode 100644 test/docker/expected_results/openssh_5.6p1_test1.txt create mode 100644 test/docker/expected_results/openssh_5.6p1_test2.txt create mode 100644 test/docker/expected_results/openssh_5.6p1_test3.txt create mode 100644 test/docker/expected_results/openssh_5.6p1_test4.txt create mode 100644 test/docker/expected_results/openssh_5.6p1_test5.txt create mode 100644 test/docker/expected_results/openssh_8.0p1_test1.txt create mode 100644 test/docker/expected_results/openssh_8.0p1_test2.txt create mode 100644 test/docker/expected_results/openssh_8.0p1_test3.txt create mode 100644 test/docker/host_ca_ed25519 create mode 100644 test/docker/host_ca_ed25519.pub create mode 100644 test/docker/host_ca_rsa_1024 create mode 100644 test/docker/host_ca_rsa_1024.pub create mode 100644 test/docker/host_ca_rsa_3072 create mode 100644 test/docker/host_ca_rsa_3072.pub create mode 100644 test/docker/moduli_1024 create mode 100644 test/docker/ssh_host_dsa_key create mode 100644 test/docker/ssh_host_dsa_key.pub create mode 100644 test/docker/ssh_host_ecdsa_key create mode 100644 test/docker/ssh_host_ecdsa_key.pub create mode 100644 test/docker/ssh_host_ed25519_key create mode 100644 test/docker/ssh_host_ed25519_key-cert.pub create mode 100644 test/docker/ssh_host_ed25519_key.pub create mode 100644 test/docker/ssh_host_rsa_key_1024 create mode 100644 test/docker/ssh_host_rsa_key_1024-cert_1024.pub create mode 100644 test/docker/ssh_host_rsa_key_1024-cert_3072.pub create mode 100644 test/docker/ssh_host_rsa_key_1024.pub create mode 100644 test/docker/ssh_host_rsa_key_3072 create mode 100644 test/docker/ssh_host_rsa_key_3072-cert_1024.pub create mode 100644 test/docker/ssh_host_rsa_key_3072-cert_3072.pub create mode 100644 test/docker/ssh_host_rsa_key_3072.pub diff --git a/docker_test.sh b/docker_test.sh new file mode 100755 index 0000000..36a4e2f --- /dev/null +++ b/docker_test.sh @@ -0,0 +1,264 @@ +#!/bin/bash + +# +# This script will set up a docker image with multiple versions of OpenSSH, then +# use it to run tests. +# +# For debugging purposes, here is a cheat sheet for manually running the docker image: +# +# docker run -p 2222:22 -it ssh-audit-test:X /bin/bash +# docker run -p 2222:22 --security-opt seccomp:unconfined -it ssh-audit-test /debug.sh +# docker run -d -p 2222:22 ssh-audit-test:X /openssh/sshd-5.6p1 -D -f /etc/ssh/sshd_config-5.6p1_test1 +# docker run -d -p 2222:22 ssh-audit-test:X /openssh/sshd-8.0p1 -D -f /etc/ssh/sshd_config-8.0p1_test1 +# + + +# This is the docker tag for the image. If this tag doesn't exist, then we assume the +# image is out of date, and generate a new one with this tag. +IMAGE_VERSION=1 + +# This is the name of our docker image. +IMAGE_NAME=ssh-audit-test + + +# Terminal colors. +CLR="\033[0m" +RED="\033[0;31m" +GREEN="\033[0;32m" +REDB="\033[1;31m" # Red + bold +GREENB="\033[1;32m" # Green + bold + + +# Returns 0 if current docker image exists. +function check_if_docker_image_exists { + images=`docker image ls | egrep "$IMAGE_NAME[[:space:]]+$IMAGE_VERSION"` +} + + +# Uncompresses and compiles the specified version of OpenSSH. +function compile_openssh { + echo "Uncompressing $1..." + tar xzf openssh-$1.tar.gz + + echo "Compiling $1..." + pushd openssh-$1 > /dev/null + ./configure && make -j 10 + + if [[ ! -f "sshd" ]]; then + echo -e "${REDB}Error: sshd not built!${CLR}" + exit 1 + fi + + echo -e "\n${GREEN}Successfully built OpenSSH ${1}${CLR}\n" + popd > /dev/null +} + + +# Creates a new docker image. +function create_docker_image { + # Create a new temporary directory. + TMP_DIR=`mktemp -d /tmp/sshaudit-docker-XXXXXXXXXX` + + # Copy the Dockerfile to our new temp directory. + cp test/docker/* $TMP_DIR + + # Make the temp directory our working directory for the duration of the build + # process. + pushd $TMP_DIR > /dev/null + + # Get the release key for OpenSSH. + get_openssh_release_key + + # Aside from checking the GPG signatures, we also compare against this known-good + # SHA-256 hash just in case. + get_openssh '5.6p1' '538af53b2b8162c21a293bb004ae2bdb141abd250f61b4cea55244749f3c6c2b' + get_openssh '8.0p1' 'bd943879e69498e8031eb6b7f44d08cdc37d59a7ab689aa0b437320c3481fd68' + + # Compile the versions of OpenSSH. + compile_openssh '5.6p1' + compile_openssh '8.0p1' + + # Rename the default config files so we know they are our originals. + mv openssh-5.6p1/sshd_config sshd_config-5.6p1_orig + mv openssh-8.0p1/sshd_config sshd_config-8.0p1_orig + + + # Create the configurations for each test. + + # + # OpenSSH v5.6p1 + # + + # Test 1: Basic test. + create_openssh_config '5.6p1' 'test1' "HostKey /etc/ssh/ssh_host_rsa_key_1024\nHostKey /etc/ssh/ssh_host_dsa_key" + + # Test 2: RSA 1024 host key with RSA 1024 certificate. + create_openssh_config '5.6p1' 'test2' "HostKey /etc/ssh/ssh_host_rsa_key_1024\nHostCertificate /etc/ssh/ssh_host_rsa_key_1024-cert_1024.pub" + + # Test 3: RSA 1024 host key with RSA 3072 certificate. + create_openssh_config '5.6p1' 'test3' "HostKey /etc/ssh/ssh_host_rsa_key_1024\nHostCertificate /etc/ssh/ssh_host_rsa_key_1024-cert_3072.pub" + + # Test 4: RSA 3072 host key with RSA 1024 certificate. + create_openssh_config '5.6p1' 'test4' "HostKey /etc/ssh/ssh_host_rsa_key_3072\nHostCertificate /etc/ssh/ssh_host_rsa_key_3072-cert_1024.pub" + + # Test 5: RSA 3072 host key with RSA 3072 certificate. + create_openssh_config '5.6p1' 'test5' "HostKey /etc/ssh/ssh_host_rsa_key_3072\nHostCertificate /etc/ssh/ssh_host_rsa_key_3072-cert_3072.pub" + + + # + # OpenSSH v8.0p1 + # + + # Test 1: Basic test. + create_openssh_config '8.0p1' 'test1' "HostKey /etc/ssh/ssh_host_rsa_key_3072\nHostKey /etc/ssh/ssh_host_ecdsa_key\nHostKey /etc/ssh/ssh_host_ed25519_key" + + # Test 2: ED25519 certificate test. + create_openssh_config '8.0p1' 'test2' "HostKey /etc/ssh/ssh_host_ed25519_key\nHostCertificate /etc/ssh/ssh_host_ed25519_key-cert.pub" + + # Test 3: Hardened installation test. + create_openssh_config '8.0p1' 'test3' "HostKey /etc/ssh/ssh_host_ed25519_key\nKexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256\nCiphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr\nMACs hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,umac-128-etm@openssh.com" + + + # Now build the docker image! + docker build --tag $IMAGE_NAME:$IMAGE_VERSION . + + popd > /dev/null + rm -rf $TMP_DIR +} + + +# Creates an OpenSSH configuration file for a specific test. +function create_openssh_config { + openssh_version=$1 + test_number=$2 + config_text=$3 + + cp sshd_config-${openssh_version}_orig sshd_config-${openssh_version}_${test_number} + echo -e "${config_text}" >> sshd_config-${openssh_version}_${test_number} +} + + +# Downloads the OpenSSH release key and adds it to the local keyring. +function get_openssh_release_key { + local release_key_fingerprint_expected='59C2 118E D206 D927 E667 EBE3 D3E5 F56B 6D92 0D30' + + echo -e "\nGetting OpenSSH release key...\n" + wget https://ftp.openbsd.org/pub/OpenBSD/OpenSSH/RELEASE_KEY.asc + + echo -e "\nImporting OpenSSH release key...\n" + gpg --import RELEASE_KEY.asc + + local release_key_fingerprint_actual=`gpg --fingerprint 6D920D30` + if [[ $release_key_fingerprint_actual != *"$release_key_fingerprint_expected"* ]]; then + echo -e "\n${REDB}Error: OpenSSH release key fingerprint does not match expected value!\n\tExpected: $release_key_fingerprint_expected\n\tActual: $release_key_fingerprint_actual\n\nTerminating.${CLR}" + exit -1 + fi + echo -e "\n\n${GREEN}OpenSSH release key matches expected value.${CLR}\n" +} + + +# Downloads the specified version of OpenSSH. +function get_openssh { + echo -e "\nGetting OpenSSH $1 sources...\n" + wget https://cdn.openbsd.org/pub/OpenBSD/OpenSSH/portable/openssh-$1.tar.gz + + echo -e "\nGetting OpenSSH $1 signature...\n" + wget https://cdn.openbsd.org/pub/OpenBSD/OpenSSH/portable/openssh-$1.tar.gz.asc + + local gpg_verify=`gpg --verify openssh-$1.tar.gz.asc openssh-$1.tar.gz 2>&1` + if [[ $gpg_verify != *"Good signature from \"Damien Miller "* ]]; then + echo -e "\n\n${REDB}Error: OpenSSH signature invalid!\n$gpg_verify\n\nTerminating.${CLR}" + exit -1 + fi + + # Check GPG's return value. 0 denotes a valid signature, and 1 is returned + # on invalid signatures. + if [[ $? != 0 ]]; then + echo -e "\n\n${REDB}Error: OpenSSH signature invalid! Verification returned code: $?\n\nTerminating.${CLR}" + exit -1 + fi + + echo -e "${GREEN}Signature on OpenSSH sources verified.${CLR}\n" + + local openssh_checksum_actual=`sha256sum openssh-$1.tar.gz | cut -f1 -d" "` + if [[ $openssh_checksum_actual != "$2" ]]; then + echo -e "${REDB}Error: OpenSSH checksum is invalid!\n Expected: $2\n Actual: $openssh_checksum_actual\n\n Terminating.${CLR}" + exit -1 + fi + +} + + +# Runs an OpenSSH test. Upon failure, a diff between the expected and actual results +# is shown, then the script immediately terminates. +function run_openssh_test { + openssh_version=$1 + test_number=$2 + + cid=`docker run -d -p 2222:22 ${IMAGE_NAME}:${IMAGE_VERSION} /openssh/sshd-${openssh_version} -D -f /etc/ssh/sshd_config-${openssh_version}_${test_number}` + if [[ $? != 0 ]]; then + echo -e "${REDB}Failed to run docker image! (exit code: $?)${CLR}" + exit 1 + fi + + ./ssh-audit.py localhost:2222 > ${TEST_RESULT_DIR}/openssh_${openssh_version}_${test_number}.txt + if [[ $? != 0 ]]; then + echo -e "${REDB}Failed to ssh-audit.py! (exit code: $?)${CLR}" + docker container stop $cid > /dev/null + exit 1 + fi + + docker container stop $cid > /dev/null + if [[ $? != 0 ]]; then + echo -e "${REDB}Failed to stop docker container ${cid}! (exit code: $?)${CLR}" + exit 1 + fi + + diff=`diff -u test/docker/expected_results/openssh_${openssh_version}_${test_number}.txt ${TEST_RESULT_DIR}/openssh_${openssh_version}_${test_number}.txt` + if [[ $? == 0 ]]; then + echo -e "OpenSSH ${openssh_version} ${test_number} ${GREEN}passed${CLR}." + else + echo -e "OpenSSH ${openssh_version} ${test_number} ${REDB}FAILED${CLR}.\n\n${diff}\n" + exit 1 + fi +} + + +# First check if docker is functional. +docker version > /dev/null +if [[ $? != 0 ]]; then + echo -e "${REDB}Error: 'docker version' command failed (error code: $?). Is docker installed and functioning?${CLR}" + exit 1 +fi + +# Check if the docker image is the most up-to-date version. If not, create it. +check_if_docker_image_exists +if [[ $? == 0 ]]; then + echo -e "\n${GREEN}Docker image $IMAGE_NAME:$IMAGE_VERSION already exists.${CLR}" +else + echo -e "\nCreating docker image $IMAGE_NAME:$IMAGE_VERSION..." + create_docker_image + echo -e "\n${GREEN}Done creating docker image!${CLR}" +fi + +# Create a temporary directory to write test results to. +TEST_RESULT_DIR=`mktemp -d /tmp/ssh-audit_test-results_XXXXXXXXXX` + +# Now run all the tests. +echo -e "\nRunning tests..." +run_openssh_test '5.6p1' 'test1' +run_openssh_test '5.6p1' 'test2' +run_openssh_test '5.6p1' 'test3' +run_openssh_test '5.6p1' 'test4' +run_openssh_test '5.6p1' 'test5' +echo "" +run_openssh_test '8.0p1' 'test1' +run_openssh_test '8.0p1' 'test2' +run_openssh_test '8.0p1' 'test3' + +# The test functions above will terminate the script on failure, so if we reached here, +# all tests are successful. +echo -e "\n${GREENB}ALL TESTS PASS!${CLR}\n" + +rm -rf $TEST_RESULT_DIR +exit 0 diff --git a/test/docker/Dockerfile b/test/docker/Dockerfile new file mode 100644 index 0000000..739be49 --- /dev/null +++ b/test/docker/Dockerfile @@ -0,0 +1,18 @@ +FROM ubuntu:16.04 + +COPY openssh-5.6p1/sshd /openssh/sshd-5.6p1 +COPY openssh-8.0p1/sshd /openssh/sshd-8.0p1 + +COPY sshd_config* /etc/ssh/ +COPY ssh_host_* /etc/ssh/ +COPY moduli_1024 /usr/local/etc/moduli + +COPY debug.sh /debug.sh + +RUN apt update 2> /dev/null +RUN apt install -y libssl-dev strace rsyslog 2> /dev/null +RUN apt clean 2> /dev/null +RUN useradd -s /bin/false sshd +RUN mkdir /var/empty + +EXPOSE 22 diff --git a/test/docker/debug.sh b/test/docker/debug.sh new file mode 100755 index 0000000..c4be343 --- /dev/null +++ b/test/docker/debug.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# This script is run on in docker container. It will enable logging for sshd in +# /var/log/auth.log. + +/etc/init.d/rsyslog start +sleep 1 +/openssh/sshd-5.6p1 -o LogLevel=DEBUG3 -f /etc/ssh/sshd_config-5.6p1_test1 +/bin/bash diff --git a/test/docker/expected_results/openssh_5.6p1_test1.txt b/test/docker/expected_results/openssh_5.6p1_test1.txt new file mode 100644 index 0000000..2ddc54e --- /dev/null +++ b/test/docker/expected_results/openssh_5.6p1_test1.txt @@ -0,0 +1,145 @@ +# general +(gen) banner: SSH-2.0-OpenSSH_5.6 +(gen) software: OpenSSH 5.6 +(gen) compatibility: OpenSSH 4.7-6.6, Dropbear SSH 0.53+ (some functionality from 0.52) +(gen) compression: enabled (zlib@openssh.com) + +# security +(cve) CVE-2016-3115 -- (CVSSv2: 5.5) bypass command restrictions via crafted X11 forwarding data +(cve) CVE-2016-1907 -- (CVSSv2: 5.0) cause DoS via crafted network traffic (out of bounds read) +(cve) CVE-2015-6564 -- (CVSSv2: 6.9) privilege escalation via leveraging sshd uid +(cve) CVE-2015-6563 -- (CVSSv2: 1.9) conduct impersonation attack +(cve) CVE-2014-2532 -- (CVSSv2: 5.8) bypass environment restrictions via specific string before wildcard +(cve) CVE-2014-1692 -- (CVSSv2: 7.5) cause DoS via triggering error condition (memory corruption) +(cve) CVE-2012-0814 -- (CVSSv2: 3.5) leak data via debug messages +(cve) CVE-2011-5000 -- (CVSSv2: 3.5) cause DoS via large value in certain length field (memory consumption) +(cve) CVE-2010-5107 -- (CVSSv2: 5.0) cause DoS via large number of connections (slot exhaustion) +(cve) CVE-2010-4755 -- (CVSSv2: 4.0) cause DoS via crafted glob expression (CPU and memory consumption) +(cve) CVE-2010-4478 -- (CVSSv2: 7.5) bypass authentication check via crafted values + +# key exchange algorithms +(kex) diffie-hellman-group-exchange-sha256 (1024-bit) -- [fail] using small 1024-bit modulus + `- [info] available since OpenSSH 4.4 +(kex) diffie-hellman-group-exchange-sha1 (1024-bit) -- [fail] using small 1024-bit modulus + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.3.0 +(kex) diffie-hellman-group14-sha1 -- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 3.9, Dropbear SSH 0.53 +(kex) diffie-hellman-group1-sha1 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [fail] disabled (in client) since OpenSSH 7.0, logjam attack + `- [warn] using small 1024-bit modulus + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.3.0, Dropbear SSH 0.28 + +# host-key algorithms +(key) ssh-rsa (1024-bit) -- [fail] using small 1024-bit modulus + `- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28 +(key) ssh-dss -- [fail] removed (in server) and disabled (in client) since OpenSSH 7.0, weak algorithm + `- [warn] using small 1024-bit modulus + `- [warn] using weak random number generator could reveal the key + `- [info] available since OpenSSH 2.1.0, Dropbear SSH 0.28 + +# encryption algorithms (ciphers) +(enc) aes128-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 +(enc) aes192-ctr -- [info] available since OpenSSH 3.7 +(enc) aes256-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 +(enc) arcfour256 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher + `- [info] available since OpenSSH 4.2 +(enc) arcfour128 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher + `- [info] available since OpenSSH 4.2 +(enc) aes128-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0, Dropbear SSH 0.28 +(enc) 3des-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.4, unsafe algorithm + `- [warn] using weak cipher + `- [warn] using weak cipher mode + `- [warn] using small 64-bit block size + `- [info] available since OpenSSH 1.2.2, Dropbear SSH 0.28 +(enc) blowfish-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [fail] disabled since Dropbear SSH 0.53 + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher mode + `- [warn] using small 64-bit block size + `- [info] available since OpenSSH 1.2.2, Dropbear SSH 0.28 +(enc) cast128-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher mode + `- [warn] using small 64-bit block size + `- [info] available since OpenSSH 2.1.0 +(enc) aes192-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0 +(enc) aes256-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0, Dropbear SSH 0.47 +(enc) arcfour -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher + `- [info] available since OpenSSH 2.1.0 +(enc) rijndael-cbc@lysator.liu.se -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0 + +# message authentication code algorithms +(mac) hmac-md5 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.1.0, Dropbear SSH 0.28 +(mac) hmac-sha1 -- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.1.0, Dropbear SSH 0.28 +(mac) umac-64@openssh.com -- [warn] using encrypt-and-MAC mode + `- [warn] using small 64-bit tag size + `- [info] available since OpenSSH 4.7 +(mac) hmac-ripemd160 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [info] available since OpenSSH 2.5.0 +(mac) hmac-ripemd160@openssh.com -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [info] available since OpenSSH 2.1.0 +(mac) hmac-sha1-96 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.47 +(mac) hmac-md5-96 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.5.0 + +# fingerprints +(fin) ssh-rsa: SHA256:YZ457EBcJTSxRKI3yXRgtAj3PBf5B9/F36b1SVooml4 + +# algorithm recommendations (for OpenSSH 5.6) +(rec) !diffie-hellman-group-exchange-sha256 -- kex algorithm to change (increase modulus size to 2048 bits or larger)  +(rec) !ssh-rsa -- key algorithm to change (increase modulus size to 2048 bits or larger)  +(rec) -3des-cbc -- enc algorithm to remove  +(rec) -aes128-cbc -- enc algorithm to remove  +(rec) -aes192-cbc -- enc algorithm to remove  +(rec) -aes256-cbc -- enc algorithm to remove  +(rec) -arcfour -- enc algorithm to remove  +(rec) -arcfour128 -- enc algorithm to remove  +(rec) -arcfour256 -- enc algorithm to remove  +(rec) -blowfish-cbc -- enc algorithm to remove  +(rec) -cast128-cbc -- enc algorithm to remove  +(rec) -diffie-hellman-group-exchange-sha1 -- kex algorithm to remove  +(rec) -diffie-hellman-group1-sha1 -- kex algorithm to remove  +(rec) -hmac-md5 -- mac algorithm to remove  +(rec) -hmac-md5-96 -- mac algorithm to remove  +(rec) -hmac-ripemd160 -- mac algorithm to remove  +(rec) -hmac-ripemd160@openssh.com -- mac algorithm to remove  +(rec) -hmac-sha1-96 -- mac algorithm to remove  +(rec) -rijndael-cbc@lysator.liu.se -- enc algorithm to remove  +(rec) -ssh-dss -- key algorithm to remove  +(rec) -diffie-hellman-group14-sha1 -- kex algorithm to remove  + diff --git a/test/docker/expected_results/openssh_5.6p1_test2.txt b/test/docker/expected_results/openssh_5.6p1_test2.txt new file mode 100644 index 0000000..8d4daab --- /dev/null +++ b/test/docker/expected_results/openssh_5.6p1_test2.txt @@ -0,0 +1,143 @@ +# general +(gen) banner: SSH-2.0-OpenSSH_5.6 +(gen) software: OpenSSH 5.6 +(gen) compatibility: OpenSSH 5.6-6.6, Dropbear SSH 0.53+ (some functionality from 0.52) +(gen) compression: enabled (zlib@openssh.com) + +# security +(cve) CVE-2016-3115 -- (CVSSv2: 5.5) bypass command restrictions via crafted X11 forwarding data +(cve) CVE-2016-1907 -- (CVSSv2: 5.0) cause DoS via crafted network traffic (out of bounds read) +(cve) CVE-2015-6564 -- (CVSSv2: 6.9) privilege escalation via leveraging sshd uid +(cve) CVE-2015-6563 -- (CVSSv2: 1.9) conduct impersonation attack +(cve) CVE-2014-2532 -- (CVSSv2: 5.8) bypass environment restrictions via specific string before wildcard +(cve) CVE-2014-1692 -- (CVSSv2: 7.5) cause DoS via triggering error condition (memory corruption) +(cve) CVE-2012-0814 -- (CVSSv2: 3.5) leak data via debug messages +(cve) CVE-2011-5000 -- (CVSSv2: 3.5) cause DoS via large value in certain length field (memory consumption) +(cve) CVE-2010-5107 -- (CVSSv2: 5.0) cause DoS via large number of connections (slot exhaustion) +(cve) CVE-2010-4755 -- (CVSSv2: 4.0) cause DoS via crafted glob expression (CPU and memory consumption) +(cve) CVE-2010-4478 -- (CVSSv2: 7.5) bypass authentication check via crafted values + +# key exchange algorithms +(kex) diffie-hellman-group-exchange-sha256 (1024-bit) -- [fail] using small 1024-bit modulus + `- [info] available since OpenSSH 4.4 +(kex) diffie-hellman-group-exchange-sha1 (1024-bit) -- [fail] using small 1024-bit modulus + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.3.0 +(kex) diffie-hellman-group14-sha1 -- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 3.9, Dropbear SSH 0.53 +(kex) diffie-hellman-group1-sha1 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [fail] disabled (in client) since OpenSSH 7.0, logjam attack + `- [warn] using small 1024-bit modulus + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.3.0, Dropbear SSH 0.28 + +# host-key algorithms +(key) ssh-rsa (1024-bit) -- [fail] using small 1024-bit modulus + `- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28 +(key) ssh-rsa-cert-v01@openssh.com (1024-bit cert/1024-bit CA) -- [fail] using small 1024-bit modulus + `- [info] available since OpenSSH 5.6 + +# encryption algorithms (ciphers) +(enc) aes128-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 +(enc) aes192-ctr -- [info] available since OpenSSH 3.7 +(enc) aes256-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 +(enc) arcfour256 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher + `- [info] available since OpenSSH 4.2 +(enc) arcfour128 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher + `- [info] available since OpenSSH 4.2 +(enc) aes128-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0, Dropbear SSH 0.28 +(enc) 3des-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.4, unsafe algorithm + `- [warn] using weak cipher + `- [warn] using weak cipher mode + `- [warn] using small 64-bit block size + `- [info] available since OpenSSH 1.2.2, Dropbear SSH 0.28 +(enc) blowfish-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [fail] disabled since Dropbear SSH 0.53 + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher mode + `- [warn] using small 64-bit block size + `- [info] available since OpenSSH 1.2.2, Dropbear SSH 0.28 +(enc) cast128-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher mode + `- [warn] using small 64-bit block size + `- [info] available since OpenSSH 2.1.0 +(enc) aes192-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0 +(enc) aes256-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0, Dropbear SSH 0.47 +(enc) arcfour -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher + `- [info] available since OpenSSH 2.1.0 +(enc) rijndael-cbc@lysator.liu.se -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0 + +# message authentication code algorithms +(mac) hmac-md5 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.1.0, Dropbear SSH 0.28 +(mac) hmac-sha1 -- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.1.0, Dropbear SSH 0.28 +(mac) umac-64@openssh.com -- [warn] using encrypt-and-MAC mode + `- [warn] using small 64-bit tag size + `- [info] available since OpenSSH 4.7 +(mac) hmac-ripemd160 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [info] available since OpenSSH 2.5.0 +(mac) hmac-ripemd160@openssh.com -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [info] available since OpenSSH 2.1.0 +(mac) hmac-sha1-96 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.47 +(mac) hmac-md5-96 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.5.0 + +# fingerprints +(fin) ssh-rsa: SHA256:YZ457EBcJTSxRKI3yXRgtAj3PBf5B9/F36b1SVooml4 + +# algorithm recommendations (for OpenSSH 5.6) +(rec) !diffie-hellman-group-exchange-sha256 -- kex algorithm to change (increase modulus size to 2048 bits or larger)  +(rec) !ssh-rsa -- key algorithm to change (increase modulus size to 2048 bits or larger)  +(rec) !ssh-rsa-cert-v01@openssh.com -- key algorithm to change (increase modulus size to 2048 bits or larger)  +(rec) -3des-cbc -- enc algorithm to remove  +(rec) -aes128-cbc -- enc algorithm to remove  +(rec) -aes192-cbc -- enc algorithm to remove  +(rec) -aes256-cbc -- enc algorithm to remove  +(rec) -arcfour -- enc algorithm to remove  +(rec) -arcfour128 -- enc algorithm to remove  +(rec) -arcfour256 -- enc algorithm to remove  +(rec) -blowfish-cbc -- enc algorithm to remove  +(rec) -cast128-cbc -- enc algorithm to remove  +(rec) -diffie-hellman-group-exchange-sha1 -- kex algorithm to remove  +(rec) -diffie-hellman-group1-sha1 -- kex algorithm to remove  +(rec) -hmac-md5 -- mac algorithm to remove  +(rec) -hmac-md5-96 -- mac algorithm to remove  +(rec) -hmac-ripemd160 -- mac algorithm to remove  +(rec) -hmac-ripemd160@openssh.com -- mac algorithm to remove  +(rec) -hmac-sha1-96 -- mac algorithm to remove  +(rec) -rijndael-cbc@lysator.liu.se -- enc algorithm to remove  +(rec) -diffie-hellman-group14-sha1 -- kex algorithm to remove  + diff --git a/test/docker/expected_results/openssh_5.6p1_test3.txt b/test/docker/expected_results/openssh_5.6p1_test3.txt new file mode 100644 index 0000000..8d66e56 --- /dev/null +++ b/test/docker/expected_results/openssh_5.6p1_test3.txt @@ -0,0 +1,143 @@ +# general +(gen) banner: SSH-2.0-OpenSSH_5.6 +(gen) software: OpenSSH 5.6 +(gen) compatibility: OpenSSH 5.6-6.6, Dropbear SSH 0.53+ (some functionality from 0.52) +(gen) compression: enabled (zlib@openssh.com) + +# security +(cve) CVE-2016-3115 -- (CVSSv2: 5.5) bypass command restrictions via crafted X11 forwarding data +(cve) CVE-2016-1907 -- (CVSSv2: 5.0) cause DoS via crafted network traffic (out of bounds read) +(cve) CVE-2015-6564 -- (CVSSv2: 6.9) privilege escalation via leveraging sshd uid +(cve) CVE-2015-6563 -- (CVSSv2: 1.9) conduct impersonation attack +(cve) CVE-2014-2532 -- (CVSSv2: 5.8) bypass environment restrictions via specific string before wildcard +(cve) CVE-2014-1692 -- (CVSSv2: 7.5) cause DoS via triggering error condition (memory corruption) +(cve) CVE-2012-0814 -- (CVSSv2: 3.5) leak data via debug messages +(cve) CVE-2011-5000 -- (CVSSv2: 3.5) cause DoS via large value in certain length field (memory consumption) +(cve) CVE-2010-5107 -- (CVSSv2: 5.0) cause DoS via large number of connections (slot exhaustion) +(cve) CVE-2010-4755 -- (CVSSv2: 4.0) cause DoS via crafted glob expression (CPU and memory consumption) +(cve) CVE-2010-4478 -- (CVSSv2: 7.5) bypass authentication check via crafted values + +# key exchange algorithms +(kex) diffie-hellman-group-exchange-sha256 (1024-bit) -- [fail] using small 1024-bit modulus + `- [info] available since OpenSSH 4.4 +(kex) diffie-hellman-group-exchange-sha1 (1024-bit) -- [fail] using small 1024-bit modulus + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.3.0 +(kex) diffie-hellman-group14-sha1 -- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 3.9, Dropbear SSH 0.53 +(kex) diffie-hellman-group1-sha1 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [fail] disabled (in client) since OpenSSH 7.0, logjam attack + `- [warn] using small 1024-bit modulus + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.3.0, Dropbear SSH 0.28 + +# host-key algorithms +(key) ssh-rsa (1024-bit) -- [fail] using small 1024-bit modulus + `- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28 +(key) ssh-rsa-cert-v01@openssh.com (1024-bit cert/3072-bit CA) -- [fail] using small 1024-bit modulus + `- [info] available since OpenSSH 5.6 + +# encryption algorithms (ciphers) +(enc) aes128-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 +(enc) aes192-ctr -- [info] available since OpenSSH 3.7 +(enc) aes256-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 +(enc) arcfour256 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher + `- [info] available since OpenSSH 4.2 +(enc) arcfour128 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher + `- [info] available since OpenSSH 4.2 +(enc) aes128-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0, Dropbear SSH 0.28 +(enc) 3des-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.4, unsafe algorithm + `- [warn] using weak cipher + `- [warn] using weak cipher mode + `- [warn] using small 64-bit block size + `- [info] available since OpenSSH 1.2.2, Dropbear SSH 0.28 +(enc) blowfish-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [fail] disabled since Dropbear SSH 0.53 + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher mode + `- [warn] using small 64-bit block size + `- [info] available since OpenSSH 1.2.2, Dropbear SSH 0.28 +(enc) cast128-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher mode + `- [warn] using small 64-bit block size + `- [info] available since OpenSSH 2.1.0 +(enc) aes192-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0 +(enc) aes256-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0, Dropbear SSH 0.47 +(enc) arcfour -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher + `- [info] available since OpenSSH 2.1.0 +(enc) rijndael-cbc@lysator.liu.se -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0 + +# message authentication code algorithms +(mac) hmac-md5 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.1.0, Dropbear SSH 0.28 +(mac) hmac-sha1 -- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.1.0, Dropbear SSH 0.28 +(mac) umac-64@openssh.com -- [warn] using encrypt-and-MAC mode + `- [warn] using small 64-bit tag size + `- [info] available since OpenSSH 4.7 +(mac) hmac-ripemd160 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [info] available since OpenSSH 2.5.0 +(mac) hmac-ripemd160@openssh.com -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [info] available since OpenSSH 2.1.0 +(mac) hmac-sha1-96 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.47 +(mac) hmac-md5-96 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.5.0 + +# fingerprints +(fin) ssh-rsa: SHA256:YZ457EBcJTSxRKI3yXRgtAj3PBf5B9/F36b1SVooml4 + +# algorithm recommendations (for OpenSSH 5.6) +(rec) !diffie-hellman-group-exchange-sha256 -- kex algorithm to change (increase modulus size to 2048 bits or larger)  +(rec) !ssh-rsa -- key algorithm to change (increase modulus size to 2048 bits or larger)  +(rec) !ssh-rsa-cert-v01@openssh.com -- key algorithm to change (increase modulus size to 2048 bits or larger)  +(rec) -3des-cbc -- enc algorithm to remove  +(rec) -aes128-cbc -- enc algorithm to remove  +(rec) -aes192-cbc -- enc algorithm to remove  +(rec) -aes256-cbc -- enc algorithm to remove  +(rec) -arcfour -- enc algorithm to remove  +(rec) -arcfour128 -- enc algorithm to remove  +(rec) -arcfour256 -- enc algorithm to remove  +(rec) -blowfish-cbc -- enc algorithm to remove  +(rec) -cast128-cbc -- enc algorithm to remove  +(rec) -diffie-hellman-group-exchange-sha1 -- kex algorithm to remove  +(rec) -diffie-hellman-group1-sha1 -- kex algorithm to remove  +(rec) -hmac-md5 -- mac algorithm to remove  +(rec) -hmac-md5-96 -- mac algorithm to remove  +(rec) -hmac-ripemd160 -- mac algorithm to remove  +(rec) -hmac-ripemd160@openssh.com -- mac algorithm to remove  +(rec) -hmac-sha1-96 -- mac algorithm to remove  +(rec) -rijndael-cbc@lysator.liu.se -- enc algorithm to remove  +(rec) -diffie-hellman-group14-sha1 -- kex algorithm to remove  + diff --git a/test/docker/expected_results/openssh_5.6p1_test4.txt b/test/docker/expected_results/openssh_5.6p1_test4.txt new file mode 100644 index 0000000..a5fa335 --- /dev/null +++ b/test/docker/expected_results/openssh_5.6p1_test4.txt @@ -0,0 +1,141 @@ +# general +(gen) banner: SSH-2.0-OpenSSH_5.6 +(gen) software: OpenSSH 5.6 +(gen) compatibility: OpenSSH 5.6-6.6, Dropbear SSH 0.53+ (some functionality from 0.52) +(gen) compression: enabled (zlib@openssh.com) + +# security +(cve) CVE-2016-3115 -- (CVSSv2: 5.5) bypass command restrictions via crafted X11 forwarding data +(cve) CVE-2016-1907 -- (CVSSv2: 5.0) cause DoS via crafted network traffic (out of bounds read) +(cve) CVE-2015-6564 -- (CVSSv2: 6.9) privilege escalation via leveraging sshd uid +(cve) CVE-2015-6563 -- (CVSSv2: 1.9) conduct impersonation attack +(cve) CVE-2014-2532 -- (CVSSv2: 5.8) bypass environment restrictions via specific string before wildcard +(cve) CVE-2014-1692 -- (CVSSv2: 7.5) cause DoS via triggering error condition (memory corruption) +(cve) CVE-2012-0814 -- (CVSSv2: 3.5) leak data via debug messages +(cve) CVE-2011-5000 -- (CVSSv2: 3.5) cause DoS via large value in certain length field (memory consumption) +(cve) CVE-2010-5107 -- (CVSSv2: 5.0) cause DoS via large number of connections (slot exhaustion) +(cve) CVE-2010-4755 -- (CVSSv2: 4.0) cause DoS via crafted glob expression (CPU and memory consumption) +(cve) CVE-2010-4478 -- (CVSSv2: 7.5) bypass authentication check via crafted values + +# key exchange algorithms +(kex) diffie-hellman-group-exchange-sha256 (1024-bit) -- [fail] using small 1024-bit modulus + `- [info] available since OpenSSH 4.4 +(kex) diffie-hellman-group-exchange-sha1 (1024-bit) -- [fail] using small 1024-bit modulus + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.3.0 +(kex) diffie-hellman-group14-sha1 -- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 3.9, Dropbear SSH 0.53 +(kex) diffie-hellman-group1-sha1 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [fail] disabled (in client) since OpenSSH 7.0, logjam attack + `- [warn] using small 1024-bit modulus + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.3.0, Dropbear SSH 0.28 + +# host-key algorithms +(key) ssh-rsa (3072-bit) -- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28 +(key) ssh-rsa-cert-v01@openssh.com (3072-bit cert/1024-bit CA) -- [fail] using small 1024-bit modulus + `- [info] available since OpenSSH 5.6 + +# encryption algorithms (ciphers) +(enc) aes128-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 +(enc) aes192-ctr -- [info] available since OpenSSH 3.7 +(enc) aes256-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 +(enc) arcfour256 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher + `- [info] available since OpenSSH 4.2 +(enc) arcfour128 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher + `- [info] available since OpenSSH 4.2 +(enc) aes128-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0, Dropbear SSH 0.28 +(enc) 3des-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.4, unsafe algorithm + `- [warn] using weak cipher + `- [warn] using weak cipher mode + `- [warn] using small 64-bit block size + `- [info] available since OpenSSH 1.2.2, Dropbear SSH 0.28 +(enc) blowfish-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [fail] disabled since Dropbear SSH 0.53 + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher mode + `- [warn] using small 64-bit block size + `- [info] available since OpenSSH 1.2.2, Dropbear SSH 0.28 +(enc) cast128-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher mode + `- [warn] using small 64-bit block size + `- [info] available since OpenSSH 2.1.0 +(enc) aes192-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0 +(enc) aes256-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0, Dropbear SSH 0.47 +(enc) arcfour -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher + `- [info] available since OpenSSH 2.1.0 +(enc) rijndael-cbc@lysator.liu.se -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0 + +# message authentication code algorithms +(mac) hmac-md5 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.1.0, Dropbear SSH 0.28 +(mac) hmac-sha1 -- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.1.0, Dropbear SSH 0.28 +(mac) umac-64@openssh.com -- [warn] using encrypt-and-MAC mode + `- [warn] using small 64-bit tag size + `- [info] available since OpenSSH 4.7 +(mac) hmac-ripemd160 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [info] available since OpenSSH 2.5.0 +(mac) hmac-ripemd160@openssh.com -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [info] available since OpenSSH 2.1.0 +(mac) hmac-sha1-96 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.47 +(mac) hmac-md5-96 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.5.0 + +# fingerprints +(fin) ssh-rsa: SHA256:nsWtdJ9Z67Vrf7OsUzQov7esXhsWAfVppArGh25u244 + +# algorithm recommendations (for OpenSSH 5.6) +(rec) !diffie-hellman-group-exchange-sha256 -- kex algorithm to change (increase modulus size to 2048 bits or larger)  +(rec) !ssh-rsa-cert-v01@openssh.com -- key algorithm to change (increase modulus size to 2048 bits or larger)  +(rec) -3des-cbc -- enc algorithm to remove  +(rec) -aes128-cbc -- enc algorithm to remove  +(rec) -aes192-cbc -- enc algorithm to remove  +(rec) -aes256-cbc -- enc algorithm to remove  +(rec) -arcfour -- enc algorithm to remove  +(rec) -arcfour128 -- enc algorithm to remove  +(rec) -arcfour256 -- enc algorithm to remove  +(rec) -blowfish-cbc -- enc algorithm to remove  +(rec) -cast128-cbc -- enc algorithm to remove  +(rec) -diffie-hellman-group-exchange-sha1 -- kex algorithm to remove  +(rec) -diffie-hellman-group1-sha1 -- kex algorithm to remove  +(rec) -hmac-md5 -- mac algorithm to remove  +(rec) -hmac-md5-96 -- mac algorithm to remove  +(rec) -hmac-ripemd160 -- mac algorithm to remove  +(rec) -hmac-ripemd160@openssh.com -- mac algorithm to remove  +(rec) -hmac-sha1-96 -- mac algorithm to remove  +(rec) -rijndael-cbc@lysator.liu.se -- enc algorithm to remove  +(rec) -diffie-hellman-group14-sha1 -- kex algorithm to remove  + diff --git a/test/docker/expected_results/openssh_5.6p1_test5.txt b/test/docker/expected_results/openssh_5.6p1_test5.txt new file mode 100644 index 0000000..7e5cf64 --- /dev/null +++ b/test/docker/expected_results/openssh_5.6p1_test5.txt @@ -0,0 +1,139 @@ +# general +(gen) banner: SSH-2.0-OpenSSH_5.6 +(gen) software: OpenSSH 5.6 +(gen) compatibility: OpenSSH 5.6-6.6, Dropbear SSH 0.53+ (some functionality from 0.52) +(gen) compression: enabled (zlib@openssh.com) + +# security +(cve) CVE-2016-3115 -- (CVSSv2: 5.5) bypass command restrictions via crafted X11 forwarding data +(cve) CVE-2016-1907 -- (CVSSv2: 5.0) cause DoS via crafted network traffic (out of bounds read) +(cve) CVE-2015-6564 -- (CVSSv2: 6.9) privilege escalation via leveraging sshd uid +(cve) CVE-2015-6563 -- (CVSSv2: 1.9) conduct impersonation attack +(cve) CVE-2014-2532 -- (CVSSv2: 5.8) bypass environment restrictions via specific string before wildcard +(cve) CVE-2014-1692 -- (CVSSv2: 7.5) cause DoS via triggering error condition (memory corruption) +(cve) CVE-2012-0814 -- (CVSSv2: 3.5) leak data via debug messages +(cve) CVE-2011-5000 -- (CVSSv2: 3.5) cause DoS via large value in certain length field (memory consumption) +(cve) CVE-2010-5107 -- (CVSSv2: 5.0) cause DoS via large number of connections (slot exhaustion) +(cve) CVE-2010-4755 -- (CVSSv2: 4.0) cause DoS via crafted glob expression (CPU and memory consumption) +(cve) CVE-2010-4478 -- (CVSSv2: 7.5) bypass authentication check via crafted values + +# key exchange algorithms +(kex) diffie-hellman-group-exchange-sha256 (1024-bit) -- [fail] using small 1024-bit modulus + `- [info] available since OpenSSH 4.4 +(kex) diffie-hellman-group-exchange-sha1 (1024-bit) -- [fail] using small 1024-bit modulus + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.3.0 +(kex) diffie-hellman-group14-sha1 -- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 3.9, Dropbear SSH 0.53 +(kex) diffie-hellman-group1-sha1 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [fail] disabled (in client) since OpenSSH 7.0, logjam attack + `- [warn] using small 1024-bit modulus + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.3.0, Dropbear SSH 0.28 + +# host-key algorithms +(key) ssh-rsa (3072-bit) -- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28 +(key) ssh-rsa-cert-v01@openssh.com (3072-bit cert/3072-bit CA) -- [info] available since OpenSSH 5.6 + +# encryption algorithms (ciphers) +(enc) aes128-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 +(enc) aes192-ctr -- [info] available since OpenSSH 3.7 +(enc) aes256-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 +(enc) arcfour256 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher + `- [info] available since OpenSSH 4.2 +(enc) arcfour128 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher + `- [info] available since OpenSSH 4.2 +(enc) aes128-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0, Dropbear SSH 0.28 +(enc) 3des-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.4, unsafe algorithm + `- [warn] using weak cipher + `- [warn] using weak cipher mode + `- [warn] using small 64-bit block size + `- [info] available since OpenSSH 1.2.2, Dropbear SSH 0.28 +(enc) blowfish-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [fail] disabled since Dropbear SSH 0.53 + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher mode + `- [warn] using small 64-bit block size + `- [info] available since OpenSSH 1.2.2, Dropbear SSH 0.28 +(enc) cast128-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher mode + `- [warn] using small 64-bit block size + `- [info] available since OpenSSH 2.1.0 +(enc) aes192-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0 +(enc) aes256-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0, Dropbear SSH 0.47 +(enc) arcfour -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher + `- [info] available since OpenSSH 2.1.0 +(enc) rijndael-cbc@lysator.liu.se -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0 + +# message authentication code algorithms +(mac) hmac-md5 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.1.0, Dropbear SSH 0.28 +(mac) hmac-sha1 -- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.1.0, Dropbear SSH 0.28 +(mac) umac-64@openssh.com -- [warn] using encrypt-and-MAC mode + `- [warn] using small 64-bit tag size + `- [info] available since OpenSSH 4.7 +(mac) hmac-ripemd160 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [info] available since OpenSSH 2.5.0 +(mac) hmac-ripemd160@openssh.com -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [info] available since OpenSSH 2.1.0 +(mac) hmac-sha1-96 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.47 +(mac) hmac-md5-96 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.5.0 + +# fingerprints +(fin) ssh-rsa: SHA256:nsWtdJ9Z67Vrf7OsUzQov7esXhsWAfVppArGh25u244 + +# algorithm recommendations (for OpenSSH 5.6) +(rec) !diffie-hellman-group-exchange-sha256 -- kex algorithm to change (increase modulus size to 2048 bits or larger)  +(rec) -3des-cbc -- enc algorithm to remove  +(rec) -aes128-cbc -- enc algorithm to remove  +(rec) -aes192-cbc -- enc algorithm to remove  +(rec) -aes256-cbc -- enc algorithm to remove  +(rec) -arcfour -- enc algorithm to remove  +(rec) -arcfour128 -- enc algorithm to remove  +(rec) -arcfour256 -- enc algorithm to remove  +(rec) -blowfish-cbc -- enc algorithm to remove  +(rec) -cast128-cbc -- enc algorithm to remove  +(rec) -diffie-hellman-group-exchange-sha1 -- kex algorithm to remove  +(rec) -diffie-hellman-group1-sha1 -- kex algorithm to remove  +(rec) -hmac-md5 -- mac algorithm to remove  +(rec) -hmac-md5-96 -- mac algorithm to remove  +(rec) -hmac-ripemd160 -- mac algorithm to remove  +(rec) -hmac-ripemd160@openssh.com -- mac algorithm to remove  +(rec) -hmac-sha1-96 -- mac algorithm to remove  +(rec) -rijndael-cbc@lysator.liu.se -- enc algorithm to remove  +(rec) -diffie-hellman-group14-sha1 -- kex algorithm to remove  + diff --git a/test/docker/expected_results/openssh_8.0p1_test1.txt b/test/docker/expected_results/openssh_8.0p1_test1.txt new file mode 100644 index 0000000..25981cf --- /dev/null +++ b/test/docker/expected_results/openssh_8.0p1_test1.txt @@ -0,0 +1,79 @@ +# general +(gen) banner: SSH-2.0-OpenSSH_8.0 +(gen) software: OpenSSH 8.0 +(gen) compatibility: OpenSSH 7.4+, Dropbear SSH 2016.73+ +(gen) compression: enabled (zlib@openssh.com) + +# key exchange algorithms +(kex) curve25519-sha256 -- [info] available since OpenSSH 7.4 +(kex) curve25519-sha256@libssh.org -- [info] available since OpenSSH 6.5, Dropbear SSH 2013.62 +(kex) ecdh-sha2-nistp256 -- [fail] using weak elliptic curves + `- [info] available since OpenSSH 5.7, Dropbear SSH 2013.62 +(kex) ecdh-sha2-nistp384 -- [fail] using weak elliptic curves + `- [info] available since OpenSSH 5.7, Dropbear SSH 2013.62 +(kex) ecdh-sha2-nistp521 -- [fail] using weak elliptic curves + `- [info] available since OpenSSH 5.7, Dropbear SSH 2013.62 +(kex) diffie-hellman-group-exchange-sha256 (2048-bit) -- [info] available since OpenSSH 4.4 +(kex) diffie-hellman-group16-sha512 -- [info] available since OpenSSH 7.3, Dropbear SSH 2016.73 +(kex) diffie-hellman-group18-sha512 -- [info] available since OpenSSH 7.3 +(kex) diffie-hellman-group14-sha256 -- [info] available since OpenSSH 7.3, Dropbear SSH 2016.73 +(kex) diffie-hellman-group14-sha1 -- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 3.9, Dropbear SSH 0.53 + +# host-key algorithms +(key) rsa-sha2-512 (3072-bit) -- [info] available since OpenSSH 7.2 +(key) rsa-sha2-256 (3072-bit) -- [info] available since OpenSSH 7.2 +(key) ssh-rsa (3072-bit) -- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28 +(key) ecdsa-sha2-nistp256 -- [fail] using weak elliptic curves + `- [warn] using weak random number generator could reveal the key + `- [info] available since OpenSSH 5.7, Dropbear SSH 2013.62 +(key) ssh-ed25519 -- [info] available since OpenSSH 6.5 + +# encryption algorithms (ciphers) +(enc) chacha20-poly1305@openssh.com -- [info] available since OpenSSH 6.5 + `- [info] default cipher since OpenSSH 6.9. +(enc) aes128-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 +(enc) aes192-ctr -- [info] available since OpenSSH 3.7 +(enc) aes256-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 +(enc) aes128-gcm@openssh.com -- [info] available since OpenSSH 6.2 +(enc) aes256-gcm@openssh.com -- [info] available since OpenSSH 6.2 + +# message authentication code algorithms +(mac) umac-64-etm@openssh.com -- [warn] using small 64-bit tag size + `- [info] available since OpenSSH 6.2 +(mac) umac-128-etm@openssh.com -- [info] available since OpenSSH 6.2 +(mac) hmac-sha2-256-etm@openssh.com -- [info] available since OpenSSH 6.2 +(mac) hmac-sha2-512-etm@openssh.com -- [info] available since OpenSSH 6.2 +(mac) hmac-sha1-etm@openssh.com -- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 6.2 +(mac) umac-64@openssh.com -- [warn] using encrypt-and-MAC mode + `- [warn] using small 64-bit tag size + `- [info] available since OpenSSH 4.7 +(mac) umac-128@openssh.com -- [warn] using encrypt-and-MAC mode + `- [info] available since OpenSSH 6.2 +(mac) hmac-sha2-256 -- [warn] using encrypt-and-MAC mode + `- [info] available since OpenSSH 5.9, Dropbear SSH 2013.56 +(mac) hmac-sha2-512 -- [warn] using encrypt-and-MAC mode + `- [info] available since OpenSSH 5.9, Dropbear SSH 2013.56 +(mac) hmac-sha1 -- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.1.0, Dropbear SSH 0.28 + +# fingerprints +(fin) ssh-ed25519: SHA256:UrnXIVH+7dlw8UqYocl48yUEcKrthGDQG2CPCgp7MxU +(fin) ssh-rsa: SHA256:nsWtdJ9Z67Vrf7OsUzQov7esXhsWAfVppArGh25u244 + +# algorithm recommendations (for OpenSSH 8.0) +(rec) -ecdh-sha2-nistp256 -- kex algorithm to remove  +(rec) -ecdh-sha2-nistp384 -- kex algorithm to remove  +(rec) -ecdh-sha2-nistp521 -- kex algorithm to remove  +(rec) -ecdsa-sha2-nistp256 -- key algorithm to remove  +(rec) -diffie-hellman-group14-sha1 -- kex algorithm to remove  +(rec) -hmac-sha1 -- mac algorithm to remove  +(rec) -hmac-sha1-etm@openssh.com -- mac algorithm to remove  +(rec) -hmac-sha2-256 -- mac algorithm to remove  +(rec) -hmac-sha2-512 -- mac algorithm to remove  +(rec) -umac-128@openssh.com -- mac algorithm to remove  +(rec) -umac-64-etm@openssh.com -- mac algorithm to remove  +(rec) -umac-64@openssh.com -- mac algorithm to remove  + diff --git a/test/docker/expected_results/openssh_8.0p1_test2.txt b/test/docker/expected_results/openssh_8.0p1_test2.txt new file mode 100644 index 0000000..3a23378 --- /dev/null +++ b/test/docker/expected_results/openssh_8.0p1_test2.txt @@ -0,0 +1,75 @@ +# general +(gen) banner: SSH-2.0-OpenSSH_8.0 +(gen) software: OpenSSH 8.0 +(gen) compatibility: OpenSSH 7.4+, Dropbear SSH 2016.73+ +(gen) compression: enabled (zlib@openssh.com) + +# key exchange algorithms +(kex) curve25519-sha256 -- [info] available since OpenSSH 7.4 +(kex) curve25519-sha256@libssh.org -- [info] available since OpenSSH 6.5, Dropbear SSH 2013.62 +(kex) ecdh-sha2-nistp256 -- [fail] using weak elliptic curves + `- [info] available since OpenSSH 5.7, Dropbear SSH 2013.62 +(kex) ecdh-sha2-nistp384 -- [fail] using weak elliptic curves + `- [info] available since OpenSSH 5.7, Dropbear SSH 2013.62 +(kex) ecdh-sha2-nistp521 -- [fail] using weak elliptic curves + `- [info] available since OpenSSH 5.7, Dropbear SSH 2013.62 +(kex) diffie-hellman-group-exchange-sha256 (2048-bit) -- [info] available since OpenSSH 4.4 +(kex) diffie-hellman-group16-sha512 -- [info] available since OpenSSH 7.3, Dropbear SSH 2016.73 +(kex) diffie-hellman-group18-sha512 -- [info] available since OpenSSH 7.3 +(kex) diffie-hellman-group14-sha256 -- [info] available since OpenSSH 7.3, Dropbear SSH 2016.73 +(kex) diffie-hellman-group14-sha1 -- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 3.9, Dropbear SSH 0.53 + +# host-key algorithms +(key) ssh-ed25519 -- [info] available since OpenSSH 6.5 +(key) ssh-ed25519-cert-v01@openssh.com -- [info] available since OpenSSH 6.5 + +# encryption algorithms (ciphers) +(enc) chacha20-poly1305@openssh.com -- [info] available since OpenSSH 6.5 + `- [info] default cipher since OpenSSH 6.9. +(enc) aes128-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 +(enc) aes192-ctr -- [info] available since OpenSSH 3.7 +(enc) aes256-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 +(enc) aes128-gcm@openssh.com -- [info] available since OpenSSH 6.2 +(enc) aes256-gcm@openssh.com -- [info] available since OpenSSH 6.2 + +# message authentication code algorithms +(mac) umac-64-etm@openssh.com -- [warn] using small 64-bit tag size + `- [info] available since OpenSSH 6.2 +(mac) umac-128-etm@openssh.com -- [info] available since OpenSSH 6.2 +(mac) hmac-sha2-256-etm@openssh.com -- [info] available since OpenSSH 6.2 +(mac) hmac-sha2-512-etm@openssh.com -- [info] available since OpenSSH 6.2 +(mac) hmac-sha1-etm@openssh.com -- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 6.2 +(mac) umac-64@openssh.com -- [warn] using encrypt-and-MAC mode + `- [warn] using small 64-bit tag size + `- [info] available since OpenSSH 4.7 +(mac) umac-128@openssh.com -- [warn] using encrypt-and-MAC mode + `- [info] available since OpenSSH 6.2 +(mac) hmac-sha2-256 -- [warn] using encrypt-and-MAC mode + `- [info] available since OpenSSH 5.9, Dropbear SSH 2013.56 +(mac) hmac-sha2-512 -- [warn] using encrypt-and-MAC mode + `- [info] available since OpenSSH 5.9, Dropbear SSH 2013.56 +(mac) hmac-sha1 -- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.1.0, Dropbear SSH 0.28 + +# fingerprints +(fin) ssh-ed25519: SHA256:UrnXIVH+7dlw8UqYocl48yUEcKrthGDQG2CPCgp7MxU + +# algorithm recommendations (for OpenSSH 8.0) +(rec) -ecdh-sha2-nistp256 -- kex algorithm to remove  +(rec) -ecdh-sha2-nistp384 -- kex algorithm to remove  +(rec) -ecdh-sha2-nistp521 -- kex algorithm to remove  +(rec) +rsa-sha2-256 -- key algorithm to append  +(rec) +rsa-sha2-512 -- key algorithm to append  +(rec) +ssh-rsa -- key algorithm to append  +(rec) -diffie-hellman-group14-sha1 -- kex algorithm to remove  +(rec) -hmac-sha1 -- mac algorithm to remove  +(rec) -hmac-sha1-etm@openssh.com -- mac algorithm to remove  +(rec) -hmac-sha2-256 -- mac algorithm to remove  +(rec) -hmac-sha2-512 -- mac algorithm to remove  +(rec) -umac-128@openssh.com -- mac algorithm to remove  +(rec) -umac-64-etm@openssh.com -- mac algorithm to remove  +(rec) -umac-64@openssh.com -- mac algorithm to remove  + diff --git a/test/docker/expected_results/openssh_8.0p1_test3.txt b/test/docker/expected_results/openssh_8.0p1_test3.txt new file mode 100644 index 0000000..81bfa9c --- /dev/null +++ b/test/docker/expected_results/openssh_8.0p1_test3.txt @@ -0,0 +1,39 @@ +# general +(gen) banner: SSH-2.0-OpenSSH_8.0 +(gen) software: OpenSSH 8.0 +(gen) compatibility: OpenSSH 7.4+, Dropbear SSH 2013.62+ +(gen) compression: enabled (zlib@openssh.com) + +# key exchange algorithms +(kex) curve25519-sha256 -- [info] available since OpenSSH 7.4 +(kex) curve25519-sha256@libssh.org -- [info] available since OpenSSH 6.5, Dropbear SSH 2013.62 +(kex) diffie-hellman-group-exchange-sha256 (2048-bit) -- [info] available since OpenSSH 4.4 + +# host-key algorithms +(key) ssh-ed25519 -- [info] available since OpenSSH 6.5 + +# encryption algorithms (ciphers) +(enc) chacha20-poly1305@openssh.com -- [info] available since OpenSSH 6.5 + `- [info] default cipher since OpenSSH 6.9. +(enc) aes256-gcm@openssh.com -- [info] available since OpenSSH 6.2 +(enc) aes128-gcm@openssh.com -- [info] available since OpenSSH 6.2 +(enc) aes256-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 +(enc) aes192-ctr -- [info] available since OpenSSH 3.7 +(enc) aes128-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 + +# message authentication code algorithms +(mac) hmac-sha2-256-etm@openssh.com -- [info] available since OpenSSH 6.2 +(mac) hmac-sha2-512-etm@openssh.com -- [info] available since OpenSSH 6.2 +(mac) umac-128-etm@openssh.com -- [info] available since OpenSSH 6.2 + +# fingerprints +(fin) ssh-ed25519: SHA256:UrnXIVH+7dlw8UqYocl48yUEcKrthGDQG2CPCgp7MxU + +# algorithm recommendations (for OpenSSH 8.0) +(rec) +diffie-hellman-group14-sha256 -- kex algorithm to append  +(rec) +diffie-hellman-group16-sha512 -- kex algorithm to append  +(rec) +diffie-hellman-group18-sha512 -- kex algorithm to append  +(rec) +rsa-sha2-256 -- key algorithm to append  +(rec) +rsa-sha2-512 -- key algorithm to append  +(rec) +ssh-rsa -- key algorithm to append  + diff --git a/test/docker/host_ca_ed25519 b/test/docker/host_ca_ed25519 new file mode 100644 index 0000000..7b8c41b --- /dev/null +++ b/test/docker/host_ca_ed25519 @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACAbM9Wp3ZPcC8Ifhu6GjNDJaoMg7KxO0el2+r9J35TltQAAAKAa0zr8GtM6 +/AAAAAtzc2gtZWQyNTUxOQAAACAbM9Wp3ZPcC8Ifhu6GjNDJaoMg7KxO0el2+r9J35TltQ +AAAEC/j/BpfmgaZqNMTkJXO4cKZBr31N5z33IRFjh5m6IDDhsz1andk9wLwh+G7oaM0Mlq +gyDsrE7R6Xb6v0nflOW1AAAAHWpkb2dAbG9jYWxob3N0LndvbmRlcmxhbmQubG9s +-----END OPENSSH PRIVATE KEY----- diff --git a/test/docker/host_ca_ed25519.pub b/test/docker/host_ca_ed25519.pub new file mode 100644 index 0000000..01e745f --- /dev/null +++ b/test/docker/host_ca_ed25519.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBsz1andk9wLwh+G7oaM0MlqgyDsrE7R6Xb6v0nflOW1 jdog@localhost.wonderland.lol diff --git a/test/docker/host_ca_rsa_1024 b/test/docker/host_ca_rsa_1024 new file mode 100644 index 0000000..337b777 --- /dev/null +++ b/test/docker/host_ca_rsa_1024 @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXgIBAAKBgQDnRlN3AFnUe2lFf5XG9UhXLr/9POruNTFbMt0zrjOUSjmAS7hS +6pDv5VEToT6DaR1EQUYaqSMpHYzZhuCK52vrydOm5XFbJ7712r9MyZQUhoVZx8Su +dBHzVDIVO3jcMMWIlrfWBMnUaUHEqpmy88Y7gKDa2TWxJg1+hg51KqHrUQIDAQAB +AoGBANALOUXRcP1tTtOP4+In/709dsONKyDBhPavGMFGsWtyIavBcbxU+bBzrq1j +3WJFCmi99xxAjjqMNInxhMgvSaoJtsiY0/FFxqRy6l/ZnRjI6hrVKR8whrPKVgBF +pvbjeQIn9txeCYA8kwl/Si762u7byq+qvupE53xMP94J02KBAkEA/Q4+Hn1Rjblw +VXynF+oXIq6iZy+8PW+Y/FIL8d31ehzfcssCMdFV6S3/wBoQkWby30oGC/xGmHGR +6ffXGilByQJBAOn3NMrBPXNkaPeQtgV3tk4s1dRDQYhbqGNz6tcgThyyPdhJCmCy +jgUEhLwAetsDI8/+3avWbo6/csOV+BvpYUkCQQDQyEp6L1z0+FV1QqY99dZmt/yn +89t0OLnZG/xc7osU1/OHq3TBE3y1KU2D+j1HKdAiZ9l7VAYOykzf46qmG/n5AkEA +2kWjfcjcIIw7lULvXZh6fuI7NwTr3V/Nb8MUA1EDLqhnJCG4SdAqyKmXf6Fe/HYo +cgKPIaIykIAxfCCsULXg6QJAOxB0CKYJlopVBdjGMlGqOEneWTmb1A2INQDE2Una +LkSd0Rr8OiEzDeemV7j3Ec4BH0HxGMnHDxMybZwoZRnRPw== +-----END RSA PRIVATE KEY----- diff --git a/test/docker/host_ca_rsa_1024.pub b/test/docker/host_ca_rsa_1024.pub new file mode 100644 index 0000000..6d861d6 --- /dev/null +++ b/test/docker/host_ca_rsa_1024.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDnRlN3AFnUe2lFf5XG9UhXLr/9POruNTFbMt0zrjOUSjmAS7hS6pDv5VEToT6DaR1EQUYaqSMpHYzZhuCK52vrydOm5XFbJ7712r9MyZQUhoVZx8SudBHzVDIVO3jcMMWIlrfWBMnUaUHEqpmy88Y7gKDa2TWxJg1+hg51KqHrUQ== jdog@localhost.wonderland.lol diff --git a/test/docker/host_ca_rsa_3072 b/test/docker/host_ca_rsa_3072 new file mode 100644 index 0000000..dd04653 --- /dev/null +++ b/test/docker/host_ca_rsa_3072 @@ -0,0 +1,39 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIG4wIBAAKCAYEAqxQEIbj8w0TrBY1fDO81curijQrdLOUr8Vl8XECWc5QGd1Lk +AG80NgdcCBPvjWxZSmYrKeqA78GUdN+KgycE0ztpxYSXKHZMaIM5Xe94BB+BocH9 +1vd/2iBzGeed1nV/zfAdq2AEHQj1TpII+a+z25yxv2PuwVTTwwo9I/6JgNq3evH4 +Hbwgr3SRfEEYZQ+YL8cOpBuNg1YZOR0k1yk23ZqAd92JybxZ4iCtOt7rcj2sFHzN +u1U544wWBwIL5yZZKTgBhY4dqfT2Ep7IzR5HdsdrvQV9qC92GM1zDE+U3AwrVKjH +s0YZq3jzcq/yvFDCcMMRz4/0pGFFU26oWma+n3vbAxKJoL+rhG8QM9+l2qFlLGsn +M0kUXAJXsPKbygpaP8Z3U4eKgTuJ2GuS9eLIFnB7mrwD75V6GgN9q5mY89DfkVSk +HaoqpY8pPdRkz9QAmMEuLtHmv29CVOpfX5v/rsm7wASAZqtUlmFu4rFGBLwvZbUl +Wu02HmgBT47g6EIfAgMBAAECggGAKVCdKtO03yd+pomcodAHFWiaK7uq7FOwCAo3 +WUQT0Xe3FAwFmgFBF6cxV5YQ7RN0gN4poGbMmpoiUxNFLSU4KhcYFSZPJutiyn6e +VQwm7L/7G2hw+AAvdSsPAPuJh6g6pC5Py/pVI/ns2/uyhTIkem3eEz18BF6LAXgw +icfHx0GKu/tBk1TCg/zfwaUq0gUxGKC27XTl+QjK8JsUMY33fQ755Xiv9PMytcR0 +cVoyfBVewFffi1UqtMQ48ZpR65G743RxrP4/wcwsfD7n5LJLdyxQkh3gIMTJ8dd/ +R5V4FlueorRgjTbLTjGDxNrCAJ+locezhEEPXsPh2q0KiIXGyz2AMxaOqFmhU8oK +aVVt8pWJ+YsrKIgc/A3s18ezO8uO5ZdtjQ+CWguduUGY7YgWezGLO1LPxhJC4d7b +Q/xpeKveTRlcScAqOUzKgSuEhcvPgj8paUcRUoiXm4qiJBY5sXJks+YGp8BGksH0 +O94no+Ns2G58MlL+RyXk3JWrc6zRAoHBANdPplY2sIuIiiEBu95f1Qar1nCBHhB2 +i+HpnsUOdSlbxwMxoF8ffeN9N+DQqaqPu1RhFa5xbB2EUSujvOnL7b/RWqe1X9Po +UIt5UjXctNP/HYcQDyjXY+rV5SZhHDyv6TBYurNZlvlBivliDz82THPRtqVxed3B +w2MeaSkKAQ8rA7PE+0j3TG+YtIij0mHOhNPJgEZ/XZ9MIQOGMycRJhwOlclBI5NP +Ak6p30ArnU2fX4qMkU3i+wqUfXS1hhDihwKBwQDLaHWPIWPVbWdcCbYQTcUmFC3i +xkxd0UuLcfS9csk61nvdFj7m8tMExX+3fIo/fHEtzDd98Alc1i6/f6ePl0CX6NDu +QIWLryI1QQRQidHCdw0wQ3N3VD4ZXJHDeqBxogVAkA7A/1QeXwcXE/Xj2ZgyDwhL +3+myjmvWtw9zJsXL0F3tpPzn+Mrf0KRkWOaluOw7hMMjVjrgu6g24HMWbHHVLRTx +dlAI7tgxCAPe2SEi+1mzaVUZ8cfgqYqC3X66UakCgcEAopxtK7+yJi/A4pzEnnYS +FS/CjMV3R0fA7aXbW0hIBCxkaW0Zib3m/eCcSxZMjZxwBpIsJctTtBcylprbGlgB +/1TF+tNoxEo4Sp4eEL/XciTC0Da4vEewFrPklM/S26KfovvgRYPsGeP+aco9aahA +pVhFcT36pBiq0DkvgucjValO6n5iqgDboYzbDDdttKCcgLc2Qgf/VUfRxy+bgm3Z +MmdxiMXBcIfDXlW9XmGSNAWhyqnPM9uxbZQoC/Tsg+QRAoHANHMcFSsz9f2+8DGk +27FiC76aUmZ1nJ9yTmO1CwDFOMHDsK+iyqSEmy9eDm8zqsko2flVuciicWjdJw4A +o/sJceJbtYO3q9weAwNf3HCdQPq30OEjrfpwBNQk1fYR1xtDJXHADC4Kf8ZbKq0/ +81/Rad8McZwsQ5mL3xLXDgdKa5KwFa48dIhnr6y6JxHxb3wule5W7w62Ierhpjzc +EEUoWSLFyrmKS7Ni1cnOTbFJZR7Q831Or2Dz/E9bYwFAQ0T5AoHAM4/zU+8rsbdD +FvvhWsj7Ivfh6pxx1Tl1Wccaauea9AJayHht0FOzkycpJrH1E+6F5MzhkFFU1SUY +60NZxzSZgbU0HBrJRcRFyo510iMcnctdTdyh8p7nweGoD0oqXzf6cHqrUep8Y8rQ +gkSVhPE31+NGlPbwz+NOflcaaAWYiDC6wjVt1asaZq292SJD4DF1fAUkbQ2hxgyQ ++G/6y5ovrcGnh7q63RLhW1TRf8dD2D2Av9UgXDmWZAZ5n838FS+X +-----END RSA PRIVATE KEY----- diff --git a/test/docker/host_ca_rsa_3072.pub b/test/docker/host_ca_rsa_3072.pub new file mode 100644 index 0000000..b728ed7 --- /dev/null +++ b/test/docker/host_ca_rsa_3072.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCrFAQhuPzDROsFjV8M7zVy6uKNCt0s5SvxWXxcQJZzlAZ3UuQAbzQ2B1wIE++NbFlKZisp6oDvwZR034qDJwTTO2nFhJcodkxogzld73gEH4Ghwf3W93/aIHMZ553WdX/N8B2rYAQdCPVOkgj5r7PbnLG/Y+7BVNPDCj0j/omA2rd68fgdvCCvdJF8QRhlD5gvxw6kG42DVhk5HSTXKTbdmoB33YnJvFniIK063utyPawUfM27VTnjjBYHAgvnJlkpOAGFjh2p9PYSnsjNHkd2x2u9BX2oL3YYzXMMT5TcDCtUqMezRhmrePNyr/K8UMJwwxHPj/SkYUVTbqhaZr6fe9sDEomgv6uEbxAz36XaoWUsayczSRRcAlew8pvKClo/xndTh4qBO4nYa5L14sgWcHuavAPvlXoaA32rmZjz0N+RVKQdqiqljyk91GTP1ACYwS4u0ea/b0JU6l9fm/+uybvABIBmq1SWYW7isUYEvC9ltSVa7TYeaAFPjuDoQh8= jdog@localhost.wonderland.lol diff --git a/test/docker/moduli_1024 b/test/docker/moduli_1024 new file mode 100644 index 0000000..bd81dae --- /dev/null +++ b/test/docker/moduli_1024 @@ -0,0 +1,44 @@ +20190821035337 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC08BE313B +20190821035338 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC08C0B443 +20190821035338 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC08D1AF8B +20190821035338 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC08E76DDB +20190821035338 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC08E8F5D3 +20190821035338 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC08EE3F1B +20190821035338 2 6 100 1023 5 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC08F28387 +20190821035339 2 6 100 1023 5 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC08F69A57 +20190821035339 2 6 100 1023 5 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC0903B157 +20190821035339 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC0905C973 +20190821035339 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC0909BCD3 +20190821035339 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC090F4A2B +20190821035340 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC0933BC13 +20190821035340 2 6 100 1023 5 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC09395757 +20190821035340 2 6 100 1023 5 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC093F40D7 +20190821035340 2 6 100 1023 5 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC09478D4F +20190821035340 2 6 100 1023 5 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC0953A4D7 +20190821035340 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC095B5C7B +20190821035341 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC09696573 +20190821035341 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC096BA243 +20190821035341 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC096F3903 +20190821035341 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC09850E4B +20190821035341 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC098A1C23 +20190821035341 2 6 100 1023 5 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC098E08E7 +20190821035342 2 6 100 1023 5 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC09A4FF7F +20190821035342 2 6 100 1023 5 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC09AE4707 +20190821035342 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC09B4CE73 +20190821035342 2 6 100 1023 5 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC09C60C6F +20190821035342 2 6 100 1023 5 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC09D2588F +20190821035343 2 6 100 1023 5 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC0A025067 +20190821035343 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC0A0E38EB +20190821035343 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC0A213923 +20190821035344 2 6 100 1023 5 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC0A390CA7 +20190821035344 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC0A3C7ADB +20190821035344 2 6 100 1023 5 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC0A44D497 +20190821035344 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC0A479B13 +20190821035345 2 6 100 1023 5 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC0A5EF01F +20190821035345 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC0A615D43 +20190821035345 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC0A6BEADB +20190821035345 2 6 100 1023 5 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC0A86309F +20190821035345 2 6 100 1023 5 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC0A991E8F +20190821035346 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC0AA32C53 +20190821035346 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC0AA9FAAB +20190821035346 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC0AAC42BB diff --git a/test/docker/ssh_host_dsa_key b/test/docker/ssh_host_dsa_key new file mode 100644 index 0000000..ecd47f9 --- /dev/null +++ b/test/docker/ssh_host_dsa_key @@ -0,0 +1,12 @@ +-----BEGIN DSA PRIVATE KEY----- +MIIBugIBAAKBgQDth1eV+A8j191R0ey0dVXL2LGNGYM+a+PomSa7suK8xNCeVLKC +YpQ6VSWpAf6FbRWev1UVo8IpbglwFZPcyFPK2G1H7p45ows2SN4CleszDD56e6W0 +3Plc+qMqSJ6LTjr4M5+HqTDOM3CS72d7MXUkfHQiagyrWQhXyc0kFsNJLwIVAKg7 +b5+NiIZzpg5IEH0tlYFQpuhBAoGAGcbq79QqNNZRuPCE/F05sCoTRGCmFnDjCuCg +WN7wNRotjMz/S3pHtCCeuTT1jT6Hy0ZFHftv0t/GF8GBRgeokUbS4ytHpOkFWcTz +8oFguDL44nq8eNfSY6bzEl84qsgEe4HP93mB4FR1ZUUgI4b7gCBOYEFl3yPiH7H1 +p7Z9E1oCgYAl1UPQkeRhElz+AgEbNsnMKu1+6O3/z95D1Wvv4OEwAImbytlBaC7p +kwJElJNsMMfGqCC8OHdJ0e4VQQUwk/GOhD0MFhVQHBtVZYbiWmVkpfHf1ouUQg3f +1IZmz2SSt6cPPEu+BEQ/Sn3mFRJ5XSTHLtnI0HJeDND5u1+6p1nXawIURv3Maige +oxmfqC24VoROJEq+sew= +-----END DSA PRIVATE KEY----- diff --git a/test/docker/ssh_host_dsa_key.pub b/test/docker/ssh_host_dsa_key.pub new file mode 100644 index 0000000..a32a5a0 --- /dev/null +++ b/test/docker/ssh_host_dsa_key.pub @@ -0,0 +1 @@ +ssh-dss AAAAB3NzaC1kc3MAAACBAO2HV5X4DyPX3VHR7LR1VcvYsY0Zgz5r4+iZJruy4rzE0J5UsoJilDpVJakB/oVtFZ6/VRWjwiluCXAVk9zIU8rYbUfunjmjCzZI3gKV6zMMPnp7pbTc+Vz6oypInotOOvgzn4epMM4zcJLvZ3sxdSR8dCJqDKtZCFfJzSQWw0kvAAAAFQCoO2+fjYiGc6YOSBB9LZWBUKboQQAAAIAZxurv1Co01lG48IT8XTmwKhNEYKYWcOMK4KBY3vA1Gi2MzP9Leke0IJ65NPWNPofLRkUd+2/S38YXwYFGB6iRRtLjK0ek6QVZxPPygWC4Mvjierx419JjpvMSXziqyAR7gc/3eYHgVHVlRSAjhvuAIE5gQWXfI+IfsfWntn0TWgAAAIAl1UPQkeRhElz+AgEbNsnMKu1+6O3/z95D1Wvv4OEwAImbytlBaC7pkwJElJNsMMfGqCC8OHdJ0e4VQQUwk/GOhD0MFhVQHBtVZYbiWmVkpfHf1ouUQg3f1IZmz2SSt6cPPEu+BEQ/Sn3mFRJ5XSTHLtnI0HJeDND5u1+6p1nXaw== diff --git a/test/docker/ssh_host_ecdsa_key b/test/docker/ssh_host_ecdsa_key new file mode 100644 index 0000000..69eea7b --- /dev/null +++ b/test/docker/ssh_host_ecdsa_key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEICq/YV5QenL0uW5g5tCjY3EWs+UBFmskY+Jjt2vd2aEmoAoGCCqGSM49 +AwEHoUQDQgAEdYSxDVUjOpW479L/nRDiAdxRB5Kuy2bgkP/LA2pnWPcGIWmFa4QU +YN2U3JsFKcLIcx5cvTehQfgrHDnaSKVdKA== +-----END EC PRIVATE KEY----- diff --git a/test/docker/ssh_host_ecdsa_key.pub b/test/docker/ssh_host_ecdsa_key.pub new file mode 100644 index 0000000..4e17058 --- /dev/null +++ b/test/docker/ssh_host_ecdsa_key.pub @@ -0,0 +1 @@ +ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBHWEsQ1VIzqVuO/S/50Q4gHcUQeSrstm4JD/ywNqZ1j3BiFphWuEFGDdlNybBSnCyHMeXL03oUH4Kxw52kilXSg= diff --git a/test/docker/ssh_host_ed25519_key b/test/docker/ssh_host_ed25519_key new file mode 100644 index 0000000..3388574 --- /dev/null +++ b/test/docker/ssh_host_ed25519_key @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACC/9RD2Ao95ODDIH8i11ekTALut8AUNqWoQx0jHlP4xygAAAKDiqVOs4qlT +rAAAAAtzc2gtZWQyNTUxOQAAACC/9RD2Ao95ODDIH8i11ekTALut8AUNqWoQx0jHlP4xyg +AAAECTmHGkq0Qea0QqTJYMXL0bpxVU7mhgwYninfVWxrA017/1EPYCj3k4MMgfyLXV6RMA +u63wBQ2pahDHSMeU/jHKAAAAHWpkb2dAbG9jYWxob3N0LndvbmRlcmxhbmQubG9s +-----END OPENSSH PRIVATE KEY----- diff --git a/test/docker/ssh_host_ed25519_key-cert.pub b/test/docker/ssh_host_ed25519_key-cert.pub new file mode 100644 index 0000000..8eef563 --- /dev/null +++ b/test/docker/ssh_host_ed25519_key-cert.pub @@ -0,0 +1 @@ +ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIO1W0I8tD0c4LypvHY1XNch3BQCw9Yy28/4KmAYql80DAAAAIL/1EPYCj3k4MMgfyLXV6RMAu63wBQ2pahDHSMeU/jHKAAAAAAAAAAAAAAACAAAABHRlc3QAAAAIAAAABHRlc3QAAAAAXV7hvAAAAACBa2YhAAAAAAAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQAAACAbM9Wp3ZPcC8Ifhu6GjNDJaoMg7KxO0el2+r9J35TltQAAAFMAAAALc3NoLWVkMjU1MTkAAABAW60bCSeIG4Ta+57zgkSbW4LIGCxtOuJJ+pP3i3S0xJJfHGnOtXbg0NQm7pulNl/wd01kgJO9A7RjbhTh7TV1AA== ssh_host_ed25519_key.pub diff --git a/test/docker/ssh_host_ed25519_key.pub b/test/docker/ssh_host_ed25519_key.pub new file mode 100644 index 0000000..e56a56a --- /dev/null +++ b/test/docker/ssh_host_ed25519_key.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL/1EPYCj3k4MMgfyLXV6RMAu63wBQ2pahDHSMeU/jHK diff --git a/test/docker/ssh_host_rsa_key_1024 b/test/docker/ssh_host_rsa_key_1024 new file mode 100644 index 0000000..e9023b6 --- /dev/null +++ b/test/docker/ssh_host_rsa_key_1024 @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXgIBAAKBgQDeCC1U7VqVg9AfrfWrXACiW6pzYOuP8tim68z+YN/dUU7JhFZ4 +0toteQkLcJBAD2miQ6ZJYkjVfhQ4FRFeOW5vcN0UYHn8ttb2mKdGJdt24ZYY5Z6J +WHQhPOpSgtWyUv6RnxU2ligEeaoPaiepUUOhoyLf4WcF7voVCAKZNqeTtQIDAQAB +AoGATGZ16s5NqDsWJ4B9k3xx/2wZZ+BGzl6a7D0habq97XLn8HGoK6UqTBFk6lnO +WSy0hZBPrNq0AzqCDJY7RrfuZqgVAu/+HEFuXencgt8Z//ueYBaGK8yAC+OrMnDG +LbSoIGRq8saaFtCzt47c+uSVsrhJ4TvK5gbceZuD/2uw10ECQQD79T0j+YWsLISK +PKvYHqEXSMPN6b+lK9hRPLoF9NMksNLSjuxxhkYHz+hJPVNT+wPtRMAYmMdPXfKa +FjuErXVFAkEA4ZgJIOeJ7OHfqGEgd29m36yFy0UaUJ+cmYuJzHAYWgW3TOanqpZm +A8EENuXvH0DtYRVytv4m/cIRVVPxWtXzsQJBALXlQUOEc0VuSi1GScVXr3KQ3JL+ +ipWixqM3VRDRw9D8Ouc5uWbnygz/wrGFLXA2ioozlP7s5Q7eQzOMk2FgnIUCQQCz +j5QUgLcjuVWQbF6vMhisCGImPUaIzcKT5KE1/DMl1E7mAuGJwlRIwKVeHP6L3d4T +3EKGrRzT9lhdlocRSiLBAkEAi3xI0MOZp4xGviPc1C1TKuqdJSr8fHwbtLozwNQO +nnF6m5S72JzZEThDBZS9zcdBp9EFpTvUGzx/O0GI454eoA== +-----END RSA PRIVATE KEY----- diff --git a/test/docker/ssh_host_rsa_key_1024-cert_1024.pub b/test/docker/ssh_host_rsa_key_1024-cert_1024.pub new file mode 100644 index 0000000..17c7738 --- /dev/null +++ b/test/docker/ssh_host_rsa_key_1024-cert_1024.pub @@ -0,0 +1 @@ +ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgFqxXSa9HTqCw5YW3DdIwVREPGxI+i56w32RnHWRg0NoAAAADAQABAAAAgQDeCC1U7VqVg9AfrfWrXACiW6pzYOuP8tim68z+YN/dUU7JhFZ40toteQkLcJBAD2miQ6ZJYkjVfhQ4FRFeOW5vcN0UYHn8ttb2mKdGJdt24ZYY5Z6JWHQhPOpSgtWyUv6RnxU2ligEeaoPaiepUUOhoyLf4WcF7voVCAKZNqeTtQAAAAAAAAAAAAAAAgAAAAR0ZXN0AAAACAAAAAR0ZXN0AAAAAF1evHgAAAAAgWtAtQAAAAAAAAAAAAAAAAAAAJcAAAAHc3NoLXJzYQAAAAMBAAEAAACBAOdGU3cAWdR7aUV/lcb1SFcuv/086u41MVsy3TOuM5RKOYBLuFLqkO/lUROhPoNpHURBRhqpIykdjNmG4Irna+vJ06blcVsnvvXav0zJlBSGhVnHxK50EfNUMhU7eNwwxYiWt9YEydRpQcSqmbLzxjuAoNrZNbEmDX6GDnUqoetRAAAAjwAAAAdzc2gtcnNhAAAAgFRc2g0eWXGmqSa6Z8rcMPHf4rNMornEHSnTzZ8Rdh4YBhDa9xMRRy6puaWPDzXxOfZh7eSjCwrzUMEXTgCIO4oX62xm/6kUnAmKhXle0+inR/hdPg03daE0SBJ4spBT49lJ4WIW38RKFNzjmYg70rTPAnP8oM/F3CC1GV117/Vv ssh_host_rsa_key_1024.pub diff --git a/test/docker/ssh_host_rsa_key_1024-cert_3072.pub b/test/docker/ssh_host_rsa_key_1024-cert_3072.pub new file mode 100644 index 0000000..ea0160a --- /dev/null +++ b/test/docker/ssh_host_rsa_key_1024-cert_3072.pub @@ -0,0 +1 @@ +ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgrJGcfyW8V6VWGT7lD1ardj2RtTP8TOjmLRNbuoGkyZQAAAADAQABAAAAgQDeCC1U7VqVg9AfrfWrXACiW6pzYOuP8tim68z+YN/dUU7JhFZ40toteQkLcJBAD2miQ6ZJYkjVfhQ4FRFeOW5vcN0UYHn8ttb2mKdGJdt24ZYY5Z6JWHQhPOpSgtWyUv6RnxU2ligEeaoPaiepUUOhoyLf4WcF7voVCAKZNqeTtQAAAAAAAAAAAAAAAgAAAAR0ZXN0AAAACAAAAAR0ZXN0AAAAAF1evHgAAAAAgWtA7gAAAAAAAAAAAAAAAAAAAZcAAAAHc3NoLXJzYQAAAAMBAAEAAAGBAKsUBCG4/MNE6wWNXwzvNXLq4o0K3SzlK/FZfFxAlnOUBndS5ABvNDYHXAgT741sWUpmKynqgO/BlHTfioMnBNM7acWElyh2TGiDOV3veAQfgaHB/db3f9ogcxnnndZ1f83wHatgBB0I9U6SCPmvs9ucsb9j7sFU08MKPSP+iYDat3rx+B28IK90kXxBGGUPmC/HDqQbjYNWGTkdJNcpNt2agHfdicm8WeIgrTre63I9rBR8zbtVOeOMFgcCC+cmWSk4AYWOHan09hKeyM0eR3bHa70FfagvdhjNcwxPlNwMK1Sox7NGGat483Kv8rxQwnDDEc+P9KRhRVNuqFpmvp972wMSiaC/q4RvEDPfpdqhZSxrJzNJFFwCV7Dym8oKWj/Gd1OHioE7idhrkvXiyBZwe5q8A++VehoDfauZmPPQ35FUpB2qKqWPKT3UZM/UAJjBLi7R5r9vQlTqX1+b/67Ju8AEgGarVJZhbuKxRgS8L2W1JVrtNh5oAU+O4OhCHwAAAY8AAAAHc3NoLXJzYQAAAYCO78ONpHp+EGWDqDLb/GPFDH32o6oaRRrIH/Bhvg1FOi5XngnHTdU7xWdnJqNE2Sl6VOrg0sTCMYcX9fZ8tVREnCo3aF7Iwow5Br67QYKayRzHANQqHaVK46lpI1gz81V00u54tX1F8oEUqm6sRmFKFuklt6CjfbR+tnpj7DrfeOTKEBOGJP2uU0jMsJr2DrBeXrzONjIJtIJ1AxWjXd2LeIWO2C6yTkcN5ggThMMaeu6QuuBPpC2PN2COfu+Mgto9g103+/SS4Wa8CzinZZn2Xe1isUxI8QRNrShy4Hl/bIZQL7mi/0rxkfw+fA7IzMk462V99gPVSp+/jK0sbUJoC3QeeglS5hWodjW+VGZfgweGQ+AE/OxkNSv+kDPMYEkjfOf4qhxS5QFvButLt6zp2UNbE5+OWvYpdjO9/DOa0ro+wCw07+dVKIcDpU2csiCcJvQ/HmKAmhch7jOHa0WaxSX0tt0xTPJWTvr6E4WZOgEnk9AvWmrKjF5tEzGYwTU= ssh_host_rsa_key_1024.pub diff --git a/test/docker/ssh_host_rsa_key_1024.pub b/test/docker/ssh_host_rsa_key_1024.pub new file mode 100644 index 0000000..1da6065 --- /dev/null +++ b/test/docker/ssh_host_rsa_key_1024.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDeCC1U7VqVg9AfrfWrXACiW6pzYOuP8tim68z+YN/dUU7JhFZ40toteQkLcJBAD2miQ6ZJYkjVfhQ4FRFeOW5vcN0UYHn8ttb2mKdGJdt24ZYY5Z6JWHQhPOpSgtWyUv6RnxU2ligEeaoPaiepUUOhoyLf4WcF7voVCAKZNqeTtQ== diff --git a/test/docker/ssh_host_rsa_key_3072 b/test/docker/ssh_host_rsa_key_3072 new file mode 100644 index 0000000..3a2c719 --- /dev/null +++ b/test/docker/ssh_host_rsa_key_3072 @@ -0,0 +1,39 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIG5AIBAAKCAYEAzhHp7eFnrQAJqOd7aihyQyIDKgxCF7H51Q3Ft3+8af+lX3Ol +Ie77Gi5GNNM+eRB4OzG+CBslxN5I3pM//sZ+gyylA1VuWZZkOlgtbOHutIkO2ldk +XtoGidla0VAxLcUcUK6cCmqwBTT31Hp4Qimp2zyeg/l5q0DhWKguY13lrm5b3YZY +rj7CW3Ktzxf8SbYz6du8KF0dHCWilzq+FLeGzXr7Yul5njVF5njkGvZ9duQ0qiVR +zqZkrkLEWgQlCM0T+PyUbvedL1MfDZPHGh7ZhU0snOvJRsxAr31tlknq+WwauZYd +DzJf1g1URcM65UwEsPlfgOW3ZoZogR1v57Im+KdsKhq2B3snEtJgdQh06JyO0ur4 +uUXo1mMtvBFhiptUtwP4g9v/IN4neeK+wBRom46m2Q1bMUBPneBOa8r2SY/3ynrz +XuVIWFOQtF60aJ+BNqvgUVCKOmz1KzoJwTqGm+EFaKM5z+UQWjIbSE3Ge4X5hXtk +Ou52v+tyDUk6boZLAgMBAAECggGAdrhxWmA7N7tG1W2Pd6iXs7+brRTk2vvpYGqP +11kbNsJXBzf8EiG5vuqb/gEaA+uOKSRORCNHzjT2LG0POHwpFO+aneIRMkHnuolk +mk9ME+zGhtpEdDUOAUsc/GxD+QePeZgvQ/0VLdrHUT3BnPSd7DXvaT9IbnZxnX8/ +QnYtRiJEgMrOuoxjswXNxvsdmWYEYJ38uBB1Hes80f3A1vSpECbjP6gdLh2pCM/r +MvGBdQaipMfdar4IUTEcKHQs1fY3mlAxnWRjYCqJPmq10d3NrdUrHb2zBE1HCC4h +aj2ycTxFhDJqGV6Y2AboHqh2c7lPJ+R2UjI9mIpALZSviHB1POcpWCAGA3NKjri9 +8jgxl3bj03ikJNfCuvlqRTa8at63W2zZTMRsxamoiO023uUOEMNBPwWXP/rVhQ8g +ufih0SY44j0EMPIuu2PoQV4ZSOtDw8xdPrchVCa078/pP5cRa4uV0bl2K4as+cYC +BhjEq2Org3ulDW2n6Mz5ZS7NbAkxAoHBAP/bgPGKX7rfrHa5MRHIgbLSmZtUoF51 +YGelc8ytRx6UT6wriJ1jQRXiI5mZlIXyVxMpIz9s4+h59kF+LpZuNLc3vTYpPOQn +RUDBVY6+SPC5MancL7bfBoHahpWEJuJB/WUE7eWvQM03/LsBtU6Nq+R632t5KdqF +A4y86qgD1vIjcBWvySLFJZGOCoNbj7ZinoBUO3ueYK6SUj8xH6TAqOJsTPvquRT3 +AFBpFBmrVc24wW7wTiLkQOhkIQs1J/ZhYwKBwQDOL07qF8wsoQBBTTXkZ59BCauz +R8kfqe5oUBwsmGJdiIHX6gutBA07sSwzVekIvCCkJFXk3TxLoBSMHEZEIdnS+HVt +gMIacYuhbh+XztdY0kadH/SMbVQD/2LZcL99vcZPq1QF3cHb0Buip5+fyAYjoEc7 +oVgvewD/TwdNcMjos/kMNh6l04kLi6vQG3WhoSBPWaoB669ppBNXSrWKe43nXVi6 +EvjGEiL+HCCnmD6LiD6p797Owu9AChP6fXInD/kCgcEAiLP3SRbt3yLzOtvn4+CF +q83qVJv6s31zbO1x2cIbZbNIfm0kKTOG6vJQoxjzyj2ZWJt6QcEkZGoFsSiCK83m +TJ5zciTGbACvd9HUrNfukO/iISeMNuEi0O65Sdm6DNnFUdw4X6grr3pihmh7PuVj +GkisZvft7Nt08hVeKzch+W4FzRCHHxTG5eZGp7icKI64sUhQH9SXQ67aUvkkNxrZ +IWFMIK1hBlqSyGPcYXqx9aDpeSTcGrhqFcCqBxr3pySRAoHAfJNO3delEC3yxoHN +FwSYzyX1rOuplE0K89G7RCKKBDNPKFKL3Wx+Rluk9htpIlLwcdxWXWJiZNsCrykC +N3YwcuyVnqTWIj4KfG3Z/tIFgPADpDnDevkvcv7iDbi2qlV4NXix2p2C3LnfiKY4 +psSnGO1lPJ0eeAmcr6VjJyIG8bqTthIY8F5gBi7Mj3+X0iFVMTxeoKxzHqP435wP +Fe3S7kCTNFH0J1Cb/eamwDwXRhz6p5h7iXd0MMAmFAmpZ/qZAoHBAPDSIvk2ocf1 +FVW8pKtKOJFIs8iQVIaOLKwPJVP8/JsB1+7mQx5KMoROb5pNpX2edN4vvG0CgqpJ +KekleqpH6nQCqYGFZ1BDhORElNILxeJHcNl0eAG++IJ2PfIpTZV30edDqMm0x7EI +8POZWAx809VzcYbE2jsgpN/EuiaG30EAI5yNvyzmZRCyQykH+eltHlCx17MWBxRQ +bb2UUfpdInTMS2vyrvkeUACkC1DGYdBVVBqqPTkHZg+Kcbs8ntQqEQ== +-----END RSA PRIVATE KEY----- diff --git a/test/docker/ssh_host_rsa_key_3072-cert_1024.pub b/test/docker/ssh_host_rsa_key_3072-cert_1024.pub new file mode 100644 index 0000000..da6b9ec --- /dev/null +++ b/test/docker/ssh_host_rsa_key_3072-cert_1024.pub @@ -0,0 +1 @@ +ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgGHz1JwF/1IcxW3pdQtpqbUjIHaFuk0cR/+l50vG+9hIAAAADAQABAAABgQDOEent4WetAAmo53tqKHJDIgMqDEIXsfnVDcW3f7xp/6Vfc6Uh7vsaLkY00z55EHg7Mb4IGyXE3kjekz/+xn6DLKUDVW5ZlmQ6WC1s4e60iQ7aV2Re2gaJ2VrRUDEtxRxQrpwKarAFNPfUenhCKanbPJ6D+XmrQOFYqC5jXeWublvdhliuPsJbcq3PF/xJtjPp27woXR0cJaKXOr4Ut4bNevti6XmeNUXmeOQa9n125DSqJVHOpmSuQsRaBCUIzRP4/JRu950vUx8Nk8caHtmFTSyc68lGzECvfW2WSer5bBq5lh0PMl/WDVRFwzrlTASw+V+A5bdmhmiBHW/nsib4p2wqGrYHeycS0mB1CHTonI7S6vi5RejWYy28EWGKm1S3A/iD2/8g3id54r7AFGibjqbZDVsxQE+d4E5ryvZJj/fKevNe5UhYU5C0XrRon4E2q+BRUIo6bPUrOgnBOoab4QVooznP5RBaMhtITcZ7hfmFe2Q67na/63INSTpuhksAAAAAAAAAAAAAAAIAAAAEdGVzdAAAAAgAAAAEdGVzdAAAAABdXry0AAAAAIFrQR8AAAAAAAAAAAAAAAAAAACXAAAAB3NzaC1yc2EAAAADAQABAAAAgQDnRlN3AFnUe2lFf5XG9UhXLr/9POruNTFbMt0zrjOUSjmAS7hS6pDv5VEToT6DaR1EQUYaqSMpHYzZhuCK52vrydOm5XFbJ7712r9MyZQUhoVZx8SudBHzVDIVO3jcMMWIlrfWBMnUaUHEqpmy88Y7gKDa2TWxJg1+hg51KqHrUQAAAI8AAAAHc3NoLXJzYQAAAIB4HaEexgQ9T6rScEbiHZx+suCaYXI7ywLYyoSEO48K8o+MmO83UTLtpPa3DXlT8hSYL8Aq6Bb5AMkDawsgsC484owPqObT/5ndLG/fctNBFcCTSL0ftte+A8xH0pZaGRoKbdxxgMqX4ubrCXpbMLGF9aAeh7MRa756XzqGlsCiSA== ssh_host_rsa_key_3072.pub diff --git a/test/docker/ssh_host_rsa_key_3072-cert_3072.pub b/test/docker/ssh_host_rsa_key_3072-cert_3072.pub new file mode 100644 index 0000000..78f49ea --- /dev/null +++ b/test/docker/ssh_host_rsa_key_3072-cert_3072.pub @@ -0,0 +1 @@ +ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAg9MVX4OlkEy3p9eC+JJp8h7j76EmI46EY/RXxCGSWTC0AAAADAQABAAABgQDOEent4WetAAmo53tqKHJDIgMqDEIXsfnVDcW3f7xp/6Vfc6Uh7vsaLkY00z55EHg7Mb4IGyXE3kjekz/+xn6DLKUDVW5ZlmQ6WC1s4e60iQ7aV2Re2gaJ2VrRUDEtxRxQrpwKarAFNPfUenhCKanbPJ6D+XmrQOFYqC5jXeWublvdhliuPsJbcq3PF/xJtjPp27woXR0cJaKXOr4Ut4bNevti6XmeNUXmeOQa9n125DSqJVHOpmSuQsRaBCUIzRP4/JRu950vUx8Nk8caHtmFTSyc68lGzECvfW2WSer5bBq5lh0PMl/WDVRFwzrlTASw+V+A5bdmhmiBHW/nsib4p2wqGrYHeycS0mB1CHTonI7S6vi5RejWYy28EWGKm1S3A/iD2/8g3id54r7AFGibjqbZDVsxQE+d4E5ryvZJj/fKevNe5UhYU5C0XrRon4E2q+BRUIo6bPUrOgnBOoab4QVooznP5RBaMhtITcZ7hfmFe2Q67na/63INSTpuhksAAAAAAAAAAAAAAAIAAAAEdGVzdAAAAAgAAAAEdGVzdAAAAABdXr0sAAAAAIFrQWwAAAAAAAAAAAAAAAAAAAGXAAAAB3NzaC1yc2EAAAADAQABAAABgQCrFAQhuPzDROsFjV8M7zVy6uKNCt0s5SvxWXxcQJZzlAZ3UuQAbzQ2B1wIE++NbFlKZisp6oDvwZR034qDJwTTO2nFhJcodkxogzld73gEH4Ghwf3W93/aIHMZ553WdX/N8B2rYAQdCPVOkgj5r7PbnLG/Y+7BVNPDCj0j/omA2rd68fgdvCCvdJF8QRhlD5gvxw6kG42DVhk5HSTXKTbdmoB33YnJvFniIK063utyPawUfM27VTnjjBYHAgvnJlkpOAGFjh2p9PYSnsjNHkd2x2u9BX2oL3YYzXMMT5TcDCtUqMezRhmrePNyr/K8UMJwwxHPj/SkYUVTbqhaZr6fe9sDEomgv6uEbxAz36XaoWUsayczSRRcAlew8pvKClo/xndTh4qBO4nYa5L14sgWcHuavAPvlXoaA32rmZjz0N+RVKQdqiqljyk91GTP1ACYwS4u0ea/b0JU6l9fm/+uybvABIBmq1SWYW7isUYEvC9ltSVa7TYeaAFPjuDoQh8AAAGPAAAAB3NzaC1yc2EAAAGAG8tCiBMSq3Of3Gmcrid2IfPmaaemYivgEEuK8ubq1rznF0vtR07/NUQ7WVzfJhUSeG0gtJ3A1ey60NjcBn0DHao4Q3ATIXnkSOIKjNolZ2urqYv9fT1LAC4I5XWGzK2aKK0NEqAYr06YPtcGOBQk5+3GPAWSJ4eQycKRz5BSuMYbKaVxU0kGSvbavG07ZntMQhia/lILyq84PjXh/JlRVpIqY+LAS0qwqkUR3gWMTmvYvYI7fXU84ReVB1ut75bY7Xx0DXHPl1Zc2MNDLGcKsByZtoO9ueZRyOlZMUJcVP5fK+OUuZKjMbCaaJnV55BQ78/rftIPYsTEEO2Sf9WT86ADa3k4S0pyWqlTxBzZcDWNt+fZFNm9wcqcYS32nDKtfixcDN8E/IJIWY7aoabPqoYnKUVQBOcIEnZf1HqsKUVmF44Dp9mKhefUs3BtcdK63j/lNXzzMrPwZQAreJqH/uV3TgYBLjMPl++ctX6tCe6Hv5zFKNhnOCSBSzcsCgIU ssh_host_rsa_key_3072.pub diff --git a/test/docker/ssh_host_rsa_key_3072.pub b/test/docker/ssh_host_rsa_key_3072.pub new file mode 100644 index 0000000..ad83cd1 --- /dev/null +++ b/test/docker/ssh_host_rsa_key_3072.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDOEent4WetAAmo53tqKHJDIgMqDEIXsfnVDcW3f7xp/6Vfc6Uh7vsaLkY00z55EHg7Mb4IGyXE3kjekz/+xn6DLKUDVW5ZlmQ6WC1s4e60iQ7aV2Re2gaJ2VrRUDEtxRxQrpwKarAFNPfUenhCKanbPJ6D+XmrQOFYqC5jXeWublvdhliuPsJbcq3PF/xJtjPp27woXR0cJaKXOr4Ut4bNevti6XmeNUXmeOQa9n125DSqJVHOpmSuQsRaBCUIzRP4/JRu950vUx8Nk8caHtmFTSyc68lGzECvfW2WSer5bBq5lh0PMl/WDVRFwzrlTASw+V+A5bdmhmiBHW/nsib4p2wqGrYHeycS0mB1CHTonI7S6vi5RejWYy28EWGKm1S3A/iD2/8g3id54r7AFGibjqbZDVsxQE+d4E5ryvZJj/fKevNe5UhYU5C0XrRon4E2q+BRUIo6bPUrOgnBOoab4QVooznP5RBaMhtITcZ7hfmFe2Q67na/63INSTpuhks= From 4ebccb8068d938ffda2da453cefa157eb1f24689 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Thu, 22 Aug 2019 16:48:23 -0400 Subject: [PATCH 074/103] Added OpenSSH v4.0 test. --- docker_test.sh | 25 +++- test/docker/Dockerfile | 2 + .../expected_results/openssh_4.0p1_test1.txt | 136 ++++++++++++++++++ test/docker/ssh1_host_key | Bin 0 -> 536 bytes test/docker/ssh1_host_key.pub | 1 + 5 files changed, 162 insertions(+), 2 deletions(-) create mode 100644 test/docker/expected_results/openssh_4.0p1_test1.txt create mode 100644 test/docker/ssh1_host_key create mode 100644 test/docker/ssh1_host_key.pub diff --git a/docker_test.sh b/docker_test.sh index 36a4e2f..77e6bb6 100755 --- a/docker_test.sh +++ b/docker_test.sh @@ -71,20 +71,32 @@ function create_docker_image { # Aside from checking the GPG signatures, we also compare against this known-good # SHA-256 hash just in case. + get_openssh '4.0p1' '5adb9b2c2002650e15216bf94ed9db9541d9a17c96fcd876784861a8890bc92b' get_openssh '5.6p1' '538af53b2b8162c21a293bb004ae2bdb141abd250f61b4cea55244749f3c6c2b' get_openssh '8.0p1' 'bd943879e69498e8031eb6b7f44d08cdc37d59a7ab689aa0b437320c3481fd68' # Compile the versions of OpenSSH. + compile_openssh '4.0p1' compile_openssh '5.6p1' compile_openssh '8.0p1' # Rename the default config files so we know they are our originals. + mv openssh-4.0p1/sshd_config sshd_config-4.0p1_orig mv openssh-5.6p1/sshd_config sshd_config-5.6p1_orig mv openssh-8.0p1/sshd_config sshd_config-8.0p1_orig # Create the configurations for each test. + + # + # OpenSSH v4.0p1 + # + + # Test 1: Basic test. + create_openssh_config '4.0p1' 'test1' "HostKey /etc/ssh/ssh1_host_key\nHostKey /etc/ssh/ssh_host_rsa_key_1024\nHostKey /etc/ssh/ssh_host_dsa_key" + + # # OpenSSH v5.6p1 # @@ -164,8 +176,15 @@ function get_openssh { echo -e "\nGetting OpenSSH $1 signature...\n" wget https://cdn.openbsd.org/pub/OpenBSD/OpenSSH/portable/openssh-$1.tar.gz.asc + openssh_sig=openssh-$1.tar.gz.asc - local gpg_verify=`gpg --verify openssh-$1.tar.gz.asc openssh-$1.tar.gz 2>&1` + # Older releases were .sigs. + if [[ ! -f $openssh_sig ]]; then + wget https://cdn.openbsd.org/pub/OpenBSD/OpenSSH/portable/openssh-$1.tar.gz.sig + openssh_sig=openssh-$1.tar.gz.sig + fi + + local gpg_verify=`gpg --verify $openssh_sig openssh-$1.tar.gz 2>&1` if [[ $gpg_verify != *"Good signature from \"Damien Miller "* ]]; then echo -e "\n\n${REDB}Error: OpenSSH signature invalid!\n$gpg_verify\n\nTerminating.${CLR}" exit -1 @@ -246,12 +265,14 @@ TEST_RESULT_DIR=`mktemp -d /tmp/ssh-audit_test-results_XXXXXXXXXX` # Now run all the tests. echo -e "\nRunning tests..." +run_openssh_test '4.0p1' 'test1' +echo run_openssh_test '5.6p1' 'test1' run_openssh_test '5.6p1' 'test2' run_openssh_test '5.6p1' 'test3' run_openssh_test '5.6p1' 'test4' run_openssh_test '5.6p1' 'test5' -echo "" +echo run_openssh_test '8.0p1' 'test1' run_openssh_test '8.0p1' 'test2' run_openssh_test '8.0p1' 'test3' diff --git a/test/docker/Dockerfile b/test/docker/Dockerfile index 739be49..41a62c2 100644 --- a/test/docker/Dockerfile +++ b/test/docker/Dockerfile @@ -1,10 +1,12 @@ FROM ubuntu:16.04 +COPY openssh-4.0p1/sshd /openssh/sshd-4.0p1 COPY openssh-5.6p1/sshd /openssh/sshd-5.6p1 COPY openssh-8.0p1/sshd /openssh/sshd-8.0p1 COPY sshd_config* /etc/ssh/ COPY ssh_host_* /etc/ssh/ +COPY ssh1_host_* /etc/ssh/ COPY moduli_1024 /usr/local/etc/moduli COPY debug.sh /debug.sh diff --git a/test/docker/expected_results/openssh_4.0p1_test1.txt b/test/docker/expected_results/openssh_4.0p1_test1.txt new file mode 100644 index 0000000..33e1967 --- /dev/null +++ b/test/docker/expected_results/openssh_4.0p1_test1.txt @@ -0,0 +1,136 @@ +# general +(gen) banner: SSH-1.99-OpenSSH_4.0 +(gen) protocol SSH1 enabled +(gen) software: OpenSSH 4.0 +(gen) compatibility: OpenSSH 3.9-6.6, Dropbear SSH 0.53+ (some functionality from 0.52) +(gen) compression: enabled (zlib) + +# security +(cve) CVE-2016-3115 -- (CVSSv2: 5.5) bypass command restrictions via crafted X11 forwarding data +(cve) CVE-2014-1692 -- (CVSSv2: 7.5) cause DoS via triggering error condition (memory corruption) +(cve) CVE-2012-0814 -- (CVSSv2: 3.5) leak data via debug messages +(cve) CVE-2011-5000 -- (CVSSv2: 3.5) cause DoS via large value in certain length field (memory consumption) +(cve) CVE-2010-5107 -- (CVSSv2: 5.0) cause DoS via large number of connections (slot exhaustion) +(cve) CVE-2010-4755 -- (CVSSv2: 4.0) cause DoS via crafted glob expression (CPU and memory consumption) +(cve) CVE-2010-4478 -- (CVSSv2: 7.5) bypass authentication check via crafted values +(cve) CVE-2008-5161 -- (CVSSv2: 2.6) recover plaintext data from ciphertext +(cve) CVE-2008-4109 -- (CVSSv2: 5.0) cause DoS via multiple login attempts (slot exhaustion) +(cve) CVE-2008-1657 -- (CVSSv2: 6.5) bypass command restrictions via modifying session file +(cve) CVE-2008-1483 -- (CVSSv2: 6.9) hijack forwarded X11 connections +(cve) CVE-2007-4752 -- (CVSSv2: 7.5) privilege escalation via causing an X client to be trusted +(cve) CVE-2007-2243 -- (CVSSv2: 5.0) discover valid usernames through different responses +(cve) CVE-2006-5052 -- (CVSSv2: 5.0) discover valid usernames through different responses +(cve) CVE-2006-5051 -- (CVSSv2: 9.3) cause DoS or execute arbitrary code (double free) +(cve) CVE-2006-4924 -- (CVSSv2: 7.8) cause DoS via crafted packet (CPU consumption) +(cve) CVE-2006-0225 -- (CVSSv2: 4.6) execute arbitrary code +(cve) CVE-2005-2798 -- (CVSSv2: 5.0) leak data about authentication credentials + +# key exchange algorithms +(kex) diffie-hellman-group-exchange-sha1 (1024-bit) -- [fail] using small 1024-bit modulus + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.3.0 +(kex) diffie-hellman-group14-sha1 -- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 3.9, Dropbear SSH 0.53 +(kex) diffie-hellman-group1-sha1 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [fail] disabled (in client) since OpenSSH 7.0, logjam attack + `- [warn] using small 1024-bit modulus + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.3.0, Dropbear SSH 0.28 + +# host-key algorithms +(key) ssh-rsa (1024-bit) -- [fail] using small 1024-bit modulus + `- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28 +(key) ssh-dss -- [fail] removed (in server) and disabled (in client) since OpenSSH 7.0, weak algorithm + `- [warn] using small 1024-bit modulus + `- [warn] using weak random number generator could reveal the key + `- [info] available since OpenSSH 2.1.0, Dropbear SSH 0.28 + +# encryption algorithms (ciphers) +(enc) aes128-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0, Dropbear SSH 0.28 +(enc) 3des-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.4, unsafe algorithm + `- [warn] using weak cipher + `- [warn] using weak cipher mode + `- [warn] using small 64-bit block size + `- [info] available since OpenSSH 1.2.2, Dropbear SSH 0.28 +(enc) blowfish-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [fail] disabled since Dropbear SSH 0.53 + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher mode + `- [warn] using small 64-bit block size + `- [info] available since OpenSSH 1.2.2, Dropbear SSH 0.28 +(enc) cast128-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher mode + `- [warn] using small 64-bit block size + `- [info] available since OpenSSH 2.1.0 +(enc) arcfour -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher + `- [info] available since OpenSSH 2.1.0 +(enc) aes192-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0 +(enc) aes256-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0, Dropbear SSH 0.47 +(enc) rijndael-cbc@lysator.liu.se -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0 +(enc) aes128-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 +(enc) aes192-ctr -- [info] available since OpenSSH 3.7 +(enc) aes256-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 + +# message authentication code algorithms +(mac) hmac-md5 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.1.0, Dropbear SSH 0.28 +(mac) hmac-sha1 -- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.1.0, Dropbear SSH 0.28 +(mac) hmac-ripemd160 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [info] available since OpenSSH 2.5.0 +(mac) hmac-ripemd160@openssh.com -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [info] available since OpenSSH 2.1.0 +(mac) hmac-sha1-96 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.47 +(mac) hmac-md5-96 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.5.0 + +# fingerprints +(fin) ssh-rsa: SHA256:YZ457EBcJTSxRKI3yXRgtAj3PBf5B9/F36b1SVooml4 + +# algorithm recommendations (for OpenSSH 4.0) +(rec) !ssh-rsa -- key algorithm to change (increase modulus size to 2048 bits or larger)  +(rec) -3des-cbc -- enc algorithm to remove  +(rec) -aes128-cbc -- enc algorithm to remove  +(rec) -aes192-cbc -- enc algorithm to remove  +(rec) -aes256-cbc -- enc algorithm to remove  +(rec) -arcfour -- enc algorithm to remove  +(rec) -blowfish-cbc -- enc algorithm to remove  +(rec) -cast128-cbc -- enc algorithm to remove  +(rec) -diffie-hellman-group-exchange-sha1 -- kex algorithm to remove  +(rec) -diffie-hellman-group1-sha1 -- kex algorithm to remove  +(rec) -hmac-md5 -- mac algorithm to remove  +(rec) -hmac-md5-96 -- mac algorithm to remove  +(rec) -hmac-ripemd160 -- mac algorithm to remove  +(rec) -hmac-ripemd160@openssh.com -- mac algorithm to remove  +(rec) -hmac-sha1-96 -- mac algorithm to remove  +(rec) -rijndael-cbc@lysator.liu.se -- enc algorithm to remove  +(rec) -ssh-dss -- key algorithm to remove  + diff --git a/test/docker/ssh1_host_key b/test/docker/ssh1_host_key new file mode 100644 index 0000000000000000000000000000000000000000..c98971c4ace747e2bdf230f626acc297ce35334b GIT binary patch literal 536 zcmV+z0_XixQ%E3CQb|@pR7D_5MOh$5NlZl`Mo&^rK~x|yE-?xK000000000400aQm z$HS6%$684{OYFU=4CZ_SP-ex!0EwZz$_VZE9`6po7Rs6`BL|(rc2kDK$yY! zX`uP~$}aQ$VgW?}2uTF(yUtt*Mfh@jis=yEoccA@g-TyS=RE)F&`D)8i&S*YV&~9o z_MrVzs-?b~}I!BF@2OHU->)BBtF-!&2(Vjy(OALjnNZU#eQ?$Nm~Tab3V}k47iD?cHVr z_a(9Xn)$p408;;RQoV z6q3w7vFt)b(|N}uvl?SL0T2mdG4mSz{@EVh amrE{iB!z!8w+&F3nPUqOYqVtm0001G Date: Mon, 26 Aug 2019 14:44:35 -0400 Subject: [PATCH 075/103] Marked 3des-ctr as a weak cipher. --- ssh-audit.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ssh-audit.py b/ssh-audit.py index 5f138e0..0797f95 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -360,7 +360,7 @@ class SSH2(object): # pylint: disable=too-few-public-methods 'des-cbc': [[], [FAIL_WEAK_CIPHER], [WARN_CIPHER_MODE, WARN_BLOCK_SIZE]], 'des-cbc-ssh1': [[], [FAIL_WEAK_CIPHER], [WARN_CIPHER_MODE, WARN_BLOCK_SIZE]], '3des-cbc': [['1.2.2,d0.28,l10.2', '6.6', None], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH74_UNSAFE, WARN_CIPHER_WEAK, WARN_CIPHER_MODE, WARN_BLOCK_SIZE]], - '3des-ctr': [['d0.52']], + '3des-ctr': [['d0.52'], [FAIL_WEAK_CIPHER]], 'blowfish-cbc': [['1.2.2,d0.28,l10.2', '6.6,d0.52', '7.1,d0.52'], [FAIL_OPENSSH67_UNSAFE, FAIL_DBEAR53_DISABLED], [WARN_OPENSSH72_LEGACY, WARN_CIPHER_MODE, WARN_BLOCK_SIZE]], 'blowfish-ctr': [[], [FAIL_OPENSSH67_UNSAFE, FAIL_DBEAR53_DISABLED], [WARN_OPENSSH72_LEGACY, WARN_CIPHER_MODE, WARN_BLOCK_SIZE]], 'twofish-cbc': [['d0.28', 'd2014.66'], [FAIL_DBEAR67_DISABLED], [WARN_CIPHER_MODE]], @@ -2683,6 +2683,9 @@ def output_fingerprints(algs, sha256=True): if algs.ssh2kex is not None: host_keys = algs.ssh2kex.host_keys() for host_key_type in algs.ssh2kex.host_keys(): + if host_keys[host_key_type] is None: + continue + fp = SSH.Fingerprint(host_keys[host_key_type]) # Workaround for Python's order-indifference in dicts. We might get a random RSA type (ssh-rsa, rsa-sha2-256, or rsa-sha2-512), so running the tool against the same server three times may give three different host key types here. So if we have any RSA type, we will simply hard-code it to 'ssh-rsa'. From 120f8985395462c1c85d2f96b0137c129d3b2576 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Mon, 26 Aug 2019 14:45:31 -0400 Subject: [PATCH 076/103] Added Dropbear test. --- docker_test.sh | 206 ++++++++++++++---- test/docker/Dockerfile | 2 + test/docker/dropbear_dss_host_key | Bin 0 -> 458 bytes test/docker/dropbear_ecdsa_host_key | Bin 0 -> 141 bytes test/docker/dropbear_rsa_host_key_1024 | Bin 0 -> 421 bytes test/docker/dropbear_rsa_host_key_3072 | Bin 0 -> 1189 bytes .../dropbear_2019.78_test1.txt | 82 +++++++ 7 files changed, 251 insertions(+), 39 deletions(-) create mode 100644 test/docker/dropbear_dss_host_key create mode 100644 test/docker/dropbear_ecdsa_host_key create mode 100644 test/docker/dropbear_rsa_host_key_1024 create mode 100644 test/docker/dropbear_rsa_host_key_3072 create mode 100644 test/docker/expected_results/dropbear_2019.78_test1.txt diff --git a/docker_test.sh b/docker_test.sh index 77e6bb6..548a676 100755 --- a/docker_test.sh +++ b/docker_test.sh @@ -15,7 +15,7 @@ # This is the docker tag for the image. If this tag doesn't exist, then we assume the # image is out of date, and generate a new one with this tag. -IMAGE_VERSION=1 +IMAGE_VERSION=2 # This is the name of our docker image. IMAGE_NAME=ssh-audit-test @@ -35,21 +35,52 @@ function check_if_docker_image_exists { } +# Uncompresses and compiles the specified version of Dropbear. +function compile_dropbear { + version=$1 + compile 'Dropbear' $version +} + + # Uncompresses and compiles the specified version of OpenSSH. function compile_openssh { - echo "Uncompressing $1..." - tar xzf openssh-$1.tar.gz + version=$1 + compile 'OpenSSH' $version +} - echo "Compiling $1..." - pushd openssh-$1 > /dev/null +function compile { + project=$1 + version=$2 + + tarball= + uncompress_options= + source_dir= + server_executable= + if [[ $project == 'OpenSSH' ]]; then + tarball="openssh-${version}.tar.gz" + uncompress_options="xzf" + source_dir="openssh-${version}" + server_executable=sshd + elif [[ $project == 'Dropbear' ]]; then + tarball="dropbear-${version}.tar.bz2" + uncompress_options="xjf" + source_dir="dropbear-${version}" + server_executable=dropbear + fi + + echo "Uncompressing ${project} ${version}..." + tar $uncompress_options $tarball + + echo "Compiling ${project} ${version}..." + pushd $source_dir > /dev/null ./configure && make -j 10 - if [[ ! -f "sshd" ]]; then - echo -e "${REDB}Error: sshd not built!${CLR}" + if [[ ! -f $server_executable ]]; then + echo -e "${REDB}Error: ${server_executable} not built!${CLR}" exit 1 fi - echo -e "\n${GREEN}Successfully built OpenSSH ${1}${CLR}\n" + echo -e "\n${GREEN}Successfully built ${project} ${version}${CLR}\n" popd > /dev/null } @@ -68,18 +99,24 @@ function create_docker_image { # Get the release key for OpenSSH. get_openssh_release_key + get_dropbear_release_key # Aside from checking the GPG signatures, we also compare against this known-good # SHA-256 hash just in case. get_openssh '4.0p1' '5adb9b2c2002650e15216bf94ed9db9541d9a17c96fcd876784861a8890bc92b' get_openssh '5.6p1' '538af53b2b8162c21a293bb004ae2bdb141abd250f61b4cea55244749f3c6c2b' get_openssh '8.0p1' 'bd943879e69498e8031eb6b7f44d08cdc37d59a7ab689aa0b437320c3481fd68' + get_dropbear '2019.78' '525965971272270995364a0eb01f35180d793182e63dd0b0c3eb0292291644a4' # Compile the versions of OpenSSH. compile_openssh '4.0p1' compile_openssh '5.6p1' compile_openssh '8.0p1' + # Compile the versions of Dropbear. + compile_dropbear '2019.78' + + # Rename the default config files so we know they are our originals. mv openssh-4.0p1/sshd_config sshd_config-4.0p1_orig mv openssh-5.6p1/sshd_config sshd_config-5.6p1_orig @@ -150,61 +187,122 @@ function create_openssh_config { } +# Downloads the Dropbear release key and adds it to the local keyring. +function get_dropbear_release_key { + get_release_key 'Dropbear' 'https://matt.ucc.asn.au/dropbear/releases/dropbear-key-2015.asc' 'F29C6773' 'F734 7EF2 EE2E 07A2 6762 8CA9 4493 1494 F29C 6773' +} + + # Downloads the OpenSSH release key and adds it to the local keyring. function get_openssh_release_key { - local release_key_fingerprint_expected='59C2 118E D206 D927 E667 EBE3 D3E5 F56B 6D92 0D30' + get_release_key 'OpenSSH' 'https://ftp.openbsd.org/pub/OpenBSD/OpenSSH/RELEASE_KEY.asc' '6D920D30' '59C2 118E D206 D927 E667 EBE3 D3E5 F56B 6D92 0D30' +} - echo -e "\nGetting OpenSSH release key...\n" - wget https://ftp.openbsd.org/pub/OpenBSD/OpenSSH/RELEASE_KEY.asc - echo -e "\nImporting OpenSSH release key...\n" - gpg --import RELEASE_KEY.asc +function get_release_key { + project=$1 + key_url=$2 + key_id=$3 + release_key_fingerprint_expected=$4 - local release_key_fingerprint_actual=`gpg --fingerprint 6D920D30` + echo -e "\nGetting ${project} release key...\n" + wget -O key.asc $2 + + echo -e "\nImporting ${project} release key...\n" + gpg --import key.asc + + rm key.asc + + local release_key_fingerprint_actual=`gpg --fingerprint ${key_id}` if [[ $release_key_fingerprint_actual != *"$release_key_fingerprint_expected"* ]]; then - echo -e "\n${REDB}Error: OpenSSH release key fingerprint does not match expected value!\n\tExpected: $release_key_fingerprint_expected\n\tActual: $release_key_fingerprint_actual\n\nTerminating.${CLR}" + echo -e "\n${REDB}Error: ${project} release key fingerprint does not match expected value!\n\tExpected: $release_key_fingerprint_expected\n\tActual: $release_key_fingerprint_actual\n\nTerminating.${CLR}" exit -1 fi - echo -e "\n\n${GREEN}OpenSSH release key matches expected value.${CLR}\n" + echo -e "\n\n${GREEN}${project} release key matches expected value.${CLR}\n" +} + + +# Downloads the specified version of Dropbear. +function get_dropbear { + version=$1 + tarball_checksum_expected=$2 + get_source 'Dropbear' $version $tarball_checksum_expected } # Downloads the specified version of OpenSSH. function get_openssh { - echo -e "\nGetting OpenSSH $1 sources...\n" - wget https://cdn.openbsd.org/pub/OpenBSD/OpenSSH/portable/openssh-$1.tar.gz + version=$1 + tarball_checksum_expected=$2 + get_source 'OpenSSH' $version $tarball_checksum_expected +} - echo -e "\nGetting OpenSSH $1 signature...\n" - wget https://cdn.openbsd.org/pub/OpenBSD/OpenSSH/portable/openssh-$1.tar.gz.asc - openssh_sig=openssh-$1.tar.gz.asc - # Older releases were .sigs. - if [[ ! -f $openssh_sig ]]; then - wget https://cdn.openbsd.org/pub/OpenBSD/OpenSSH/portable/openssh-$1.tar.gz.sig - openssh_sig=openssh-$1.tar.gz.sig +function get_source { + project=$1 + version=$2 + tarball_checksum_expected=$3 + + base_url= + tarball= + sig= + signer= + if [[ $project == 'OpenSSH' ]]; then + base_url='https://cdn.openbsd.org/pub/OpenBSD/OpenSSH/portable/' + tarball="openssh-${version}.tar.gz" + sig="${tarball}.asc" + signer="Damien Miller " + elif [[ $project == 'Dropbear' ]]; then + base_url='https://matt.ucc.asn.au/dropbear/releases/' + tarball="dropbear-${version}.tar.bz2" + sig="${tarball}.asc" + signer="Dropbear SSH Release Signing " fi - local gpg_verify=`gpg --verify $openssh_sig openssh-$1.tar.gz 2>&1` - if [[ $gpg_verify != *"Good signature from \"Damien Miller "* ]]; then - echo -e "\n\n${REDB}Error: OpenSSH signature invalid!\n$gpg_verify\n\nTerminating.${CLR}" + echo -e "\nGetting ${project} ${version} sources...\n" + wget "${base_url}${tarball}" + + echo -e "\nGetting ${project} ${version} signature...\n" + wget "${base_url}${sig}" + + + # Older OpenSSH releases were .sigs. + if [[ ($project == 'OpenSSH') && (! -f $sig) ]]; then + wget ${base_url}openssh-${version}.tar.gz.sig + sig=openssh-${version}.tar.gz.sig + fi + + local gpg_verify=`gpg --verify ${sig} ${tarball} 2>&1` + if [[ $gpg_verify != *"Good signature from \"${signer}"* ]]; then + echo -e "\n\n${REDB}Error: ${project} signature invalid!\n$gpg_verify\n\nTerminating.${CLR}" exit -1 fi # Check GPG's return value. 0 denotes a valid signature, and 1 is returned # on invalid signatures. if [[ $? != 0 ]]; then - echo -e "\n\n${REDB}Error: OpenSSH signature invalid! Verification returned code: $?\n\nTerminating.${CLR}" + echo -e "\n\n${REDB}Error: ${project} signature invalid! Verification returned code: $?\n\nTerminating.${CLR}" exit -1 fi - echo -e "${GREEN}Signature on OpenSSH sources verified.${CLR}\n" + echo -e "${GREEN}Signature on ${project} sources verified.${CLR}\n" - local openssh_checksum_actual=`sha256sum openssh-$1.tar.gz | cut -f1 -d" "` - if [[ $openssh_checksum_actual != "$2" ]]; then - echo -e "${REDB}Error: OpenSSH checksum is invalid!\n Expected: $2\n Actual: $openssh_checksum_actual\n\n Terminating.${CLR}" + local checksum_actual=`sha256sum ${tarball} | cut -f1 -d" "` + if [[ $checksum_actual != $tarball_checksum_expected ]]; then + echo -e "${REDB}Error: ${project} checksum is invalid!\n Expected: ${tarball_checksum_expected}\n Actual: ${checksum_actual}\n\n Terminating.${CLR}" exit -1 fi +} + +# Runs a Dropbear test. Upon failure, a diff between the expected and actual results +# is shown, then the script immediately terminates. +function run_dropbear_test { + dropbear_version=$1 + test_number=$2 + options=$3 + + run_test 'Dropbear' $dropbear_version $test_number "$options" } @@ -214,15 +312,42 @@ function run_openssh_test { openssh_version=$1 test_number=$2 - cid=`docker run -d -p 2222:22 ${IMAGE_NAME}:${IMAGE_VERSION} /openssh/sshd-${openssh_version} -D -f /etc/ssh/sshd_config-${openssh_version}_${test_number}` + run_test 'OpenSSH' $openssh_version $test_number '' +} + + +function run_test { + server_type=$1 + version=$2 + test_number=$3 + options=$4 + + server_exec= + test_result= + expected_result= + test_name= + if [[ $server_type == 'OpenSSH' ]]; then + server_exec="/openssh/sshd-${version} -D -f /etc/ssh/sshd_config-${version}_${test_number}" + test_result="${TEST_RESULT_DIR}/openssh_${version}_${test_number}.txt" + expected_result="test/docker/expected_results/openssh_${version}_${test_number}.txt" + test_name="OpenSSH ${version} ${test_number}" + options= + elif [[ $server_type == 'Dropbear' ]]; then + server_exec="/dropbear/dropbear-${version} -F ${options}" + test_result="${TEST_RESULT_DIR}/dropbear_${version}_${test_number}.txt" + expected_result="test/docker/expected_results/dropbear_${version}_${test_number}.txt" + test_name="Dropbear ${version} ${test_number}" + fi + + cid=`docker run -d -p 2222:22 ${IMAGE_NAME}:${IMAGE_VERSION} ${server_exec}` if [[ $? != 0 ]]; then echo -e "${REDB}Failed to run docker image! (exit code: $?)${CLR}" exit 1 fi - ./ssh-audit.py localhost:2222 > ${TEST_RESULT_DIR}/openssh_${openssh_version}_${test_number}.txt + ./ssh-audit.py localhost:2222 > $test_result if [[ $? != 0 ]]; then - echo -e "${REDB}Failed to ssh-audit.py! (exit code: $?)${CLR}" + echo -e "${REDB}Failed to run ssh-audit.py! (exit code: $?)${CLR}" docker container stop $cid > /dev/null exit 1 fi @@ -233,16 +358,17 @@ function run_openssh_test { exit 1 fi - diff=`diff -u test/docker/expected_results/openssh_${openssh_version}_${test_number}.txt ${TEST_RESULT_DIR}/openssh_${openssh_version}_${test_number}.txt` + diff=`diff -u ${expected_result} ${test_result}` if [[ $? == 0 ]]; then - echo -e "OpenSSH ${openssh_version} ${test_number} ${GREEN}passed${CLR}." + echo -e "${test_name} ${GREEN}passed${CLR}." else - echo -e "OpenSSH ${openssh_version} ${test_number} ${REDB}FAILED${CLR}.\n\n${diff}\n" + echo -e "${test_name} ${REDB}FAILED${CLR}.\n\n${diff}\n" exit 1 fi } + # First check if docker is functional. docker version > /dev/null if [[ $? != 0 ]]; then @@ -276,6 +402,8 @@ echo run_openssh_test '8.0p1' 'test1' run_openssh_test '8.0p1' 'test2' run_openssh_test '8.0p1' 'test3' +echo +run_dropbear_test '2019.78' 'test1' '-r /etc/dropbear/dropbear_rsa_host_key_1024 -r /etc/dropbear/dropbear_dss_host_key -r /etc/dropbear/dropbear_ecdsa_host_key' # The test functions above will terminate the script on failure, so if we reached here, # all tests are successful. diff --git a/test/docker/Dockerfile b/test/docker/Dockerfile index 41a62c2..1f2b05b 100644 --- a/test/docker/Dockerfile +++ b/test/docker/Dockerfile @@ -3,7 +3,9 @@ FROM ubuntu:16.04 COPY openssh-4.0p1/sshd /openssh/sshd-4.0p1 COPY openssh-5.6p1/sshd /openssh/sshd-5.6p1 COPY openssh-8.0p1/sshd /openssh/sshd-8.0p1 +COPY dropbear-2019.78/dropbear /dropbear/dropbear-2019.78 +COPY dropbear_*_host_key* /etc/dropbear/ COPY sshd_config* /etc/ssh/ COPY ssh_host_* /etc/ssh/ COPY ssh1_host_* /etc/ssh/ diff --git a/test/docker/dropbear_dss_host_key b/test/docker/dropbear_dss_host_key new file mode 100644 index 0000000000000000000000000000000000000000..3388632b0fc98e850d7ccf09cd6e8e2038360e55 GIT binary patch literal 458 zcmV;*0X6;r000Mbb7(DOb8`Ry0D%Co?vAb8N10T7&t5a7KnBrOWHzJcUMz$TRebBg zbS3B5a{g@0ZPPxUg57h>RivS8%#uM?Ga2c)1JpXT=r%vFt*#*P)%U zdRufUEj6F$ODd|8z%}SL{-PF#{x82#eMkzxR~@!<)mw9M5Jm*Vo+qc&gKp0N000#L z!OYNY%9>%c-+?rF2Z0$TN3T3p0001hG9|O=EkwE;+%sWMycr2(Dt@vV{7UXSb*Tw> z=--ghyKg!Q(crUCD2l?L?=#+H%%%}f8LyC+HWlX?Qr2@@)entgZa;PJ)$XmfZcH2IyQ2QVZ86tNVJ_iaU!`w0001h zD@H#-_1`-AMZDaov}tLx$NX;|{o|H$q4M-oS1rJe(*Q>!?o9(TU&!>;@AtHxH=PX%arH0l1S;6!Nt zyhK82*h2+mK$tr;GJ!{M}7DVyu8g*Q)c+{;%R{qXd4mh&aYWZt}N@cUg9kQCmzbVcr}Uc*Pe-9Q5s o8MdB3vg`V>vt9qP;th_8i|1aQpE#lK-=fQ>?QJ7gna|S%0PtcxC;$Ke literal 0 HcmV?d00001 diff --git a/test/docker/dropbear_rsa_host_key_1024 b/test/docker/dropbear_rsa_host_key_1024 new file mode 100644 index 0000000000000000000000000000000000000000..d9ce33192211f98e08d492e7419fe6af68740e1d GIT binary patch literal 421 zcmV;W0b2e5000Mbb7(Dcb724g00RL40RR91fdJmKFherJPd3s2qy(yadX!9QR|QKp z2rUd=1```2|XeA$H#38}1yf89A$4keNBd7F`-9>u2?%*j5-CA5ma zN($D>wktN-4*ZmPbIwrpic4Lispi+J$oVwq?7Zr5t>wfawS4vnciro?m=^xn4_Dqx zJSzYI0DuB9a9g6MyPf$kry8dZCO#FR!HPRH$Z6DDjIN}WWuHdrWJ3s@3xccQ@yyUL z2hE4V)eE_K*(Q(8qIcWd@06_r!i2r~u5RRa6)643pwLKTX zZO|qFQ*>8E31G23!SvoM;u7u+3l+?CQ*tyEqV=4MC_b4qmp002P% z;lX9go^~YC9+0NbWmx{*^Jew`cRo^lLD?UP6EU|6HT&i&SwV%6A_Z-vFXhDROSl&R PmW+ql0k`SljVV|k+jGO+ literal 0 HcmV?d00001 diff --git a/test/docker/dropbear_rsa_host_key_3072 b/test/docker/dropbear_rsa_host_key_3072 new file mode 100644 index 0000000000000000000000000000000000000000..006249a89e68a047e9f355312ccf79e8566f2c81 GIT binary patch literal 1189 zcmV;W1X}w5000Mbb7(Dcb724g00RL40RR92fdHDh-7vrii(-xoBNb5B`Sy9xkg!eY zTS*3L@_nYDzm!Cr9)7fE4l?w?8f!=u`J)!>v9)9~DgW>ex}E_W6`qI0Ca3s&_{-d4 z`eC6AARDHV6`qHCs*NdEXEJvekvliwPDk_D4~WMNXKE|s{6#F+ z$1JYslLZW3u7ifDh`Pwl`L-wSvR$D7cbv_Y+Bo{*Csm1Zfyr-~3|{_bu61XeEe8x| z=X7+6m6bLAYtu0O8M?EY2=TyA=BBUakBb#=SFUD#7un5w^^(&1xfF_=)+3~%`fC6H z0e}&FHA-jZgrJ&kZW*-_pok1Z!fqlR7&RvDuE72Y93rfFki}GiahEJWru2Sic~~4| zMU{|D73F(POeI94xfbPV-OVDUZjpa7GDX2Fksnkia~@U%=GtHXC2%*OiRGjwuOLwo zJg%?+(NHBDq}(&}l-8%u(Y^Gu&B)N|rMYzVch)|^`N#+I;qfPcG|0eU8*B3Bt*t@4 zlHs)nj6zQ`j*W%6{x+(VNh=kM*?yHi4hu9o)J}aoK3Zq9LOl+P@Uf=|4F&&qO?IJ}!4dAm>L4CQ6TrqAV{Bq=JdEyN3js0pU5wWfn%$$| zYHoFarfZn!dW8U=`IvL31UyhJqMQw%Dk<9)(6P90cQ6mV)w{n4kN`01+4Ys3(uxtb z+)D^ZIt-`RJQRQdm0H0$c>VLj`tbk&0Kov%L>OvdQw#PvarYX$kO}dyKdFKE%pQb0 zW{e0u9`ugUBN`~r>(KIqPlZZ%Os_qP{y{|2SB$lf#fCddWi;#65RO4xDD#89c;~%@ z++l6h05M6CB6R5-%bK5ea7>JJzY#9(fIZw+0BhM@F|^am zPHRTjDm-k))>zR1006-Nx|-KR2~sTt%TqY}zPDoOEn!cYKl}?oaGtElp&AFfx|ige zXJLxMzqmFyHyGPQD1xBJM6`wXN1b>Fc)9Q<+}dO=i}RST`@1`2VOd;{jtf%&Y6YPj zBopc~RgT`Wmurh$<)=eE4|SM2?`ThX8TR0J+6LC3z;J`_0%koKD0801BIRm`oM)S$ zk?d7>Dnx22rbw~{KNqoSGjB3Fy6@$kgt8 DS2su* literal 0 HcmV?d00001 diff --git a/test/docker/expected_results/dropbear_2019.78_test1.txt b/test/docker/expected_results/dropbear_2019.78_test1.txt new file mode 100644 index 0000000..9edb398 --- /dev/null +++ b/test/docker/expected_results/dropbear_2019.78_test1.txt @@ -0,0 +1,82 @@ +# general +(gen) banner: SSH-2.0-dropbear_2019.78 +(gen) software: Dropbear SSH 2019.78 +(gen) compatibility: OpenSSH 7.4+ (some functionality from 6.6), Dropbear SSH 2016.73+ +(gen) compression: enabled (zlib@openssh.com) + +# key exchange algorithms +(kex) curve25519-sha256 -- [info] available since OpenSSH 7.4 +(kex) curve25519-sha256@libssh.org -- [info] available since OpenSSH 6.5, Dropbear SSH 2013.62 +(kex) ecdh-sha2-nistp521 -- [fail] using weak elliptic curves + `- [info] available since OpenSSH 5.7, Dropbear SSH 2013.62 +(kex) ecdh-sha2-nistp384 -- [fail] using weak elliptic curves + `- [info] available since OpenSSH 5.7, Dropbear SSH 2013.62 +(kex) ecdh-sha2-nistp256 -- [fail] using weak elliptic curves + `- [info] available since OpenSSH 5.7, Dropbear SSH 2013.62 +(kex) diffie-hellman-group14-sha256 -- [info] available since OpenSSH 7.3, Dropbear SSH 2016.73 +(kex) diffie-hellman-group14-sha1 -- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 3.9, Dropbear SSH 0.53 +(kex) kexguess2@matt.ucc.asn.au -- [info] available since Dropbear SSH 2013.57 + +# host-key algorithms +(key) ecdsa-sha2-nistp256 -- [fail] using weak elliptic curves + `- [warn] using weak random number generator could reveal the key + `- [info] available since OpenSSH 5.7, Dropbear SSH 2013.62 +(key) ssh-rsa (1024-bit) -- [fail] using small 1024-bit modulus + `- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28 +(key) ssh-dss -- [fail] removed (in server) and disabled (in client) since OpenSSH 7.0, weak algorithm + `- [warn] using small 1024-bit modulus + `- [warn] using weak random number generator could reveal the key + `- [info] available since OpenSSH 2.1.0, Dropbear SSH 0.28 + +# encryption algorithms (ciphers) +(enc) aes128-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 +(enc) aes256-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 +(enc) aes128-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0, Dropbear SSH 0.28 +(enc) aes256-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0, Dropbear SSH 0.47 +(enc) 3des-ctr -- [fail] using weak cipher + `- [info] available since Dropbear SSH 0.52 +(enc) 3des-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.4, unsafe algorithm + `- [warn] using weak cipher + `- [warn] using weak cipher mode + `- [warn] using small 64-bit block size + `- [info] available since OpenSSH 1.2.2, Dropbear SSH 0.28 + +# message authentication code algorithms +(mac) hmac-sha1-96 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.47 +(mac) hmac-sha1 -- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.1.0, Dropbear SSH 0.28 +(mac) hmac-sha2-256 -- [warn] using encrypt-and-MAC mode + `- [info] available since OpenSSH 5.9, Dropbear SSH 2013.56 + +# fingerprints +(fin) ssh-rsa: SHA256:CDfAU12pjQS7/91kg7gYacza0U/6PDbE04Ic3IpYxkM + +# algorithm recommendations (for Dropbear SSH 2019.78) +(rec) !ssh-rsa -- key algorithm to change (increase modulus size to 2048 bits or larger)  +(rec) -3des-cbc -- enc algorithm to remove  +(rec) -3des-ctr -- enc algorithm to remove  +(rec) -aes128-cbc -- enc algorithm to remove  +(rec) -aes256-cbc -- enc algorithm to remove  +(rec) -ecdh-sha2-nistp256 -- kex algorithm to remove  +(rec) -ecdh-sha2-nistp384 -- kex algorithm to remove  +(rec) -ecdh-sha2-nistp521 -- kex algorithm to remove  +(rec) -ecdsa-sha2-nistp256 -- key algorithm to remove  +(rec) -hmac-sha1-96 -- mac algorithm to remove  +(rec) -ssh-dss -- key algorithm to remove  +(rec) +diffie-hellman-group16-sha512 -- kex algorithm to append  +(rec) +twofish128-ctr -- enc algorithm to append  +(rec) +twofish256-ctr -- enc algorithm to append  +(rec) -diffie-hellman-group14-sha1 -- kex algorithm to remove  +(rec) -hmac-sha1 -- mac algorithm to remove  + From af7e2a088c131e5ea4725acf3391bfe2e71cf870 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Mon, 26 Aug 2019 15:19:49 -0400 Subject: [PATCH 077/103] Added hmac-sha512 and hmac-sha512@ssh.com MACs. Added diffie-hellman-group17-sha512 key exchange. --- README.md | 4 ++-- ssh-audit.py | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7495ad0..890a9a5 100644 --- a/README.md +++ b/README.md @@ -46,9 +46,9 @@ usage: ssh-audit.py [-1246pbnvlt] - Added RSA certificate key length test. - Added Diffie-Hellman modulus size test. - Now outputs host key fingerprints for RSA and ED25519. - - Added 2 new key exchanges: `sntrup4591761x25519-sha512@tinyssh.org`, `diffie-hellman-group-exchange-sha256@ssh.com`. + - Added 3 new key exchanges: `sntrup4591761x25519-sha512@tinyssh.org`, `diffie-hellman-group-exchange-sha256@ssh.com`, `diffie-hellman-group17-sha512`. - Added 3 new encryption algorithms: `des-cbc-ssh1`, `blowfish-ctr`, `twofish-ctr`. - - Added 8 new MACs: `hmac-sha2-56`, `hmac-sha2-224`, `hmac-sha2-384`, `hmac-sha3-256`, `hmac-sha3-384`, `hmac-sha3-512`, `hmac-sha256`, `hmac-sha256@ssh.com`. + - Added 10 new MACs: `hmac-sha2-56`, `hmac-sha2-224`, `hmac-sha2-384`, `hmac-sha3-256`, `hmac-sha3-384`, `hmac-sha3-512`, `hmac-sha256`, `hmac-sha256@ssh.com`, `hmac-sha512`, `hmac-512@ssh.com`. - Added command line argument (-t / --timeout) for connection & reading timeouts. - Updated CVEs for libssh & Dropbear. diff --git a/ssh-audit.py b/ssh-audit.py index 0797f95..bfdcda6 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -322,6 +322,7 @@ class SSH2(object): # pylint: disable=too-few-public-methods 'diffie-hellman-group14-sha256': [['7.3,d2016.73']], 'diffie-hellman-group15-sha512': [[]], 'diffie-hellman-group16-sha512': [['7.3,d2016.73']], + 'diffie-hellman-group17-sha512': [[]], 'diffie-hellman-group18-sha512': [['7.3']], 'diffie-hellman-group-exchange-sha1': [['2.3.0', '6.6', None], [FAIL_OPENSSH67_UNSAFE], [WARN_HASH_WEAK]], 'diffie-hellman-group-exchange-sha256': [['4.4']], @@ -410,6 +411,8 @@ class SSH2(object): # pylint: disable=too-few-public-methods 'hmac-sha3-512': [[], [], [WARN_ENCRYPT_AND_MAC]], 'hmac-sha256': [[], [], [WARN_ENCRYPT_AND_MAC]], 'hmac-sha256@ssh.com': [[], [], [WARN_ENCRYPT_AND_MAC]], + 'hmac-sha512': [[], [], [WARN_ENCRYPT_AND_MAC]], + 'hmac-sha512@ssh.com': [[], [], [WARN_ENCRYPT_AND_MAC]], 'hmac-md5': [['2.1.0,d0.28', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC, WARN_HASH_WEAK]], 'hmac-md5-96': [['2.5.0', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC, WARN_HASH_WEAK]], 'hmac-ripemd160': [['2.5.0', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC]], From 6846b1bf29bf1376277ff5c29557754e76a2a475 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Mon, 26 Aug 2019 15:28:37 -0400 Subject: [PATCH 078/103] Added two KEX algorithms: diffie-hellman-group16-sha256 and diffie-hellman-group-exchange-sha512@ssh.com. --- README.md | 2 +- ssh-audit.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 890a9a5..30cb1ed 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ usage: ssh-audit.py [-1246pbnvlt] - Added RSA certificate key length test. - Added Diffie-Hellman modulus size test. - Now outputs host key fingerprints for RSA and ED25519. - - Added 3 new key exchanges: `sntrup4591761x25519-sha512@tinyssh.org`, `diffie-hellman-group-exchange-sha256@ssh.com`, `diffie-hellman-group17-sha512`. + - Added 5 new key exchanges: `sntrup4591761x25519-sha512@tinyssh.org`, `diffie-hellman-group-exchange-sha256@ssh.com`, `diffie-hellman-group-exchange-sha512@ssh.com`, `diffie-hellman-group16-sha256`, `diffie-hellman-group17-sha512`. - Added 3 new encryption algorithms: `des-cbc-ssh1`, `blowfish-ctr`, `twofish-ctr`. - Added 10 new MACs: `hmac-sha2-56`, `hmac-sha2-224`, `hmac-sha2-384`, `hmac-sha3-256`, `hmac-sha3-384`, `hmac-sha3-512`, `hmac-sha256`, `hmac-sha256@ssh.com`, `hmac-sha512`, `hmac-512@ssh.com`. - Added command line argument (-t / --timeout) for connection & reading timeouts. diff --git a/ssh-audit.py b/ssh-audit.py index bfdcda6..91438a9 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -321,12 +321,14 @@ class SSH2(object): # pylint: disable=too-few-public-methods 'diffie-hellman-group14-sha1': [['3.9,d0.53,l10.6.0'], [], [WARN_HASH_WEAK]], 'diffie-hellman-group14-sha256': [['7.3,d2016.73']], 'diffie-hellman-group15-sha512': [[]], + 'diffie-hellman-group16-sha256': [[]], 'diffie-hellman-group16-sha512': [['7.3,d2016.73']], 'diffie-hellman-group17-sha512': [[]], 'diffie-hellman-group18-sha512': [['7.3']], 'diffie-hellman-group-exchange-sha1': [['2.3.0', '6.6', None], [FAIL_OPENSSH67_UNSAFE], [WARN_HASH_WEAK]], 'diffie-hellman-group-exchange-sha256': [['4.4']], 'diffie-hellman-group-exchange-sha256@ssh.com': [[]], + 'diffie-hellman-group-exchange-sha512@ssh.com': [[]], 'ecdh-sha2-nistp256': [['5.7,d2013.62,l10.6.0'], [WARN_CURVES_WEAK]], 'ecdh-sha2-nistp384': [['5.7,d2013.62'], [WARN_CURVES_WEAK]], 'ecdh-sha2-nistp521': [['5.7,d2013.62'], [WARN_CURVES_WEAK]], From 747177c1c74bfe490cabdbf8b8ae1cfe740292a3 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Tue, 27 Aug 2019 17:02:03 -0400 Subject: [PATCH 079/103] Added TinySSH support. Fixes #7. --- ssh-audit.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ssh-audit.py b/ssh-audit.py index 91438a9..1ea93cd 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -1206,6 +1206,7 @@ class SSH(object): # pylint: disable=too-few-public-methods OpenSSH = 'OpenSSH' DropbearSSH = 'Dropbear SSH' LibSSH = 'libssh' + TinySSH = 'TinySSH' class Software(object): def __init__(self, vendor, product, version, patch, os_version): @@ -1406,6 +1407,9 @@ class SSH(object): # pylint: disable=too-few-public-methods if bool(mx): v, p = 'Cisco', 'IOS/PIX sshd' return cls(v, p, mx.group(1), None, None) + mx = re.match(r'^tinyssh_(.*)', software) + if bool(mx): + return cls(None, SSH.Product.TinySSH, mx.group(1), None, None) return None class Banner(object): @@ -1675,7 +1679,8 @@ class SSH(object): # pylint: disable=too-few-public-methods # pylint: disable=too-many-locals,too-many-statements vproducts = [SSH.Product.OpenSSH, SSH.Product.DropbearSSH, - SSH.Product.LibSSH] + SSH.Product.LibSSH, + SSH.Product.TinySSH] if software is not None: if software.product not in vproducts: software = None From 722141356756b17666b1b391d6aba5f3f713fa80 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Tue, 27 Aug 2019 22:28:24 -0400 Subject: [PATCH 080/103] Added TinySSH test. --- docker_test.sh | 111 +++++++++++++++--- test/docker/.ed25519.sk | 1 + test/docker/Dockerfile | 12 +- test/docker/ed25519.pk | 1 + .../tinyssh_20190101_test1.txt | 25 ++++ 5 files changed, 132 insertions(+), 18 deletions(-) create mode 100644 test/docker/.ed25519.sk create mode 100644 test/docker/ed25519.pk create mode 100644 test/docker/expected_results/tinyssh_20190101_test1.txt diff --git a/docker_test.sh b/docker_test.sh index 548a676..fa437a7 100755 --- a/docker_test.sh +++ b/docker_test.sh @@ -15,7 +15,7 @@ # This is the docker tag for the image. If this tag doesn't exist, then we assume the # image is out of date, and generate a new one with this tag. -IMAGE_VERSION=2 +IMAGE_VERSION=3 # This is the name of our docker image. IMAGE_NAME=ssh-audit-test @@ -48,6 +48,14 @@ function compile_openssh { compile 'OpenSSH' $version } + +# Uncompresses and compiles the specified version of TinySSH. +function compile_tinyssh { + version=$1 + compile 'TinySSH' $version +} + + function compile { project=$1 version=$2 @@ -66,6 +74,11 @@ function compile { uncompress_options="xjf" source_dir="dropbear-${version}" server_executable=dropbear + elif [[ $project == 'TinySSH' ]]; then + tarball="${version}.tar.gz" + uncompress_options="xzf" + source_dir="tinyssh-${version}" + server_executable='build/bin/tinysshd' fi echo "Uncompressing ${project} ${version}..." @@ -73,7 +86,13 @@ function compile { echo "Compiling ${project} ${version}..." pushd $source_dir > /dev/null - ./configure && make -j 10 + + # TinySSH has no configure script... only a Makefile. + if [[ $project == 'TinySSH' ]]; then + make -j 10 + else + ./configure && make -j 10 + fi if [[ ! -f $server_executable ]]; then echo -e "${REDB}Error: ${server_executable} not built!${CLR}" @@ -90,16 +109,17 @@ function create_docker_image { # Create a new temporary directory. TMP_DIR=`mktemp -d /tmp/sshaudit-docker-XXXXXXXXXX` - # Copy the Dockerfile to our new temp directory. - cp test/docker/* $TMP_DIR + # Copy the Dockerfile and all files in the test/docker/ dir to our new temp directory. + find test/docker/ -maxdepth 1 -type f | xargs cp -t $TMP_DIR # Make the temp directory our working directory for the duration of the build # process. pushd $TMP_DIR > /dev/null - # Get the release key for OpenSSH. - get_openssh_release_key + # Get the release keys. get_dropbear_release_key + get_openssh_release_key + get_tinyssh_release_key # Aside from checking the GPG signatures, we also compare against this known-good # SHA-256 hash just in case. @@ -107,6 +127,7 @@ function create_docker_image { get_openssh '5.6p1' '538af53b2b8162c21a293bb004ae2bdb141abd250f61b4cea55244749f3c6c2b' get_openssh '8.0p1' 'bd943879e69498e8031eb6b7f44d08cdc37d59a7ab689aa0b437320c3481fd68' get_dropbear '2019.78' '525965971272270995364a0eb01f35180d793182e63dd0b0c3eb0292291644a4' + get_tinyssh '20190101' '554a9a94e53b370f0cd0c5fbbd322c34d1f695cbcea6a6a32dcb8c9f595b3fea' # Compile the versions of OpenSSH. compile_openssh '4.0p1' @@ -116,6 +137,9 @@ function create_docker_image { # Compile the versions of Dropbear. compile_dropbear '2019.78' + # Compile the versions of TinySSH. + compile_tinyssh '20190101' + # Rename the default config files so we know they are our originals. mv openssh-4.0p1/sshd_config sshd_config-4.0p1_orig @@ -199,19 +223,30 @@ function get_openssh_release_key { } +# Downloads the TinySSH release key and adds it to the local keyring. +function get_tinyssh_release_key { + get_release_key 'TinySSH' '' '96939FF9' 'AADF 2EDF 5529 F170 2772 C8A2 DEC4 D246 931E F49B' +} + + function get_release_key { project=$1 key_url=$2 key_id=$3 release_key_fingerprint_expected=$4 - echo -e "\nGetting ${project} release key...\n" - wget -O key.asc $2 + # The TinySSH release key isn't on any website, apparently. + if [[ $project == 'TinySSH' ]]; then + gpg --recv-key $key_id + else + echo -e "\nGetting ${project} release key...\n" + wget -O key.asc $2 - echo -e "\nImporting ${project} release key...\n" - gpg --import key.asc + echo -e "\nImporting ${project} release key...\n" + gpg --import key.asc - rm key.asc + rm key.asc + fi local release_key_fingerprint_actual=`gpg --fingerprint ${key_id}` if [[ $release_key_fingerprint_actual != *"$release_key_fingerprint_expected"* ]]; then @@ -238,37 +273,54 @@ function get_openssh { } +# Downloads the specified version of TinySSH. +function get_tinyssh { + version=$1 + tarball_checksum_expected=$2 + get_source 'TinySSH' $version $tarball_checksum_expected +} + + function get_source { project=$1 version=$2 tarball_checksum_expected=$3 - base_url= + base_url_source= + base_url_sig= tarball= sig= signer= if [[ $project == 'OpenSSH' ]]; then - base_url='https://cdn.openbsd.org/pub/OpenBSD/OpenSSH/portable/' + base_url_source='https://cdn.openbsd.org/pub/OpenBSD/OpenSSH/portable/' + base_url_sig=$base_url_source tarball="openssh-${version}.tar.gz" sig="${tarball}.asc" signer="Damien Miller " elif [[ $project == 'Dropbear' ]]; then - base_url='https://matt.ucc.asn.au/dropbear/releases/' + base_url_source='https://matt.ucc.asn.au/dropbear/releases/' + base_url_sig=$base_url_source tarball="dropbear-${version}.tar.bz2" sig="${tarball}.asc" signer="Dropbear SSH Release Signing " + elif [[ $project == 'TinySSH' ]]; then + base_url_source='https://github.com/janmojzis/tinyssh/archive/' + base_url_sig="https://github.com/janmojzis/tinyssh/releases/download/${version}/" + tarball="${version}.tar.gz" + sig="${tarball}.asc" + signer="Jan Mojžíš " fi echo -e "\nGetting ${project} ${version} sources...\n" - wget "${base_url}${tarball}" + wget "${base_url_source}${tarball}" echo -e "\nGetting ${project} ${version} signature...\n" - wget "${base_url}${sig}" + wget "${base_url_sig}${sig}" # Older OpenSSH releases were .sigs. if [[ ($project == 'OpenSSH') && (! -f $sig) ]]; then - wget ${base_url}openssh-${version}.tar.gz.sig + wget ${base_url_sig}openssh-${version}.tar.gz.sig sig=openssh-${version}.tar.gz.sig fi @@ -316,6 +368,16 @@ function run_openssh_test { } +# Runs a TinySSH test. Upon failure, a diff between the expected and actual results +# is shown, then the script immediately terminates. +function run_tinyssh_test { + tinyssh_version=$1 + test_number=$2 + + run_test 'TinySSH' $tinyssh_version $test_number '' +} + + function run_test { server_type=$1 version=$2 @@ -337,6 +399,11 @@ function run_test { test_result="${TEST_RESULT_DIR}/dropbear_${version}_${test_number}.txt" expected_result="test/docker/expected_results/dropbear_${version}_${test_number}.txt" test_name="Dropbear ${version} ${test_number}" + elif [[ $server_type == 'TinySSH' ]]; then + server_exec="/usr/bin/tcpserver -HRDl0 0.0.0.0 22 /tinysshd/tinyssh-20190101 -v /etc/tinyssh/" + test_result="${TEST_RESULT_DIR}/tinyssh_${version}_${test_number}.txt" + expected_result="test/docker/expected_results/tinyssh_${version}_${test_number}.txt" + test_name="TinySSH ${version} ${test_number}" fi cid=`docker run -d -p 2222:22 ${IMAGE_NAME}:${IMAGE_VERSION} ${server_exec}` @@ -358,6 +425,14 @@ function run_test { exit 1 fi + # TinySSH outputs a random string in each banner, which breaks our test. So + # we need to filter out the banner part of the output so we get stable, repeatable + # results. + if [[ $server_type == 'TinySSH' ]]; then + grep -v "(gen) banner: " ${test_result} > "${test_result}.tmp" + mv "${test_result}.tmp" ${test_result} + fi + diff=`diff -u ${expected_result} ${test_result}` if [[ $? == 0 ]]; then echo -e "${test_name} ${GREEN}passed${CLR}." @@ -404,6 +479,8 @@ run_openssh_test '8.0p1' 'test2' run_openssh_test '8.0p1' 'test3' echo run_dropbear_test '2019.78' 'test1' '-r /etc/dropbear/dropbear_rsa_host_key_1024 -r /etc/dropbear/dropbear_dss_host_key -r /etc/dropbear/dropbear_ecdsa_host_key' +echo +run_tinyssh_test '20190101' 'test1' # The test functions above will terminate the script on failure, so if we reached here, # all tests are successful. diff --git a/test/docker/.ed25519.sk b/test/docker/.ed25519.sk new file mode 100644 index 0000000..58b097b --- /dev/null +++ b/test/docker/.ed25519.sk @@ -0,0 +1 @@ +iÜ›ãÈÍíVÌè¿ÓZ/Dì<†î|S“zË=°:×1vu}¢ï„JòÝ·ŸŠ"à^Bb&U‰ìP« ’CJ? \ No newline at end of file diff --git a/test/docker/Dockerfile b/test/docker/Dockerfile index 1f2b05b..eef0139 100644 --- a/test/docker/Dockerfile +++ b/test/docker/Dockerfile @@ -4,17 +4,27 @@ COPY openssh-4.0p1/sshd /openssh/sshd-4.0p1 COPY openssh-5.6p1/sshd /openssh/sshd-5.6p1 COPY openssh-8.0p1/sshd /openssh/sshd-8.0p1 COPY dropbear-2019.78/dropbear /dropbear/dropbear-2019.78 +COPY tinyssh-20190101/build/bin/tinysshd /tinysshd/tinyssh-20190101 +# Dropbear host keys. COPY dropbear_*_host_key* /etc/dropbear/ + +# OpenSSH configs. COPY sshd_config* /etc/ssh/ + +# OpenSSH host keys & moduli file. COPY ssh_host_* /etc/ssh/ COPY ssh1_host_* /etc/ssh/ COPY moduli_1024 /usr/local/etc/moduli +# TinySSH host keys. +COPY ed25519.pk /etc/tinyssh/ +COPY .ed25519.sk /etc/tinyssh/ + COPY debug.sh /debug.sh RUN apt update 2> /dev/null -RUN apt install -y libssl-dev strace rsyslog 2> /dev/null +RUN apt install -y libssl-dev strace rsyslog ucspi-tcp 2> /dev/null RUN apt clean 2> /dev/null RUN useradd -s /bin/false sshd RUN mkdir /var/empty diff --git a/test/docker/ed25519.pk b/test/docker/ed25519.pk new file mode 100644 index 0000000..82cfb47 --- /dev/null +++ b/test/docker/ed25519.pk @@ -0,0 +1 @@ +1vu}¢ï„JòÝ·ŸŠ"à^Bb&U‰ìP« ’CJ? \ No newline at end of file diff --git a/test/docker/expected_results/tinyssh_20190101_test1.txt b/test/docker/expected_results/tinyssh_20190101_test1.txt new file mode 100644 index 0000000..b6df3b1 --- /dev/null +++ b/test/docker/expected_results/tinyssh_20190101_test1.txt @@ -0,0 +1,25 @@ +# general +(gen) software: TinySSH noversion +(gen) compatibility: OpenSSH 8.0+, Dropbear SSH 2013.62+ +(gen) compression: disabled + +# key exchange algorithms +(kex) curve25519-sha256 -- [info] available since OpenSSH 7.4 +(kex) curve25519-sha256@libssh.org -- [info] available since OpenSSH 6.5, Dropbear SSH 2013.62 +(kex) sntrup4591761x25519-sha512@tinyssh.org -- [warn] using experimental algorithm + `- [info] available since OpenSSH 8.0 + +# host-key algorithms +(key) ssh-ed25519 -- [info] available since OpenSSH 6.5 + +# encryption algorithms (ciphers) +(enc) chacha20-poly1305@openssh.com -- [info] available since OpenSSH 6.5 + `- [info] default cipher since OpenSSH 6.9. + +# message authentication code algorithms +(mac) hmac-sha2-256 -- [warn] using encrypt-and-MAC mode + `- [info] available since OpenSSH 5.9, Dropbear SSH 2013.56 + +# fingerprints +(fin) ssh-ed25519: SHA256:89ocln1x7KNqnMgWffGoYtD70ksJ4FrH7BMJHa7SrwU + From c185a25af115911e755d9a0f999512fad913fa4b Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Wed, 28 Aug 2019 00:37:55 -0400 Subject: [PATCH 081/103] For unrecognized servers, only recommend algorithm changes & removals, not additions (since they can be very inaccurate). --- ssh-audit.py | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/ssh-audit.py b/ssh-audit.py index 1ea93cd..ca4db6f 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -1681,21 +1681,27 @@ class SSH(object): # pylint: disable=too-few-public-methods SSH.Product.DropbearSSH, SSH.Product.LibSSH, SSH.Product.TinySSH] + # Set to True if server is not one of vproducts, above. + unknown_software = False if software is not None: if software.product not in vproducts: - software = None - if software is None: - ssh_timeframe = self.get_ssh_timeframe(for_server) - for product in vproducts: - if product not in ssh_timeframe: - continue - version = ssh_timeframe.get_from(product, for_server) - if version is not None: - software = SSH.Software(None, product, version, None, None) - break + unknown_software = True +# +# The code below is commented out because it would try to guess what the server is, +# usually resulting in wild & incorrect recommendations. +# +# if software is None: +# ssh_timeframe = self.get_ssh_timeframe(for_server) +# for product in vproducts: +# if product not in ssh_timeframe: +# continue +# version = ssh_timeframe.get_from(product, for_server) +# if version is not None: +# software = SSH.Software(None, product, version, None, None) +# break rec = {} # type: Dict[int, Dict[str, Dict[str, Dict[str, int]]]] if software is None: - return software, rec + unknown_software = True for alg_pair in self.values: sshv, alg_db = alg_pair.sshv, alg_pair.db rec[sshv] = {} @@ -1708,15 +1714,17 @@ class SSH(object): # pylint: disable=too-few-public-methods if len(versions) == 0 or versions[0] is None: continue matches = False + if unknown_software: + matches = True for v in versions[0].split(','): ssh_prefix, ssh_version, is_cli = SSH.Algorithm.get_ssh_version(v) if not ssh_version: continue - if ssh_prefix != software.product: + if (software is not None) and (ssh_prefix != software.product): continue if is_cli and for_server: continue - if software.compare_version(ssh_version) < 0: + if (software is not None) and (software.compare_version(ssh_version) < 0): continue matches = True break @@ -1740,6 +1748,9 @@ class SSH(object): # pylint: disable=too-few-public-methods rec[sshv][alg_type]['chg'][n] = faults else: rec[sshv][alg_type]['del'][n] = faults + # If we are working with unknown software, drop all add recommendations, because we don't know if they're valid. + if unknown_software: + rec[sshv][alg_type]['add'] = {} add_count = len(rec[sshv][alg_type]['add']) del_count = len(rec[sshv][alg_type]['del']) chg_count = len(rec[sshv][alg_type]['chg']) From f7cbe71aba19400fc1bbb3a19e0c20fe6e175c06 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Thu, 29 Aug 2019 15:34:19 -0400 Subject: [PATCH 082/103] Updated for v2.0.0 release. --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 30cb1ed..7d0fb52 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ # ssh-audit + **ssh-audit** is a tool for ssh server auditing. ## Features @@ -37,10 +39,10 @@ usage: ssh-audit.py [-1246pbnvlt] * verbose flag `-v` will prefix each line with section type and algorithm name. ### Example -![screenshot](https://cloud.githubusercontent.com/assets/7356025/19233757/3e09b168-8ef0-11e6-91b4-e880bacd0b8a.png) +![screenshot](https://user-images.githubusercontent.com/2982011/63970414-104bdb00-ca72-11e9-832f-3e535be32811.png) ## ChangeLog -### v2.0.0-dev (???) +### v2.0.0 (2019-08-29) - Forked from https://github.com/arthepsy/ssh-audit (development was stalled, and developer went MIA). - Added RSA host key length test. - Added RSA certificate key length test. From 6f60722455044881bdec4ab4b07da057f95ec3b9 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Thu, 29 Aug 2019 15:53:35 -0400 Subject: [PATCH 083/103] Fixed version number. --- ssh-audit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ssh-audit.py b/ssh-audit.py index ca4db6f..06e62cd 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -27,7 +27,7 @@ from __future__ import print_function import binascii, os, io, sys, socket, struct, random, errno, getopt, re, hashlib, base64 -VERSION = 'v2.0.0-dev' +VERSION = 'v2.0.0' if sys.version_info.major < 3: print("\n!!!! NOTE: Python 2 is being considered for deprecation. If you have a good reason to need continued Python 2 support, please e-mail jtesta@positronsecurity.com with your rationale.\n\n") From f5431559ff239c7516747a12a3b565d22eb71dea Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Thu, 29 Aug 2019 16:52:38 -0400 Subject: [PATCH 084/103] Bumped version number. --- README.md | 3 +++ ssh-audit.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7d0fb52..0a28136 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,9 @@ usage: ssh-audit.py [-1246pbnvlt] ![screenshot](https://user-images.githubusercontent.com/2982011/63970414-104bdb00-ca72-11e9-832f-3e535be32811.png) ## ChangeLog +### v2.1.0 (???) + - ??? + ### v2.0.0 (2019-08-29) - Forked from https://github.com/arthepsy/ssh-audit (development was stalled, and developer went MIA). - Added RSA host key length test. diff --git a/ssh-audit.py b/ssh-audit.py index 06e62cd..c4cc04d 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -27,7 +27,7 @@ from __future__ import print_function import binascii, os, io, sys, socket, struct, random, errno, getopt, re, hashlib, base64 -VERSION = 'v2.0.0' +VERSION = 'v2.1.0-dev' if sys.version_info.major < 3: print("\n!!!! NOTE: Python 2 is being considered for deprecation. If you have a good reason to need continued Python 2 support, please e-mail jtesta@positronsecurity.com with your rationale.\n\n") From bce9e2b1524382f6606a05d5fb487fe31cc45b08 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Tue, 3 Sep 2019 20:41:53 -0400 Subject: [PATCH 085/103] Added new KEX: diffie-hellman-group15-sha256. --- README.md | 2 +- ssh-audit.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0a28136..eecfe0b 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ usage: ssh-audit.py [-1246pbnvlt] ## ChangeLog ### v2.1.0 (???) - - ??? + - Added 1 new key exchange: `diffie-hellman-group15-sha256`. ### v2.0.0 (2019-08-29) - Forked from https://github.com/arthepsy/ssh-audit (development was stalled, and developer went MIA). diff --git a/ssh-audit.py b/ssh-audit.py index c4cc04d..8fa3955 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -320,6 +320,7 @@ class SSH2(object): # pylint: disable=too-few-public-methods 'diffie-hellman-group1-sha1': [['2.3.0,d0.28,l10.2', '6.6', '6.9'], [FAIL_OPENSSH67_UNSAFE, FAIL_OPENSSH70_LOGJAM], [WARN_MODULUS_SIZE, WARN_HASH_WEAK]], 'diffie-hellman-group14-sha1': [['3.9,d0.53,l10.6.0'], [], [WARN_HASH_WEAK]], 'diffie-hellman-group14-sha256': [['7.3,d2016.73']], + 'diffie-hellman-group15-sha256': [[]], 'diffie-hellman-group15-sha512': [[]], 'diffie-hellman-group16-sha256': [[]], 'diffie-hellman-group16-sha512': [['7.3,d2016.73']], From eac81455a90c7313c7176e8a349cc28c7f1fb61b Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Wed, 4 Sep 2019 15:05:07 -0400 Subject: [PATCH 086/103] Added PyPI package support. --- pypi/MANIFEST.in | 1 + pypi/Makefile | 14 ++++++++++++++ pypi/setup.py | 38 ++++++++++++++++++++++++++++++++++++++ pypi/sshaudit/__init__.py | 0 pypi/sshaudit/__main__.py | 4 ++++ ssh-audit.py | 6 +++++- 6 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 pypi/MANIFEST.in create mode 100644 pypi/Makefile create mode 100644 pypi/setup.py create mode 100644 pypi/sshaudit/__init__.py create mode 100644 pypi/sshaudit/__main__.py diff --git a/pypi/MANIFEST.in b/pypi/MANIFEST.in new file mode 100644 index 0000000..90fc18d --- /dev/null +++ b/pypi/MANIFEST.in @@ -0,0 +1 @@ +include sshaudit/LICENSE diff --git a/pypi/Makefile b/pypi/Makefile new file mode 100644 index 0000000..804b804 --- /dev/null +++ b/pypi/Makefile @@ -0,0 +1,14 @@ +all: + cp ../ssh-audit.py sshaudit/sshaudit.py + cp ../LICENSE sshaudit/LICENSE + cp ../README.md sshaudit/README.md + python3 setup.py sdist bdist_wheel + +uploadtest: + twine upload --repository-url https://test.pypi.org/legacy/ dist/* + +uploadprod: + twine upload dist/* + +clean: + rm -rf build/ dist/ *.egg-info/ sshaudit/sshaudit.py sshaudit/LICENSE sshaudit/README.md diff --git a/pypi/setup.py b/pypi/setup.py new file mode 100644 index 0000000..e9c4c8d --- /dev/null +++ b/pypi/setup.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- + + +import re +from setuptools import setup + + +version = re.search('^VERSION\s*=\s*\'v(\d\.\d\.\d)\'', open('sshaudit/sshaudit.py').read(), re.M).group(1) +print("\n\nPackaging ssh-audit v%s...\n\n" % version) + +with open("sshaudit/README.md", "rb") as f: + long_descr = f.read().decode("utf-8") + + +setup( + name = "ssh-audit", + packages = ["sshaudit"], + license = 'MIT', + entry_points = { + "console_scripts": ['ssh-audit = sshaudit.sshaudit:main'] + }, + version = version, + description = "An SSH server configuration security auditing tool", + long_description = long_descr, + long_description_content_type = "text/markdown", + author = "Joe Testa", + author_email = "jtesta@positronsecurity.com", + url = "https://github.com/jtesta/ssh-audit", + classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Information Technology", + "Intended Audience :: System Administrators", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Topic :: Security", + "Topic :: Security :: Cryptography" + ]) diff --git a/pypi/sshaudit/__init__.py b/pypi/sshaudit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pypi/sshaudit/__main__.py b/pypi/sshaudit/__main__.py new file mode 100644 index 0000000..514349d --- /dev/null +++ b/pypi/sshaudit/__main__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from .sshaudit import main +main() diff --git a/ssh-audit.py b/ssh-audit.py index 8fa3955..c84a865 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -3012,6 +3012,10 @@ def audit(aconf, sshv=None): utils = Utils() out = Output() -if __name__ == '__main__': # pragma: nocover + +def main(): conf = AuditConf.from_cmdline(sys.argv[1:], usage) audit(conf) + +if __name__ == '__main__': # pragma: nocover + main() From 209bcab42783f80819beae49189a7cdc37c90170 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Wed, 4 Sep 2019 15:06:49 -0400 Subject: [PATCH 087/103] Added entries to .gitignore. --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 9dc68e2..18e72f1 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ venv*/ .coverage* reports/ .scannerwork/ +pypi/sshaudit/LICENSE +pypi/sshaudit/README.md +pypi/sshaudit/sshaudit.py From 0df63c20ac29e3e9f0e51af53a94c367baa3a854 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Thu, 5 Sep 2019 18:52:32 -0400 Subject: [PATCH 088/103] Updated screen shot. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index eecfe0b..893cf85 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ usage: ssh-audit.py [-1246pbnvlt] * verbose flag `-v` will prefix each line with section type and algorithm name. ### Example -![screenshot](https://user-images.githubusercontent.com/2982011/63970414-104bdb00-ca72-11e9-832f-3e535be32811.png) +![screenshot](https://user-images.githubusercontent.com/2982011/64388792-317e6f80-d00e-11e9-826e-a4934769bb07.png) ## ChangeLog ### v2.1.0 (???) From bbf6204ce1104158adc5cc58984481c533ec2098 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Thu, 19 Sep 2019 20:08:10 -0400 Subject: [PATCH 089/103] Add support for Sun_SSH (on Solaris). Add 'gss-group1-sha1-toWM5Slw5Ew8Mqkay+al2g==' key exchange. --- ssh-audit.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ssh-audit.py b/ssh-audit.py index c84a865..e1db77b 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -318,6 +318,7 @@ class SSH2(object): # pylint: disable=too-few-public-methods # Format: 'algorithm_name': [['version_first_appeared_in'], [reason_for_failure1, reason_for_failure2, ...], [warning1, warning2, ...]] 'kex': { 'diffie-hellman-group1-sha1': [['2.3.0,d0.28,l10.2', '6.6', '6.9'], [FAIL_OPENSSH67_UNSAFE, FAIL_OPENSSH70_LOGJAM], [WARN_MODULUS_SIZE, WARN_HASH_WEAK]], + 'gss-group1-sha1-toWM5Slw5Ew8Mqkay+al2g==': [[], [FAIL_OPENSSH67_UNSAFE, FAIL_OPENSSH70_LOGJAM], [WARN_MODULUS_SIZE, WARN_HASH_WEAK]], 'diffie-hellman-group14-sha1': [['3.9,d0.53,l10.6.0'], [], [WARN_HASH_WEAK]], 'diffie-hellman-group14-sha256': [['7.3,d2016.73']], 'diffie-hellman-group15-sha256': [[]], @@ -1194,6 +1195,7 @@ class SSH(object): # pylint: disable=too-few-public-methods class Protocol(object): # pylint: disable=too-few-public-methods # pylint: disable=bad-whitespace SMSG_PUBLIC_KEY = 2 + MSG_DEBUG = 4 MSG_KEXINIT = 20 MSG_NEWKEYS = 21 MSG_KEXDH_INIT = 30 @@ -2518,10 +2520,14 @@ class KexGroupExchange(KexDH): s.send_packet() packet_type, payload = s.read_packet(2) - if packet_type != SSH.Protocol.MSG_KEXDH_GEX_GROUP: + if (packet_type != SSH.Protocol.MSG_KEXDH_GEX_GROUP) and (packet_type != SSH.Protocol.MSG_DEBUG): # TODO: replace with a better exception type. raise Exception('Expected MSG_KEXDH_GEX_REPLY (%d), but got %d instead.' % (SSH.Protocol.MSG_KEXDH_GEX_REPLY, packet_type)) + # Skip any & all MSG_DEBUG messages. + while packet_type == SSH.Protocol.MSG_DEBUG: + packet_type, payload = s.read_packet(2) + # Parse the modulus (p) and generator (g) values from the server. ptr = 0 p_len = struct.unpack('>I', payload[ptr:ptr + 4])[0] From 14af53cf04c644c8f393cdd38f7e34aa342eab9f Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Thu, 19 Sep 2019 20:10:37 -0400 Subject: [PATCH 090/103] Updated ChangeLog for v2.1.0. --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 893cf85..656eb56 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,8 @@ usage: ssh-audit.py [-1246pbnvlt] ## ChangeLog ### v2.1.0 (???) - - Added 1 new key exchange: `diffie-hellman-group15-sha256`. + - Fixed crash while scanning Solaris Sun_SSH. + - Added 2 new key exchanges: `gss-group1-sha1-toWM5Slw5Ew8Mqkay+al2g==`, `diffie-hellman-group15-sha256`. ### v2.0.0 (2019-08-29) - Forked from https://github.com/arthepsy/ssh-audit (development was stalled, and developer went MIA). From 8c5493ae3e8783ddee48e3a1b5a90ec8bf8804a2 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Thu, 19 Sep 2019 22:19:26 -0400 Subject: [PATCH 091/103] Added 2 key exchanges (ecdh-sha2-1.3.132.0.10, curve448-sha512), 1 host key type (ecdsa-sha2-1.3.132.0.10), and 2 MACs (hmac-sha2-256-96-etm@openssh.com, hmac-sha2-512-96-etm@openssh.com). --- README.md | 4 +++- ssh-audit.py | 7 +++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 656eb56..ddabb9e 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,9 @@ usage: ssh-audit.py [-1246pbnvlt] ## ChangeLog ### v2.1.0 (???) - Fixed crash while scanning Solaris Sun_SSH. - - Added 2 new key exchanges: `gss-group1-sha1-toWM5Slw5Ew8Mqkay+al2g==`, `diffie-hellman-group15-sha256`. + - Added 5 new key exchanges: `gss-group1-sha1-toWM5Slw5Ew8Mqkay+al2g==`, `diffie-hellman-group15-sha256`, `ecdh-sha2-1.3.132.0.10`, `curve448-sha512`. + - Added 1 new host key type: `ecdsa-sha2-1.3.132.0.10`. + - Added 2 new MACs: `hmac-sha2-256-96-etm@openssh.com`, `hmac-sha2-512-96-etm@openssh.com`. ### v2.0.0 (2019-08-29) - Forked from https://github.com/arthepsy/ssh-audit (development was stalled, and developer went MIA). diff --git a/ssh-audit.py b/ssh-audit.py index e1db77b..3eb4871 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -312,6 +312,7 @@ class SSH2(object): # pylint: disable=too-few-public-methods WARN_CIPHER_WEAK = 'using weak cipher' WARN_ENCRYPT_AND_MAC = 'using encrypt-and-MAC mode' WARN_TAG_SIZE = 'using small 64-bit tag size' + WARN_TAG_SIZE_96 = 'using small 96-bit tag size' WARN_EXPERIMENTAL = 'using experimental algorithm' ALGORITHMS = { @@ -334,8 +335,10 @@ class SSH2(object): # pylint: disable=too-few-public-methods 'ecdh-sha2-nistp256': [['5.7,d2013.62,l10.6.0'], [WARN_CURVES_WEAK]], 'ecdh-sha2-nistp384': [['5.7,d2013.62'], [WARN_CURVES_WEAK]], 'ecdh-sha2-nistp521': [['5.7,d2013.62'], [WARN_CURVES_WEAK]], + 'ecdh-sha2-1.3.132.0.10': [[]], # ECDH over secp256k1 (i.e.: the Bitcoin curve) 'curve25519-sha256@libssh.org': [['6.5,d2013.62,l10.6.0']], 'curve25519-sha256': [['7.4']], + 'curve448-sha512': [[]], 'kexguess2@matt.ucc.asn.au': [['d2013.57']], 'rsa1024-sha1': [[], [], [WARN_MODULUS_SIZE, WARN_HASH_WEAK]], 'rsa2048-sha256': [[]], @@ -359,6 +362,7 @@ class SSH2(object): # pylint: disable=too-few-public-methods 'ecdsa-sha2-nistp384-cert-v01@openssh.com': [['5.7'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]], 'ecdsa-sha2-nistp521-cert-v01@openssh.com': [['5.7'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]], 'ssh-rsa-sha256@ssh.com': [[]], + 'ecdsa-sha2-1.3.132.0.10': [[], [], [WARN_RNDSIG_KEY]], # ECDSA over secp256k1 (i.e.: the Bitcoin curve) }, 'enc': { 'none': [['1.2.2,d2013.56,l10.2'], [FAIL_PLAINTEXT]], @@ -410,6 +414,7 @@ class SSH2(object): # pylint: disable=too-few-public-methods 'hmac-sha2-384': [[], [], [WARN_ENCRYPT_AND_MAC]], 'hmac-sha2-512': [['5.9,d2013.56,l10.7.0'], [], [WARN_ENCRYPT_AND_MAC]], 'hmac-sha2-512-96': [['5.9', '6.0'], [FAIL_OPENSSH61_REMOVE], [WARN_ENCRYPT_AND_MAC]], + 'hmac-sha2-256-96-etm@openssh.com': [[]], 'hmac-sha3-256': [[], [], [WARN_ENCRYPT_AND_MAC]], 'hmac-sha3-384': [[], [], [WARN_ENCRYPT_AND_MAC]], 'hmac-sha3-512': [[], [], [WARN_ENCRYPT_AND_MAC]], @@ -425,6 +430,8 @@ class SSH2(object): # pylint: disable=too-few-public-methods 'umac-128@openssh.com': [['6.2'], [], [WARN_ENCRYPT_AND_MAC]], 'hmac-sha1-etm@openssh.com': [['6.2'], [], [WARN_HASH_WEAK]], 'hmac-sha1-96-etm@openssh.com': [['6.2', '6.6', None], [FAIL_OPENSSH67_UNSAFE], [WARN_HASH_WEAK]], + 'hmac-sha2-256-96-etm@openssh.com': [[], [], [WARN_TAG_SIZE_96]], # Despite the @openssh.com tag, it doesn't appear that this was ever shipped with OpenSSH; it is only implemented in AsyncSSH (?). + 'hmac-sha2-512-96-etm@openssh.com': [[], [], [WARN_TAG_SIZE_96]], # Despite the @openssh.com tag, it doesn't appear that this was ever shipped with OpenSSH; it is only implemented in AsyncSSH (?). 'hmac-sha2-256-etm@openssh.com': [['6.2']], 'hmac-sha2-512-etm@openssh.com': [['6.2']], 'hmac-md5-etm@openssh.com': [['6.2', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_HASH_WEAK]], From 08677d65b1909f69cf7f0aa24899c79dce379091 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Thu, 19 Sep 2019 22:25:30 -0400 Subject: [PATCH 092/103] Added potential fix for additional crash against Sun_SSH. --- ssh-audit.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ssh-audit.py b/ssh-audit.py index 3eb4871..0417d8b 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -2210,6 +2210,11 @@ class KexDH(object): # pragma: nocover # key blob (from which the fingerprint can be calculated). def recv_reply(self, s, parse_host_key_size=True): packet_type, payload = s.read_packet(2) + + # Skip any & all MSG_DEBUG messages. + while packet_type == SSH.Protocol.MSG_DEBUG: + packet_type, payload = s.read_packet(2) + if packet_type != -1 and packet_type not in [SSH.Protocol.MSG_KEXDH_REPLY, SSH.Protocol.MSG_KEXDH_GEX_REPLY]: # 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)) From fd3a1f7d4171636f79384bc8c74e1248d9c82a66 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Fri, 27 Sep 2019 18:14:36 -0400 Subject: [PATCH 093/103] Added client audit functionality. (#3) --- ssh-audit.py | 198 ++++++++++++++---- .../dropbear_2019.78_test1.txt | 3 + .../expected_results/openssh_4.0p1_test1.txt | 3 + .../expected_results/openssh_5.6p1_test1.txt | 3 + .../expected_results/openssh_5.6p1_test2.txt | 3 + .../expected_results/openssh_5.6p1_test3.txt | 3 + .../expected_results/openssh_5.6p1_test4.txt | 3 + .../expected_results/openssh_5.6p1_test5.txt | 3 + .../expected_results/openssh_8.0p1_test1.txt | 3 + .../expected_results/openssh_8.0p1_test2.txt | 3 + 10 files changed, 182 insertions(+), 43 deletions(-) diff --git a/ssh-audit.py b/ssh-audit.py index 0417d8b..a36c911 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -28,6 +28,7 @@ from __future__ import print_function import binascii, os, io, sys, socket, struct, random, errno, getopt, re, hashlib, base64 VERSION = 'v2.1.0-dev' +SSH_HEADER = 'SSH-{0}-OpenSSH_8.0' # SSH software to impersonate if sys.version_info.major < 3: print("\n!!!! NOTE: Python 2 is being considered for deprecation. If you have a good reason to need continued Python 2 support, please e-mail jtesta@positronsecurity.com with your rationale.\n\n") @@ -69,10 +70,11 @@ def usage(err=None): uout.info(' -6, --ipv6 enable IPv6 (order of precedence)') uout.info(' -p, --port= port to connect') uout.info(' -b, --batch batch output') + uout.info(' -c, --client-audit starts a server on port 2222 to audit client\n software config (use -p to change port)') uout.info(' -n, --no-colors disable colors') uout.info(' -v, --verbose verbose output') uout.info(' -l, --level= minimum output level (info|warn|fail)') - uout.info(' -t, --timeout= timeout (in seconds) for connection and reading\n (default: 5)') + uout.info(' -t, --timeout= timeout (in seconds) for connection and reading\n (default: 5)') uout.sep() sys.exit(1) @@ -86,6 +88,7 @@ class AuditConf(object): self.ssh1 = True self.ssh2 = True self.batch = False + self.client_audit = False self.colors = True self.verbose = False self.level = 'info' @@ -97,7 +100,7 @@ class AuditConf(object): def __setattr__(self, name, value): # type: (str, Union[str, int, bool, Sequence[int]]) -> None valid = False - if name in ['ssh1', 'ssh2', 'batch', 'colors', 'verbose']: + if name in ['ssh1', 'ssh2', 'batch', 'client_audit', 'colors', 'verbose']: valid, value = True, True if bool(value) else False elif name in ['ipv4', 'ipv6']: valid = False @@ -144,9 +147,9 @@ class AuditConf(object): # pylint: disable=too-many-branches aconf = cls() try: - sopts = 'h1246p:bnvl:t:' + sopts = 'h1246p:bcnvl:t:' lopts = ['help', 'ssh1', 'ssh2', 'ipv4', 'ipv6', 'port', - 'batch', 'no-colors', 'verbose', 'level=', 'timeout='] + 'batch', 'client-audit', 'no-colors', 'verbose', 'level=', 'timeout='] opts, args = getopt.getopt(args, sopts, lopts) except getopt.GetoptError as err: usage_cb(str(err)) @@ -168,6 +171,8 @@ class AuditConf(object): elif o in ('-b', '--batch'): aconf.batch = True aconf.verbose = True + elif o in ('-c', '--client-audit'): + aconf.client_audit = True elif o in ('-n', '--no-colors'): aconf.colors = False elif o in ('-v', '--verbose'): @@ -178,23 +183,28 @@ class AuditConf(object): aconf.level = a elif o in ('-t', '--timeout'): aconf.timeout = float(a) - if len(args) == 0: + if len(args) == 0 and aconf.client_audit == False: usage_cb() - if oport is not None: - host = args[0] - else: - mx = re.match(r'^\[([^\]]+)\](?::(.*))?$', args[0]) - if bool(mx): - host, oport = mx.group(1), mx.group(2) + if aconf.client_audit == False: + if oport is not None: + host = args[0] else: - s = args[0].split(':') - if len(s) > 2: - host, oport = args[0], '22' + mx = re.match(r'^\[([^\]]+)\](?::(.*))?$', args[0]) + if bool(mx): + host, oport = mx.group(1), mx.group(2) else: - host, oport = s[0], s[1] if len(s) > 1 else '22' + s = args[0].split(':') + if len(s) > 2: + host, oport = args[0], '22' + else: + host, oport = s[0], s[1] if len(s) > 1 else '22' + if not host: + usage_cb('host is empty') + else: + host = None + if oport is None: + oport = '2222' port = utils.parse_int(oport) - if not host: - usage_cb('host is empty') if port <= 0 or port > 65535: usage_cb('port {0} is not valid'.format(oport)) aconf.host = host @@ -343,6 +353,7 @@ class SSH2(object): # pylint: disable=too-few-public-methods 'rsa1024-sha1': [[], [], [WARN_MODULUS_SIZE, WARN_HASH_WEAK]], 'rsa2048-sha256': [[]], 'sntrup4591761x25519-sha512@tinyssh.org': [['8.0'], [], [WARN_EXPERIMENTAL]], + 'ext-info-c': [[]], # Extension negotiation (RFC 8308) }, 'key': { 'rsa-sha2-256': [['7.2']], @@ -1217,6 +1228,7 @@ class SSH(object): # pylint: disable=too-few-public-methods DropbearSSH = 'Dropbear SSH' LibSSH = 'libssh' TinySSH = 'TinySSH' + PuTTY = 'PuTTY' class Software(object): def __init__(self, vendor, product, version, patch, os_version): @@ -1420,6 +1432,9 @@ class SSH(object): # pylint: disable=too-few-public-methods mx = re.match(r'^tinyssh_(.*)', software) if bool(mx): return cls(None, SSH.Product.TinySSH, mx.group(1), None, None) + mx = re.match(r'^PuTTY_Release_(.*)', software) + if bool(mx): + return cls(None, SSH.Product.PuTTY, mx.group(1), None, None) return None class Banner(object): @@ -1910,7 +1925,19 @@ class SSH(object): # pylint: disable=too-few-public-methods ['3.0', '3.0p1', 1, 'CVE-2001-1507', 7.5, 'bypass authentication'], ['1.2.3', '3.0.1p1', 5, 'CVE-2001-0872', 7.2, 'privilege escalation via crafted environment variables'], ['1.2.3', '2.1.1', 1, 'CVE-2001-0361', 4.0, 'recover plaintext from ciphertext'], - ['1.2', '2.1', 1, 'CVE-2000-0525', 10.0, 'execute arbitrary code (improper privileges)']] + ['1.2', '2.1', 1, 'CVE-2000-0525', 10.0, 'execute arbitrary code (improper privileges)']], + 'PuTTY': [ + ['0.0', '0.71', 2, 'CVE-2019-XXXX', 5.0, 'undefined vulnerability in obsolete SSHv1 protocol handling'], + ['0.0', '0.71', 6, 'CVE-2019-XXXX', 5.0, 'local privilege escalation in Pageant'], + ['0.0', '0.70', 2, 'CVE-2019-9898', 7.5, 'potential recycling of random numbers'], + ['0.0', '0.70', 2, 'CVE-2019-9897', 5.0, 'multiple denial-of-service issues from writing to the terminal'], + ['0.0', '0.70', 6, 'CVE-2019-9896', 4.6, 'local application hijacking through malicious Windows help file'], + ['0.0', '0.70', 2, 'CVE-2019-9894', 6.4, 'buffer overflow in RSA key exchange'], + ['0.0', '0.69', 6, 'CVE-2016-6167', 4.4, 'local application hijacking through untrusted DLL loading'], + ['0.0', '0.67', 2, 'CVE-2017-6542', 7.5, 'buffer overflow in UNIX client that can result in privilege escalation or denial-of-service'], + ['0.0', '0.66', 2, 'CVE-2016-2563', 7.5, 'buffer overflow in SCP command-line utility'], + ['0.0', '0.65', 2, 'CVE-2015-5309', 4.3, 'integer overflow in terminal-handling code'], + ] } # type: Dict[str, List[List[Any]]] TXT = { 'Dropbear SSH': [ @@ -1931,12 +1958,13 @@ class SSH(object): # pylint: disable=too-few-public-methods # type: (Optional[str], int) -> None super(SSH.Socket, self).__init__() self.__sock = None # type: Optional[socket.socket] + self.__sock_server = None self.__block_size = 8 self.__state = 0 self.__header = [] # type: List[text_type] self.__banner = None # type: Optional[SSH.Banner] - if host is None: - raise ValueError('undefined host') +# if host is None: +# raise ValueError('undefined host') nport = utils.parse_int(port) if nport < 1 or nport > 65535: raise ValueError('invalid port: {0}'.format(port)) @@ -1971,7 +1999,26 @@ class SSH(object): # pylint: disable=too-few-public-methods except socket.error as e: out.fail('[exception] {0}'.format(e)) sys.exit(1) - + + + # Listens on a server socket and accepts one connection (used for + # auditing client connections). + def listen_and_accept(self): + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.__sock_server = s + + # TODO: listen on IPv6 address if necessary. + s.bind(('0.0.0.0', self.__port)) + s.listen() + + c, addr = s.accept() + self.client_host = addr[0] + self.client_port = addr[1] + c.settimeout(self.__timeout) + self.__sock = c + + def connect(self): # type: () -> None err = None @@ -1998,15 +2045,15 @@ class SSH(object): # pylint: disable=too-few-public-methods # type: (int) -> Tuple[Optional[SSH.Banner], List[text_type], Optional[str]] if self.__sock is None: return self.__banner, self.__header, 'not connected' - banner = 'SSH-{0}-OpenSSH_7.4'.format('1.5' if sshv == 1 else '2.0') - rto = self.__sock.gettimeout() - self.__sock.settimeout(0.7) - s, e = self.recv() - self.__sock.settimeout(rto) - if s < 0: - return self.__banner, self.__header, e + banner = SSH_HEADER.format('1.5' if sshv == 1 else '2.0') if self.__state < self.SM_BANNER_SENT: self.send_banner(banner) +# rto = self.__sock.gettimeout() +# self.__sock.settimeout(0.7) + s, e = self.recv() +# self.__sock.settimeout(rto) + if s < 0: + return self.__banner, self.__header, e e = None while self.__banner is None: if not s > 0: @@ -2163,6 +2210,7 @@ class SSH(object): # pylint: disable=too-few-public-methods def __cleanup(self): # type: () -> None self._close_socket(self.__sock) + self._close_socket(self.__sock_server) self.__sock = None @@ -2643,8 +2691,13 @@ def output_algorithm(alg_db, alg_type, alg_name, unknown_algs, alg_max_len=0, al f(' ' * len(prefix + alg_name) + comment) -def output_compatibility(algs, for_server=True): +def output_compatibility(algs, client_audit, for_server=True): # type: (SSH.Algorithms, bool) -> None + + # Don't output any compatibility info if we're doing a client audit. + if client_audit: + return + ssh_timeframe = algs.get_ssh_timeframe(for_server) comp_text = [] for ssh_prod in [SSH.Product.OpenSSH, SSH.Product.DropbearSSH]: @@ -2669,7 +2722,7 @@ def output_compatibility(algs, for_server=True): out.good('(gen) compatibility: ' + ', '.join(comp_text)) -def output_security_sub(sub, software, padlen): +def output_security_sub(sub, software, client_audit, padlen): # type: (str, Optional[SSH.Software], int) -> None secdb = SSH.Security.CVE if sub == 'cve' else SSH.Security.TXT if software is None or software.product not in secdb: @@ -2680,9 +2733,11 @@ def output_security_sub(sub, software, padlen): continue target, name = line[2:4] # type: int, str is_server = target & 1 == 1 - # is_client = target & 2 == 2 + is_client = target & 2 == 2 # is_local = target & 4 == 4 - if not is_server: + + # If this security entry applies only to servers, but we're testing a client, then skip it. Similarly, skip entries that apply only to clients, but we're testing a server. + if (is_server and not is_client and client_audit) or (is_client and not is_server and not client_audit): continue p = '' if out.batch else ' ' * (padlen - len(name)) if sub == 'cve': @@ -2698,13 +2753,13 @@ def output_security_sub(sub, software, padlen): out.fail('(sec) {0}{1} -- {2}'.format(name, p, descr)) -def output_security(banner, padlen): +def output_security(banner, client_audit, padlen): # type: (Optional[SSH.Banner], int) -> None with OutputBuffer() as obuf: if banner is not None: software = SSH.Software.parse(banner) - output_security_sub('cve', software, padlen) - output_security_sub('txt', software, padlen) + output_security_sub('cve', software, client_audit, padlen) + output_security_sub('txt', software, client_audit, padlen) if len(obuf) > 0: out.head('# security') obuf.flush() @@ -2749,8 +2804,39 @@ def output_fingerprints(algs, sha256=True): out.sep() +# Returns True if no warnings or failures encountered in configuration. def output_recommendations(algs, software, padlen=0): # type: (SSH.Algorithms, Optional[SSH.Software], int) -> None + + ret = True + # PuTTY's algorithms cannot be modified, so there's no point in issuing recommendations. + if software.product == SSH.Product.PuTTY: + max_vuln_version = 0.0 + max_cvssv2_severity = 0.0 + # Search the CVE database for the most recent vulnerable version and the max CVSSv2 score. + for cve_list in SSH.Security.CVE['PuTTY']: + vuln_version = float(cve_list[1]) + cvssv2_severity = cve_list[4] + + if vuln_version > max_vuln_version: + max_vuln_version = vuln_version + if cvssv2_severity > max_cvssv2_severity: + max_cvssv2_severity = cvssv2_severity + + fn = out.warn + if max_cvssv2_severity > 8.0: + fn = out.fail + + # Assuming that PuTTY versions will always increment by 0.01, we can calculate the first safe version by adding 0.01 to the latest vulnerable version. + current_version = float(software.version) + upgrade_to_version = max_vuln_version + 0.01 + if current_version < upgrade_to_version: + out.head('# recommendations') + fn('(rec) Upgrade to PuTTY v%.2f' % upgrade_to_version) + out.sep() + ret = False + return ret + for_server = True with OutputBuffer() as obuf: software, alg_rec = algs.get_recommendations(software, for_server) @@ -2768,12 +2854,14 @@ def output_recommendations(algs, software, padlen=0): chg_additional_info = '' if action == 'del': an, sg, fn = 'remove', '-', out.warn + ret = False if alg_rec[sshv][alg_type][action][name] >= 10: fn = out.fail elif action == 'add': an, sg, fn = 'append', '+', out.good elif action == 'chg': an, sg, fn = 'change', '!', out.fail + ret = False chg_additional_info = ' (increase modulus size to 2048 bits or larger)' b = '(SSH{0})'.format(sshv) if sshv == 1 else '' fm = '(rec) {0}{1}{2}-- {3} algorithm to {4}{5} {6}' @@ -2786,9 +2874,27 @@ def output_recommendations(algs, software, padlen=0): out.head('# algorithm recommendations {0}'.format(title)) obuf.flush(True) # Sort the output so that it is always stable (needed for repeatable testing). out.sep() + return ret -def output(banner, header, kex=None, pkm=None): +# Output additional information & notes. +def output_info(algs, software, client_audit, any_problems, padlen=0): + with OutputBuffer() as obuf: + # Tell user that PuTTY cannot be hardened at the protocol-level. + if client_audit and (software.product == SSH.Product.PuTTY): + out.warn('(nfo) PuTTY does not have the option of restricting any algorithms during the SSH handshake.') + + # If any warnings or failures were given, print a link to the hardening guides. + if any_problems: + out.warn('(nfo) For hardening guides on common OSes, please see: ') + + if len(obuf) > 0: + out.head('# additional info') + obuf.flush() + out.sep() + + +def output(banner, header, client_audit=False, kex=None, pkm=None): # type: (Optional[SSH.Banner], List[text_type], Optional[SSH2.Kex], Optional[SSH1.PublicKeyMessage]) -> None sshv = 1 if pkm is not None else 2 algs = SSH.Algorithms(pkm, kex) @@ -2807,7 +2913,7 @@ def output(banner, header, kex=None, pkm=None): out.good('(gen) software: {0}'.format(software)) else: software = None - output_compatibility(algs) + output_compatibility(algs, client_audit) if kex is not None: compressions = [x for x in kex.server.compression if x != 'none'] if len(compressions) > 0: @@ -2820,7 +2926,7 @@ def output(banner, header, kex=None, pkm=None): obuf.flush() out.sep() maxlen = algs.maxlen + 1 - output_security(banner, maxlen) + output_security(banner, client_audit, maxlen) unknown_algorithms = [] # Filled in by output_algorithms() with unidentified algs. if pkm is not None: adb = SSH1.KexDB.ALGORITHMS @@ -2843,7 +2949,9 @@ def output(banner, header, kex=None, pkm=None): title, atype = 'message authentication code algorithms', 'mac' output_algorithms(title, adb, atype, kex.server.mac, unknown_algorithms, maxlen) output_fingerprints(algs, True) - output_recommendations(algs, software, maxlen) + perfect_config = output_recommendations(algs, software, maxlen) + output_info(algs, software, client_audit, not perfect_config) + # If we encountered any unknown algorithms, ask the user to report them. if len(unknown_algorithms) > 0: @@ -2979,7 +3087,10 @@ def audit(aconf, sshv=None): out.level = aconf.level out.use_colors = aconf.colors s = SSH.Socket(aconf.host, aconf.port, aconf.ipvo, aconf.timeout) - s.connect() + if aconf.client_audit: + s.listen_and_accept() + else: + s.connect() if sshv is None: sshv = 2 if aconf.ssh2 else 1 err = None @@ -3023,9 +3134,10 @@ def audit(aconf, sshv=None): output(banner, header, pkm=pkm) elif sshv == 2: kex = SSH2.Kex.parse(payload) - SSH2.HostKeyTest.run(s, kex) - SSH2.GEXTest.run(s, kex) - output(banner, header, kex=kex) + if aconf.client_audit is False: + SSH2.HostKeyTest.run(s, kex) + SSH2.GEXTest.run(s, kex) + output(banner, header, client_audit=aconf.client_audit, kex=kex) utils = Utils() diff --git a/test/docker/expected_results/dropbear_2019.78_test1.txt b/test/docker/expected_results/dropbear_2019.78_test1.txt index 9edb398..c9b4e9c 100644 --- a/test/docker/expected_results/dropbear_2019.78_test1.txt +++ b/test/docker/expected_results/dropbear_2019.78_test1.txt @@ -80,3 +80,6 @@ (rec) -diffie-hellman-group14-sha1 -- kex algorithm to remove  (rec) -hmac-sha1 -- mac algorithm to remove  +# additional info +(nfo) For hardening guides on common OSes, please see:  + diff --git a/test/docker/expected_results/openssh_4.0p1_test1.txt b/test/docker/expected_results/openssh_4.0p1_test1.txt index 33e1967..1ab525a 100644 --- a/test/docker/expected_results/openssh_4.0p1_test1.txt +++ b/test/docker/expected_results/openssh_4.0p1_test1.txt @@ -134,3 +134,6 @@ (rec) -rijndael-cbc@lysator.liu.se -- enc algorithm to remove  (rec) -ssh-dss -- key algorithm to remove  +# additional info +(nfo) For hardening guides on common OSes, please see:  + diff --git a/test/docker/expected_results/openssh_5.6p1_test1.txt b/test/docker/expected_results/openssh_5.6p1_test1.txt index 2ddc54e..72883da 100644 --- a/test/docker/expected_results/openssh_5.6p1_test1.txt +++ b/test/docker/expected_results/openssh_5.6p1_test1.txt @@ -143,3 +143,6 @@ (rec) -ssh-dss -- key algorithm to remove  (rec) -diffie-hellman-group14-sha1 -- kex algorithm to remove  +# additional info +(nfo) For hardening guides on common OSes, please see:  + diff --git a/test/docker/expected_results/openssh_5.6p1_test2.txt b/test/docker/expected_results/openssh_5.6p1_test2.txt index 8d4daab..d952848 100644 --- a/test/docker/expected_results/openssh_5.6p1_test2.txt +++ b/test/docker/expected_results/openssh_5.6p1_test2.txt @@ -141,3 +141,6 @@ (rec) -rijndael-cbc@lysator.liu.se -- enc algorithm to remove  (rec) -diffie-hellman-group14-sha1 -- kex algorithm to remove  +# additional info +(nfo) For hardening guides on common OSes, please see:  + diff --git a/test/docker/expected_results/openssh_5.6p1_test3.txt b/test/docker/expected_results/openssh_5.6p1_test3.txt index 8d66e56..cb1217e 100644 --- a/test/docker/expected_results/openssh_5.6p1_test3.txt +++ b/test/docker/expected_results/openssh_5.6p1_test3.txt @@ -141,3 +141,6 @@ (rec) -rijndael-cbc@lysator.liu.se -- enc algorithm to remove  (rec) -diffie-hellman-group14-sha1 -- kex algorithm to remove  +# additional info +(nfo) For hardening guides on common OSes, please see:  + diff --git a/test/docker/expected_results/openssh_5.6p1_test4.txt b/test/docker/expected_results/openssh_5.6p1_test4.txt index a5fa335..9b84b6b 100644 --- a/test/docker/expected_results/openssh_5.6p1_test4.txt +++ b/test/docker/expected_results/openssh_5.6p1_test4.txt @@ -139,3 +139,6 @@ (rec) -rijndael-cbc@lysator.liu.se -- enc algorithm to remove  (rec) -diffie-hellman-group14-sha1 -- kex algorithm to remove  +# additional info +(nfo) For hardening guides on common OSes, please see:  + diff --git a/test/docker/expected_results/openssh_5.6p1_test5.txt b/test/docker/expected_results/openssh_5.6p1_test5.txt index 7e5cf64..e2e3479 100644 --- a/test/docker/expected_results/openssh_5.6p1_test5.txt +++ b/test/docker/expected_results/openssh_5.6p1_test5.txt @@ -137,3 +137,6 @@ (rec) -rijndael-cbc@lysator.liu.se -- enc algorithm to remove  (rec) -diffie-hellman-group14-sha1 -- kex algorithm to remove  +# additional info +(nfo) For hardening guides on common OSes, please see:  + diff --git a/test/docker/expected_results/openssh_8.0p1_test1.txt b/test/docker/expected_results/openssh_8.0p1_test1.txt index 25981cf..a468e45 100644 --- a/test/docker/expected_results/openssh_8.0p1_test1.txt +++ b/test/docker/expected_results/openssh_8.0p1_test1.txt @@ -77,3 +77,6 @@ (rec) -umac-64-etm@openssh.com -- mac algorithm to remove  (rec) -umac-64@openssh.com -- mac algorithm to remove  +# additional info +(nfo) For hardening guides on common OSes, please see:  + diff --git a/test/docker/expected_results/openssh_8.0p1_test2.txt b/test/docker/expected_results/openssh_8.0p1_test2.txt index 3a23378..116b885 100644 --- a/test/docker/expected_results/openssh_8.0p1_test2.txt +++ b/test/docker/expected_results/openssh_8.0p1_test2.txt @@ -73,3 +73,6 @@ (rec) -umac-64-etm@openssh.com -- mac algorithm to remove  (rec) -umac-64@openssh.com -- mac algorithm to remove  +# additional info +(nfo) For hardening guides on common OSes, please see:  + From 9759480ae4281edce223db88b4111362a3c6a750 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Fri, 27 Sep 2019 18:16:50 -0400 Subject: [PATCH 094/103] Updated ChangeLog. --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ddabb9e..1d2066d 100644 --- a/README.md +++ b/README.md @@ -28,11 +28,13 @@ usage: ssh-audit.py [-1246pbnvlt] -6, --ipv6 enable IPv6 (order of precedence) -p, --port= port to connect -b, --batch batch output + -c, --client-audit starts a server on port 2222 to audit client + software config (use -p to change port) -n, --no-colors disable colors -v, --verbose verbose output -l, --level= minimum output level (info|warn|fail) -t, --timeout= timeout (in seconds) for connection and reading - (default: 5) + (default: 5) ``` * if both IPv4 and IPv6 are used, order of precedence can be set by using either `-46` or `-64`. * batch flag `-b` will output sections without header and without empty lines (implies verbose flag). @@ -43,6 +45,7 @@ usage: ssh-audit.py [-1246pbnvlt] ## ChangeLog ### v2.1.0 (???) + - Added client software auditing functionality (see -c / --client-audit option). - Fixed crash while scanning Solaris Sun_SSH. - Added 5 new key exchanges: `gss-group1-sha1-toWM5Slw5Ew8Mqkay+al2g==`, `diffie-hellman-group15-sha256`, `ecdh-sha2-1.3.132.0.10`, `curve448-sha512`. - Added 1 new host key type: `ecdsa-sha2-1.3.132.0.10`. From 166c93ace4e22dbf4cfd737c4c0755bd37d341dd Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Fri, 27 Sep 2019 18:19:49 -0400 Subject: [PATCH 095/103] Updated project description to mention client auditing ability. --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1d2066d..9c83423 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![codecov](https://codecov.io/gh/arthepsy/ssh-audit/branch/develop/graph/badge.svg)](https://codecov.io/gh/arthepsy/ssh-audit) [![Quality Gate](https://sonarqube.com/api/badges/gate?key=arthepsy-github%3Assh-audit%3Adevelop&template=ROUNDED)](https://sq.evolutiongaming.com/dashboard?id=arthepsy-github%3Assh-audit%3Adevelop) --> -**ssh-audit** is a tool for ssh server auditing. +**ssh-audit** is a tool for ssh server & client auditing. ## Features - SSH1 and SSH2 protocol server support; @@ -16,7 +16,8 @@ - output security information (related issues, assigned CVE list, etc); - analyze SSH version compatibility based on algorithm information; - historical information from OpenSSH, Dropbear SSH and libssh; -- no dependencies, compatible with Python 2.6+, Python 3.x and PyPy; +- no dependencies +- analyze SSH client configuration; ## Usage ``` From 1d707276d7f41d79b966a1ebfee133b96acf06e4 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Mon, 7 Oct 2019 10:59:52 -0400 Subject: [PATCH 096/103] Updated README. --- README.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 9c83423..1291c73 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,11 @@ [![codecov](https://codecov.io/gh/arthepsy/ssh-audit/branch/develop/graph/badge.svg)](https://codecov.io/gh/arthepsy/ssh-audit) [![Quality Gate](https://sonarqube.com/api/badges/gate?key=arthepsy-github%3Assh-audit%3Adevelop&template=ROUNDED)](https://sq.evolutiongaming.com/dashboard?id=arthepsy-github%3Assh-audit%3Adevelop) --> -**ssh-audit** is a tool for ssh server & client auditing. +**ssh-audit** is a tool for ssh server & client configuration auditing. ## Features - SSH1 and SSH2 protocol server support; +- analyze SSH client configuration; - grab banner, recognize device or software and operating system, detect compression; - gather key-exchange, host-key, encryption and message authentication code algorithms; - output algorithm information (available since, removed/disabled, unsafe/weak/legacy, etc); @@ -17,11 +18,10 @@ - analyze SSH version compatibility based on algorithm information; - historical information from OpenSSH, Dropbear SSH and libssh; - no dependencies -- analyze SSH client configuration; ## Usage ``` -usage: ssh-audit.py [-1246pbnvlt] +usage: ssh-audit.py [-1246pbcnvlt] -1, --ssh1 force ssh version 1 only -2, --ssh2 force ssh version 2 only @@ -41,14 +41,17 @@ usage: ssh-audit.py [-1246pbnvlt] * batch flag `-b` will output sections without header and without empty lines (implies verbose flag). * verbose flag `-v` will prefix each line with section type and algorithm name. -### Example +### Server Audit Example ![screenshot](https://user-images.githubusercontent.com/2982011/64388792-317e6f80-d00e-11e9-826e-a4934769bb07.png) +### Client Audit Example +TODO + ## ChangeLog ### v2.1.0 (???) - - Added client software auditing functionality (see -c / --client-audit option). + - Added client software auditing functionality (see `-c` / `--client-audit` option). - Fixed crash while scanning Solaris Sun_SSH. - - Added 5 new key exchanges: `gss-group1-sha1-toWM5Slw5Ew8Mqkay+al2g==`, `diffie-hellman-group15-sha256`, `ecdh-sha2-1.3.132.0.10`, `curve448-sha512`. + - Added 4 new key exchanges: `gss-group1-sha1-toWM5Slw5Ew8Mqkay+al2g==`, `diffie-hellman-group15-sha256`, `ecdh-sha2-1.3.132.0.10`, `curve448-sha512`. - Added 1 new host key type: `ecdsa-sha2-1.3.132.0.10`. - Added 2 new MACs: `hmac-sha2-256-96-etm@openssh.com`, `hmac-sha2-512-96-etm@openssh.com`. From 4c9b871f5c5cfedea05ea5bf844eeb09221b68ed Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Mon, 7 Oct 2019 11:00:11 -0400 Subject: [PATCH 097/103] Removed duplicate MAC. --- ssh-audit.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ssh-audit.py b/ssh-audit.py index a36c911..cee3cda 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -425,7 +425,6 @@ class SSH2(object): # pylint: disable=too-few-public-methods 'hmac-sha2-384': [[], [], [WARN_ENCRYPT_AND_MAC]], 'hmac-sha2-512': [['5.9,d2013.56,l10.7.0'], [], [WARN_ENCRYPT_AND_MAC]], 'hmac-sha2-512-96': [['5.9', '6.0'], [FAIL_OPENSSH61_REMOVE], [WARN_ENCRYPT_AND_MAC]], - 'hmac-sha2-256-96-etm@openssh.com': [[]], 'hmac-sha3-256': [[], [], [WARN_ENCRYPT_AND_MAC]], 'hmac-sha3-384': [[], [], [WARN_ENCRYPT_AND_MAC]], 'hmac-sha3-512': [[], [], [WARN_ENCRYPT_AND_MAC]], From 83544836c93d1893943f5209558588d6100c2844 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Wed, 9 Oct 2019 20:57:31 -0400 Subject: [PATCH 098/103] Fixed client parsing crash. --- ssh-audit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ssh-audit.py b/ssh-audit.py index cee3cda..121265c 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -2809,7 +2809,7 @@ def output_recommendations(algs, software, padlen=0): ret = True # PuTTY's algorithms cannot be modified, so there's no point in issuing recommendations. - if software.product == SSH.Product.PuTTY: + if (software is not None) and (software.product == SSH.Product.PuTTY): max_vuln_version = 0.0 max_cvssv2_severity = 0.0 # Search the CVE database for the most recent vulnerable version and the max CVSSv2 score. @@ -2880,7 +2880,7 @@ def output_recommendations(algs, software, padlen=0): def output_info(algs, software, client_audit, any_problems, padlen=0): with OutputBuffer() as obuf: # Tell user that PuTTY cannot be hardened at the protocol-level. - if client_audit and (software.product == SSH.Product.PuTTY): + if client_audit and (software is not None) and (software.product == SSH.Product.PuTTY): out.warn('(nfo) PuTTY does not have the option of restricting any algorithms during the SSH handshake.') # If any warnings or failures were given, print a link to the hardening guides. From 4ebefdf89457dfb24ab474c9b15ae67559306b5b Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Wed, 9 Oct 2019 21:12:09 -0400 Subject: [PATCH 099/103] Updated usage message. --- ssh-audit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ssh-audit.py b/ssh-audit.py index 121265c..4e64ec5 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -62,7 +62,7 @@ def usage(err=None): uout.head('# {0} {1}, https://github.com/jtesta/ssh-audit\n'.format(p, VERSION)) if err is not None and len(err) > 0: uout.fail('\n' + err) - uout.info('usage: {0} [-1246pbnvlt] \n'.format(p)) + uout.info('usage: {0} [-1246pbcnvlt] \n'.format(p)) uout.info(' -h, --help print this help') uout.info(' -1, --ssh1 force ssh version 1 only') uout.info(' -2, --ssh2 force ssh version 2 only') From e3a59a3e21f1ff697d8c7edcd066a1d18921594c Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Wed, 9 Oct 2019 22:29:56 -0400 Subject: [PATCH 100/103] Client auditing feature now supports IPv6. --- ssh-audit.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/ssh-audit.py b/ssh-audit.py index 4e64ec5..7d6d641 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -25,7 +25,7 @@ THE SOFTWARE. """ from __future__ import print_function -import binascii, os, io, sys, socket, struct, random, errno, getopt, re, hashlib, base64 +import base64, binascii, errno, hashlib, getopt, io, os, random, re, select, socket, struct, sys VERSION = 'v2.1.0-dev' SSH_HEADER = 'SSH-{0}-OpenSSH_8.0' # SSH software to impersonate @@ -1957,7 +1957,7 @@ class SSH(object): # pylint: disable=too-few-public-methods # type: (Optional[str], int) -> None super(SSH.Socket, self).__init__() self.__sock = None # type: Optional[socket.socket] - self.__sock_server = None + self.__sock_map = {} self.__block_size = 8 self.__state = 0 self.__header = [] # type: List[text_type] @@ -2003,15 +2003,27 @@ class SSH(object): # pylint: disable=too-few-public-methods # Listens on a server socket and accepts one connection (used for # auditing client connections). def listen_and_accept(self): + + # Socket to listen on all IPv4 addresses. s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - self.__sock_server = s - - # TODO: listen on IPv6 address if necessary. s.bind(('0.0.0.0', self.__port)) s.listen() + self.__sock_map[s.fileno()] = s - c, addr = s.accept() + # Socket to listen on all IPv6 addresses. + s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1) + s.bind(('::', self.__port)) + s.listen() + self.__sock_map[s.fileno()] = s + + # Wait for a connection on either socket. + fds = select.select(self.__sock_map.keys(), [], []) + + # Accept the connection. + c, addr = self.__sock_map[fds[0][0]].accept() self.client_host = addr[0] self.client_port = addr[1] c.settimeout(self.__timeout) @@ -2209,7 +2221,8 @@ class SSH(object): # pylint: disable=too-few-public-methods def __cleanup(self): # type: () -> None self._close_socket(self.__sock) - self._close_socket(self.__sock_server) + for fd in self.__sock_map: + self._close_socket(self.__sock_map[fd]) self.__sock = None From fd85e247e713fee52b30f6d2ffcc1ab67c86e714 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Thu, 10 Oct 2019 23:09:45 -0400 Subject: [PATCH 101/103] Improved IPv4/IPv6 error handling during client testing. --- ssh-audit.py | 39 ++++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/ssh-audit.py b/ssh-audit.py index 7d6d641..6add274 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -2004,20 +2004,33 @@ class SSH(object): # pylint: disable=too-few-public-methods # auditing client connections). def listen_and_accept(self): - # Socket to listen on all IPv4 addresses. - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - s.bind(('0.0.0.0', self.__port)) - s.listen() - self.__sock_map[s.fileno()] = s + try: + # Socket to listen on all IPv4 addresses. + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind(('0.0.0.0', self.__port)) + s.listen() + self.__sock_map[s.fileno()] = s + except Exception as e: + print("Warning: failed to listen on any IPv4 interfaces.") + pass - # Socket to listen on all IPv6 addresses. - s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) - s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1) - s.bind(('::', self.__port)) - s.listen() - self.__sock_map[s.fileno()] = s + try: + # Socket to listen on all IPv6 addresses. + s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1) + s.bind(('::', self.__port)) + s.listen() + self.__sock_map[s.fileno()] = s + except Exception as e: + print("Warning: failed to listen on any IPv6 interfaces.") + pass + + # If we failed to listen on any interfaces, terminate. + if len(self.__sock_map.keys()) == 0: + print("Error: failed to listen on any IPv4 and IPv6 interfaces!") + exit(-1) # Wait for a connection on either socket. fds = select.select(self.__sock_map.keys(), [], []) From e62b548677dbfda3a518606431cc9c1fc0778e9a Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Mon, 21 Oct 2019 11:50:23 -0400 Subject: [PATCH 102/103] Updated info on curve25519-sha256 kex. --- ssh-audit.py | 2 +- test/docker/expected_results/dropbear_2019.78_test1.txt | 4 ++-- test/docker/expected_results/openssh_8.0p1_test1.txt | 4 ++-- test/docker/expected_results/openssh_8.0p1_test2.txt | 4 ++-- test/docker/expected_results/openssh_8.0p1_test3.txt | 4 ++-- test/docker/expected_results/tinyssh_20190101_test1.txt | 4 ++-- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/ssh-audit.py b/ssh-audit.py index 6add274..f7ce02f 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -347,7 +347,7 @@ class SSH2(object): # pylint: disable=too-few-public-methods 'ecdh-sha2-nistp521': [['5.7,d2013.62'], [WARN_CURVES_WEAK]], 'ecdh-sha2-1.3.132.0.10': [[]], # ECDH over secp256k1 (i.e.: the Bitcoin curve) 'curve25519-sha256@libssh.org': [['6.5,d2013.62,l10.6.0']], - 'curve25519-sha256': [['7.4']], + 'curve25519-sha256': [['7.4,d2018.76']], 'curve448-sha512': [[]], 'kexguess2@matt.ucc.asn.au': [['d2013.57']], 'rsa1024-sha1': [[], [], [WARN_MODULUS_SIZE, WARN_HASH_WEAK]], diff --git a/test/docker/expected_results/dropbear_2019.78_test1.txt b/test/docker/expected_results/dropbear_2019.78_test1.txt index c9b4e9c..f4ee85e 100644 --- a/test/docker/expected_results/dropbear_2019.78_test1.txt +++ b/test/docker/expected_results/dropbear_2019.78_test1.txt @@ -1,11 +1,11 @@ # general (gen) banner: SSH-2.0-dropbear_2019.78 (gen) software: Dropbear SSH 2019.78 -(gen) compatibility: OpenSSH 7.4+ (some functionality from 6.6), Dropbear SSH 2016.73+ +(gen) compatibility: OpenSSH 7.4+ (some functionality from 6.6), Dropbear SSH 2018.76+ (gen) compression: enabled (zlib@openssh.com) # key exchange algorithms -(kex) curve25519-sha256 -- [info] available since OpenSSH 7.4 +(kex) curve25519-sha256 -- [info] available since OpenSSH 7.4, Dropbear SSH 2018.76 (kex) curve25519-sha256@libssh.org -- [info] available since OpenSSH 6.5, Dropbear SSH 2013.62 (kex) ecdh-sha2-nistp521 -- [fail] using weak elliptic curves `- [info] available since OpenSSH 5.7, Dropbear SSH 2013.62 diff --git a/test/docker/expected_results/openssh_8.0p1_test1.txt b/test/docker/expected_results/openssh_8.0p1_test1.txt index a468e45..129f107 100644 --- a/test/docker/expected_results/openssh_8.0p1_test1.txt +++ b/test/docker/expected_results/openssh_8.0p1_test1.txt @@ -1,11 +1,11 @@ # general (gen) banner: SSH-2.0-OpenSSH_8.0 (gen) software: OpenSSH 8.0 -(gen) compatibility: OpenSSH 7.4+, Dropbear SSH 2016.73+ +(gen) compatibility: OpenSSH 7.4+, Dropbear SSH 2018.76+ (gen) compression: enabled (zlib@openssh.com) # key exchange algorithms -(kex) curve25519-sha256 -- [info] available since OpenSSH 7.4 +(kex) curve25519-sha256 -- [info] available since OpenSSH 7.4, Dropbear SSH 2018.76 (kex) curve25519-sha256@libssh.org -- [info] available since OpenSSH 6.5, Dropbear SSH 2013.62 (kex) ecdh-sha2-nistp256 -- [fail] using weak elliptic curves `- [info] available since OpenSSH 5.7, Dropbear SSH 2013.62 diff --git a/test/docker/expected_results/openssh_8.0p1_test2.txt b/test/docker/expected_results/openssh_8.0p1_test2.txt index 116b885..bf35175 100644 --- a/test/docker/expected_results/openssh_8.0p1_test2.txt +++ b/test/docker/expected_results/openssh_8.0p1_test2.txt @@ -1,11 +1,11 @@ # general (gen) banner: SSH-2.0-OpenSSH_8.0 (gen) software: OpenSSH 8.0 -(gen) compatibility: OpenSSH 7.4+, Dropbear SSH 2016.73+ +(gen) compatibility: OpenSSH 7.4+, Dropbear SSH 2018.76+ (gen) compression: enabled (zlib@openssh.com) # key exchange algorithms -(kex) curve25519-sha256 -- [info] available since OpenSSH 7.4 +(kex) curve25519-sha256 -- [info] available since OpenSSH 7.4, Dropbear SSH 2018.76 (kex) curve25519-sha256@libssh.org -- [info] available since OpenSSH 6.5, Dropbear SSH 2013.62 (kex) ecdh-sha2-nistp256 -- [fail] using weak elliptic curves `- [info] available since OpenSSH 5.7, Dropbear SSH 2013.62 diff --git a/test/docker/expected_results/openssh_8.0p1_test3.txt b/test/docker/expected_results/openssh_8.0p1_test3.txt index 81bfa9c..9a5bcc3 100644 --- a/test/docker/expected_results/openssh_8.0p1_test3.txt +++ b/test/docker/expected_results/openssh_8.0p1_test3.txt @@ -1,11 +1,11 @@ # general (gen) banner: SSH-2.0-OpenSSH_8.0 (gen) software: OpenSSH 8.0 -(gen) compatibility: OpenSSH 7.4+, Dropbear SSH 2013.62+ +(gen) compatibility: OpenSSH 7.4+, Dropbear SSH 2018.76+ (gen) compression: enabled (zlib@openssh.com) # key exchange algorithms -(kex) curve25519-sha256 -- [info] available since OpenSSH 7.4 +(kex) curve25519-sha256 -- [info] available since OpenSSH 7.4, Dropbear SSH 2018.76 (kex) curve25519-sha256@libssh.org -- [info] available since OpenSSH 6.5, Dropbear SSH 2013.62 (kex) diffie-hellman-group-exchange-sha256 (2048-bit) -- [info] available since OpenSSH 4.4 diff --git a/test/docker/expected_results/tinyssh_20190101_test1.txt b/test/docker/expected_results/tinyssh_20190101_test1.txt index b6df3b1..26efda2 100644 --- a/test/docker/expected_results/tinyssh_20190101_test1.txt +++ b/test/docker/expected_results/tinyssh_20190101_test1.txt @@ -1,10 +1,10 @@ # general (gen) software: TinySSH noversion -(gen) compatibility: OpenSSH 8.0+, Dropbear SSH 2013.62+ +(gen) compatibility: OpenSSH 8.0+, Dropbear SSH 2018.76+ (gen) compression: disabled # key exchange algorithms -(kex) curve25519-sha256 -- [info] available since OpenSSH 7.4 +(kex) curve25519-sha256 -- [info] available since OpenSSH 7.4, Dropbear SSH 2018.76 (kex) curve25519-sha256@libssh.org -- [info] available since OpenSSH 6.5, Dropbear SSH 2013.62 (kex) sntrup4591761x25519-sha512@tinyssh.org -- [warn] using experimental algorithm `- [info] available since OpenSSH 8.0 From 8a3ae321f14366aeb3411c0054a34c585338065d Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Fri, 25 Oct 2019 11:27:22 -0400 Subject: [PATCH 103/103] Added five kex algorithms: gss-gex-sha1-toWM5Slw5Ew8Mqkay+al2g==, gss-group14-sha1-, gss-group14-sha1-toWM5Slw5Ew8Mqkay+al2g==, gss-group14-sha256-toWM5Slw5Ew8Mqkay+al2g==, gss-group15-sha512-toWM5Slw5Ew8Mqkay+al2g==; added four ciphers: idea-cbc, serpent128-cbc, serpent192-cbc, serpent256-cbc; added four MACs: hmac-ripemd, hmac-sha256-96@ssh.com, umac-32@openssh.com, umac-96@openssh.com. --- README.md | 5 +++-- ssh-audit.py | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1291c73..e4e503d 100644 --- a/README.md +++ b/README.md @@ -51,9 +51,10 @@ TODO ### v2.1.0 (???) - Added client software auditing functionality (see `-c` / `--client-audit` option). - Fixed crash while scanning Solaris Sun_SSH. - - Added 4 new key exchanges: `gss-group1-sha1-toWM5Slw5Ew8Mqkay+al2g==`, `diffie-hellman-group15-sha256`, `ecdh-sha2-1.3.132.0.10`, `curve448-sha512`. + - Added 9 new key exchanges: `gss-group1-sha1-toWM5Slw5Ew8Mqkay+al2g==`, `gss-gex-sha1-toWM5Slw5Ew8Mqkay+al2g==`, `gss-group14-sha1-`, `gss-group14-sha1-toWM5Slw5Ew8Mqkay+al2g==`, `gss-group14-sha256-toWM5Slw5Ew8Mqkay+al2g==`, `gss-group15-sha512-toWM5Slw5Ew8Mqkay+al2g==`, `diffie-hellman-group15-sha256`, `ecdh-sha2-1.3.132.0.10`, `curve448-sha512`. - Added 1 new host key type: `ecdsa-sha2-1.3.132.0.10`. - - Added 2 new MACs: `hmac-sha2-256-96-etm@openssh.com`, `hmac-sha2-512-96-etm@openssh.com`. + - Added 4 new ciphers: `idea-cbc`, `serpent128-cbc`, `serpent192-cbc`, `serpent256-cbc`. + - Added 6 new MACs: `hmac-sha2-256-96-etm@openssh.com`, `hmac-sha2-512-96-etm@openssh.com`, `hmac-ripemd`, `hmac-sha256-96@ssh.com`, `umac-32@openssh.com`, `umac-96@openssh.com`. ### v2.0.0 (2019-08-29) - Forked from https://github.com/arthepsy/ssh-audit (development was stalled, and developer went MIA). diff --git a/ssh-audit.py b/ssh-audit.py index f7ce02f..40522e7 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -313,6 +313,7 @@ class SSH2(object): # pylint: disable=too-few-public-methods FAIL_DEPRECATED_CIPHER = 'deprecated cipher' FAIL_WEAK_CIPHER = 'using weak cipher' FAIL_PLAINTEXT = 'no encryption/integrity' + FAIL_DEPRECATED_MAC = 'deprecated MAC' WARN_CURVES_WEAK = 'using weak elliptic curves' WARN_RNDSIG_KEY = 'using weak random number generator could reveal the key' WARN_MODULUS_SIZE = 'using small 1024-bit modulus' @@ -330,6 +331,11 @@ class SSH2(object): # pylint: disable=too-few-public-methods 'kex': { 'diffie-hellman-group1-sha1': [['2.3.0,d0.28,l10.2', '6.6', '6.9'], [FAIL_OPENSSH67_UNSAFE, FAIL_OPENSSH70_LOGJAM], [WARN_MODULUS_SIZE, WARN_HASH_WEAK]], 'gss-group1-sha1-toWM5Slw5Ew8Mqkay+al2g==': [[], [FAIL_OPENSSH67_UNSAFE, FAIL_OPENSSH70_LOGJAM], [WARN_MODULUS_SIZE, WARN_HASH_WEAK]], + 'gss-gex-sha1-toWM5Slw5Ew8Mqkay+al2g==': [[], [], [WARN_HASH_WEAK]], + 'gss-group14-sha1-': [[], [], [WARN_HASH_WEAK]], + 'gss-group14-sha1-toWM5Slw5Ew8Mqkay+al2g==': [[], [], [WARN_HASH_WEAK]], + 'gss-group14-sha256-toWM5Slw5Ew8Mqkay+al2g==': [[]], + 'gss-group15-sha512-toWM5Slw5Ew8Mqkay+al2g==': [[]], 'diffie-hellman-group14-sha1': [['3.9,d0.53,l10.6.0'], [], [WARN_HASH_WEAK]], 'diffie-hellman-group14-sha256': [['7.3,d2016.73']], 'diffie-hellman-group15-sha256': [[]], @@ -354,6 +360,7 @@ class SSH2(object): # pylint: disable=too-few-public-methods 'rsa2048-sha256': [[]], 'sntrup4591761x25519-sha512@tinyssh.org': [['8.0'], [], [WARN_EXPERIMENTAL]], 'ext-info-c': [[]], # Extension negotiation (RFC 8308) + 'ext-info-s': [[]], # Extension negotiation (RFC 8308) }, 'key': { 'rsa-sha2-256': [['7.2']], @@ -391,9 +398,13 @@ class SSH2(object): # pylint: disable=too-few-public-methods 'twofish128-ctr': [['d2015.68']], 'twofish192-ctr': [[]], 'twofish256-ctr': [['d2015.68']], + 'serpent128-cbc': [[], [FAIL_DEPRECATED_CIPHER], [WARN_CIPHER_MODE]], + 'serpent192-cbc': [[], [FAIL_DEPRECATED_CIPHER], [WARN_CIPHER_MODE]], + 'serpent256-cbc': [[], [FAIL_DEPRECATED_CIPHER], [WARN_CIPHER_MODE]], 'serpent128-ctr': [[], [FAIL_DEPRECATED_CIPHER]], 'serpent192-ctr': [[], [FAIL_DEPRECATED_CIPHER]], 'serpent256-ctr': [[], [FAIL_DEPRECATED_CIPHER]], + 'idea-cbc': [[], [FAIL_DEPRECATED_CIPHER], [WARN_CIPHER_MODE]], 'idea-ctr': [[], [FAIL_DEPRECATED_CIPHER]], 'cast128-ctr': [[], [FAIL_DEPRECATED_CIPHER]], 'cast128-cbc': [['2.1.0', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_CIPHER_MODE, WARN_BLOCK_SIZE]], @@ -429,11 +440,13 @@ class SSH2(object): # pylint: disable=too-few-public-methods 'hmac-sha3-384': [[], [], [WARN_ENCRYPT_AND_MAC]], 'hmac-sha3-512': [[], [], [WARN_ENCRYPT_AND_MAC]], 'hmac-sha256': [[], [], [WARN_ENCRYPT_AND_MAC]], + 'hmac-sha256-96@ssh.com': [[], [], [WARN_ENCRYPT_AND_MAC, WARN_TAG_SIZE]], 'hmac-sha256@ssh.com': [[], [], [WARN_ENCRYPT_AND_MAC]], 'hmac-sha512': [[], [], [WARN_ENCRYPT_AND_MAC]], 'hmac-sha512@ssh.com': [[], [], [WARN_ENCRYPT_AND_MAC]], 'hmac-md5': [['2.1.0,d0.28', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC, WARN_HASH_WEAK]], 'hmac-md5-96': [['2.5.0', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC, WARN_HASH_WEAK]], + 'hmac-ripemd': [[], [FAIL_DEPRECATED_MAC], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC]], 'hmac-ripemd160': [['2.5.0', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC]], 'hmac-ripemd160@openssh.com': [['2.1.0', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC]], 'umac-64@openssh.com': [['4.7'], [], [WARN_ENCRYPT_AND_MAC, WARN_TAG_SIZE]], @@ -447,7 +460,9 @@ class SSH2(object): # pylint: disable=too-few-public-methods 'hmac-md5-etm@openssh.com': [['6.2', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_HASH_WEAK]], 'hmac-md5-96-etm@openssh.com': [['6.2', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_HASH_WEAK]], 'hmac-ripemd160-etm@openssh.com': [['6.2', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY]], + 'umac-32@openssh.com': [[], [], [WARN_ENCRYPT_AND_MAC, WARN_TAG_SIZE]], # Despite having the @openssh.com suffix, this may never have shipped with OpenSSH (!). 'umac-64-etm@openssh.com': [['6.2'], [], [WARN_TAG_SIZE]], + 'umac-96@openssh.com': [[], [], [WARN_ENCRYPT_AND_MAC]], # Despite having the @openssh.com suffix, this may never have shipped with OpenSSH (!). 'umac-128-etm@openssh.com': [['6.2']], } } # type: Dict[str, Dict[str, List[List[Optional[str]]]]]