|
|
@ -25,11 +25,24 @@
|
|
|
|
THE SOFTWARE.
|
|
|
|
THE SOFTWARE.
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
from __future__ import print_function
|
|
|
|
from __future__ import print_function
|
|
|
|
import base64, binascii, errno, hashlib, getopt, io, os, random, re, select, socket, struct, sys, json
|
|
|
|
import base64
|
|
|
|
|
|
|
|
import binascii
|
|
|
|
|
|
|
|
import errno
|
|
|
|
|
|
|
|
import getopt
|
|
|
|
|
|
|
|
import hashlib
|
|
|
|
|
|
|
|
import io
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
|
|
|
import os
|
|
|
|
|
|
|
|
import random
|
|
|
|
|
|
|
|
import re
|
|
|
|
|
|
|
|
import select
|
|
|
|
|
|
|
|
import socket
|
|
|
|
|
|
|
|
import struct
|
|
|
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
VERSION = 'v2.2.1-dev'
|
|
|
|
VERSION = 'v2.2.1-dev'
|
|
|
|
SSH_HEADER = 'SSH-{0}-OpenSSH_8.0' # SSH software to impersonate
|
|
|
|
SSH_HEADER = 'SSH-{0}-OpenSSH_8.0' # SSH software to impersonate
|
|
|
|
|
|
|
|
|
|
|
|
if sys.version_info.major < 3:
|
|
|
|
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")
|
|
|
|
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")
|
|
|
@ -41,7 +54,7 @@ if sys.version_info >= (3,): # pragma: nocover
|
|
|
|
else: # pragma: nocover
|
|
|
|
else: # pragma: nocover
|
|
|
|
import StringIO as _StringIO # pylint: disable=import-error
|
|
|
|
import StringIO as _StringIO # pylint: disable=import-error
|
|
|
|
StringIO = BytesIO = _StringIO.StringIO
|
|
|
|
StringIO = BytesIO = _StringIO.StringIO
|
|
|
|
text_type = unicode # pylint: disable=undefined-variable
|
|
|
|
text_type = unicode # pylint: disable=undefined-variable # noqa: F821
|
|
|
|
binary_type = str
|
|
|
|
binary_type = str
|
|
|
|
try: # pragma: nocover
|
|
|
|
try: # pragma: nocover
|
|
|
|
# pylint: disable=unused-import
|
|
|
|
# pylint: disable=unused-import
|
|
|
@ -99,7 +112,7 @@ class AuditConf(object):
|
|
|
|
self.ipv4 = False
|
|
|
|
self.ipv4 = False
|
|
|
|
self.ipv6 = False
|
|
|
|
self.ipv6 = False
|
|
|
|
self.timeout = 5.0
|
|
|
|
self.timeout = 5.0
|
|
|
|
self.timeout_set = False # Set to True when the user explicitly sets it.
|
|
|
|
self.timeout_set = False # Set to True when the user explicitly sets it.
|
|
|
|
|
|
|
|
|
|
|
|
def __setattr__(self, name, value):
|
|
|
|
def __setattr__(self, name, value):
|
|
|
|
# type: (str, Union[str, int, bool, Sequence[int]]) -> None
|
|
|
|
# type: (str, Union[str, int, bool, Sequence[int]]) -> None
|
|
|
@ -190,9 +203,9 @@ class AuditConf(object):
|
|
|
|
elif o in ('-t', '--timeout'):
|
|
|
|
elif o in ('-t', '--timeout'):
|
|
|
|
aconf.timeout = float(a)
|
|
|
|
aconf.timeout = float(a)
|
|
|
|
aconf.timeout_set = True
|
|
|
|
aconf.timeout_set = True
|
|
|
|
if len(args) == 0 and aconf.client_audit == False:
|
|
|
|
if len(args) == 0 and aconf.client_audit is False:
|
|
|
|
usage_cb()
|
|
|
|
usage_cb()
|
|
|
|
if aconf.client_audit == False:
|
|
|
|
if aconf.client_audit is False:
|
|
|
|
if oport is not None:
|
|
|
|
if oport is not None:
|
|
|
|
host = args[0]
|
|
|
|
host = args[0]
|
|
|
|
else:
|
|
|
|
else:
|
|
|
@ -310,7 +323,7 @@ class SSH2(object): # pylint: disable=too-few-public-methods
|
|
|
|
WARN_OPENSSH74_UNSAFE = 'disabled (in client) since OpenSSH 7.4, unsafe algorithm'
|
|
|
|
WARN_OPENSSH74_UNSAFE = 'disabled (in client) since OpenSSH 7.4, unsafe algorithm'
|
|
|
|
WARN_OPENSSH72_LEGACY = 'disabled (in client) since OpenSSH 7.2, legacy 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_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_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'
|
|
|
|
FAIL_OPENSSH70_LOGJAM = 'disabled (in client) since OpenSSH 7.0, logjam attack'
|
|
|
|
INFO_OPENSSH69_CHACHA = 'default cipher since OpenSSH 6.9.'
|
|
|
|
INFO_OPENSSH69_CHACHA = 'default cipher since OpenSSH 6.9.'
|
|
|
|
FAIL_OPENSSH67_UNSAFE = 'removed (in server) since OpenSSH 6.7, unsafe algorithm'
|
|
|
|
FAIL_OPENSSH67_UNSAFE = 'removed (in server) since OpenSSH 6.7, unsafe algorithm'
|
|
|
@ -319,21 +332,21 @@ class SSH2(object): # pylint: disable=too-few-public-methods
|
|
|
|
FAIL_DBEAR67_DISABLED = 'disabled since Dropbear SSH 2015.67'
|
|
|
|
FAIL_DBEAR67_DISABLED = 'disabled since Dropbear SSH 2015.67'
|
|
|
|
FAIL_DBEAR53_DISABLED = 'disabled since Dropbear SSH 0.53'
|
|
|
|
FAIL_DBEAR53_DISABLED = 'disabled since Dropbear SSH 0.53'
|
|
|
|
FAIL_DEPRECATED_CIPHER = 'deprecated cipher'
|
|
|
|
FAIL_DEPRECATED_CIPHER = 'deprecated cipher'
|
|
|
|
FAIL_WEAK_CIPHER = 'using weak cipher'
|
|
|
|
FAIL_WEAK_CIPHER = 'using weak cipher'
|
|
|
|
FAIL_WEAK_ALGORITHM = 'using weak/obsolete algorithm'
|
|
|
|
FAIL_WEAK_ALGORITHM = 'using weak/obsolete algorithm'
|
|
|
|
FAIL_PLAINTEXT = 'no encryption/integrity'
|
|
|
|
FAIL_PLAINTEXT = 'no encryption/integrity'
|
|
|
|
FAIL_DEPRECATED_MAC = 'deprecated MAC'
|
|
|
|
FAIL_DEPRECATED_MAC = 'deprecated MAC'
|
|
|
|
WARN_CURVES_WEAK = 'using weak elliptic curves'
|
|
|
|
WARN_CURVES_WEAK = 'using weak elliptic curves'
|
|
|
|
WARN_RNDSIG_KEY = 'using weak random number generator could reveal the key'
|
|
|
|
WARN_RNDSIG_KEY = 'using weak random number generator could reveal the key'
|
|
|
|
WARN_MODULUS_SIZE = 'using small 1024-bit modulus'
|
|
|
|
WARN_MODULUS_SIZE = 'using small 1024-bit modulus'
|
|
|
|
WARN_HASH_WEAK = 'using weak hashing algorithm'
|
|
|
|
WARN_HASH_WEAK = 'using weak hashing algorithm'
|
|
|
|
WARN_CIPHER_MODE = 'using weak cipher mode'
|
|
|
|
WARN_CIPHER_MODE = 'using weak cipher mode'
|
|
|
|
WARN_BLOCK_SIZE = 'using small 64-bit block size'
|
|
|
|
WARN_BLOCK_SIZE = 'using small 64-bit block size'
|
|
|
|
WARN_CIPHER_WEAK = 'using weak cipher'
|
|
|
|
WARN_CIPHER_WEAK = 'using weak cipher'
|
|
|
|
WARN_ENCRYPT_AND_MAC = 'using encrypt-and-MAC mode'
|
|
|
|
WARN_ENCRYPT_AND_MAC = 'using encrypt-and-MAC mode'
|
|
|
|
WARN_TAG_SIZE = 'using small 64-bit tag size'
|
|
|
|
WARN_TAG_SIZE = 'using small 64-bit tag size'
|
|
|
|
WARN_TAG_SIZE_96 = 'using small 96-bit tag size'
|
|
|
|
WARN_TAG_SIZE_96 = 'using small 96-bit tag size'
|
|
|
|
WARN_EXPERIMENTAL = 'using experimental algorithm'
|
|
|
|
WARN_EXPERIMENTAL = 'using experimental algorithm'
|
|
|
|
|
|
|
|
|
|
|
|
ALGORITHMS = {
|
|
|
|
ALGORITHMS = {
|
|
|
|
# Format: 'algorithm_name': [['version_first_appeared_in'], [reason_for_failure1, reason_for_failure2, ...], [warning1, warning2, ...]]
|
|
|
|
# Format: 'algorithm_name': [['version_first_appeared_in'], [reason_for_failure1, reason_for_failure2, ...], [warning1, warning2, ...]]
|
|
|
@ -378,7 +391,7 @@ class SSH2(object): # pylint: disable=too-few-public-methods
|
|
|
|
'ecdh-sha2-nistp384': [['5.7,d2013.62'], [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-nistp521': [['5.7,d2013.62'], [WARN_CURVES_WEAK]],
|
|
|
|
'ecdh-sha2-nistt571': [[], [WARN_CURVES_WEAK]],
|
|
|
|
'ecdh-sha2-nistt571': [[], [WARN_CURVES_WEAK]],
|
|
|
|
'ecdh-sha2-1.3.132.0.10': [[]], # ECDH over secp256k1 (i.e.: the Bitcoin curve)
|
|
|
|
'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@libssh.org': [['6.5,d2013.62,l10.6.0']],
|
|
|
|
'curve25519-sha256': [['7.4,d2018.76']],
|
|
|
|
'curve25519-sha256': [['7.4,d2018.76']],
|
|
|
|
'curve448-sha512': [[]],
|
|
|
|
'curve448-sha512': [[]],
|
|
|
@ -386,8 +399,8 @@ class SSH2(object): # pylint: disable=too-few-public-methods
|
|
|
|
'rsa1024-sha1': [[], [], [WARN_MODULUS_SIZE, WARN_HASH_WEAK]],
|
|
|
|
'rsa1024-sha1': [[], [], [WARN_MODULUS_SIZE, WARN_HASH_WEAK]],
|
|
|
|
'rsa2048-sha256': [[]],
|
|
|
|
'rsa2048-sha256': [[]],
|
|
|
|
'sntrup4591761x25519-sha512@tinyssh.org': [['8.0'], [], [WARN_EXPERIMENTAL]],
|
|
|
|
'sntrup4591761x25519-sha512@tinyssh.org': [['8.0'], [], [WARN_EXPERIMENTAL]],
|
|
|
|
'ext-info-c': [[]], # Extension negotiation (RFC 8308)
|
|
|
|
'ext-info-c': [[]], # Extension negotiation (RFC 8308)
|
|
|
|
'ext-info-s': [[]], # Extension negotiation (RFC 8308)
|
|
|
|
'ext-info-s': [[]], # Extension negotiation (RFC 8308)
|
|
|
|
},
|
|
|
|
},
|
|
|
|
'key': {
|
|
|
|
'key': {
|
|
|
|
'ssh-rsa1': [[], [FAIL_WEAK_ALGORITHM]],
|
|
|
|
'ssh-rsa1': [[], [FAIL_WEAK_ALGORITHM]],
|
|
|
@ -400,7 +413,7 @@ class SSH2(object): # pylint: disable=too-few-public-methods
|
|
|
|
'ecdsa-sha2-nistp256': [['5.7,d2013.62,l10.6.4'], [WARN_CURVES_WEAK], [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-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]],
|
|
|
|
'ecdsa-sha2-nistp521': [['5.7,d2013.62,l10.6.4'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]],
|
|
|
|
'ecdsa-sha2-1.3.132.0.10': [[], [], [WARN_RNDSIG_KEY]], # ECDSA over secp256k1 (i.e.: the Bitcoin curve)
|
|
|
|
'ecdsa-sha2-1.3.132.0.10': [[], [], [WARN_RNDSIG_KEY]], # ECDSA over secp256k1 (i.e.: the Bitcoin curve)
|
|
|
|
'x509v3-sign-dss': [[], [FAIL_OPENSSH70_WEAK], [WARN_MODULUS_SIZE, WARN_RNDSIG_KEY]],
|
|
|
|
'x509v3-sign-dss': [[], [FAIL_OPENSSH70_WEAK], [WARN_MODULUS_SIZE, WARN_RNDSIG_KEY]],
|
|
|
|
'x509v3-sign-rsa': [[], [], [WARN_HASH_WEAK]],
|
|
|
|
'x509v3-sign-rsa': [[], [], [WARN_HASH_WEAK]],
|
|
|
|
'x509v3-sign-rsa-sha256@ssh.com': [[]],
|
|
|
|
'x509v3-sign-rsa-sha256@ssh.com': [[]],
|
|
|
@ -416,7 +429,7 @@ class SSH2(object): # pylint: disable=too-few-public-methods
|
|
|
|
'rsa-sha2-256-cert-v01@openssh.com': [['7.8']],
|
|
|
|
'rsa-sha2-256-cert-v01@openssh.com': [['7.8']],
|
|
|
|
'rsa-sha2-512-cert-v01@openssh.com': [['7.8']],
|
|
|
|
'rsa-sha2-512-cert-v01@openssh.com': [['7.8']],
|
|
|
|
'ssh-rsa-sha256@ssh.com': [[]],
|
|
|
|
'ssh-rsa-sha256@ssh.com': [[]],
|
|
|
|
'ecdsa-sha2-1.3.132.0.10': [[], [], [WARN_RNDSIG_KEY]], # ECDSA over secp256k1 (i.e.: the Bitcoin curve)
|
|
|
|
'ecdsa-sha2-1.3.132.0.10': [[], [], [WARN_RNDSIG_KEY]], # ECDSA over secp256k1 (i.e.: the Bitcoin curve)
|
|
|
|
'sk-ecdsa-sha2-nistp256-cert-v01@openssh.com': [['8.2'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]],
|
|
|
|
'sk-ecdsa-sha2-nistp256-cert-v01@openssh.com': [['8.2'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]],
|
|
|
|
'sk-ecdsa-sha2-nistp256@openssh.com': [['8.2'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]],
|
|
|
|
'sk-ecdsa-sha2-nistp256@openssh.com': [['8.2'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]],
|
|
|
|
'sk-ssh-ed25519-cert-v01@openssh.com': [['8.2']],
|
|
|
|
'sk-ssh-ed25519-cert-v01@openssh.com': [['8.2']],
|
|
|
@ -508,20 +521,20 @@ class SSH2(object): # pylint: disable=too-few-public-methods
|
|
|
|
'umac-128@openssh.com': [['6.2'], [], [WARN_ENCRYPT_AND_MAC]],
|
|
|
|
'umac-128@openssh.com': [['6.2'], [], [WARN_ENCRYPT_AND_MAC]],
|
|
|
|
'hmac-sha1-etm@openssh.com': [['6.2'], [], [WARN_HASH_WEAK]],
|
|
|
|
'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-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-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-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-256-etm@openssh.com': [['6.2']],
|
|
|
|
'hmac-sha2-512-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-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-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]],
|
|
|
|
'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-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-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-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']],
|
|
|
|
'umac-128-etm@openssh.com': [['6.2']],
|
|
|
|
'aes128-gcm': [[]],
|
|
|
|
'aes128-gcm': [[]],
|
|
|
|
'aes256-gcm': [[]],
|
|
|
|
'aes256-gcm': [[]],
|
|
|
|
'chacha20-poly1305@openssh.com': [[]], # Despite the @openssh.com tag, this was never shipped as a MAC in OpenSSH (only as a cipher); it is only implemented as a MAC in Syncplify.
|
|
|
|
'chacha20-poly1305@openssh.com': [[]], # Despite the @openssh.com tag, this was never shipped as a MAC in OpenSSH (only as a cipher); it is only implemented as a MAC in Syncplify.
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} # type: Dict[str, Dict[str, List[List[Optional[str]]]]]
|
|
|
|
} # type: Dict[str, Dict[str, List[List[Optional[str]]]]]
|
|
|
|
|
|
|
|
|
|
|
@ -700,7 +713,7 @@ class SSH2(object): # pylint: disable=too-few-public-methods
|
|
|
|
'ecdh-sha2-nistp256': KexNISTP256,
|
|
|
|
'ecdh-sha2-nistp256': KexNISTP256,
|
|
|
|
'ecdh-sha2-nistp384': KexNISTP384,
|
|
|
|
'ecdh-sha2-nistp384': KexNISTP384,
|
|
|
|
'ecdh-sha2-nistp521': KexNISTP521,
|
|
|
|
'ecdh-sha2-nistp521': KexNISTP521,
|
|
|
|
#'kexguess2@matt.ucc.asn.au': ???
|
|
|
|
# 'kexguess2@matt.ucc.asn.au': ???
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# Pick the first kex algorithm that the server supports, which we
|
|
|
|
# Pick the first kex algorithm that the server supports, which we
|
|
|
@ -735,14 +748,14 @@ class SSH2(object): # pylint: disable=too-few-public-methods
|
|
|
|
# If the connection is closed, re-open it and get the kex again.
|
|
|
|
# If the connection is closed, re-open it and get the kex again.
|
|
|
|
if not s.is_connected():
|
|
|
|
if not s.is_connected():
|
|
|
|
s.connect()
|
|
|
|
s.connect()
|
|
|
|
unused = None # pylint: disable=unused-variable
|
|
|
|
unused = None # pylint: disable=unused-variable
|
|
|
|
unused, unused, err = s.get_banner()
|
|
|
|
unused, unused, err = s.get_banner()
|
|
|
|
if err is not None:
|
|
|
|
if err is not None:
|
|
|
|
s.close()
|
|
|
|
s.close()
|
|
|
|
return
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Parse the server's initial KEX.
|
|
|
|
# Parse the server's initial KEX.
|
|
|
|
packet_type = 0 # pylint: disable=unused-variable
|
|
|
|
packet_type = 0 # pylint: disable=unused-variable
|
|
|
|
packet_type, payload = s.read_packet()
|
|
|
|
packet_type, payload = s.read_packet()
|
|
|
|
SSH2.Kex.parse(payload)
|
|
|
|
SSH2.Kex.parse(payload)
|
|
|
|
|
|
|
|
|
|
|
@ -798,7 +811,6 @@ class SSH2(object): # pylint: disable=too-few-public-methods
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
host_key_types[host_key_type]['parsed'] = True
|
|
|
|
host_key_types[host_key_type]['parsed'] = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Performs DH group exchanges to find what moduli are supported, and checks
|
|
|
|
# Performs DH group exchanges to find what moduli are supported, and checks
|
|
|
|
# their size.
|
|
|
|
# their size.
|
|
|
|
class GEXTest(object):
|
|
|
|
class GEXTest(object):
|
|
|
@ -811,14 +823,14 @@ class SSH2(object): # pylint: disable=too-few-public-methods
|
|
|
|
return
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
s.connect()
|
|
|
|
s.connect()
|
|
|
|
unused = None # pylint: disable=unused-variable
|
|
|
|
unused = None # pylint: disable=unused-variable
|
|
|
|
unused, unused, err = s.get_banner()
|
|
|
|
unused, unused, err = s.get_banner()
|
|
|
|
if err is not None:
|
|
|
|
if err is not None:
|
|
|
|
s.close()
|
|
|
|
s.close()
|
|
|
|
return False
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
# Parse the server's initial KEX.
|
|
|
|
# Parse the server's initial KEX.
|
|
|
|
packet_type = 0 # pylint: disable=unused-variable
|
|
|
|
packet_type = 0 # pylint: disable=unused-variable
|
|
|
|
packet_type, payload = s.read_packet(2)
|
|
|
|
packet_type, payload = s.read_packet(2)
|
|
|
|
kex = SSH2.Kex.parse(payload)
|
|
|
|
kex = SSH2.Kex.parse(payload)
|
|
|
|
|
|
|
|
|
|
|
@ -865,7 +877,7 @@ class SSH2(object): # pylint: disable=too-few-public-methods
|
|
|
|
# got here, doesn't mean the server is vulnerable...
|
|
|
|
# got here, doesn't mean the server is vulnerable...
|
|
|
|
smallest_modulus = kex_group.get_dh_modulus_size()
|
|
|
|
smallest_modulus = kex_group.get_dh_modulus_size()
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e: # pylint: disable=bare-except
|
|
|
|
except Exception: # pylint: disable=bare-except
|
|
|
|
pass
|
|
|
|
pass
|
|
|
|
finally:
|
|
|
|
finally:
|
|
|
|
s.close()
|
|
|
|
s.close()
|
|
|
@ -887,9 +899,9 @@ class SSH2(object): # pylint: disable=too-few-public-methods
|
|
|
|
kex_group.send_init_gex(s, bits, bits, bits)
|
|
|
|
kex_group.send_init_gex(s, bits, bits, bits)
|
|
|
|
kex_group.recv_reply(s, False)
|
|
|
|
kex_group.recv_reply(s, False)
|
|
|
|
smallest_modulus = kex_group.get_dh_modulus_size()
|
|
|
|
smallest_modulus = kex_group.get_dh_modulus_size()
|
|
|
|
except Exception as e: # pylint: disable=bare-except
|
|
|
|
except Exception: # pylint: disable=bare-except
|
|
|
|
#import traceback
|
|
|
|
# import traceback
|
|
|
|
#print(traceback.format_exc())
|
|
|
|
# print(traceback.format_exc())
|
|
|
|
pass
|
|
|
|
pass
|
|
|
|
finally:
|
|
|
|
finally:
|
|
|
|
# The server is in a state that is not re-testable,
|
|
|
|
# The server is in a state that is not re-testable,
|
|
|
@ -897,7 +909,6 @@ class SSH2(object): # pylint: disable=too-few-public-methods
|
|
|
|
# connection.
|
|
|
|
# connection.
|
|
|
|
s.close()
|
|
|
|
s.close()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if smallest_modulus > 0:
|
|
|
|
if smallest_modulus > 0:
|
|
|
|
kex.set_dh_modulus_size(gex_alg, smallest_modulus)
|
|
|
|
kex.set_dh_modulus_size(gex_alg, smallest_modulus)
|
|
|
|
|
|
|
|
|
|
|
@ -919,6 +930,7 @@ class SSH2(object): # pylint: disable=too-few-public-methods
|
|
|
|
if reconnect_failed:
|
|
|
|
if reconnect_failed:
|
|
|
|
break
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SSH1(object):
|
|
|
|
class SSH1(object):
|
|
|
|
class CRC32(object):
|
|
|
|
class CRC32(object):
|
|
|
|
def __init__(self):
|
|
|
|
def __init__(self):
|
|
|
@ -935,8 +947,8 @@ class SSH1(object):
|
|
|
|
|
|
|
|
|
|
|
|
def calc(self, v):
|
|
|
|
def calc(self, v):
|
|
|
|
# type: (binary_type) -> int
|
|
|
|
# type: (binary_type) -> int
|
|
|
|
crc, l = 0, len(v)
|
|
|
|
crc, length = 0, len(v)
|
|
|
|
for i in range(l):
|
|
|
|
for i in range(length):
|
|
|
|
n = ord(v[i:i + 1])
|
|
|
|
n = ord(v[i:i + 1])
|
|
|
|
n = n ^ (crc & 0xff)
|
|
|
|
n = n ^ (crc & 0xff)
|
|
|
|
crc = (crc >> 8) ^ self._table[n]
|
|
|
|
crc = (crc >> 8) ^ self._table[n]
|
|
|
@ -955,11 +967,11 @@ class SSH1(object):
|
|
|
|
|
|
|
|
|
|
|
|
class KexDB(object): # pylint: disable=too-few-public-methods
|
|
|
|
class KexDB(object): # pylint: disable=too-few-public-methods
|
|
|
|
# pylint: disable=bad-whitespace
|
|
|
|
# pylint: disable=bad-whitespace
|
|
|
|
FAIL_PLAINTEXT = 'no encryption/integrity'
|
|
|
|
FAIL_PLAINTEXT = 'no encryption/integrity'
|
|
|
|
FAIL_OPENSSH37_REMOVE = 'removed since OpenSSH 3.7'
|
|
|
|
FAIL_OPENSSH37_REMOVE = 'removed since OpenSSH 3.7'
|
|
|
|
FAIL_NA_BROKEN = 'not implemented in OpenSSH, broken algorithm'
|
|
|
|
FAIL_NA_BROKEN = 'not implemented in OpenSSH, broken algorithm'
|
|
|
|
FAIL_NA_UNSAFE = 'not implemented in OpenSSH (server), unsafe algorithm'
|
|
|
|
FAIL_NA_UNSAFE = 'not implemented in OpenSSH (server), unsafe algorithm'
|
|
|
|
TEXT_CIPHER_IDEA = 'cipher used by commercial SSH'
|
|
|
|
TEXT_CIPHER_IDEA = 'cipher used by commercial SSH'
|
|
|
|
|
|
|
|
|
|
|
|
ALGORITHMS = {
|
|
|
|
ALGORITHMS = {
|
|
|
|
'key': {
|
|
|
|
'key': {
|
|
|
@ -1189,6 +1201,7 @@ class ReadBuf(object):
|
|
|
|
self._len = 0
|
|
|
|
self._len = 0
|
|
|
|
super(ReadBuf, self).reset()
|
|
|
|
super(ReadBuf, self).reset()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class WriteBuf(object):
|
|
|
|
class WriteBuf(object):
|
|
|
|
def __init__(self, data=None):
|
|
|
|
def __init__(self, data=None):
|
|
|
|
# type: (Optional[binary_type]) -> None
|
|
|
|
# type: (Optional[binary_type]) -> None
|
|
|
@ -1285,15 +1298,15 @@ class SSH(object): # pylint: disable=too-few-public-methods
|
|
|
|
class Protocol(object): # pylint: disable=too-few-public-methods
|
|
|
|
class Protocol(object): # pylint: disable=too-few-public-methods
|
|
|
|
# pylint: disable=bad-whitespace
|
|
|
|
# pylint: disable=bad-whitespace
|
|
|
|
SMSG_PUBLIC_KEY = 2
|
|
|
|
SMSG_PUBLIC_KEY = 2
|
|
|
|
MSG_DEBUG = 4
|
|
|
|
MSG_DEBUG = 4
|
|
|
|
MSG_KEXINIT = 20
|
|
|
|
MSG_KEXINIT = 20
|
|
|
|
MSG_NEWKEYS = 21
|
|
|
|
MSG_NEWKEYS = 21
|
|
|
|
MSG_KEXDH_INIT = 30
|
|
|
|
MSG_KEXDH_INIT = 30
|
|
|
|
MSG_KEXDH_REPLY = 31
|
|
|
|
MSG_KEXDH_REPLY = 31
|
|
|
|
MSG_KEXDH_GEX_REQUEST = 34
|
|
|
|
MSG_KEXDH_GEX_REQUEST = 34
|
|
|
|
MSG_KEXDH_GEX_GROUP = 31
|
|
|
|
MSG_KEXDH_GEX_GROUP = 31
|
|
|
|
MSG_KEXDH_GEX_INIT = 32
|
|
|
|
MSG_KEXDH_GEX_INIT = 32
|
|
|
|
MSG_KEXDH_GEX_REPLY = 33
|
|
|
|
MSG_KEXDH_GEX_REPLY = 33
|
|
|
|
|
|
|
|
|
|
|
|
class Product(object): # pylint: disable=too-few-public-methods
|
|
|
|
class Product(object): # pylint: disable=too-few-public-methods
|
|
|
|
OpenSSH = 'OpenSSH'
|
|
|
|
OpenSSH = 'OpenSSH'
|
|
|
@ -1609,7 +1622,7 @@ class SSH(object): # pylint: disable=too-few-public-methods
|
|
|
|
|
|
|
|
|
|
|
|
def __getitem__(self, product):
|
|
|
|
def __getitem__(self, product):
|
|
|
|
# type: (str) -> Sequence[Optional[str]]
|
|
|
|
# type: (str) -> Sequence[Optional[str]]
|
|
|
|
return tuple(self.__storage.get(product, [None]*4))
|
|
|
|
return tuple(self.__storage.get(product, [None] * 4))
|
|
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
def __str__(self):
|
|
|
|
# type: () -> str
|
|
|
|
# type: () -> str
|
|
|
@ -1640,7 +1653,7 @@ class SSH(object): # pylint: disable=too-few-public-methods
|
|
|
|
ssh_versions[ssh_prod] = ssh_ver
|
|
|
|
ssh_versions[ssh_prod] = ssh_ver
|
|
|
|
for ssh_product, ssh_version in ssh_versions.items():
|
|
|
|
for ssh_product, ssh_version in ssh_versions.items():
|
|
|
|
if ssh_product not in self.__storage:
|
|
|
|
if ssh_product not in self.__storage:
|
|
|
|
self.__storage[ssh_product] = [None]*4
|
|
|
|
self.__storage[ssh_product] = [None] * 4
|
|
|
|
prev = self[ssh_product][pos]
|
|
|
|
prev = self[ssh_product][pos]
|
|
|
|
if (prev is None or
|
|
|
|
if (prev is None or
|
|
|
|
(prev < ssh_version and pos % 2 == 0) or
|
|
|
|
(prev < ssh_version and pos % 2 == 0) or
|
|
|
@ -1783,19 +1796,18 @@ class SSH(object): # pylint: disable=too-few-public-methods
|
|
|
|
if software is not None:
|
|
|
|
if software is not None:
|
|
|
|
if software.product not in vproducts:
|
|
|
|
if software.product not in vproducts:
|
|
|
|
unknown_software = True
|
|
|
|
unknown_software = True
|
|
|
|
#
|
|
|
|
|
|
|
|
# The code below is commented out because it would try to guess what the server is,
|
|
|
|
# The code below is commented out because it would try to guess what the server is,
|
|
|
|
# usually resulting in wild & incorrect recommendations.
|
|
|
|
# usually resulting in wild & incorrect recommendations.
|
|
|
|
#
|
|
|
|
# if software is None:
|
|
|
|
# if software is None:
|
|
|
|
# ssh_timeframe = self.get_ssh_timeframe(for_server)
|
|
|
|
# ssh_timeframe = self.get_ssh_timeframe(for_server)
|
|
|
|
# for product in vproducts:
|
|
|
|
# for product in vproducts:
|
|
|
|
# if product not in ssh_timeframe:
|
|
|
|
# if product not in ssh_timeframe:
|
|
|
|
# continue
|
|
|
|
# continue
|
|
|
|
# version = ssh_timeframe.get_from(product, for_server)
|
|
|
|
# version = ssh_timeframe.get_from(product, for_server)
|
|
|
|
# if version is not None:
|
|
|
|
# if version is not None:
|
|
|
|
# software = SSH.Software(None, product, version, None, None)
|
|
|
|
# software = SSH.Software(None, product, version, None, None)
|
|
|
|
# break
|
|
|
|
# break
|
|
|
|
|
|
|
|
rec = {} # type: Dict[int, Dict[str, Dict[str, Dict[str, int]]]]
|
|
|
|
rec = {} # type: Dict[int, Dict[str, Dict[str, Dict[str, int]]]]
|
|
|
|
if software is None:
|
|
|
|
if software is None:
|
|
|
|
unknown_software = True
|
|
|
|
unknown_software = True
|
|
|
@ -2057,7 +2069,6 @@ class SSH(object): # pylint: disable=too-few-public-methods
|
|
|
|
self.client_host = None
|
|
|
|
self.client_host = None
|
|
|
|
self.client_port = None
|
|
|
|
self.client_port = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _resolve(self, ipvo):
|
|
|
|
def _resolve(self, ipvo):
|
|
|
|
# type: (Sequence[int]) -> Iterable[Tuple[int, Tuple[Any, ...]]]
|
|
|
|
# type: (Sequence[int]) -> Iterable[Tuple[int, Tuple[Any, ...]]]
|
|
|
|
ipvo = tuple([x for x in utils.unique_seq(ipvo) if x in (4, 6)])
|
|
|
|
ipvo = tuple([x for x in utils.unique_seq(ipvo) if x in (4, 6)])
|
|
|
@ -2081,7 +2092,6 @@ class SSH(object): # pylint: disable=too-few-public-methods
|
|
|
|
out.fail('[exception] {0}'.format(e))
|
|
|
|
out.fail('[exception] {0}'.format(e))
|
|
|
|
sys.exit(1)
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Listens on a server socket and accepts one connection (used for
|
|
|
|
# Listens on a server socket and accepts one connection (used for
|
|
|
|
# auditing client connections).
|
|
|
|
# auditing client connections).
|
|
|
|
def listen_and_accept(self):
|
|
|
|
def listen_and_accept(self):
|
|
|
@ -2093,7 +2103,7 @@ class SSH(object): # pylint: disable=too-few-public-methods
|
|
|
|
s.bind(('0.0.0.0', self.__port))
|
|
|
|
s.bind(('0.0.0.0', self.__port))
|
|
|
|
s.listen()
|
|
|
|
s.listen()
|
|
|
|
self.__sock_map[s.fileno()] = s
|
|
|
|
self.__sock_map[s.fileno()] = s
|
|
|
|
except Exception as e:
|
|
|
|
except Exception:
|
|
|
|
print("Warning: failed to listen on any IPv4 interfaces.")
|
|
|
|
print("Warning: failed to listen on any IPv4 interfaces.")
|
|
|
|
pass
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
@ -2105,7 +2115,7 @@ class SSH(object): # pylint: disable=too-few-public-methods
|
|
|
|
s.bind(('::', self.__port))
|
|
|
|
s.bind(('::', self.__port))
|
|
|
|
s.listen()
|
|
|
|
s.listen()
|
|
|
|
self.__sock_map[s.fileno()] = s
|
|
|
|
self.__sock_map[s.fileno()] = s
|
|
|
|
except Exception as e:
|
|
|
|
except Exception:
|
|
|
|
print("Warning: failed to listen on any IPv6 interfaces.")
|
|
|
|
print("Warning: failed to listen on any IPv6 interfaces.")
|
|
|
|
pass
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
@ -2139,7 +2149,6 @@ class SSH(object): # pylint: disable=too-few-public-methods
|
|
|
|
c.settimeout(self.__timeout)
|
|
|
|
c.settimeout(self.__timeout)
|
|
|
|
self.__sock = c
|
|
|
|
self.__sock = c
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def connect(self):
|
|
|
|
def connect(self):
|
|
|
|
# type: () -> None
|
|
|
|
# type: () -> None
|
|
|
|
err = None
|
|
|
|
err = None
|
|
|
@ -2169,10 +2178,10 @@ class SSH(object): # pylint: disable=too-few-public-methods
|
|
|
|
banner = SSH_HEADER.format('1.5' if sshv == 1 else '2.0')
|
|
|
|
banner = SSH_HEADER.format('1.5' if sshv == 1 else '2.0')
|
|
|
|
if self.__state < self.SM_BANNER_SENT:
|
|
|
|
if self.__state < self.SM_BANNER_SENT:
|
|
|
|
self.send_banner(banner)
|
|
|
|
self.send_banner(banner)
|
|
|
|
# rto = self.__sock.gettimeout()
|
|
|
|
# rto = self.__sock.gettimeout()
|
|
|
|
# self.__sock.settimeout(0.7)
|
|
|
|
# self.__sock.settimeout(0.7)
|
|
|
|
s, e = self.recv()
|
|
|
|
s, e = self.recv()
|
|
|
|
# self.__sock.settimeout(rto)
|
|
|
|
# self.__sock.settimeout(rto)
|
|
|
|
if s < 0:
|
|
|
|
if s < 0:
|
|
|
|
return self.__banner, self.__header, e
|
|
|
|
return self.__banner, self.__header, e
|
|
|
|
e = None
|
|
|
|
e = None
|
|
|
@ -2352,12 +2361,11 @@ class KexDH(object): # pragma: nocover
|
|
|
|
self.__hostkey_type = None
|
|
|
|
self.__hostkey_type = None
|
|
|
|
self.__hostkey_e = 0
|
|
|
|
self.__hostkey_e = 0
|
|
|
|
self.__hostkey_n = 0
|
|
|
|
self.__hostkey_n = 0
|
|
|
|
self.__hostkey_n_len = 0 # 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.__ca_n_len = 0 # Length of the CA key modulus (if hostkey is a cert).
|
|
|
|
self.__f = 0
|
|
|
|
self.__f = 0
|
|
|
|
self.__h_sig = 0
|
|
|
|
self.__h_sig = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def set_params(self, g, p):
|
|
|
|
def set_params(self, g, p):
|
|
|
|
self.__g = g
|
|
|
|
self.__g = g
|
|
|
|
self.__p = p
|
|
|
|
self.__p = p
|
|
|
@ -2365,7 +2373,6 @@ class KexDH(object): # pragma: nocover
|
|
|
|
self.__x = 0
|
|
|
|
self.__x = 0
|
|
|
|
self.__e = 0
|
|
|
|
self.__e = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def send_init(self, s, init_msg=SSH.Protocol.MSG_KEXDH_INIT):
|
|
|
|
def send_init(self, s, init_msg=SSH.Protocol.MSG_KEXDH_INIT):
|
|
|
|
# type: (SSH.Socket) -> None
|
|
|
|
# type: (SSH.Socket) -> None
|
|
|
|
r = random.SystemRandom()
|
|
|
|
r = random.SystemRandom()
|
|
|
@ -2395,17 +2402,16 @@ class KexDH(object): # pragma: nocover
|
|
|
|
return None
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
hostkey_len = f_len = h_sig_len = 0 # pylint: disable=unused-variable
|
|
|
|
hostkey_len = f_len = h_sig_len = 0 # pylint: disable=unused-variable
|
|
|
|
hostkey_type_len = hostkey_e_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
|
|
|
|
key_id_len = principles_len = 0 # pylint: disable=unused-variable
|
|
|
|
critical_options_len = extensions_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
|
|
|
|
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
|
|
|
|
ca_key_len = ca_key_type_len = ca_key_e_len = 0 # pylint: disable=unused-variable
|
|
|
|
|
|
|
|
|
|
|
|
key_id = principles = None # pylint: disable=unused-variable
|
|
|
|
key_id = principles = None # pylint: disable=unused-variable
|
|
|
|
critical_options = extensions = 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
|
|
|
|
nonce = ca_key = ca_key_type = None # pylint: disable=unused-variable
|
|
|
|
ca_key_e = ca_key_n = None # pylint: disable=unused-variable
|
|
|
|
ca_key_e = ca_key_n = None # pylint: disable=unused-variable
|
|
|
|
|
|
|
|
|
|
|
|
# Get the host key blob, F, and signature.
|
|
|
|
# Get the host key blob, F, and signature.
|
|
|
|
ptr = 0
|
|
|
|
ptr = 0
|
|
|
@ -2455,12 +2461,10 @@ class KexDH(object): # pragma: nocover
|
|
|
|
# The principles, which are... I don't know what.
|
|
|
|
# The principles, which are... I don't know what.
|
|
|
|
principles, principles_len, ptr = KexDH.__get_bytes(hostkey, ptr)
|
|
|
|
principles, principles_len, ptr = KexDH.__get_bytes(hostkey, ptr)
|
|
|
|
|
|
|
|
|
|
|
|
# The timestamp that this certificate is valid after.
|
|
|
|
# Skip over the timestamp that this certificate is valid after.
|
|
|
|
valid_after = hostkey[ptr:ptr + 8]
|
|
|
|
|
|
|
|
ptr += 8
|
|
|
|
ptr += 8
|
|
|
|
|
|
|
|
|
|
|
|
# The timestamp that this certificate is valid before.
|
|
|
|
# Skip over the timestamp that this certificate is valid before.
|
|
|
|
valid_before = hostkey[ptr:ptr + 8]
|
|
|
|
|
|
|
|
ptr += 8
|
|
|
|
ptr += 8
|
|
|
|
|
|
|
|
|
|
|
|
# TODO: validate the principles, and time range.
|
|
|
|
# TODO: validate the principles, and time range.
|
|
|
@ -2895,7 +2899,7 @@ def output_fingerprints(algs, sha256=True):
|
|
|
|
if algs.ssh1kex is not None:
|
|
|
|
if algs.ssh1kex is not None:
|
|
|
|
name = 'ssh-rsa1'
|
|
|
|
name = 'ssh-rsa1'
|
|
|
|
fp = SSH.Fingerprint(algs.ssh1kex.host_key_fingerprint_data)
|
|
|
|
fp = SSH.Fingerprint(algs.ssh1kex.host_key_fingerprint_data)
|
|
|
|
#bits = algs.ssh1kex.host_key_bits
|
|
|
|
# bits = algs.ssh1kex.host_key_bits
|
|
|
|
fps.append((name, fp))
|
|
|
|
fps.append((name, fp))
|
|
|
|
if algs.ssh2kex is not None:
|
|
|
|
if algs.ssh2kex is not None:
|
|
|
|
host_keys = algs.ssh2kex.host_keys()
|
|
|
|
host_keys = algs.ssh2kex.host_keys()
|
|
|
@ -2917,8 +2921,8 @@ def output_fingerprints(algs, sha256=True):
|
|
|
|
for fpp in fps:
|
|
|
|
for fpp in fps:
|
|
|
|
name, fp = fpp
|
|
|
|
name, fp = fpp
|
|
|
|
fpo = fp.sha256 if sha256 else fp.md5
|
|
|
|
fpo = fp.sha256 if sha256 else fp.md5
|
|
|
|
#p = '' if out.batch else ' ' * (padlen - len(name))
|
|
|
|
# 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} -- {2} {3}'.format(name, p, bits, fpo))
|
|
|
|
out.good('(fin) {0}: {1}'.format(name, fpo))
|
|
|
|
out.good('(fin) {0}: {1}'.format(name, fpo))
|
|
|
|
if len(obuf) > 0:
|
|
|
|
if len(obuf) > 0:
|
|
|
|
out.head('# fingerprints')
|
|
|
|
out.head('# fingerprints')
|
|
|
@ -2994,7 +2998,7 @@ def output_recommendations(algs, software, padlen=0):
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
title = ''
|
|
|
|
title = ''
|
|
|
|
out.head('# algorithm recommendations {0}'.format(title))
|
|
|
|
out.head('# algorithm recommendations {0}'.format(title))
|
|
|
|
obuf.flush(True) # Sort the output so that it is always stable (needed for repeatable testing).
|
|
|
|
obuf.flush(True) # Sort the output so that it is always stable (needed for repeatable testing).
|
|
|
|
out.sep()
|
|
|
|
out.sep()
|
|
|
|
return ret
|
|
|
|
return ret
|
|
|
|
|
|
|
|
|
|
|
@ -3018,7 +3022,7 @@ def output_info(algs, software, client_audit, any_problems, padlen=0):
|
|
|
|
|
|
|
|
|
|
|
|
def output(banner, header, client_host=None, kex=None, pkm=None):
|
|
|
|
def output(banner, header, client_host=None, kex=None, pkm=None):
|
|
|
|
# type: (Optional[SSH.Banner], List[text_type], Optional[SSH2.Kex], Optional[SSH1.PublicKeyMessage]) -> None
|
|
|
|
# type: (Optional[SSH.Banner], List[text_type], Optional[SSH2.Kex], Optional[SSH1.PublicKeyMessage]) -> None
|
|
|
|
client_audit = (client_host != None) # If set, this is a client audit.
|
|
|
|
client_audit = client_host is not None # If set, this is a client audit.
|
|
|
|
sshv = 1 if pkm is not None else 2
|
|
|
|
sshv = 1 if pkm is not None else 2
|
|
|
|
algs = SSH.Algorithms(pkm, kex)
|
|
|
|
algs = SSH.Algorithms(pkm, kex)
|
|
|
|
with OutputBuffer() as obuf:
|
|
|
|
with OutputBuffer() as obuf:
|
|
|
@ -3077,11 +3081,11 @@ def output(banner, header, client_host=None, kex=None, pkm=None):
|
|
|
|
perfect_config = output_recommendations(algs, software, maxlen)
|
|
|
|
perfect_config = output_recommendations(algs, software, maxlen)
|
|
|
|
output_info(algs, software, client_audit, not perfect_config)
|
|
|
|
output_info(algs, software, client_audit, not perfect_config)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# If we encountered any unknown algorithms, ask the user to report them.
|
|
|
|
# If we encountered any unknown algorithms, ask the user to report them.
|
|
|
|
if len(unknown_algorithms) > 0:
|
|
|
|
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 <https://github.com/jtesta/ssh-audit/issues>.\n" % ','.join(unknown_algorithms))
|
|
|
|
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 <https://github.com/jtesta/ssh-audit/issues>.\n" % ','.join(unknown_algorithms))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Utils(object):
|
|
|
|
class Utils(object):
|
|
|
|
@classmethod
|
|
|
|
@classmethod
|
|
|
|
def _type_err(cls, v, target):
|
|
|
|
def _type_err(cls, v, target):
|
|
|
@ -3204,6 +3208,7 @@ class Utils(object):
|
|
|
|
except: # pylint: disable=bare-except
|
|
|
|
except: # pylint: disable=bare-except
|
|
|
|
return -1.0
|
|
|
|
return -1.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def build_struct(banner, kex=None, pkm=None, client_host=None):
|
|
|
|
def build_struct(banner, kex=None, pkm=None, client_host=None):
|
|
|
|
res = {
|
|
|
|
res = {
|
|
|
|
"banner": {
|
|
|
|
"banner": {
|
|
|
@ -3281,6 +3286,7 @@ def build_struct(banner, kex=None, pkm=None, client_host=None):
|
|
|
|
|
|
|
|
|
|
|
|
return res
|
|
|
|
return res
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def audit(aconf, sshv=None):
|
|
|
|
def audit(aconf, sshv=None):
|
|
|
|
# type: (AuditConf, Optional[int]) -> None
|
|
|
|
# type: (AuditConf, Optional[int]) -> None
|
|
|
|
out.batch = aconf.batch
|
|
|
|
out.batch = aconf.batch
|
|
|
@ -3350,9 +3356,11 @@ def audit(aconf, sshv=None):
|
|
|
|
utils = Utils()
|
|
|
|
utils = Utils()
|
|
|
|
out = Output()
|
|
|
|
out = Output()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
def main():
|
|
|
|
conf = AuditConf.from_cmdline(sys.argv[1:], usage)
|
|
|
|
conf = AuditConf.from_cmdline(sys.argv[1:], usage)
|
|
|
|
audit(conf)
|
|
|
|
audit(conf)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__': # pragma: nocover
|
|
|
|
if __name__ == '__main__': # pragma: nocover
|
|
|
|
main()
|
|
|
|
main()
|
|
|
|