16 Commits

Author SHA1 Message Date
Joe Testa 4845a8fdee Updated README. 2025-08-06 08:40:36 -04:00
Joe Testa 11a902cb14 Removed SSHv1 support (#298). 2025-07-26 19:57:11 -04:00
Joe Testa b456bb31b9 Added note on mlkem768x25519-sha256 that it is the default key exchange since OpenSSH 10.0. 2025-06-16 18:59:36 -04:00
Joe Testa 32085b2fa5 Added two new ciphers: AEAD_CAMELLIA_128_GCM, AEAD_CAMELLIA_256_GCM. 2025-05-18 18:46:40 -04:00
Joe Testa 5ddd8cca5b Added 2 new key exchanges: mlkem768nistp256-sha256, mlkem1024nistp384-sha384. 2025-04-18 18:29:18 -04:00
Joe Testa b90db2c1af Fixed mypy failure. 2025-04-18 17:06:29 -04:00
playoutsideplay 68c827c239 Update LICENSE (#319)
Updated year
2025-04-18 16:27:44 -04:00
Joe Testa e318787a5c Batch mode no longer automatically enables verbose mode. 2024-12-05 10:06:58 -05:00
Joe Testa d9c703c777 When running against multiple hosts, now prints each target host regardless of output level. (#309) 2024-12-05 09:41:26 -05:00
Joe Testa 28a1e23986 Added warnings to all key exchanges that do not provide protection against quantum attacks. 2024-11-25 15:56:51 -05:00
Joe Testa a01baadfa8 Additional cleanups after merging #304. 2024-11-22 12:28:02 -05:00
oam7575 45abc3aaf4 Argparse v3 - RC1 (#304)
* Argparse v3 - RC1

* Argparse v3 - RC1

Argparse v3 RC1 - post feedback

Argparse v3 - RC2
2024-11-22 12:26:20 -05:00
Joe Testa 99c64787d9 Updated description of -m option. 2024-10-16 16:39:11 -04:00
Joe Testa 3fa62c3ac5 Fixed man page parsing error. (#301) 2024-10-16 16:23:20 -04:00
Joe Testa d7fff591fa Bumped version to v3.4.0-dev. 2024-10-15 18:30:08 -04:00
Joe Testa 84647ecb32 Updated packaging notes. 2024-10-15 18:29:25 -04:00
47 changed files with 786 additions and 1101 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (C) 2017-2024 Joe Testa (jtesta@positronsecurity.com)
Copyright (C) 2017-2025 Joe Testa (jtesta@positronsecurity.com)
Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu)
+9 -9
View File
@@ -15,10 +15,10 @@ An executable can only be made on a Windows host because the PyInstaller tool (h
# PyPI
To create package and upload to test server (hint: use username '\_\_token\_\_' and API token for test.pypi.org):
To create package and upload to test server (hint: use API token for test.pypi.org):
```
$ sudo apt install python3-virtualenv python3.10-venv
$ sudo apt install python3-virtualenv python3.12-venv
$ make -f Makefile.pypi
$ make -f Makefile.pypi uploadtest
```
@@ -26,12 +26,12 @@ To create package and upload to test server (hint: use username '\_\_token\_\_'
To download from test server and verify:
```
$ virtualenv -p /usr/bin/python3 /tmp/pypi_test
$ virtualenv /tmp/pypi_test
$ cd /tmp/pypi_test; source bin/activate
$ pip3 install --index-url https://test.pypi.org/simple ssh-audit
```
To upload to production server (hint: use username '\_\_token\_\_' and API token for production pypi.org):
To upload to production server (hint: use API token for production pypi.org):
```
$ make -f Makefile.pypi uploadprod
@@ -40,7 +40,7 @@ To upload to production server (hint: use username '\_\_token\_\_' and API token
To download from production server and verify:
```
$ virtualenv -p /usr/bin/python3 /tmp/pypi_prod
$ virtualenv /tmp/pypi_prod
$ cd /tmp/pypi_prod; source bin/activate
$ pip3 install ssh-audit
```
@@ -48,14 +48,14 @@ To download from production server and verify:
# Snap
To create the snap package, run a fully-updated Ubuntu Server 22.04 VM.
To create the Snap package, run a fully-updated Ubuntu Server 24.04 VM.
Create the snap package with:
Create the Snap package with:
```
$ ./build_snap.sh
```
Upload the snap with:
Upload the Snap with:
```
$ snapcraft export-login ~/snap_creds.txt
@@ -68,7 +68,7 @@ Upload the snap with:
# Docker
Ensure that the buildx plugin is available by following the installation instructions available at: https://docs.docker.com/engine/install/ubuntu/
Ensure that the `buildx` plugin is available by following the installation instructions available at: https://docs.docker.com/engine/install/ubuntu/
Build a local image with:
+63 -59
View File
@@ -26,8 +26,7 @@
- [ChangeLog](#changelog)
## Features
- SSH1 and SSH2 protocol server support;
- analyze SSH client configuration;
- analyze SSH both server and 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 security information (available since, removed/disabled, unsafe/weak/legacy, etc);
@@ -41,64 +40,59 @@
## Usage
```
usage: ssh-audit.py [options] <host>
usage: ssh-audit.py [-h] [-4] [-6] [-b] [-c] [-d]
[-g <min1:pref1:max1[,min2:pref2:max2,...]> / <x-y[:step]>] [-j] [-l {info,warn,fail}] [-L]
[-M custom_policy.txt] [-m] [-n] [-P "Built-In Policy Name" / custom_policy.txt] [-p N]
[-T targets.txt] [-t N] [-v] [--conn-rate-test N[:max_rate]] [--dheat N[:kex[:e_len]]]
[--lookup alg1[,alg2,...]] [--skip-rate-test] [--threads N]
[host]
-h, --help print this help
-1, --ssh1 force ssh version 1 only
-2, --ssh2 force ssh version 2 only
-4, --ipv4 enable IPv4 (order of precedence)
-6, --ipv6 enable IPv6 (order of precedence)
-b, --batch batch output
-c, --client-audit starts a server on port 2222 to audit client
software config (use -p to change port;
use -t to change timeout)
--conn-rate-test=N[:max_rate] perform a connection rate test (useful
for collecting metrics related to
susceptibility of the DHEat vuln).
Testing is conducted with N concurrent
sockets with an optional maximum rate
of connections per second.
-d, --debug Enable debug output.
--dheat=N[:kex[:e_len]] continuously perform the DHEat DoS attack
(CVE-2002-20001) against the target using N
concurrent sockets. Optionally, a specific
key exchange algorithm can be specified
instead of allowing it to be automatically
chosen. Additionally, a small length of
the fake e value sent to the server can
be chosen for a more efficient attack (such
as 4).
-g, --gex-test=<x[,y,...]> dh gex modulus size test
<min1:pref1:max1[,min2:pref2:max2,...]>
<x-y[:step]>
-j, --json JSON output (use -jj to enable indents)
-l, --level=<level> minimum output level (info|warn|fail)
-L, --list-policies list all the official, built-in policies. Use with -v
to view policy change logs.
--lookup=<alg1,alg2,...> looks up an algorithm(s) without
connecting to a server
-m, --manual print the man page (Docker, PyPI, Snap, and Windows
builds only)
-M, --make-policy=<policy.txt> creates a policy based on the target server
(i.e.: the target server has the ideal
configuration that other servers should
adhere to)
-n, --no-colors disable colors
-p, --port=<port> port to connect
-P, --policy=<"policy name" | policy.txt> run a policy test using the
specified policy
--skip-rate-test skip the connection rate test during standard audits
(used to safely infer whether the DHEat attack
is viable)
-t, --timeout=<secs> timeout (in seconds) for connection and reading
(default: 5)
-T, --targets=<hosts.txt> a file containing a list of target hosts (one
per line, format HOST[:PORT]). Use -p/--port
to set the default port for all hosts. Use
--threads to control concurrent scans.
--threads=<threads> number of threads to use when scanning multiple
targets (-T/--targets) (default: 32)
-v, --verbose verbose output
positional arguments:
host target hostname or IPv4/IPv6 address
optional arguments:
-h, --help show this help message and exit
-4, --ipv4 enable IPv4 (order of precedence)
-6, --ipv6 enable IPv6 (order of precedence)
-b, --batch batch output
-c, --client-audit starts a server on port 2222 to audit client software config (use -p to change port; use -t
to change timeout)
-d, --debug enable debugging output
-g <min1:pref1:max1[,min2:pref2:max2,...]> / <x-y[:step]>, --gex-test <min1:pref1:max1[,min2:pref2:max2,...]> / <x-y[:step]>
conducts a very customized Diffie-Hellman GEX modulus size test. Tests an array of minimum,
preferred, and maximum values, or a range of values with an optional incremental step amount
-j, --json enable JSON output (use -jj to enable indentation for better readability)
-l {info,warn,fail}, --level {info,warn,fail}
minimum output level (default: info)
-L, --list-policies list all the official, built-in policies. Combine with -v to view policy change logs
-M custom_policy.txt, --make-policy custom_policy.txt
creates a policy based on the target server (i.e.: the target server has the ideal
configuration that other servers should adhere to), and stores it in the file path specified
-m, --manual print the man page (Docker, PyPI, Snap, and Windows builds only)
-n, --no-colors disable colors (automatic when the NO_COLOR environment variable is set)
-P "Built-In Policy Name" / custom_policy.txt, --policy "Built-In Policy Name" / custom_policy.txt
run a policy test using the specified policy (use -L to see built-in policies, or specify
filesystem path to custom policy created by -M)
-p N, --port N the TCP port to connect to (or to listen on when -c is used)
-T targets.txt, --targets targets.txt
a file containing a list of target hosts (one per line, format HOST[:PORT]). Use -p/--port
to set the default port for all hosts. Use --threads to control concurrent scans
-t N, --timeout N timeout (in seconds) for connection and reading (default: 5)
-v, --verbose enable verbose output
--conn-rate-test N[:max_rate]
perform a connection rate test (useful for collecting metrics related to susceptibility of
the DHEat vuln). Testing is conducted with N concurrent sockets with an optional maximum
rate of connections per second
--dheat N[:kex[:e_len]]
continuously perform the DHEat DoS attack (CVE-2002-20001) against the target using N
concurrent sockets. Optionally, a specific key exchange algorithm can be specified instead
of allowing it to be automatically chosen. Additionally, a small length of the fake e value
sent to the server can be chosen for a more efficient attack (such as 4).
--lookup alg1[,alg2,...]
looks up an algorithm(s) without connecting to a server.
--skip-rate-test skip the connection rate test during standard audits (used to safely infer whether the DHEat
attack is viable)
--threads N number of threads to use when scanning multiple targets (-T/--targets) (default: 32)
```
* 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).
@@ -219,6 +213,16 @@ For convenience, a web front-end on top of the command-line tool is available at
## ChangeLog
### v3.4.0-dev
- BIG THANKS to [realmiwi](https://github.com/realmiwi) for being the project's *very first sponsor!!*
- Added warning to all key exchanges that do not include protections against quantum attacks due to the Harvest Now, Decrypt Later strategy (see https://en.wikipedia.org/wiki/Harvest_now,_decrypt_later).
- Removed SSHv1 support (rationale is documented in: https://github.com/jtesta/ssh-audit/issues/298).
- Migrated from deprecated `getopt` module to `argparse`; partial credit [oam7575](https://github.com/oam7575).
- When running against multiple hosts, now prints each target host regardless of output level.
- Batch mode (`-b`) no longer automatically enables verbose mode, due to sometimes confusing results; users can still explicitly enable verbose mode using the `-v` flag.
- Added 2 new key exchanges: `mlkem768nistp256-sha256`, `mlkem1024nistp384-sha384`.
- Added 2 new ciphers: `AEAD_CAMELLIA_128_GCM`, `AEAD_CAMELLIA_256_GCM`.
### v3.3.0 (2024-10-15)
- Added Python 3.13 support.
- Added built-in policies for Ubuntu 24.04 LTS server & client, OpenSSH 9.8, and OpenSSH 9.9.
+1 -10
View File
@@ -111,18 +111,9 @@ echo "Processing man page at ${MAN_PAGE} and placing output into ${GLOBALS_PY}..
# * 'MAN_KEEP_FORMATTING' preserves the backspace-overwrite sequence when
# redirected to a file or a pipe.
# * sed converts unicode hyphens into an ASCI equivalent.
# * The 'ul' command converts the backspace-overwrite sequence to an ANSI
# escape sequence. Not required under Cygwin because man outputs ANSI escape
# codes automatically.
echo BUILTIN_MAN_PAGE = '"""' >> "${GLOBALS_PY}"
if [[ "${PLATFORM}" == CYGWIN* ]]; then
MANWIDTH=80 MAN_KEEP_FORMATTING=1 man "${MAN_PAGE}" | sed $'s/\u2010/-/g' >> "${GLOBALS_PY}"
else
MANWIDTH=80 MAN_KEEP_FORMATTING=1 man "${MAN_PAGE}" | ul | sed $'s/\u2010/-/g' >> "${GLOBALS_PY}"
fi
MANWIDTH=80 MAN_KEEP_FORMATTING=1 man "${MAN_PAGE}" | sed $'s/\u2010/-/g' >> "${GLOBALS_PY}"
echo '"""' >> "${GLOBALS_PY}"
echo "Done."
+3 -24
View File
@@ -1,7 +1,7 @@
"""
The MIT License (MIT)
Copyright (C) 2017-2021 Joe Testa (jtesta@positronsecurity.com)
Copyright (C) 2017-2025 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
@@ -29,8 +29,6 @@ from typing import Callable, Optional, Union, Any # noqa: F401
from ssh_audit.algorithm import Algorithm
from ssh_audit.product import Product
from ssh_audit.software import Software
from ssh_audit.ssh1_kexdb import SSH1_KexDB
from ssh_audit.ssh1_publickeymessage import SSH1_PublicKeyMessage
from ssh_audit.ssh2_kex import SSH2_Kex
from ssh_audit.ssh2_kexdb import SSH2_KexDB
from ssh_audit.timeframe import Timeframe
@@ -38,28 +36,13 @@ from ssh_audit.utils import Utils
class Algorithms:
def __init__(self, pkm: Optional[SSH1_PublicKeyMessage], kex: Optional[SSH2_Kex]) -> None:
self.__ssh1kex = pkm
def __init__(self, kex: Optional[SSH2_Kex]) -> None:
self.__ssh2kex = kex
@property
def ssh1kex(self) -> Optional[SSH1_PublicKeyMessage]:
return self.__ssh1kex
@property
def ssh2kex(self) -> Optional[SSH2_Kex]:
return self.__ssh2kex
@property
def ssh1(self) -> Optional['Algorithms.Item']:
if self.ssh1kex is None:
return None
item = Algorithms.Item(1, SSH1_KexDB.get_db())
item.add('key', ['ssh-rsa1'])
item.add('enc', self.ssh1kex.supported_ciphers)
item.add('aut', self.ssh1kex.supported_authentications)
return item
@property
def ssh2(self) -> Optional['Algorithms.Item']:
if self.ssh2kex is None:
@@ -73,7 +56,7 @@ class Algorithms:
@property
def values(self) -> Iterable['Algorithms.Item']:
for item in [self.ssh1, self.ssh2]:
for item in [self.ssh2]:
if item is not None:
yield item
@@ -82,10 +65,6 @@ class Algorithms:
def _ml(items: Sequence[str]) -> 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)
if self.ssh2kex is not None:
maxlen = max(_ml(self.ssh2kex.kex_algorithms),
_ml(self.ssh2kex.key_algorithms),
+2 -4
View File
@@ -1,7 +1,7 @@
"""
The MIT License (MIT)
Copyright (C) 2017-2024 Joe Testa (jtesta@positronsecurity.com)
Copyright (C) 2017-2025 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
@@ -35,8 +35,6 @@ class AuditConf:
def __init__(self, host: str = '', port: int = 22) -> None:
self.host = host
self.port = port
self.ssh1 = True
self.ssh2 = True
self.batch = False
self.client_audit = False
self.colors = True
@@ -73,7 +71,7 @@ class AuditConf:
def __setattr__(self, name: str, value: Union[str, int, float, bool, Sequence[int]]) -> None:
valid = False
if name in ['batch', 'client_audit', 'colors', 'json', 'json_print_indent', 'list_policies', 'manual', 'make_policy', 'ssh1', 'ssh2', 'timeout_set', 'verbose', 'debug', 'skip_rate_test']:
if name in ['batch', 'client_audit', 'colors', 'json', 'json_print_indent', 'list_policies', 'manual', 'make_policy', 'timeout_set', 'verbose', 'debug', 'skip_rate_test']:
valid, value = True, bool(value)
elif name in ['ipv4', 'ipv6']:
valid, value = True, bool(value)
+2 -2
View File
@@ -759,9 +759,9 @@ class DHEat:
r = socket.getaddrinfo(host, 0, family, socket.SOCK_STREAM)
for address_family, socktype, _, _, addr in r:
if socktype == socket.SOCK_STREAM:
return address_family, addr[0]
return int(address_family), str(addr[0])
return -1, ''
return int(socket.AF_UNSPEC), ''
def _run(self) -> bool:
+2 -2
View File
@@ -1,7 +1,7 @@
"""
The MIT License (MIT)
Copyright (C) 2017-2023 Joe Testa (jtesta@positronsecurity.com)
Copyright (C) 2017-2025 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
@@ -64,7 +64,7 @@ class GEXTest:
try:
# Parse the server's KEX.
_, payload = s.read_packet(2)
_, payload = s.read_packet()
SSH2_Kex.parse(out, payload)
except (KexDHException, struct.error):
out.v("Failed to parse server's kex. Stack trace:\n%s" % str(traceback.format_exc()), write_now=True)
+1 -1
View File
@@ -22,7 +22,7 @@
THE SOFTWARE.
"""
# The version to display.
VERSION = 'v3.3.0'
VERSION = 'v3.4.0-dev'
# SSH software to impersonate
SSH_HEADER = 'SSH-{0}-OpenSSH_8.2'
+5 -5
View File
@@ -1,7 +1,7 @@
"""
The MIT License (MIT)
Copyright (C) 2017-2023 Joe Testa (jtesta@positronsecurity.com)
Copyright (C) 2017-2025 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
@@ -88,11 +88,11 @@ class KexDH: # pragma: nocover
self.__ca_key_type = ''
self.__ca_n_len = 0
packet_type, payload = s.read_packet(2)
packet_type, payload = s.read_packet()
# Skip any & all MSG_DEBUG messages.
while packet_type == Protocol.MSG_DEBUG:
packet_type, payload = s.read_packet(2)
packet_type, payload = s.read_packet()
if packet_type != -1 and packet_type not in [Protocol.MSG_KEXDH_REPLY, Protocol.MSG_KEXDH_GEX_REPLY]: # pylint: disable=no-else-raise
raise KexDHException('Expected MSG_KEXDH_REPLY (%d) or MSG_KEXDH_GEX_REPLY (%d), but got %d instead.' % (Protocol.MSG_KEXDH_REPLY, Protocol.MSG_KEXDH_GEX_REPLY, packet_type))
@@ -380,13 +380,13 @@ class KexGroupExchange(KexDH):
s.write_int(maxbits)
s.send_packet()
packet_type, payload = s.read_packet(2)
packet_type, payload = s.read_packet()
if packet_type not in [Protocol.MSG_KEXDH_GEX_GROUP, Protocol.MSG_DEBUG]:
raise KexDHException('Expected MSG_KEXDH_GEX_REPLY (%d), but got %d instead.' % (Protocol.MSG_KEXDH_GEX_REPLY, packet_type))
# Skip any & all MSG_DEBUG messages.
while packet_type == Protocol.MSG_DEBUG:
packet_type, payload = s.read_packet(2)
packet_type, payload = s.read_packet()
try:
# Parse the modulus (p) and generator (g) values from the server.
+13 -11
View File
@@ -54,11 +54,11 @@ class OutputBuffer:
self.__is_color_supported = ('colorama' in sys.modules) or (os.name == 'posix')
self.line_ended = True
def _print(self, level: str, s: str = '', line_ended: bool = True) -> None:
def _print(self, level: str, s: str = '', line_ended: bool = True, always_print: bool = False) -> None:
'''Saves output to buffer (if in buffered mode), or immediately prints to stdout otherwise.'''
# If we're logging only 'warn' or above, and this is an 'info', ignore message.
if self.get_level(level) < self.__level:
# If we're logging only 'warn' or above, and this is an 'info', ignore message, unless always_print is True (useful for printing informational lines regardless of the level setting).
if (always_print is False) and (self.get_level(level) < self.__level):
return
if self.use_colors and self.colors_supported and len(s) > 0 and level != 'info':
@@ -145,20 +145,22 @@ class OutputBuffer:
self._print('head', s, line_ended)
return self
def fail(self, s: str, line_ended: bool = True) -> 'OutputBuffer':
self._print('fail', s, line_ended)
def fail(self, s: str, line_ended: bool = True, write_now: bool = False, always_print: bool = False) -> 'OutputBuffer':
self._print('fail', s, line_ended, always_print=always_print)
if write_now:
self.write()
return self
def warn(self, s: str, line_ended: bool = True) -> 'OutputBuffer':
self._print('warn', s, line_ended)
def warn(self, s: str, line_ended: bool = True, always_print: bool = False) -> 'OutputBuffer':
self._print('warn', s, line_ended, always_print=always_print)
return self
def info(self, s: str, line_ended: bool = True) -> 'OutputBuffer':
self._print('info', s, line_ended)
def info(self, s: str, line_ended: bool = True, always_print: bool = False) -> 'OutputBuffer':
self._print('info', s, line_ended, always_print=always_print)
return self
def good(self, s: str, line_ended: bool = True) -> 'OutputBuffer':
self._print('good', s, line_ended)
def good(self, s: str, line_ended: bool = True, always_print: bool = False) -> 'OutputBuffer':
self._print('good', s, line_ended, always_print=always_print)
return self
def sep(self) -> 'OutputBuffer':
-40
View File
@@ -1,40 +0,0 @@
"""
The MIT License (MIT)
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
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.
"""
# pylint: disable=unused-import
from typing import Dict, List, Set, Sequence, Tuple, Iterable # noqa: F401
from typing import Callable, Optional, Union, Any # noqa: F401
from ssh_audit.ssh1_crc32 import SSH1_CRC32
class SSH1:
_crc32: Optional[SSH1_CRC32] = None
CIPHERS = ['none', 'idea', 'des', '3des', 'tss', 'rc4', 'blowfish']
AUTHS = ['none', 'rhosts', 'rsa', 'password', 'rhosts_rsa', 'tis', 'kerberos']
@classmethod
def crc32(cls, v: bytes) -> int:
if cls._crc32 is None:
cls._crc32 = SSH1_CRC32()
return cls._crc32.calc(v)
-47
View File
@@ -1,47 +0,0 @@
"""
The MIT License (MIT)
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
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.
"""
# pylint: disable=unused-import
from typing import Dict, List, Set, Sequence, Tuple, Iterable # noqa: F401
from typing import Callable, Optional, Union, Any # noqa: F401
class SSH1_CRC32:
def __init__(self) -> None:
self._table = [0] * 256
for i in range(256):
crc = 0
n = i
for _ in range(8):
x = (crc ^ n) & 1
crc = (crc >> 1) ^ (x * 0xedb88320)
n = n >> 1
self._table[i] = crc
def calc(self, v: bytes) -> int:
crc, length = 0, len(v)
for i in range(length):
n = ord(v[i:i + 1])
n = n ^ (crc & 0xff)
crc = (crc >> 8) ^ self._table[n]
return crc
-84
View File
@@ -1,84 +0,0 @@
"""
The MIT License (MIT)
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
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.
"""
# pylint: disable=unused-import
import copy
import threading
from typing import Dict, List, Set, Sequence, Tuple, Iterable # noqa: F401
from typing import Callable, Optional, Union, Any # noqa: F401
class SSH1_KexDB: # pylint: disable=too-few-public-methods
FAIL_PLAINTEXT = 'no encryption/integrity'
FAIL_OPENSSH37_REMOVE = 'removed since OpenSSH 3.7'
FAIL_NA_BROKEN = 'not implemented in OpenSSH, broken algorithm'
FAIL_NA_UNSAFE = 'not implemented in OpenSSH (server), unsafe algorithm'
TEXT_CIPHER_IDEA = 'cipher used by commercial SSH'
DB_PER_THREAD: Dict[int, Dict[str, Dict[str, List[List[Optional[str]]]]]] = {}
MASTER_DB: Dict[str, Dict[str, List[List[Optional[str]]]]] = {
'key': {
'ssh-rsa1': [['1.2.2']],
},
'enc': {
'none': [['1.2.2'], [FAIL_PLAINTEXT]],
'idea': [[None], [], [], [TEXT_CIPHER_IDEA]],
'des': [['2.3.0C'], [FAIL_NA_UNSAFE]],
'3des': [['1.2.2']],
'tss': [[''], [FAIL_NA_BROKEN]],
'rc4': [[], [FAIL_NA_BROKEN]],
'blowfish': [['1.2.2']],
},
'aut': {
'rhosts': [['1.2.2', '3.6'], [FAIL_OPENSSH37_REMOVE]],
'rsa': [['1.2.2']],
'password': [['1.2.2']],
'rhosts_rsa': [['1.2.2']],
'tis': [['1.2.2']],
'kerberos': [['1.2.2', '3.6'], [FAIL_OPENSSH37_REMOVE]],
}
}
@staticmethod
def get_db() -> Dict[str, Dict[str, List[List[Optional[str]]]]]:
'''Returns a copy of the MASTER_DB that is private to the calling thread. This prevents multiple threads from polluting the results of other threads.'''
calling_thread_id = threading.get_ident()
if calling_thread_id not in SSH1_KexDB.DB_PER_THREAD:
SSH1_KexDB.DB_PER_THREAD[calling_thread_id] = copy.deepcopy(SSH1_KexDB.MASTER_DB)
return SSH1_KexDB.DB_PER_THREAD[calling_thread_id]
@staticmethod
def thread_exit() -> None:
'''Deletes the calling thread's copy of the MASTER_DB. This is needed because, in rare circumstances, a terminated thread's ID can be re-used by new threads.'''
calling_thread_id = threading.get_ident()
if calling_thread_id in SSH1_KexDB.DB_PER_THREAD:
del SSH1_KexDB.DB_PER_THREAD[calling_thread_id]
-144
View File
@@ -1,144 +0,0 @@
"""
The MIT License (MIT)
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
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.
"""
# pylint: disable=unused-import
from typing import Dict, List, Set, Sequence, Tuple, Iterable # noqa: F401
from typing import Callable, Optional, Union, Any # noqa: F401
from ssh_audit.ssh1 import SSH1
from ssh_audit.readbuf import ReadBuf
from ssh_audit.utils import Utils
from ssh_audit.writebuf import WriteBuf
class SSH1_PublicKeyMessage:
def __init__(self, cookie: bytes, skey: Tuple[int, int, int], hkey: Tuple[int, int, int], pflags: int, cmask: int, amask: int) -> None:
if len(skey) != 3:
raise ValueError('invalid server key pair: {}'.format(skey))
if len(hkey) != 3:
raise ValueError('invalid host key pair: {}'.format(hkey))
self.__cookie = cookie
self.__server_key = skey
self.__host_key = hkey
self.__protocol_flags = pflags
self.__supported_ciphers_mask = cmask
self.__supported_authentications_mask = amask
@property
def cookie(self) -> bytes:
return self.__cookie
@property
def server_key_bits(self) -> int:
return self.__server_key[0]
@property
def server_key_public_exponent(self) -> int:
return self.__server_key[1]
@property
def server_key_public_modulus(self) -> int:
return self.__server_key[2]
@property
def host_key_bits(self) -> int:
return self.__host_key[0]
@property
def host_key_public_exponent(self) -> int:
return self.__host_key[1]
@property
def host_key_public_modulus(self) -> int:
return self.__host_key[2]
@property
def host_key_fingerprint_data(self) -> bytes:
# pylint: disable=protected-access
mod = WriteBuf._create_mpint(self.host_key_public_modulus, False)
e = WriteBuf._create_mpint(self.host_key_public_exponent, False)
return mod + e
@property
def protocol_flags(self) -> int:
return self.__protocol_flags
@property
def supported_ciphers_mask(self) -> int:
return self.__supported_ciphers_mask
@property
def supported_ciphers(self) -> List[str]:
ciphers = []
for i in range(len(SSH1.CIPHERS)): # pylint: disable=consider-using-enumerate
if self.__supported_ciphers_mask & (1 << i) != 0:
ciphers.append(Utils.to_text(SSH1.CIPHERS[i]))
return ciphers
@property
def supported_authentications_mask(self) -> int:
return self.__supported_authentications_mask
@property
def supported_authentications(self) -> List[str]:
auths = []
for i in range(1, len(SSH1.AUTHS)):
if self.__supported_authentications_mask & (1 << i) != 0:
auths.append(Utils.to_text(SSH1.AUTHS[i]))
return auths
def write(self, wbuf: 'WriteBuf') -> None:
wbuf.write(self.cookie)
wbuf.write_int(self.server_key_bits)
wbuf.write_mpint1(self.server_key_public_exponent)
wbuf.write_mpint1(self.server_key_public_modulus)
wbuf.write_int(self.host_key_bits)
wbuf.write_mpint1(self.host_key_public_exponent)
wbuf.write_mpint1(self.host_key_public_modulus)
wbuf.write_int(self.protocol_flags)
wbuf.write_int(self.supported_ciphers_mask)
wbuf.write_int(self.supported_authentications_mask)
@property
def payload(self) -> bytes:
wbuf = WriteBuf()
self.write(wbuf)
return wbuf.write_flush()
@classmethod
def parse(cls, payload: bytes) -> 'SSH1_PublicKeyMessage':
buf = ReadBuf(payload)
cookie = buf.read(8)
server_key_bits = buf.read_int()
server_key_exponent = buf.read_mpint1()
server_key_modulus = buf.read_mpint1()
skey = (server_key_bits, server_key_exponent, server_key_modulus)
host_key_bits = buf.read_int()
host_key_exponent = buf.read_mpint1()
host_key_modulus = buf.read_mpint1()
hkey = (host_key_bits, host_key_exponent, host_key_modulus)
pflags = buf.read_int()
cmask = buf.read_int()
amask = buf.read_int()
pkm = cls(cookie, skey, hkey, pflags, cmask, amask)
return pkm
+109 -102
View File
@@ -1,7 +1,7 @@
"""
The MIT License (MIT)
Copyright (C) 2017-2024 Joe Testa (jtesta@positronsecurity.com)
Copyright (C) 2017-2025 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
@@ -57,6 +57,7 @@ class SSH2_KexDB: # pylint: disable=too-few-public-methods
WARN_CIPHER_MODE = 'using weak cipher mode'
WARN_ENCRYPT_AND_MAC = 'using encrypt-and-MAC mode'
WARN_EXPERIMENTAL = 'using experimental algorithm'
WARN_NOT_PQ_SAFE = 'does not provide protection against post-quantum attacks'
WARN_RNDSIG_KEY = 'using weak random number generator could reveal the key'
WARN_TAG_SIZE = 'using small 64-bit tag size'
WARN_TAG_SIZE_96 = 'using small 96-bit tag size'
@@ -65,12 +66,14 @@ class SSH2_KexDB: # pylint: disable=too-few-public-methods
INFO_DEFAULT_OPENSSH_KEX_65_TO_73 = 'default key exchange from OpenSSH 6.5 to 7.3'
INFO_DEFAULT_OPENSSH_KEX_74_TO_89 = 'default key exchange from OpenSSH 7.4 to 8.9'
INFO_DEFAULT_OPENSSH_KEX_90_TO_98 = 'default key exchange from OpenSSH 9.0 to 9.8'
INFO_DEFAULT_OPENSSH_KEX_99 = 'default key exchange since OpenSSH 9.9'
INFO_DEFAULT_OPENSSH_KEX_99 = 'default key exchange in OpenSSH 9.9'
INFO_DEFAULT_OPENSSH_KEX_100 = 'default key exchange since OpenSSH 10.0'
INFO_DEPRECATED_IN_OPENSSH88 = 'deprecated in OpenSSH 8.8: https://www.openssh.com/txt/release-8.8'
INFO_DISABLED_IN_DBEAR67 = 'disabled in Dropbear SSH 2015.67'
INFO_DISABLED_IN_OPENSSH70 = 'disabled in OpenSSH 7.0: https://www.openssh.com/txt/release-7.0'
INFO_NEVER_IMPLEMENTED_IN_OPENSSH = 'despite the @openssh.com tag, this was never implemented in OpenSSH'
INFO_HYBRID_PQ_X25519_KEX = 'hybrid key exchange based on post-quantum resistant algorithm and proven conventional X25519 algorithm'
INFO_HYBRID_PQ_NISTP_KEX = 'hybrid key exchange based on post-quantum resistant algorithm and a suspected back-doored NIST P-curve'
INFO_REMOVED_IN_OPENSSH61 = 'removed since OpenSSH 6.1, removed from specification'
INFO_REMOVED_IN_OPENSSH69 = 'removed in OpenSSH 6.9: https://www.openssh.com/txt/release-6.9'
INFO_REMOVED_IN_OPENSSH70 = 'removed in OpenSSH 7.0: https://www.openssh.com/txt/release-7.0'
@@ -84,117 +87,119 @@ class SSH2_KexDB: # pylint: disable=too-few-public-methods
MASTER_DB: Dict[str, Dict[str, List[List[Optional[str]]]]] = {
# Format: 'algorithm_name': [['version_first_appeared_in'], [reason_for_failure1, reason_for_failure2, ...], [warning1, warning2, ...], [info1, info2, ...]]
'kex': {
'Curve25519SHA256': [[]],
'curve25519-sha256': [['7.4,d2018.76'], [], [], [INFO_DEFAULT_OPENSSH_KEX_74_TO_89]],
'curve25519-sha256@libssh.org': [['6.4,d2013.62,l10.6.0'], [], [], [INFO_DEFAULT_OPENSSH_KEX_65_TO_73]],
'curve448-sha512': [[]],
'curve448-sha512@libssh.org': [[]],
'diffie-hellman-group14-sha1': [['3.9,d0.53,l10.6.0'], [FAIL_SHA1], [WARN_2048BIT_MODULUS]],
'diffie-hellman-group14-sha224@ssh.com': [[]],
'diffie-hellman-group14-sha256': [['7.3,d2016.73'], [], [WARN_2048BIT_MODULUS]],
'diffie-hellman-group14-sha256@ssh.com': [[], [], [WARN_2048BIT_MODULUS]],
'diffie-hellman-group15-sha256': [[]],
'diffie-hellman-group15-sha256@ssh.com': [[]],
'diffie-hellman-group15-sha384@ssh.com': [[]],
'diffie-hellman-group15-sha512': [[]],
'diffie-hellman-group16-sha256': [[]],
'diffie-hellman-group16-sha384@ssh.com': [[]],
'diffie-hellman-group16-sha512': [['7.3,d2016.73']],
'diffie-hellman-group16-sha512@ssh.com': [[]],
'diffie-hellman-group17-sha512': [[]],
'diffie-hellman_group17-sha512': [[]],
'diffie-hellman-group18-sha512': [['7.3']],
'diffie-hellman-group18-sha512@ssh.com': [[]],
'diffie-hellman-group1-sha1': [['2.3.0,d0.28,l10.2', '6.6', '6.9'], [FAIL_1024BIT_MODULUS, FAIL_LOGJAM_ATTACK, FAIL_SHA1], [], [INFO_REMOVED_IN_OPENSSH69]],
'diffie-hellman-group1-sha256': [[], [FAIL_1024BIT_MODULUS]],
'diffie-hellman-group-exchange-sha1': [['2.3.0', '6.6', None], [FAIL_SHA1]],
'diffie-hellman-group-exchange-sha224@ssh.com': [[]],
'diffie-hellman-group-exchange-sha256': [['4.4']],
'diffie-hellman-group-exchange-sha256@ssh.com': [[]],
'diffie-hellman-group-exchange-sha384@ssh.com': [[]],
'diffie-hellman-group-exchange-sha512@ssh.com': [[]],
'Curve25519SHA256': [[], [], [WARN_NOT_PQ_SAFE]],
'curve25519-sha256': [['7.4,d2018.76'], [], [WARN_NOT_PQ_SAFE], [INFO_DEFAULT_OPENSSH_KEX_74_TO_89]],
'curve25519-sha256@libssh.org': [['6.4,d2013.62,l10.6.0'], [], [WARN_NOT_PQ_SAFE], [INFO_DEFAULT_OPENSSH_KEX_65_TO_73]],
'curve448-sha512': [[], [], [WARN_NOT_PQ_SAFE]],
'curve448-sha512@libssh.org': [[], [], [WARN_NOT_PQ_SAFE]],
'diffie-hellman-group14-sha1': [['3.9,d0.53,l10.6.0'], [FAIL_SHA1], [WARN_2048BIT_MODULUS, WARN_NOT_PQ_SAFE]],
'diffie-hellman-group14-sha224@ssh.com': [[], [], [WARN_2048BIT_MODULUS, WARN_NOT_PQ_SAFE]],
'diffie-hellman-group14-sha256': [['7.3,d2016.73'], [], [WARN_2048BIT_MODULUS, WARN_NOT_PQ_SAFE]],
'diffie-hellman-group14-sha256@ssh.com': [[], [], [WARN_2048BIT_MODULUS, WARN_NOT_PQ_SAFE]],
'diffie-hellman-group15-sha256': [[], [], [WARN_NOT_PQ_SAFE]],
'diffie-hellman-group15-sha256@ssh.com': [[], [], [WARN_NOT_PQ_SAFE]],
'diffie-hellman-group15-sha384@ssh.com': [[], [], [WARN_NOT_PQ_SAFE]],
'diffie-hellman-group15-sha512': [[], [], [WARN_NOT_PQ_SAFE]],
'diffie-hellman-group16-sha256': [[], [], [WARN_NOT_PQ_SAFE]],
'diffie-hellman-group16-sha384@ssh.com': [[], [], [WARN_NOT_PQ_SAFE]],
'diffie-hellman-group16-sha512': [['7.3,d2016.73'], [], [WARN_NOT_PQ_SAFE]],
'diffie-hellman-group16-sha512@ssh.com': [[], [], [WARN_NOT_PQ_SAFE]],
'diffie-hellman-group17-sha512': [[], [], [WARN_NOT_PQ_SAFE]],
'diffie-hellman_group17-sha512': [[], [], [WARN_NOT_PQ_SAFE]],
'diffie-hellman-group18-sha512': [['7.3'], [], [WARN_NOT_PQ_SAFE]],
'diffie-hellman-group18-sha512@ssh.com': [[], [], [WARN_NOT_PQ_SAFE]],
'diffie-hellman-group1-sha1': [['2.3.0,d0.28,l10.2', '6.6', '6.9'], [FAIL_1024BIT_MODULUS, FAIL_LOGJAM_ATTACK, FAIL_SHA1], [WARN_NOT_PQ_SAFE], [INFO_REMOVED_IN_OPENSSH69]],
'diffie-hellman-group1-sha256': [[], [FAIL_1024BIT_MODULUS], [WARN_NOT_PQ_SAFE]],
'diffie-hellman-group-exchange-sha1': [['2.3.0', '6.6', None], [FAIL_SHA1], [WARN_NOT_PQ_SAFE]],
'diffie-hellman-group-exchange-sha224@ssh.com': [[], [], [WARN_NOT_PQ_SAFE]],
'diffie-hellman-group-exchange-sha256': [['4.4'], [], [WARN_NOT_PQ_SAFE]],
'diffie-hellman-group-exchange-sha256@ssh.com': [[], [], [WARN_NOT_PQ_SAFE]],
'diffie-hellman-group-exchange-sha384@ssh.com': [[], [], [WARN_NOT_PQ_SAFE]],
'diffie-hellman-group-exchange-sha512@ssh.com': [[], [], [WARN_NOT_PQ_SAFE]],
'ecdh-nistp256-kyber-512r3-sha256-d00@openquantumsafe.org': [[], [FAIL_NSA_BACKDOORED_CURVE]],
'ecdh-nistp384-kyber-768r3-sha384-d00@openquantumsafe.org': [[], [FAIL_NSA_BACKDOORED_CURVE]],
'ecdh-nistp521-kyber-1024r3-sha512-d00@openquantumsafe.org': [[], [FAIL_NSA_BACKDOORED_CURVE]],
'ecdh-sha2-1.2.840.10045.3.1.1': [[], [FAIL_SMALL_ECC_MODULUS, FAIL_NSA_BACKDOORED_CURVE]], # NIST P-192 / secp192r1
'ecdh-sha2-1.2.840.10045.3.1.7': [[], [FAIL_NSA_BACKDOORED_CURVE]], # NIST P-256 / secp256r1
'ecdh-sha2-1.3.132.0.10': [[]], # ECDH over secp256k1 (i.e.: the Bitcoin curve)
'ecdh-sha2-1.3.132.0.16': [[], [FAIL_UNPROVEN]], # sect283k1
'ecdh-sha2-1.3.132.0.1': [[], [FAIL_UNPROVEN, FAIL_SMALL_ECC_MODULUS]], # sect163k1
'ecdh-sha2-1.3.132.0.26': [[], [FAIL_UNPROVEN, FAIL_SMALL_ECC_MODULUS]], # sect233k1
'ecdh-sha2-1.3.132.0.27': [[], [FAIL_SMALL_ECC_MODULUS, FAIL_NSA_BACKDOORED_CURVE]], # sect233r1
'ecdh-sha2-1.3.132.0.33': [[], [FAIL_SMALL_ECC_MODULUS, FAIL_NSA_BACKDOORED_CURVE]], # NIST P-224 / secp224r1
'ecdh-sha2-1.3.132.0.34': [[], [FAIL_NSA_BACKDOORED_CURVE]], # NIST P-384 / secp384r1
'ecdh-sha2-1.3.132.0.35': [[], [FAIL_NSA_BACKDOORED_CURVE]], # NIST P-521 / secp521r1
'ecdh-sha2-1.3.132.0.36': [[], [FAIL_UNPROVEN]], # sect409k1
'ecdh-sha2-1.3.132.0.37': [[], [FAIL_NSA_BACKDOORED_CURVE]], # sect409r1
'ecdh-sha2-1.3.132.0.38': [[], [FAIL_UNPROVEN]], # sect571k1
'ecdh-sha2-1.2.840.10045.3.1.1': [[], [FAIL_SMALL_ECC_MODULUS, FAIL_NSA_BACKDOORED_CURVE], [WARN_NOT_PQ_SAFE]], # NIST P-192 / secp192r1
'ecdh-sha2-1.2.840.10045.3.1.7': [[], [FAIL_NSA_BACKDOORED_CURVE], [WARN_NOT_PQ_SAFE]], # NIST P-256 / secp256r1
'ecdh-sha2-1.3.132.0.10': [[], [], [WARN_NOT_PQ_SAFE]], # ECDH over secp256k1 (i.e.: the Bitcoin curve)
'ecdh-sha2-1.3.132.0.16': [[], [FAIL_UNPROVEN], [WARN_NOT_PQ_SAFE]], # sect283k1
'ecdh-sha2-1.3.132.0.1': [[], [FAIL_UNPROVEN, FAIL_SMALL_ECC_MODULUS], [WARN_NOT_PQ_SAFE]], # sect163k1
'ecdh-sha2-1.3.132.0.26': [[], [FAIL_UNPROVEN, FAIL_SMALL_ECC_MODULUS], [WARN_NOT_PQ_SAFE]], # sect233k1
'ecdh-sha2-1.3.132.0.27': [[], [FAIL_SMALL_ECC_MODULUS, FAIL_NSA_BACKDOORED_CURVE], [WARN_NOT_PQ_SAFE]], # sect233r1
'ecdh-sha2-1.3.132.0.33': [[], [FAIL_SMALL_ECC_MODULUS, FAIL_NSA_BACKDOORED_CURVE], [WARN_NOT_PQ_SAFE]], # NIST P-224 / secp224r1
'ecdh-sha2-1.3.132.0.34': [[], [FAIL_NSA_BACKDOORED_CURVE], [WARN_NOT_PQ_SAFE]], # NIST P-384 / secp384r1
'ecdh-sha2-1.3.132.0.35': [[], [FAIL_NSA_BACKDOORED_CURVE], [WARN_NOT_PQ_SAFE]], # NIST P-521 / secp521r1
'ecdh-sha2-1.3.132.0.36': [[], [FAIL_UNPROVEN], [WARN_NOT_PQ_SAFE]], # sect409k1
'ecdh-sha2-1.3.132.0.37': [[], [FAIL_NSA_BACKDOORED_CURVE], [WARN_NOT_PQ_SAFE]], # sect409r1
'ecdh-sha2-1.3.132.0.38': [[], [FAIL_UNPROVEN], [WARN_NOT_PQ_SAFE]], # sect571k1
# Note: the base64 strings, according to draft 6 of RFC5656, is Base64(MD5(DER(OID))). The final RFC5656 dropped the base64 strings in favor of plain OID concatenation, but apparently some SSH servers implement them anyway. See: https://datatracker.ietf.org/doc/html/draft-green-secsh-ecc-06#section-9.2
'ecdh-sha2-4MHB+NBt3AlaSRQ7MnB4cg==': [[], [FAIL_UNPROVEN, FAIL_SMALL_ECC_MODULUS]], # sect163k1
'ecdh-sha2-5pPrSUQtIaTjUSt5VZNBjg==': [[], [FAIL_SMALL_ECC_MODULUS, FAIL_NSA_BACKDOORED_CURVE]], # NIST P-192 / secp192r1
'ecdh-sha2-9UzNcgwTlEnSCECZa7V1mw==': [[], [FAIL_NSA_BACKDOORED_CURVE]], # NIST P-256 / secp256r1
'ecdh-sha2-brainpoolp256r1@genua.de': [[], [FAIL_UNPROVEN]],
'ecdh-sha2-brainpoolp384r1@genua.de': [[], [FAIL_UNPROVEN]],
'ecdh-sha2-brainpoolp521r1@genua.de': [[], [FAIL_UNPROVEN]],
'ecdh-sha2-curve25519': [[], []],
'ecdh-sha2-D3FefCjYoJ/kfXgAyLddYA==': [[], [FAIL_NSA_BACKDOORED_CURVE]], # sect409r1
'ecdh-sha2-h/SsxnLCtRBh7I9ATyeB3A==': [[], [FAIL_NSA_BACKDOORED_CURVE]], # NIST P-521 / secp521r1
'ecdh-sha2-m/FtSAmrV4j/Wy6RVUaK7A==': [[], [FAIL_UNPROVEN]], # sect409k1
'ecdh-sha2-mNVwCXAoS1HGmHpLvBC94w==': [[], [FAIL_UNPROVEN]], # sect571k1
'ecdh-sha2-nistb233': [[], [FAIL_UNPROVEN, FAIL_SMALL_ECC_MODULUS]],
'ecdh-sha2-nistb409': [[], [FAIL_UNPROVEN]],
'ecdh-sha2-nistk163': [[], [FAIL_UNPROVEN, FAIL_SMALL_ECC_MODULUS]],
'ecdh-sha2-nistk233': [[], [FAIL_UNPROVEN, FAIL_SMALL_ECC_MODULUS]],
'ecdh-sha2-nistk283': [[], [FAIL_UNPROVEN]],
'ecdh-sha2-nistk409': [[], [FAIL_UNPROVEN]],
'ecdh-sha2-nistp192': [[], [FAIL_NSA_BACKDOORED_CURVE]],
'ecdh-sha2-nistp224': [[], [FAIL_NSA_BACKDOORED_CURVE]],
'ecdh-sha2-nistp256': [['5.7,d2013.62,l10.6.0'], [FAIL_NSA_BACKDOORED_CURVE]],
'ecdh-sha2-nistp384': [['5.7,d2013.62'], [FAIL_NSA_BACKDOORED_CURVE]],
'ecdh-sha2-nistp521': [['5.7,d2013.62'], [FAIL_NSA_BACKDOORED_CURVE]],
'ecdh-sha2-nistt571': [[], [FAIL_UNPROVEN]],
'ecdh-sha2-qCbG5Cn/jjsZ7nBeR7EnOA==': [[FAIL_SMALL_ECC_MODULUS, FAIL_NSA_BACKDOORED_CURVE]], # sect233r1
'ecdh-sha2-qcFQaMAMGhTziMT0z+Tuzw==': [[], [FAIL_NSA_BACKDOORED_CURVE]], # NIST P-384 / secp384r1
'ecdh-sha2-VqBg4QRPjxx1EXZdV0GdWQ==': [[], [FAIL_NSA_BACKDOORED_CURVE, FAIL_SMALL_ECC_MODULUS]], # NIST P-224 / secp224r1
'ecdh-sha2-wiRIU8TKjMZ418sMqlqtvQ==': [[], [FAIL_UNPROVEN]], # sect283k1
'ecdh-sha2-zD/b3hu/71952ArpUG4OjQ==': [[], [FAIL_UNPROVEN, FAIL_SMALL_ECC_MODULUS]], # sect233k1
'ecmqv-sha2': [[], [FAIL_UNPROVEN]],
'ecdh-sha2-4MHB+NBt3AlaSRQ7MnB4cg==': [[], [FAIL_UNPROVEN, FAIL_SMALL_ECC_MODULUS], [WARN_NOT_PQ_SAFE]], # sect163k1
'ecdh-sha2-5pPrSUQtIaTjUSt5VZNBjg==': [[], [FAIL_SMALL_ECC_MODULUS, FAIL_NSA_BACKDOORED_CURVE], [WARN_NOT_PQ_SAFE]], # NIST P-192 / secp192r1
'ecdh-sha2-9UzNcgwTlEnSCECZa7V1mw==': [[], [FAIL_NSA_BACKDOORED_CURVE], [WARN_NOT_PQ_SAFE]], # NIST P-256 / secp256r1
'ecdh-sha2-brainpoolp256r1@genua.de': [[], [FAIL_UNPROVEN], [WARN_NOT_PQ_SAFE]],
'ecdh-sha2-brainpoolp384r1@genua.de': [[], [FAIL_UNPROVEN], [WARN_NOT_PQ_SAFE]],
'ecdh-sha2-brainpoolp521r1@genua.de': [[], [FAIL_UNPROVEN], [WARN_NOT_PQ_SAFE]],
'ecdh-sha2-curve25519': [[], [], [WARN_NOT_PQ_SAFE]],
'ecdh-sha2-D3FefCjYoJ/kfXgAyLddYA==': [[], [FAIL_NSA_BACKDOORED_CURVE], [WARN_NOT_PQ_SAFE]], # sect409r1
'ecdh-sha2-h/SsxnLCtRBh7I9ATyeB3A==': [[], [FAIL_NSA_BACKDOORED_CURVE], [WARN_NOT_PQ_SAFE]], # NIST P-521 / secp521r1
'ecdh-sha2-m/FtSAmrV4j/Wy6RVUaK7A==': [[], [FAIL_UNPROVEN], [WARN_NOT_PQ_SAFE]], # sect409k1
'ecdh-sha2-mNVwCXAoS1HGmHpLvBC94w==': [[], [FAIL_UNPROVEN], [WARN_NOT_PQ_SAFE]], # sect571k1
'ecdh-sha2-nistb233': [[], [FAIL_UNPROVEN, FAIL_SMALL_ECC_MODULUS], [WARN_NOT_PQ_SAFE]],
'ecdh-sha2-nistb409': [[], [FAIL_UNPROVEN], [WARN_NOT_PQ_SAFE]],
'ecdh-sha2-nistk163': [[], [FAIL_UNPROVEN, FAIL_SMALL_ECC_MODULUS], [WARN_NOT_PQ_SAFE]],
'ecdh-sha2-nistk233': [[], [FAIL_UNPROVEN, FAIL_SMALL_ECC_MODULUS], [WARN_NOT_PQ_SAFE]],
'ecdh-sha2-nistk283': [[], [FAIL_UNPROVEN], [WARN_NOT_PQ_SAFE]],
'ecdh-sha2-nistk409': [[], [FAIL_UNPROVEN], [WARN_NOT_PQ_SAFE]],
'ecdh-sha2-nistp192': [[], [FAIL_NSA_BACKDOORED_CURVE], [WARN_NOT_PQ_SAFE]],
'ecdh-sha2-nistp224': [[], [FAIL_NSA_BACKDOORED_CURVE], [WARN_NOT_PQ_SAFE]],
'ecdh-sha2-nistp256': [['5.7,d2013.62,l10.6.0'], [FAIL_NSA_BACKDOORED_CURVE], [WARN_NOT_PQ_SAFE]],
'ecdh-sha2-nistp384': [['5.7,d2013.62'], [FAIL_NSA_BACKDOORED_CURVE], [WARN_NOT_PQ_SAFE]],
'ecdh-sha2-nistp521': [['5.7,d2013.62'], [FAIL_NSA_BACKDOORED_CURVE], [WARN_NOT_PQ_SAFE]],
'ecdh-sha2-nistt571': [[], [FAIL_UNPROVEN], [WARN_NOT_PQ_SAFE]],
'ecdh-sha2-qCbG5Cn/jjsZ7nBeR7EnOA==': [[], [FAIL_SMALL_ECC_MODULUS, FAIL_NSA_BACKDOORED_CURVE], [WARN_NOT_PQ_SAFE]], # sect233r1
'ecdh-sha2-qcFQaMAMGhTziMT0z+Tuzw==': [[], [FAIL_NSA_BACKDOORED_CURVE], [WARN_NOT_PQ_SAFE]], # NIST P-384 / secp384r1
'ecdh-sha2-VqBg4QRPjxx1EXZdV0GdWQ==': [[], [FAIL_NSA_BACKDOORED_CURVE, FAIL_SMALL_ECC_MODULUS], [WARN_NOT_PQ_SAFE]], # NIST P-224 / secp224r1
'ecdh-sha2-wiRIU8TKjMZ418sMqlqtvQ==': [[], [FAIL_UNPROVEN], [WARN_NOT_PQ_SAFE]], # sect283k1
'ecdh-sha2-zD/b3hu/71952ArpUG4OjQ==': [[], [FAIL_UNPROVEN, FAIL_SMALL_ECC_MODULUS], [WARN_NOT_PQ_SAFE]], # sect233k1
'ecmqv-sha2': [[], [FAIL_UNPROVEN], [WARN_NOT_PQ_SAFE]],
'ext-info-c': [['7.2'], [], [], [INFO_EXTENSION_NEGOTIATION]], # Extension negotiation (RFC 8308)
'ext-info-s': [['9.6'], [], [], [INFO_EXTENSION_NEGOTIATION]], # Extension negotiation (RFC 8308)
'kex-strict-c-v00@openssh.com': [[], [], [], [INFO_STRICT_KEX]], # Strict KEX marker (countermeasure for CVE-2023-48795).
'kex-strict-s-v00@openssh.com': [[], [], [], [INFO_STRICT_KEX]], # Strict KEX marker (countermeasure for CVE-2023-48795).
# The GSS kex algorithms get special wildcard handling, since they include variable base64 data after their standard prefixes.
'gss-13.3.132.0.10-sha256-*': [[], [FAIL_UNKNOWN]],
'gss-curve25519-sha256-*': [[]],
'gss-curve448-sha512-*': [[]],
'gss-gex-sha1-*': [[], [FAIL_SHA1]],
'gss-gex-sha256-*': [[]],
'gss-group14-sha1-*': [[], [FAIL_SHA1], [WARN_2048BIT_MODULUS]],
'gss-group14-sha256-*': [[], [], [WARN_2048BIT_MODULUS]],
'gss-group15-sha512-*': [[]],
'gss-group16-sha512-*': [[]],
'gss-group17-sha512-*': [[]],
'gss-group18-sha512-*': [[]],
'gss-group1-sha1-*': [[], [FAIL_1024BIT_MODULUS, FAIL_LOGJAM_ATTACK, FAIL_SHA1]],
'gss-nistp256-sha256-*': [[], [FAIL_NSA_BACKDOORED_CURVE]],
'gss-nistp384-sha256-*': [[], [FAIL_NSA_BACKDOORED_CURVE]],
'gss-nistp384-sha384-*': [[], [FAIL_NSA_BACKDOORED_CURVE]],
'gss-nistp521-sha512-*': [[], [FAIL_NSA_BACKDOORED_CURVE]],
'kexAlgoCurve25519SHA256': [[]],
'kexAlgoDH14SHA1': [[], [FAIL_SHA1], [WARN_2048BIT_MODULUS]],
'kexAlgoDH1SHA1': [[], [FAIL_1024BIT_MODULUS, FAIL_LOGJAM_ATTACK, FAIL_SHA1]],
'kexAlgoECDH256': [[], [FAIL_NSA_BACKDOORED_CURVE]],
'kexAlgoECDH384': [[], [FAIL_NSA_BACKDOORED_CURVE]],
'kexAlgoECDH521': [[], [FAIL_NSA_BACKDOORED_CURVE]],
'kexguess2@matt.ucc.asn.au': [['d2013.57']],
'm383-sha384@libassh.org': [[], [FAIL_UNPROVEN]],
'm511-sha512@libassh.org': [[], [FAIL_UNPROVEN]],
'mlkem768x25519-sha256': [['9.9'], [], [], [INFO_HYBRID_PQ_X25519_KEX]],
'rsa1024-sha1': [[], [FAIL_1024BIT_MODULUS, FAIL_SHA1]],
'rsa2048-sha256': [[], [], [WARN_2048BIT_MODULUS]],
'sm2kep-sha2-nistp256': [[], [FAIL_NSA_BACKDOORED_CURVE, FAIL_UNTRUSTED]],
'gss-13.3.132.0.10-sha256-*': [[], [FAIL_UNKNOWN], [WARN_NOT_PQ_SAFE]],
'gss-curve25519-sha256-*': [[], [], [WARN_NOT_PQ_SAFE]],
'gss-curve448-sha512-*': [[], [], [WARN_NOT_PQ_SAFE]],
'gss-gex-sha1-*': [[], [FAIL_SHA1], [WARN_NOT_PQ_SAFE]],
'gss-gex-sha256-*': [[], [], [WARN_NOT_PQ_SAFE]],
'gss-group14-sha1-*': [[], [FAIL_SHA1], [WARN_2048BIT_MODULUS, WARN_NOT_PQ_SAFE]],
'gss-group14-sha256-*': [[], [], [WARN_2048BIT_MODULUS, WARN_NOT_PQ_SAFE]],
'gss-group15-sha512-*': [[], [], [WARN_NOT_PQ_SAFE]],
'gss-group16-sha512-*': [[], [], [WARN_NOT_PQ_SAFE]],
'gss-group17-sha512-*': [[], [], [WARN_NOT_PQ_SAFE]],
'gss-group18-sha512-*': [[], [], [WARN_NOT_PQ_SAFE]],
'gss-group1-sha1-*': [[], [FAIL_1024BIT_MODULUS, FAIL_LOGJAM_ATTACK, FAIL_SHA1], [WARN_NOT_PQ_SAFE]],
'gss-nistp256-sha256-*': [[], [FAIL_NSA_BACKDOORED_CURVE], [WARN_NOT_PQ_SAFE]],
'gss-nistp384-sha256-*': [[], [FAIL_NSA_BACKDOORED_CURVE], [WARN_NOT_PQ_SAFE]],
'gss-nistp384-sha384-*': [[], [FAIL_NSA_BACKDOORED_CURVE], [WARN_NOT_PQ_SAFE]],
'gss-nistp521-sha512-*': [[], [FAIL_NSA_BACKDOORED_CURVE], [WARN_NOT_PQ_SAFE]],
'kexAlgoCurve25519SHA256': [[], [], [WARN_NOT_PQ_SAFE]],
'kexAlgoDH14SHA1': [[], [FAIL_SHA1], [WARN_2048BIT_MODULUS, WARN_NOT_PQ_SAFE]],
'kexAlgoDH1SHA1': [[], [FAIL_1024BIT_MODULUS, FAIL_LOGJAM_ATTACK, FAIL_SHA1], [WARN_NOT_PQ_SAFE]],
'kexAlgoECDH256': [[], [FAIL_NSA_BACKDOORED_CURVE], [WARN_NOT_PQ_SAFE]],
'kexAlgoECDH384': [[], [FAIL_NSA_BACKDOORED_CURVE], [WARN_NOT_PQ_SAFE]],
'kexAlgoECDH521': [[], [FAIL_NSA_BACKDOORED_CURVE], [WARN_NOT_PQ_SAFE]],
'kexguess2@matt.ucc.asn.au': [['d2013.57'], [], [WARN_NOT_PQ_SAFE]],
'm383-sha384@libassh.org': [[], [FAIL_UNPROVEN], [WARN_NOT_PQ_SAFE]],
'm511-sha512@libassh.org': [[], [FAIL_UNPROVEN], [WARN_NOT_PQ_SAFE]],
'mlkem768x25519-sha256': [['9.9'], [], [], [INFO_DEFAULT_OPENSSH_KEX_100, INFO_HYBRID_PQ_X25519_KEX]],
'mlkem768nistp256-sha256': [[], [FAIL_NSA_BACKDOORED_CURVE], [], [INFO_HYBRID_PQ_NISTP_KEX]],
'mlkem1024nistp384-sha384': [[], [FAIL_NSA_BACKDOORED_CURVE], [], [INFO_HYBRID_PQ_NISTP_KEX]],
'rsa1024-sha1': [[], [FAIL_1024BIT_MODULUS, FAIL_SHA1], [WARN_NOT_PQ_SAFE]],
'rsa2048-sha256': [[], [], [WARN_2048BIT_MODULUS, WARN_NOT_PQ_SAFE]],
'sm2kep-sha2-nistp256': [[], [FAIL_NSA_BACKDOORED_CURVE, FAIL_UNTRUSTED], [WARN_NOT_PQ_SAFE]],
'sntrup4591761x25519-sha512@tinyssh.org': [['8.0', '8.4'], [], [WARN_EXPERIMENTAL], [INFO_WITHDRAWN_PQ_ALG]],
'sntrup761x25519-sha512': [['9.9'], [], [], [INFO_DEFAULT_OPENSSH_KEX_99, INFO_HYBRID_PQ_X25519_KEX]],
'sntrup761x25519-sha512@openssh.com': [['8.5'], [], [], [INFO_DEFAULT_OPENSSH_KEX_90_TO_98, INFO_HYBRID_PQ_X25519_KEX]],
@@ -297,6 +302,8 @@ class SSH2_KexDB: # pylint: disable=too-few-public-methods
'3des-ofb': [[], [FAIL_3DES], [WARN_CIPHER_MODE]],
'AEAD_AES_128_GCM': [[]],
'AEAD_AES_256_GCM': [[]],
'AEAD_CAMELLIA_128_GCM': [[]],
'AEAD_CAMELLIA_256_GCM': [[]],
'aes128-cbc': [['2.3.0,d0.28,l10.2', '6.6', None], [], [WARN_CIPHER_MODE]],
'aes128-ctr': [['3.7,d0.52,l10.4.1']],
'aes128-gcm': [[]],
+181 -258
View File
@@ -2,7 +2,7 @@
"""
The MIT License (MIT)
Copyright (C) 2017-2024 Joe Testa (jtesta@positronsecurity.com)
Copyright (C) 2017-2025 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
@@ -23,9 +23,9 @@
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""
import argparse
import concurrent.futures
import copy
import getopt # pylint: disable=deprecated-module
import json
import multiprocessing
import os
@@ -33,6 +33,7 @@ import re
import sys
import traceback
# pylint: disable=unused-import
from typing import Dict, List, Set, Sequence, Tuple, Iterable # noqa: F401
from typing import cast, Callable, Optional, Union, Any # noqa: F401
@@ -55,8 +56,6 @@ from ssh_audit.policy import Policy
from ssh_audit.product import Product
from ssh_audit.protocol import Protocol
from ssh_audit.software import Software
from ssh_audit.ssh1_kexdb import SSH1_KexDB
from ssh_audit.ssh1_publickeymessage import SSH1_PublicKeyMessage
from ssh_audit.ssh2_kex import SSH2_Kex
from ssh_audit.ssh2_kexdb import SSH2_KexDB
from ssh_audit.ssh_socket import SSH_Socket
@@ -82,61 +81,6 @@ if sys.platform == 'win32':
# no_idna_workaround = True
def usage(uout: OutputBuffer, err: Optional[str] = None) -> None:
retval = exitcodes.GOOD
p = os.path.basename(sys.argv[0])
uout.head('# {} {}, https://github.com/jtesta/ssh-audit\n'.format(p, VERSION))
if err is not None and len(err) > 0:
uout.fail(err + '\n')
retval = exitcodes.UNKNOWN_ERROR
uout.info('usage: {0} [options] <host>\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')
uout.info(' -4, --ipv4 enable IPv4 (order of precedence)')
uout.info(' -6, --ipv6 enable IPv6 (order of precedence)')
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;\n use -t to change timeout)')
uout.info(' --conn-rate-test=N[:max_rate] perform a connection rate test (useful')
uout.info(' for collecting metrics related to')
uout.info(' susceptibility of the DHEat vuln).')
uout.info(' Testing is conducted with N concurrent')
uout.info(' sockets with an optional maximum rate')
uout.info(' of connections per second.')
uout.info(' -d, --debug debug output')
uout.info(' --dheat=N[:kex[:e_len]] continuously perform the DHEat DoS attack')
uout.info(' (CVE-2002-20001) against the target using N')
uout.info(' concurrent sockets. Optionally, a specific')
uout.info(' key exchange algorithm can be specified')
uout.info(' instead of allowing it to be automatically')
uout.info(' chosen. Additionally, a small length of')
uout.info(' the fake e value sent to the server can')
uout.info(' be chosen for a more efficient attack (such')
uout.info(' as 4).')
uout.info(' -g, --gex-test=<x[,y,...]> dh gex modulus size test')
uout.info(' <min1:pref1:max1[,min2:pref2:max2,...]>')
uout.info(' <x-y[:step]>')
uout.info(' -j, --json JSON output (use -jj to enable indents)')
uout.info(' -l, --level=<level> minimum output level (info|warn|fail)')
uout.info(' -L, --list-policies list all the official, built-in policies. Use with -v')
uout.info(' to view policy change logs.')
uout.info(' --lookup=<alg1,alg2,...> looks up an algorithm(s) without\n connecting to a server')
uout.info(' -M, --make-policy=<policy.txt> creates a policy based on the target server\n (i.e.: the target server has the ideal\n configuration that other servers should\n adhere to)')
uout.info(' -m, --manual print the man page (Windows only)')
uout.info(' -n, --no-colors disable colors (automatic when the NO_COLOR')
uout.info(' environment variable is set)')
uout.info(' -p, --port=<port> port to connect')
uout.info(' -P, --policy=<policy.txt> run a policy test using the specified policy')
uout.info(' --skip-rate-test skip the connection rate test during standard audits\n (used to safely infer whether the DHEat attack\n is viable)')
uout.info(' -t, --timeout=<secs> timeout (in seconds) for connection and reading\n (default: 5)')
uout.info(' -T, --targets=<hosts.txt> a file containing a list of target hosts (one\n per line, format HOST[:PORT]). Use -p/--port\n to set the default port for all hosts. Use\n --threads to control concurrent scans.')
uout.info(' --threads=<threads> number of threads to use when scanning multiple\n targets (-T/--targets) (default: 32)')
uout.info(' -v, --verbose verbose output')
uout.sep()
uout.write()
sys.exit(retval)
def output_algorithms(out: OutputBuffer, title: str, alg_db: Dict[str, Dict[str, List[List[Optional[str]]]]], alg_type: str, algorithms: List[str], unknown_algs: List[str], is_json_output: bool, program_retval: int, maxlen: int = 0, host_keys: Optional[Dict[str, Dict[str, Union[bytes, str, int]]]] = None, dh_modulus_sizes: Optional[Dict[str, int]] = None) -> int: # pylint: disable=too-many-arguments
with out:
for algorithm in algorithms:
@@ -288,11 +232,6 @@ def output_security(out: OutputBuffer, banner: Optional[Banner], padlen: int, is
def output_fingerprints(out: OutputBuffer, algs: Algorithms, is_json_output: bool) -> None:
with out:
fps = {}
if algs.ssh1kex is not None:
name = 'ssh-rsa1'
fp = Fingerprint(algs.ssh1kex.host_key_fingerprint_data)
# bits = algs.ssh1kex.host_key_bits
fps[name] = fp
if algs.ssh2kex is not None:
host_keys = algs.ssh2kex.host_keys()
for host_key_type in algs.ssh2kex.host_keys():
@@ -371,7 +310,7 @@ def output_recommendations(out: OutputBuffer, algs: Algorithms, algorithm_recomm
notes = " (%s)" % notes
fm = '(rec) {0}{1}{2}-- {3} algorithm to {4}{5} '
fn(fm.format(sg, name, p, alg_type, an, notes))
fn(fm.format(sg, name, p, alg_type, an, notes)) # type: ignore[operator]
if not out.is_section_empty() and not is_json_output:
if software is not None:
@@ -563,12 +502,11 @@ def post_process_findings(banner: Optional[Banner], algs: Algorithms, client_aud
# Returns a exitcodes.* flag to denote if any failures or warnings were encountered.
def output(out: OutputBuffer, aconf: AuditConf, banner: Optional[Banner], header: List[str], client_host: Optional[str] = None, kex: Optional[SSH2_Kex] = None, pkm: Optional[SSH1_PublicKeyMessage] = None, print_target: bool = False, dh_rate_test_notes: str = "") -> int:
def output(out: OutputBuffer, aconf: AuditConf, banner: Optional[Banner], header: List[str], client_host: Optional[str] = None, kex: Optional[SSH2_Kex] = None, print_target: bool = False, dh_rate_test_notes: str = "") -> int:
program_retval = exitcodes.GOOD
client_audit = client_host is not None # If set, this is a client audit.
sshv = 1 if pkm is not None else 2
algs = Algorithms(pkm, kex)
algs = Algorithms(kex)
# Perform post-processing on the findings to make final adjustments before outputting the results.
algorithm_recommendation_suppress_list, additional_notes = post_process_findings(banner, algs, client_audit, dh_rate_test_notes)
@@ -586,14 +524,14 @@ def output(out: OutputBuffer, aconf: AuditConf, banner: Optional[Banner], header
else:
host = '%s:%d' % (aconf.host, aconf.port)
out.good('(gen) target: {}'. format(host))
out.good('(gen) target: {}'. format(host), always_print=True)
if client_audit:
out.good('(gen) client IP: {}'.format(client_host))
out.good('(gen) client IP: {}'.format(client_host), always_print=True)
if len(header) > 0:
out.info('(gen) header: ' + '\n'.join(header))
if banner is not None:
banner_line = '(gen) banner: {}'.format(banner)
if sshv == 1 or banner.protocol[0] == 1:
if banner.protocol[0] == 1:
out.fail(banner_line)
out.fail('(gen) protocol SSH1 enabled')
else:
@@ -625,18 +563,6 @@ def output(out: OutputBuffer, aconf: AuditConf, banner: Optional[Banner], header
# Filled in by output_algorithms() with unidentified algs.
unknown_algorithms: List[str] = []
# SSHv1
if pkm is not None:
adb = SSH1_KexDB.get_db()
ciphers = pkm.supported_ciphers
auths = pkm.supported_authentications
title, atype = 'SSH1 host-key algorithms', 'key'
program_retval = output_algorithms(out, title, adb, atype, ['ssh-rsa1'], unknown_algorithms, aconf.json, program_retval, maxlen)
title, atype = 'SSH1 encryption algorithms (ciphers)', 'enc'
program_retval = output_algorithms(out, title, adb, atype, ciphers, unknown_algorithms, aconf.json, program_retval, maxlen)
title, atype = 'SSH1 authentication types', 'aut'
program_retval = output_algorithms(out, title, adb, atype, auths, unknown_algorithms, aconf.json, program_retval, maxlen)
# SSHv2
if kex is not None:
adb = SSH2_KexDB.get_db()
@@ -823,7 +749,7 @@ def make_policy(aconf: AuditConf, banner: Optional['Banner'], kex: Optional['SSH
print(err)
def process_commandline(out: OutputBuffer, args: List[str], usage_cb: Callable[..., None]) -> 'AuditConf': # pylint: disable=too-many-statements
def process_commandline(out: OutputBuffer, args: List[str]) -> 'AuditConf': # pylint: disable=too-many-statements
# pylint: disable=too-many-branches
aconf = AuditConf()
@@ -836,82 +762,87 @@ def process_commandline(out: OutputBuffer, args: List[str], usage_cb: Callable[.
aconf.colors = enable_colors
out.use_colors = enable_colors
try:
sopts = 'h1246M:p:P:jbcnvl:t:T:Lmdg:'
lopts = ['help', 'ssh1', 'ssh2', 'ipv4', 'ipv6', 'make-policy=', 'port=', 'policy=', 'json', 'batch', 'client-audit', 'no-colors', 'verbose', 'level=', 'timeout=', 'targets=', 'list-policies', 'lookup=', 'threads=', 'manual', 'debug', 'gex-test=', 'dheat=', 'skip-rate-test', 'conn-rate-test=']
opts, args = getopt.gnu_getopt(args, sopts, lopts)
except getopt.GetoptError as err:
usage_cb(out, str(err))
aconf.ssh1, aconf.ssh2 = False, False
host: str = ''
oport: Optional[str] = None
port: int = 0
for o, a in opts:
if o in ('-h', '--help'):
usage_cb(out)
elif o in ('-1', '--ssh1'):
aconf.ssh1 = True
elif o in ('-2', '--ssh2'):
aconf.ssh2 = True
elif o in ('-4', '--ipv4'):
aconf.ipv4 = True
elif o in ('-6', '--ipv6'):
aconf.ipv6 = True
elif o in ('-p', '--port'):
oport = a
elif o in ('-b', '--batch'):
aconf.batch = True
aconf.verbose = True
elif o in ('-c', '--client-audit'):
aconf.client_audit = True
elif o in ('-j', '--json'):
if aconf.json: # If specified twice, enable indent printing.
aconf.json_print_indent = True
else:
aconf.json = True
elif o in ('-v', '--verbose'):
aconf.verbose = True
out.verbose = True
elif o in ('-l', '--level'):
if a not in ('info', 'warn', 'fail'):
usage_cb(out, 'level {} is not valid'.format(a))
aconf.level = a
elif o in ('-t', '--timeout'):
aconf.timeout = float(a)
aconf.timeout_set = True
elif o in ('-M', '--make-policy'):
aconf.make_policy = True
aconf.policy_file = a
elif o in ('-P', '--policy'):
aconf.policy_file = a
elif o in ('-T', '--targets'):
aconf.target_file = a
port: int = 22
# If we're on Windows, and we can't use the idna workaround, force only one thread to be used (otherwise a crash would occur).
# if no_idna_workaround:
# print("\nWARNING: the idna module was not found on this system, thus only single-threaded scanning will be done (this is a workaround for this Windows-specific crash: https://github.com/python/cpython/issues/73474). Multi-threaded scanning can be enabled by installing the idna module (pip install idna).\n")
# aconf.threads = 1
elif o == '--threads':
aconf.threads = int(a)
# if no_idna_workaround:
# aconf.threads = 1
elif o in ('-L', '--list-policies'):
aconf.list_policies = True
elif o == '--lookup':
aconf.lookup = a
elif o in ('-m', '--manual'):
aconf.manual = True
elif o in ('-d', '--debug'):
parser = argparse.ArgumentParser(description="# {} {}, https://github.com/jtesta/ssh-audit".format(os.path.basename(sys.argv[0]), VERSION), allow_abbrev=False)
# Add short options to the parser
parser.add_argument("-4", "--ipv4", action="store_true", dest="ipv4", default=False, help="enable IPv4 (order of precedence)")
parser.add_argument("-6", "--ipv6", action="store_true", dest="ipv6", default=False, help="enable IPv6 (order of precedence)")
parser.add_argument("-b", "--batch", action="store_true", dest="batch", default=False, help="batch output")
parser.add_argument("-c", "--client-audit", action="store_true", dest="client_audit", default=False, help="starts a server on port 2222 to audit client software config (use -p to change port; use -t to change timeout)")
parser.add_argument("-d", "--debug", action="store_true", dest="debug", default=False, help="enable debugging output")
parser.add_argument("-g", "--gex-test", action="store", dest="gex_test", metavar="<min1:pref1:max1[,min2:pref2:max2,...]> / <x-y[:step]>", type=str, default=None, help="conducts a very customized Diffie-Hellman GEX modulus size test. Tests an array of minimum, preferred, and maximum values, or a range of values with an optional incremental step amount")
parser.add_argument("-j", "--json", action="count", dest="json", default=0, help="enable JSON output (use -jj to enable indentation for better readability)")
parser.add_argument("-l", "--level", action="store", dest="level", type=str, choices=["info", "warn", "fail"], default="info", help="minimum output level (default: %(default)s)")
parser.add_argument("-L", "--list-policies", action="store_true", dest="list_policies", default=False, help="list all the official, built-in policies. Combine with -v to view policy change logs")
parser.add_argument("-M", "--make-policy", action="store", dest="make_policy", metavar="custom_policy.txt", type=str, default=None, help="creates a policy based on the target server (i.e.: the target server has the ideal configuration that other servers should adhere to), and stores it in the file path specified")
parser.add_argument("-m", "--manual", action="store_true", dest="manual", default=False, help="print the man page (Docker, PyPI, Snap, and Windows builds only)")
parser.add_argument("-n", "--no-colors", action="store_true", dest="no_colors", default=False, help="disable colors (automatic when the NO_COLOR environment variable is set)")
parser.add_argument("-P", "--policy", action="store", dest="policy", metavar="\"Built-In Policy Name\" / custom_policy.txt", type=str, default=None, help="run a policy test using the specified policy (use -L to see built-in policies, or specify filesystem path to custom policy created by -M)")
parser.add_argument("-p", "--port", action="store", dest="oport", metavar="N", type=int, default=None, help="the TCP port to connect to (or to listen on when -c is used)")
parser.add_argument("-T", "--targets", action="store", dest="targets", metavar="targets.txt", type=str, default=None, help="a file containing a list of target hosts (one per line, format HOST[:PORT]). Use -p/--port to set the default port for all hosts. Use --threads to control concurrent scans")
parser.add_argument("-t", "--timeout", action="store", dest="timeout", metavar="N", type=int, default=5, help="timeout (in seconds) for connection and reading (default: %(default)s)")
parser.add_argument("-v", "--verbose", action="store_true", dest="verbose", default=False, help="enable verbose output")
# Add long options to the parser
parser.add_argument("--conn-rate-test", action="store", dest="conn_rate_test", metavar="N[:max_rate]", type=str, default=None, help="perform a connection rate test (useful for collecting metrics related to susceptibility of the DHEat vuln). Testing is conducted with N concurrent sockets with an optional maximum rate of connections per second")
parser.add_argument("--dheat", action="store", dest="dheat", metavar="N[:kex[:e_len]]", type=str, default=None, help="continuously perform the DHEat DoS attack (CVE-2002-20001) against the target using N concurrent sockets. Optionally, a specific key exchange algorithm can be specified instead of allowing it to be automatically chosen. Additionally, a small length of the fake e value sent to the server can be chosen for a more efficient attack (such as 4).")
parser.add_argument("--lookup", action="store", dest="lookup", metavar="alg1[,alg2,...]", type=str, default=None, help="looks up an algorithm(s) without connecting to a server.")
parser.add_argument("--skip-rate-test", action="store_true", dest="skip_rate_test", default=False, help="skip the connection rate test during standard audits (used to safely infer whether the DHEat attack is viable)")
parser.add_argument("--threads", action="store", dest="threads", metavar="N", type=int, default=32, help="number of threads to use when scanning multiple targets (-T/--targets) (default: %(default)s)")
# The mandatory target option. Or rather, mandatory when -L, -T, or --lookup are not used.
parser.add_argument("host", nargs="?", action="store", type=str, default="", help="target hostname or IPv4/IPv6 address")
# If no arguments were given, print the help and exit.
if len(args) < 1:
parser.print_help()
sys.exit(exitcodes.UNKNOWN_ERROR)
oport: Optional[int] = None
try:
argument = parser.parse_args(args=args)
# Set simple flags.
aconf.client_audit = argument.client_audit
aconf.ipv4 = argument.ipv4
aconf.ipv6 = argument.ipv6
aconf.level = argument.level
aconf.list_policies = argument.list_policies
aconf.manual = argument.manual
aconf.skip_rate_test = argument.skip_rate_test
oport = argument.oport
if argument.batch is True:
aconf.batch = True
# If one -j was given, turn on JSON output. If -jj was given, enable indentation.
aconf.json = argument.json > 0
if argument.json > 1:
aconf.json_print_indent = True
if argument.conn_rate_test is not None:
aconf.conn_rate_test = argument.conn_rate_test
if argument.debug is True:
aconf.debug = True
out.debug = True
elif o in ('-g', '--gex-test'):
if argument.dheat is not None:
aconf.dheat = argument.dheat
if argument.gex_test is not None:
dh_gex = argument.gex_test
permitted_syntax = get_permitted_syntax_for_gex_test()
if not any(re.search(regex_str, a) for regex_str in permitted_syntax.values()):
usage_cb(out, '{} {} is not valid'.format(o, a))
if not any(re.search(regex_str, dh_gex) for regex_str in permitted_syntax.values()):
out.fail('{} is not valid'.format(dh_gex), write_now=True)
sys.exit(exitcodes.UNKNOWN_ERROR)
if re.search(permitted_syntax['RANGE'], a):
extracted_digits = re.findall(r'\d+', a)
if re.search(permitted_syntax['RANGE'], dh_gex):
extracted_digits = re.findall(r'\d+', dh_gex)
bits_left_bound = int(extracted_digits[0])
bits_right_bound = int(extracted_digits[1])
@@ -920,27 +851,52 @@ def process_commandline(out: OutputBuffer, args: List[str], usage_cb: Callable[.
bits_step = int(extracted_digits[2])
if bits_step <= 0:
usage_cb(out, '{} {} is not valid'.format(o, bits_step))
out.fail('the step field cannot be 0 or less: {}'.format(bits_step), write_now=True)
sys.exit(exitcodes.UNKNOWN_ERROR)
if all(x < 0 for x in (bits_left_bound, bits_right_bound)):
usage_cb(out, '{} {} {} is not valid'.format(o, bits_left_bound, bits_right_bound))
out.fail('{} {} {} is not valid'.format(dh_gex, bits_left_bound, bits_right_bound), write_now=True)
sys.exit(exitcodes.UNKNOWN_ERROR)
aconf.gex_test = a
elif o == '--dheat':
aconf.dheat = a
elif o == '--skip-rate-test':
aconf.skip_rate_test = True
elif o == '--conn-rate-test':
aconf.conn_rate_test = a
aconf.gex_test = dh_gex
if argument.lookup is not None:
aconf.lookup = argument.lookup
if len(args) == 0 and aconf.client_audit is False and aconf.target_file is None and aconf.list_policies is False and aconf.lookup == '' and aconf.manual is False:
usage_cb(out)
if argument.make_policy is not None:
aconf.make_policy = True
aconf.policy_file = argument.make_policy
if argument.policy is not None:
aconf.policy_file = argument.policy
if argument.targets is not None:
aconf.target_file = argument.targets
if argument.threads is not None:
aconf.threads = argument.threads
if argument.timeout is not None:
aconf.timeout = float(argument.timeout)
aconf.timeout_set = True
if argument.verbose is True:
aconf.verbose = True
out.verbose = True
except argparse.ArgumentError as err:
out.fail(str(err), write_now=True)
parser.print_help()
sys.exit(exitcodes.UNKNOWN_ERROR)
if argument.host == "" and argument.client_audit is False and argument.targets is None and argument.list_policies is False and argument.lookup is None and argument.manual is False:
out.fail("target host must be specified, unless -c, -m, -L, -T, or --lookup are used", write_now=True)
sys.exit(exitcodes.UNKNOWN_ERROR)
if aconf.manual:
return aconf
if aconf.lookup != '':
if aconf.lookup != "":
return aconf
if aconf.list_policies:
@@ -949,27 +905,25 @@ def process_commandline(out: OutputBuffer, args: List[str], usage_cb: Callable[.
if aconf.client_audit is False and aconf.target_file is None:
if oport is not None:
host = args[0]
host = argument.host
else:
host, port = Utils.parse_host_and_port(args[0])
if not host and aconf.target_file is None:
usage_cb(out, 'host is empty')
host, port = Utils.parse_host_and_port(argument.host)
if port == 0 and oport is None:
if aconf.client_audit: # The default port to listen on during a client audit is 2222.
port = 2222
else:
port = 22
if not host and aconf.target_file is None:
out.fail("target host is not specified", write_now=True)
sys.exit(exitcodes.UNKNOWN_ERROR)
if oport is None and aconf.client_audit: # The default port to listen on during a client audit is 2222.
port = 2222
if oport is not None:
port = Utils.parse_int(oport)
if port <= 0 or port > 65535:
usage_cb(out, 'port {} is not valid'.format(oport))
if port < 1 or port > 65535:
out.fail("port must be greater than 0 and less than 65535: {}".format(oport), write_now=True)
sys.exit(exitcodes.UNKNOWN_ERROR)
aconf.host = host
aconf.port = port
if not (aconf.ssh1 or aconf.ssh2):
aconf.ssh1, aconf.ssh2 = True, True
# If a file containing a list of targets was given, read it.
if aconf.target_file is not None:
@@ -996,26 +950,23 @@ def process_commandline(out: OutputBuffer, args: List[str], usage_cb: Callable[.
try:
aconf.policy = Policy(policy_file=aconf.policy_file, json_output=aconf.json)
except Exception as e:
out.fail("Error while loading policy file: %s: %s" % (str(e), traceback.format_exc()))
out.write()
out.fail("Error while loading policy file: %s: %s" % (str(e), traceback.format_exc()), write_now=True)
sys.exit(exitcodes.UNKNOWN_ERROR)
# If the user wants to do a client audit, but provided a server policy, terminate.
if aconf.client_audit and aconf.policy.is_server_policy():
out.fail("Error: client audit selected, but server policy provided.")
out.write()
out.fail("Error: client audit selected, but server policy provided.", write_now=True)
sys.exit(exitcodes.UNKNOWN_ERROR)
# If the user wants to do a server audit, but provided a client policy, terminate.
if aconf.client_audit is False and aconf.policy.is_server_policy() is False:
out.fail("Error: server audit selected, but client policy provided.")
out.write()
out.fail("Error: server audit selected, but client policy provided.", write_now=True)
sys.exit(exitcodes.UNKNOWN_ERROR)
return aconf
def build_struct(target_host: str, banner: Optional['Banner'], kex: Optional['SSH2_Kex'] = None, pkm: Optional['SSH1_PublicKeyMessage'] = None, client_host: Optional[str] = None, software: Optional[Software] = None, algorithms: Optional[Algorithms] = None, algorithm_recommendation_suppress_list: Optional[List[str]] = None, additional_notes: List[str] = []) -> Any: # pylint: disable=dangerous-default-value
def build_struct(target_host: str, banner: Optional['Banner'], kex: Optional['SSH2_Kex'] = None, client_host: Optional[str] = None, software: Optional[Software] = None, algorithms: Optional[Algorithms] = None, algorithm_recommendation_suppress_list: Optional[List[str]] = None, additional_notes: List[str] = []) -> Any: # pylint: disable=dangerous-default-value
def fetch_notes(algorithm: str, alg_type: str) -> Dict[str, List[Optional[str]]]:
'''Returns a dictionary containing the messages in the "fail", "warn", and "info" levels for this algorithm.'''
@@ -1160,22 +1111,6 @@ def build_struct(target_host: str, banner: Optional['Banner'], kex: Optional['SS
'hash_alg': 'MD5',
'hash': fp.md5[4:]
})
else:
pkm_supported_ciphers = None
pkm_supported_authentications = None
pkm_fp = None
if pkm is not None:
pkm_supported_ciphers = pkm.supported_ciphers
pkm_supported_authentications = pkm.supported_authentications
pkm_fp = Fingerprint(pkm.host_key_fingerprint_data).sha256
res['key'] = ['ssh-rsa1']
res['enc'] = pkm_supported_ciphers
res['aut'] = pkm_supported_authentications
res['fingerprints'] = [{
'type': 'ssh-rsa1',
'fp': pkm_fp,
}]
# Historically, CVE information was returned. Now we'll just return an empty dictionary so as to not break any legacy clients.
res['cves'] = []
@@ -1190,7 +1125,7 @@ def build_struct(target_host: str, banner: Optional['Banner'], kex: Optional['SS
# Returns one of the exitcodes.* flags.
def audit(out: OutputBuffer, aconf: AuditConf, sshv: Optional[int] = None, print_target: bool = False) -> int:
def audit(out: OutputBuffer, aconf: AuditConf, print_target: bool = False) -> int:
program_retval = exitcodes.GOOD
out.batch = aconf.batch
out.verbose = aconf.verbose
@@ -1216,10 +1151,8 @@ def audit(out: OutputBuffer, aconf: AuditConf, sshv: Optional[int] = None, print
out.write()
sys.exit(exitcodes.CONNECTION_ERROR)
if sshv is None:
sshv = 2 if aconf.ssh2 else 1
err = None
banner, header, err = s.get_banner(sshv)
banner, header, err = s.get_banner()
if banner is None:
if err is None:
err = '[exception] did not receive banner.'
@@ -1228,7 +1161,7 @@ def audit(out: OutputBuffer, aconf: AuditConf, sshv: Optional[int] = None, print
if err is None:
s.send_kexinit() # Send the algorithms we support (except we don't since this isn't a real SSH connection).
packet_type, payload = s.read_packet(sshv)
packet_type, payload = s.read_packet()
if packet_type < 0:
try:
if len(payload) > 0:
@@ -1237,17 +1170,10 @@ def audit(out: OutputBuffer, aconf: AuditConf, sshv: Optional[int] = None, print
payload_txt = 'empty'
except UnicodeDecodeError:
payload_txt = '"{}"'.format(repr(payload).lstrip('b')[1:-1])
if payload_txt == 'Protocol major versions differ.':
if sshv == 2 and aconf.ssh1:
ret = audit(out, aconf, 1)
out.write()
return ret
err = '[exception] error reading packet ({})'.format(payload_txt)
else:
err_pair = None
if sshv == 1 and packet_type != Protocol.SMSG_PUBLIC_KEY:
err_pair = ('SMSG_PUBLIC_KEY', Protocol.SMSG_PUBLIC_KEY)
elif sshv == 2 and packet_type != Protocol.MSG_KEXINIT:
if packet_type != Protocol.MSG_KEXINIT:
err_pair = ('MSG_KEXINIT', Protocol.MSG_KEXINIT)
if err_pair is not None:
fmt = '[exception] did not receive {0} ({1}), ' + \
@@ -1257,52 +1183,50 @@ def audit(out: OutputBuffer, aconf: AuditConf, sshv: Optional[int] = None, print
output(out, aconf, banner, header)
out.fail(err)
return exitcodes.CONNECTION_ERROR
if sshv == 1:
program_retval = output(out, aconf, banner, header, pkm=SSH1_PublicKeyMessage.parse(payload))
elif sshv == 2:
try:
kex = SSH2_Kex.parse(out, payload)
out.d(str(kex))
except Exception:
out.fail("Failed to parse server's kex. Stack trace:\n%s" % str(traceback.format_exc()))
return exitcodes.CONNECTION_ERROR
if aconf.dheat is not None:
DHEat(out, aconf, banner, kex).run()
return exitcodes.GOOD
elif aconf.conn_rate_test_enabled:
DHEat.dh_rate_test(out, aconf, kex, 0, 0, 0)
return exitcodes.GOOD
try:
kex = SSH2_Kex.parse(out, payload)
out.d(str(kex))
except Exception:
out.fail("Failed to parse server's kex. Stack trace:\n%s" % str(traceback.format_exc()))
return exitcodes.CONNECTION_ERROR
dh_rate_test_notes = ""
if aconf.client_audit is False:
HostKeyTest.run(out, s, kex)
if aconf.gex_test != '':
return run_gex_granular_modulus_size_test(out, s, kex, aconf)
else:
GEXTest.run(out, s, banner, kex)
# Skip the rate test if the user specified "--skip-rate-test".
if aconf.skip_rate_test:
out.d("Skipping rate test due to --skip-rate-test option.")
else:
# Try to open many TCP connections against the server if any Diffie-Hellman key exchanges are present; this tests potential vulnerability to the DHEat DOS attack. Use 3 concurrent sockets over at most 1.5 seconds to open at most 38 connections (stops if 1.5 seconds elapse, or 38 connections are opened--whichever comes first). If more than 25 connections per second were observed, flag the DH algorithms with a warning about the DHEat DOS vuln.
dh_rate_test_notes = DHEat.dh_rate_test(out, aconf, kex, 1.5, 38, 3)
# This is a standard audit scan.
if (aconf.policy is None) and (aconf.make_policy is False):
program_retval = output(out, aconf, banner, header, client_host=s.client_host, kex=kex, print_target=print_target, dh_rate_test_notes=dh_rate_test_notes)
# This is a policy test.
elif (aconf.policy is not None) and (aconf.make_policy is False):
program_retval = exitcodes.GOOD if evaluate_policy(out, aconf, banner, s.client_host, kex=kex) else exitcodes.FAILURE
# A new policy should be made from this scan.
elif (aconf.policy is None) and (aconf.make_policy is True):
make_policy(aconf, banner, kex, s.client_host)
if aconf.dheat is not None:
DHEat(out, aconf, banner, kex).run()
return exitcodes.GOOD
elif aconf.conn_rate_test_enabled:
DHEat.dh_rate_test(out, aconf, kex, 0, 0, 0)
return exitcodes.GOOD
dh_rate_test_notes = ""
if aconf.client_audit is False:
HostKeyTest.run(out, s, kex)
if aconf.gex_test != '':
return run_gex_granular_modulus_size_test(out, s, kex, aconf)
else:
raise RuntimeError('Internal error while handling output: %r %r' % (aconf.policy is None, aconf.make_policy))
GEXTest.run(out, s, banner, kex)
# Skip the rate test if the user specified "--skip-rate-test".
if aconf.skip_rate_test:
out.d("Skipping rate test due to --skip-rate-test option.")
else:
# Try to open many TCP connections against the server if any Diffie-Hellman key exchanges are present; this tests potential vulnerability to the DHEat DOS attack. Use 3 concurrent sockets over at most 1.5 seconds to open at most 38 connections (stops if 1.5 seconds elapse, or 38 connections are opened--whichever comes first). If more than 25 connections per second were observed, flag the DH algorithms with a warning about the DHEat DOS vuln.
dh_rate_test_notes = DHEat.dh_rate_test(out, aconf, kex, 1.5, 38, 3)
# This is a standard audit scan.
if (aconf.policy is None) and (aconf.make_policy is False):
program_retval = output(out, aconf, banner, header, client_host=s.client_host, kex=kex, print_target=print_target, dh_rate_test_notes=dh_rate_test_notes)
# This is a policy test.
elif (aconf.policy is not None) and (aconf.make_policy is False):
program_retval = exitcodes.GOOD if evaluate_policy(out, aconf, banner, s.client_host, kex=kex) else exitcodes.FAILURE
# A new policy should be made from this scan.
elif (aconf.policy is None) and (aconf.make_policy is True):
make_policy(aconf, banner, kex, s.client_host)
else:
raise RuntimeError('Internal error while handling output: %r %r' % (aconf.policy is None, aconf.make_policy))
return program_retval
@@ -1499,7 +1423,7 @@ def run_gex_granular_modulus_size_test(out: OutputBuffer, s: 'SSH_Socket', kex:
def main() -> int:
out = OutputBuffer()
aconf = process_commandline(out, sys.argv[1:], usage)
aconf = process_commandline(out, sys.argv[1:])
# If we're on Windows, but the colorama module could not be imported, print a warning if we're in verbose mode.
if (sys.platform == 'win32') and ('colorama' not in sys.modules):
@@ -1569,7 +1493,6 @@ def main() -> int:
print(']')
# Send notification that this thread is exiting. This deletes the thread's local copy of the algorithm databases.
SSH1_KexDB.thread_exit()
SSH2_KexDB.thread_exit()
else: # Just a scan against a single target.
+13 -34
View File
@@ -1,7 +1,7 @@
"""
The MIT License (MIT)
Copyright (C) 2017-2021 Joe Testa (jtesta@positronsecurity.com)
Copyright (C) 2017-2025 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
@@ -39,7 +39,6 @@ from ssh_audit.globals import SSH_HEADER
from ssh_audit.outputbuffer import OutputBuffer
from ssh_audit.protocol import Protocol
from ssh_audit.readbuf import ReadBuf
from ssh_audit.ssh1 import SSH1
from ssh_audit.ssh2_kex import SSH2_Kex
from ssh_audit.ssh2_kexparty import SSH2_KexParty
from ssh_audit.utils import Utils
@@ -173,7 +172,7 @@ class SSH_Socket(ReadBuf, WriteBuf):
errm = 'cannot connect to {} port {}: {}'.format(*errt)
return '[exception] {}'.format(errm)
def get_banner(self, sshv: int = 2) -> Tuple[Optional['Banner'], List[str], Optional[str]]:
def get_banner(self) -> Tuple[Optional['Banner'], List[str], Optional[str]]:
self.__outputbuffer.d('Getting banner...', write_now=True)
if self.__sock is None:
@@ -181,7 +180,7 @@ class SSH_Socket(ReadBuf, WriteBuf):
if self.__banner is not None:
return self.__banner, self.__header, None
banner = SSH_HEADER.format('1.5' if sshv == 1 else '2.0')
banner = SSH_HEADER.format('2.0')
if self.__state < self.SM_BANNER_SENT:
self.send_banner(banner)
@@ -254,47 +253,27 @@ class SSH_Socket(ReadBuf, WriteBuf):
if s < 0:
raise SSH_Socket.InsufficientReadException(e)
def read_packet(self, sshv: int = 2) -> Tuple[int, bytes]:
def read_packet(self) -> Tuple[int, bytes]:
try:
header = WriteBuf()
self.ensure_read(4)
packet_length = self.read_int()
header.write_int(packet_length)
# XXX: validate length
if sshv == 1:
padding_length = 8 - packet_length % 8
self.ensure_read(padding_length)
padding = self.read(padding_length)
header.write(padding)
payload_length = packet_length
check_size = padding_length + payload_length
else:
self.ensure_read(1)
padding_length = self.read_byte()
header.write_byte(padding_length)
payload_length = packet_length - padding_length - 1
check_size = 4 + 1 + payload_length + padding_length
self.ensure_read(1)
padding_length = self.read_byte()
header.write_byte(padding_length)
payload_length = packet_length - padding_length - 1
check_size = 4 + 1 + payload_length + padding_length
if check_size % self.__block_size != 0:
self.__outputbuffer.fail('[exception] invalid ssh packet (block size)').write()
sys.exit(exitcodes.CONNECTION_ERROR)
self.ensure_read(payload_length)
if sshv == 1:
payload = self.read(payload_length - 4)
header.write(payload)
crc = self.read_int()
header.write_int(crc)
else:
payload = self.read(payload_length)
header.write(payload)
payload = self.read(payload_length)
header.write(payload)
packet_type = ord(payload[0:1])
if sshv == 1:
rcrc = SSH1.crc32(padding + payload)
if crc != rcrc:
self.__outputbuffer.fail('[exception] packet checksum CRC32 mismatch.').write()
sys.exit(exitcodes.CONNECTION_ERROR)
else:
self.ensure_read(padding_length)
padding = self.read(padding_length)
self.ensure_read(padding_length)
_ = self.read(padding_length)
payload = payload[1:]
return packet_type, payload
except SSH_Socket.InsufficientReadException as ex:
+1 -1
View File
@@ -129,7 +129,7 @@ class Utils:
return -1.0
@staticmethod
def parse_host_and_port(host_and_port: str, default_port: int = 0) -> Tuple[str, int]:
def parse_host_and_port(host_and_port: str, default_port: int = 22) -> Tuple[str, int]:
'''Parses a string into a tuple of its host and port. The port is 0 if not specified.'''
host = host_and_port
port = default_port
+1 -11
View File
@@ -1,4 +1,4 @@
.TH SSH-AUDIT 1 "September 24, 2024"
.TH SSH-AUDIT 1 "July 26, 2025"
.SH NAME
\fBssh-audit\fP \- SSH server & client configuration auditor
.SH SYNOPSIS
@@ -16,16 +16,6 @@ See <https://www.ssh\-audit.com/> for official hardening guides for common platf
.br
Print short summary of options.
.TP
.B -1, \-\-ssh1
.br
Only perform an audit using SSH protocol version 1.
.TP
.B -2, \-\-ssh2
.br
Only perform an audit using SSH protocol version 2.
.TP
.B -4, \-\-ipv4
.br
@@ -116,6 +116,9 @@
"info": [
"default key exchange from OpenSSH 7.4 to 8.9",
"available since OpenSSH 7.4, Dropbear SSH 2018.76"
],
"warn": [
"does not provide protection against post-quantum attacks"
]
}
},
@@ -125,6 +128,9 @@
"info": [
"default key exchange from OpenSSH 6.5 to 7.3",
"available since OpenSSH 6.4, Dropbear SSH 2013.62"
],
"warn": [
"does not provide protection against post-quantum attacks"
]
}
},
@@ -136,6 +142,9 @@
],
"info": [
"available since OpenSSH 5.7, Dropbear SSH 2013.62"
],
"warn": [
"does not provide protection against post-quantum attacks"
]
}
},
@@ -147,6 +156,9 @@
],
"info": [
"available since OpenSSH 5.7, Dropbear SSH 2013.62"
],
"warn": [
"does not provide protection against post-quantum attacks"
]
}
},
@@ -158,6 +170,9 @@
],
"info": [
"available since OpenSSH 5.7, Dropbear SSH 2013.62"
],
"warn": [
"does not provide protection against post-quantum attacks"
]
}
},
@@ -168,7 +183,8 @@
"available since OpenSSH 7.3, Dropbear SSH 2016.73"
],
"warn": [
"2048-bit modulus only provides 112-bits of symmetric strength"
"2048-bit modulus only provides 112-bits of symmetric strength",
"does not provide protection against post-quantum attacks"
]
}
},
@@ -182,7 +198,8 @@
"available since OpenSSH 3.9, Dropbear SSH 0.53"
],
"warn": [
"2048-bit modulus only provides 112-bits of symmetric strength"
"2048-bit modulus only provides 112-bits of symmetric strength",
"does not provide protection against post-quantum attacks"
]
}
},
@@ -191,6 +208,9 @@
"notes": {
"info": [
"available since Dropbear SSH 2013.57"
],
"warn": [
"does not provide protection against post-quantum attacks"
]
}
}
@@ -349,12 +369,6 @@
"name": "twofish256-ctr",
"notes": ""
}
],
"kex": [
{
"name": "diffie-hellman-group16-sha512",
"notes": ""
}
]
}
},
@@ -371,9 +385,21 @@
}
],
"kex": [
{
"name": "curve25519-sha256",
"notes": ""
},
{
"name": "curve25519-sha256@libssh.org",
"notes": ""
},
{
"name": "diffie-hellman-group14-sha256",
"notes": ""
},
{
"name": "kexguess2@matt.ucc.asn.au",
"notes": ""
}
],
"mac": [
@@ -5,22 +5,30 @@
(gen) compression: enabled (zlib@openssh.com)
# key exchange algorithms
(kex) curve25519-sha256 -- [info] available since OpenSSH 7.4, Dropbear SSH 2018.76
 `- [info] default key exchange from OpenSSH 7.4 to 8.9
(kex) curve25519-sha256@libssh.org -- [info] available since OpenSSH 6.4, Dropbear SSH 2013.62
 `- [info] default key exchange from OpenSSH 6.5 to 7.3
(kex) curve25519-sha256 -- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 7.4, Dropbear SSH 2018.76
`- [info] default key exchange from OpenSSH 7.4 to 8.9
(kex) curve25519-sha256@libssh.org -- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 6.4, Dropbear SSH 2013.62
`- [info] default key exchange from OpenSSH 6.5 to 7.3
(kex) ecdh-sha2-nistp521 -- [fail] using elliptic curves that are suspected as being backdoored by the U.S. National Security Agency
 `- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 5.7, Dropbear SSH 2013.62
(kex) ecdh-sha2-nistp384 -- [fail] using elliptic curves that are suspected as being backdoored by the U.S. National Security Agency
 `- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 5.7, Dropbear SSH 2013.62
(kex) ecdh-sha2-nistp256 -- [fail] using elliptic curves that are suspected as being backdoored by the U.S. National Security Agency
 `- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 5.7, Dropbear SSH 2013.62
(kex) diffie-hellman-group14-sha256 -- [warn] 2048-bit modulus only provides 112-bits of symmetric strength
 `- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 7.3, Dropbear SSH 2016.73
(kex) diffie-hellman-group14-sha1 -- [fail] using broken SHA-1 hash algorithm
 `- [warn] 2048-bit modulus only provides 112-bits of symmetric strength
 `- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 3.9, Dropbear SSH 0.53
(kex) kexguess2@matt.ucc.asn.au -- [info] available since Dropbear SSH 2013.57
(kex) kexguess2@matt.ucc.asn.au -- [warn] does not provide protection against post-quantum attacks
`- [info] available since Dropbear SSH 2013.57
# host-key algorithms
(key) ecdsa-sha2-nistp256 -- [fail] using elliptic curves that are suspected as being backdoored by the U.S. National Security Agency
@@ -74,13 +82,15 @@
(rec) -hmac-sha1-96 -- mac algorithm to remove 
(rec) -ssh-dss -- key algorithm to remove 
(rec) -ssh-rsa -- 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) -aes128-cbc -- enc algorithm to remove 
(rec) -aes256-cbc -- enc algorithm to remove 
(rec) -curve25519-sha256 -- kex algorithm to remove 
(rec) -curve25519-sha256@libssh.org -- kex algorithm to remove 
(rec) -diffie-hellman-group14-sha256 -- kex algorithm to remove 
(rec) -hmac-sha2-256 -- mac algorithm to remove 
(rec) -kexguess2@matt.ucc.asn.au -- kex algorithm to remove 
# additional info
(nfo) For hardening guides on common OSes, please see: <https://www.ssh-audit.com/hardening_guides.html>
@@ -173,6 +173,9 @@
],
"info": [
"available since OpenSSH 2.3.0"
],
"warn": [
"does not provide protection against post-quantum attacks"
]
}
},
@@ -186,7 +189,8 @@
"available since OpenSSH 3.9, Dropbear SSH 0.53"
],
"warn": [
"2048-bit modulus only provides 112-bits of symmetric strength"
"2048-bit modulus only provides 112-bits of symmetric strength",
"does not provide protection against post-quantum attacks"
]
}
},
@@ -201,6 +205,9 @@
"info": [
"removed in OpenSSH 6.9: https://www.openssh.com/txt/release-6.9",
"available since OpenSSH 2.3.0, Dropbear SSH 0.28"
],
"warn": [
"does not provide protection against post-quantum attacks"
]
}
}
@@ -10,13 +10,16 @@
# key exchange algorithms
(kex) diffie-hellman-group-exchange-sha1 (1024-bit) -- [fail] using small 1024-bit modulus
 `- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 2.3.0
(kex) diffie-hellman-group14-sha1 -- [fail] using broken SHA-1 hash algorithm
 `- [warn] 2048-bit modulus only provides 112-bits of symmetric strength
 `- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 3.9, Dropbear SSH 0.53
(kex) diffie-hellman-group1-sha1 -- [fail] using small 1024-bit modulus
 `- [fail] vulnerable to the Logjam attack: https://en.wikipedia.org/wiki/Logjam_(computer_security)
 `- [fail] using broken SHA-1 hash algorithm
 `- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 2.3.0, Dropbear SSH 0.28
`- [info] removed in OpenSSH 6.9: https://www.openssh.com/txt/release-6.9
@@ -195,6 +195,9 @@
],
"info": [
"available since OpenSSH 4.4"
],
"warn": [
"does not provide protection against post-quantum attacks"
]
}
},
@@ -207,6 +210,9 @@
],
"info": [
"available since OpenSSH 2.3.0"
],
"warn": [
"does not provide protection against post-quantum attacks"
]
}
},
@@ -220,7 +226,8 @@
"available since OpenSSH 3.9, Dropbear SSH 0.53"
],
"warn": [
"2048-bit modulus only provides 112-bits of symmetric strength"
"2048-bit modulus only provides 112-bits of symmetric strength",
"does not provide protection against post-quantum attacks"
]
}
},
@@ -235,6 +242,9 @@
"info": [
"removed in OpenSSH 6.9: https://www.openssh.com/txt/release-6.9",
"available since OpenSSH 2.3.0, Dropbear SSH 0.28"
],
"warn": [
"does not provide protection against post-quantum attacks"
]
}
}
@@ -6,15 +6,19 @@
# key exchange algorithms
(kex) diffie-hellman-group-exchange-sha256 (1024-bit) -- [fail] using small 1024-bit modulus
 `- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 4.4
(kex) diffie-hellman-group-exchange-sha1 (1024-bit) -- [fail] using small 1024-bit modulus
 `- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 2.3.0
(kex) diffie-hellman-group14-sha1 -- [fail] using broken SHA-1 hash algorithm
 `- [warn] 2048-bit modulus only provides 112-bits of symmetric strength
 `- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 3.9, Dropbear SSH 0.53
(kex) diffie-hellman-group1-sha1 -- [fail] using small 1024-bit modulus
 `- [fail] vulnerable to the Logjam attack: https://en.wikipedia.org/wiki/Logjam_(computer_security)
 `- [fail] using broken SHA-1 hash algorithm
 `- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 2.3.0, Dropbear SSH 0.28
`- [info] removed in OpenSSH 6.9: https://www.openssh.com/txt/release-6.9
@@ -185,6 +185,9 @@
],
"info": [
"available since OpenSSH 4.4"
],
"warn": [
"does not provide protection against post-quantum attacks"
]
}
},
@@ -197,6 +200,9 @@
],
"info": [
"available since OpenSSH 2.3.0"
],
"warn": [
"does not provide protection against post-quantum attacks"
]
}
},
@@ -210,7 +216,8 @@
"available since OpenSSH 3.9, Dropbear SSH 0.53"
],
"warn": [
"2048-bit modulus only provides 112-bits of symmetric strength"
"2048-bit modulus only provides 112-bits of symmetric strength",
"does not provide protection against post-quantum attacks"
]
}
},
@@ -225,6 +232,9 @@
"info": [
"removed in OpenSSH 6.9: https://www.openssh.com/txt/release-6.9",
"available since OpenSSH 2.3.0, Dropbear SSH 0.28"
],
"warn": [
"does not provide protection against post-quantum attacks"
]
}
}
@@ -6,15 +6,19 @@
# key exchange algorithms
(kex) diffie-hellman-group-exchange-sha256 (1024-bit) -- [fail] using small 1024-bit modulus
 `- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 4.4
(kex) diffie-hellman-group-exchange-sha1 (1024-bit) -- [fail] using small 1024-bit modulus
 `- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 2.3.0
(kex) diffie-hellman-group14-sha1 -- [fail] using broken SHA-1 hash algorithm
 `- [warn] 2048-bit modulus only provides 112-bits of symmetric strength
 `- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 3.9, Dropbear SSH 0.53
(kex) diffie-hellman-group1-sha1 -- [fail] using small 1024-bit modulus
 `- [fail] vulnerable to the Logjam attack: https://en.wikipedia.org/wiki/Logjam_(computer_security)
 `- [fail] using broken SHA-1 hash algorithm
 `- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 2.3.0, Dropbear SSH 0.28
`- [info] removed in OpenSSH 6.9: https://www.openssh.com/txt/release-6.9
@@ -185,6 +185,9 @@
],
"info": [
"available since OpenSSH 4.4"
],
"warn": [
"does not provide protection against post-quantum attacks"
]
}
},
@@ -197,6 +200,9 @@
],
"info": [
"available since OpenSSH 2.3.0"
],
"warn": [
"does not provide protection against post-quantum attacks"
]
}
},
@@ -210,7 +216,8 @@
"available since OpenSSH 3.9, Dropbear SSH 0.53"
],
"warn": [
"2048-bit modulus only provides 112-bits of symmetric strength"
"2048-bit modulus only provides 112-bits of symmetric strength",
"does not provide protection against post-quantum attacks"
]
}
},
@@ -225,6 +232,9 @@
"info": [
"removed in OpenSSH 6.9: https://www.openssh.com/txt/release-6.9",
"available since OpenSSH 2.3.0, Dropbear SSH 0.28"
],
"warn": [
"does not provide protection against post-quantum attacks"
]
}
}
@@ -6,15 +6,19 @@
# key exchange algorithms
(kex) diffie-hellman-group-exchange-sha256 (1024-bit) -- [fail] using small 1024-bit modulus
 `- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 4.4
(kex) diffie-hellman-group-exchange-sha1 (1024-bit) -- [fail] using small 1024-bit modulus
 `- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 2.3.0
(kex) diffie-hellman-group14-sha1 -- [fail] using broken SHA-1 hash algorithm
 `- [warn] 2048-bit modulus only provides 112-bits of symmetric strength
 `- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 3.9, Dropbear SSH 0.53
(kex) diffie-hellman-group1-sha1 -- [fail] using small 1024-bit modulus
 `- [fail] vulnerable to the Logjam attack: https://en.wikipedia.org/wiki/Logjam_(computer_security)
 `- [fail] using broken SHA-1 hash algorithm
 `- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 2.3.0, Dropbear SSH 0.28
`- [info] removed in OpenSSH 6.9: https://www.openssh.com/txt/release-6.9
@@ -185,6 +185,9 @@
],
"info": [
"available since OpenSSH 4.4"
],
"warn": [
"does not provide protection against post-quantum attacks"
]
}
},
@@ -197,6 +200,9 @@
],
"info": [
"available since OpenSSH 2.3.0"
],
"warn": [
"does not provide protection against post-quantum attacks"
]
}
},
@@ -210,7 +216,8 @@
"available since OpenSSH 3.9, Dropbear SSH 0.53"
],
"warn": [
"2048-bit modulus only provides 112-bits of symmetric strength"
"2048-bit modulus only provides 112-bits of symmetric strength",
"does not provide protection against post-quantum attacks"
]
}
},
@@ -225,6 +232,9 @@
"info": [
"removed in OpenSSH 6.9: https://www.openssh.com/txt/release-6.9",
"available since OpenSSH 2.3.0, Dropbear SSH 0.28"
],
"warn": [
"does not provide protection against post-quantum attacks"
]
}
}
@@ -6,15 +6,19 @@
# key exchange algorithms
(kex) diffie-hellman-group-exchange-sha256 (1024-bit) -- [fail] using small 1024-bit modulus
 `- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 4.4
(kex) diffie-hellman-group-exchange-sha1 (1024-bit) -- [fail] using small 1024-bit modulus
 `- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 2.3.0
(kex) diffie-hellman-group14-sha1 -- [fail] using broken SHA-1 hash algorithm
 `- [warn] 2048-bit modulus only provides 112-bits of symmetric strength
 `- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 3.9, Dropbear SSH 0.53
(kex) diffie-hellman-group1-sha1 -- [fail] using small 1024-bit modulus
 `- [fail] vulnerable to the Logjam attack: https://en.wikipedia.org/wiki/Logjam_(computer_security)
 `- [fail] using broken SHA-1 hash algorithm
 `- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 2.3.0, Dropbear SSH 0.28
`- [info] removed in OpenSSH 6.9: https://www.openssh.com/txt/release-6.9
@@ -185,6 +185,9 @@
],
"info": [
"available since OpenSSH 4.4"
],
"warn": [
"does not provide protection against post-quantum attacks"
]
}
},
@@ -197,6 +200,9 @@
],
"info": [
"available since OpenSSH 2.3.0"
],
"warn": [
"does not provide protection against post-quantum attacks"
]
}
},
@@ -210,7 +216,8 @@
"available since OpenSSH 3.9, Dropbear SSH 0.53"
],
"warn": [
"2048-bit modulus only provides 112-bits of symmetric strength"
"2048-bit modulus only provides 112-bits of symmetric strength",
"does not provide protection against post-quantum attacks"
]
}
},
@@ -225,6 +232,9 @@
"info": [
"removed in OpenSSH 6.9: https://www.openssh.com/txt/release-6.9",
"available since OpenSSH 2.3.0, Dropbear SSH 0.28"
],
"warn": [
"does not provide protection against post-quantum attacks"
]
}
}
@@ -6,15 +6,19 @@
# key exchange algorithms
(kex) diffie-hellman-group-exchange-sha256 (1024-bit) -- [fail] using small 1024-bit modulus
 `- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 4.4
(kex) diffie-hellman-group-exchange-sha1 (1024-bit) -- [fail] using small 1024-bit modulus
 `- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 2.3.0
(kex) diffie-hellman-group14-sha1 -- [fail] using broken SHA-1 hash algorithm
 `- [warn] 2048-bit modulus only provides 112-bits of symmetric strength
 `- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 3.9, Dropbear SSH 0.53
(kex) diffie-hellman-group1-sha1 -- [fail] using small 1024-bit modulus
 `- [fail] vulnerable to the Logjam attack: https://en.wikipedia.org/wiki/Logjam_(computer_security)
 `- [fail] using broken SHA-1 hash algorithm
 `- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 2.3.0, Dropbear SSH 0.28
`- [info] removed in OpenSSH 6.9: https://www.openssh.com/txt/release-6.9
@@ -104,6 +104,9 @@
"info": [
"default key exchange from OpenSSH 7.4 to 8.9",
"available since OpenSSH 7.4, Dropbear SSH 2018.76"
],
"warn": [
"does not provide protection against post-quantum attacks"
]
}
},
@@ -113,6 +116,9 @@
"info": [
"default key exchange from OpenSSH 6.5 to 7.3",
"available since OpenSSH 6.4, Dropbear SSH 2013.62"
],
"warn": [
"does not provide protection against post-quantum attacks"
]
}
},
@@ -124,6 +130,9 @@
],
"info": [
"available since OpenSSH 5.7, Dropbear SSH 2013.62"
],
"warn": [
"does not provide protection against post-quantum attacks"
]
}
},
@@ -135,6 +144,9 @@
],
"info": [
"available since OpenSSH 5.7, Dropbear SSH 2013.62"
],
"warn": [
"does not provide protection against post-quantum attacks"
]
}
},
@@ -146,6 +158,9 @@
],
"info": [
"available since OpenSSH 5.7, Dropbear SSH 2013.62"
],
"warn": [
"does not provide protection against post-quantum attacks"
]
}
},
@@ -156,6 +171,9 @@
"info": [
"OpenSSH's GEX fallback mechanism was triggered during testing. Very old SSH clients will still be able to create connections using a 2048-bit modulus, though modern clients will use 4096. This can only be disabled by recompiling the code (see https://github.com/openssh/openssh-portable/blob/V_9_4/dh.c#L477).",
"available since OpenSSH 4.4"
],
"warn": [
"does not provide protection against post-quantum attacks"
]
}
},
@@ -164,6 +182,9 @@
"notes": {
"info": [
"available since OpenSSH 7.3, Dropbear SSH 2016.73"
],
"warn": [
"does not provide protection against post-quantum attacks"
]
}
},
@@ -172,6 +193,9 @@
"notes": {
"info": [
"available since OpenSSH 7.3"
],
"warn": [
"does not provide protection against post-quantum attacks"
]
}
},
@@ -182,7 +206,8 @@
"available since OpenSSH 7.3, Dropbear SSH 2016.73"
],
"warn": [
"2048-bit modulus only provides 112-bits of symmetric strength"
"2048-bit modulus only provides 112-bits of symmetric strength",
"does not provide protection against post-quantum attacks"
]
}
},
@@ -196,7 +221,8 @@
"available since OpenSSH 3.9, Dropbear SSH 0.53"
],
"warn": [
"2048-bit modulus only provides 112-bits of symmetric strength"
"2048-bit modulus only provides 112-bits of symmetric strength",
"does not provide protection against post-quantum attacks"
]
}
}
@@ -407,6 +433,14 @@
}
},
"warning": {
"chg": {
"kex": [
{
"name": "diffie-hellman-group-exchange-sha256",
"notes": "increase modulus size to 3072 bits or larger"
}
]
},
"del": {
"enc": [
{
@@ -415,9 +449,25 @@
}
],
"kex": [
{
"name": "curve25519-sha256",
"notes": ""
},
{
"name": "curve25519-sha256@libssh.org",
"notes": ""
},
{
"name": "diffie-hellman-group14-sha256",
"notes": ""
},
{
"name": "diffie-hellman-group16-sha512",
"notes": ""
},
{
"name": "diffie-hellman-group18-sha512",
"notes": ""
}
],
"mac": [
@@ -5,24 +5,34 @@
(gen) compression: enabled (zlib@openssh.com)
# key exchange algorithms
(kex) curve25519-sha256 -- [info] available since OpenSSH 7.4, Dropbear SSH 2018.76
 `- [info] default key exchange from OpenSSH 7.4 to 8.9
(kex) curve25519-sha256@libssh.org -- [info] available since OpenSSH 6.4, Dropbear SSH 2013.62
 `- [info] default key exchange from OpenSSH 6.5 to 7.3
(kex) curve25519-sha256 -- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 7.4, Dropbear SSH 2018.76
`- [info] default key exchange from OpenSSH 7.4 to 8.9
(kex) curve25519-sha256@libssh.org -- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 6.4, Dropbear SSH 2013.62
`- [info] default key exchange from OpenSSH 6.5 to 7.3
(kex) ecdh-sha2-nistp256 -- [fail] using elliptic curves that are suspected as being backdoored by the U.S. National Security Agency
 `- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 5.7, Dropbear SSH 2013.62
(kex) ecdh-sha2-nistp384 -- [fail] using elliptic curves that are suspected as being backdoored by the U.S. National Security Agency
 `- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 5.7, Dropbear SSH 2013.62
(kex) ecdh-sha2-nistp521 -- [fail] using elliptic curves that are suspected as being backdoored by the U.S. National Security Agency
 `- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 5.7, Dropbear SSH 2013.62
(kex) diffie-hellman-group-exchange-sha256 (4096-bit) -- [info] available since OpenSSH 4.4
 `- [info] OpenSSH's GEX fallback mechanism was triggered during testing. Very old SSH clients will still be able to create connections using a 2048-bit modulus, though modern clients will use 4096. This can only be disabled by recompiling the code (see https://github.com/openssh/openssh-portable/blob/V_9_4/dh.c#L477).
(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-group-exchange-sha256 (4096-bit) -- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 4.4
`- [info] OpenSSH's GEX fallback mechanism was triggered during testing. Very old SSH clients will still be able to create connections using a 2048-bit modulus, though modern clients will use 4096. This can only be disabled by recompiling the code (see https://github.com/openssh/openssh-portable/blob/V_9_4/dh.c#L477).
(kex) diffie-hellman-group16-sha512 -- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 7.3, Dropbear SSH 2016.73
(kex) diffie-hellman-group18-sha512 -- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 7.3
(kex) diffie-hellman-group14-sha256 -- [warn] 2048-bit modulus only provides 112-bits of symmetric strength
 `- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 7.3, Dropbear SSH 2016.73
(kex) diffie-hellman-group14-sha1 -- [fail] using broken SHA-1 hash algorithm
 `- [warn] 2048-bit modulus only provides 112-bits of symmetric strength
 `- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 3.9, Dropbear SSH 0.53
# host-key algorithms
@@ -80,8 +90,13 @@
(rec) -hmac-sha1 -- mac algorithm to remove 
(rec) -hmac-sha1-etm@openssh.com -- mac algorithm to remove 
(rec) -ssh-rsa -- key algorithm to remove 
(rec) !diffie-hellman-group-exchange-sha256 -- kex algorithm to change (increase modulus size to 3072 bits or larger) 
(rec) -chacha20-poly1305@openssh.com -- enc algorithm to remove 
(rec) -curve25519-sha256 -- kex algorithm to remove 
(rec) -curve25519-sha256@libssh.org -- kex algorithm to remove 
(rec) -diffie-hellman-group14-sha256 -- kex algorithm to remove 
(rec) -diffie-hellman-group16-sha512 -- kex algorithm to remove 
(rec) -diffie-hellman-group18-sha512 -- kex 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 
@@ -84,6 +84,9 @@
"info": [
"default key exchange from OpenSSH 7.4 to 8.9",
"available since OpenSSH 7.4, Dropbear SSH 2018.76"
],
"warn": [
"does not provide protection against post-quantum attacks"
]
}
},
@@ -93,6 +96,9 @@
"info": [
"default key exchange from OpenSSH 6.5 to 7.3",
"available since OpenSSH 6.4, Dropbear SSH 2013.62"
],
"warn": [
"does not provide protection against post-quantum attacks"
]
}
},
@@ -104,6 +110,9 @@
],
"info": [
"available since OpenSSH 5.7, Dropbear SSH 2013.62"
],
"warn": [
"does not provide protection against post-quantum attacks"
]
}
},
@@ -115,6 +124,9 @@
],
"info": [
"available since OpenSSH 5.7, Dropbear SSH 2013.62"
],
"warn": [
"does not provide protection against post-quantum attacks"
]
}
},
@@ -126,6 +138,9 @@
],
"info": [
"available since OpenSSH 5.7, Dropbear SSH 2013.62"
],
"warn": [
"does not provide protection against post-quantum attacks"
]
}
},
@@ -136,6 +151,9 @@
"info": [
"OpenSSH's GEX fallback mechanism was triggered during testing. Very old SSH clients will still be able to create connections using a 2048-bit modulus, though modern clients will use 4096. This can only be disabled by recompiling the code (see https://github.com/openssh/openssh-portable/blob/V_9_4/dh.c#L477).",
"available since OpenSSH 4.4"
],
"warn": [
"does not provide protection against post-quantum attacks"
]
}
},
@@ -144,6 +162,9 @@
"notes": {
"info": [
"available since OpenSSH 7.3, Dropbear SSH 2016.73"
],
"warn": [
"does not provide protection against post-quantum attacks"
]
}
},
@@ -152,6 +173,9 @@
"notes": {
"info": [
"available since OpenSSH 7.3"
],
"warn": [
"does not provide protection against post-quantum attacks"
]
}
},
@@ -162,7 +186,8 @@
"available since OpenSSH 7.3, Dropbear SSH 2016.73"
],
"warn": [
"2048-bit modulus only provides 112-bits of symmetric strength"
"2048-bit modulus only provides 112-bits of symmetric strength",
"does not provide protection against post-quantum attacks"
]
}
},
@@ -176,7 +201,8 @@
"available since OpenSSH 3.9, Dropbear SSH 0.53"
],
"warn": [
"2048-bit modulus only provides 112-bits of symmetric strength"
"2048-bit modulus only provides 112-bits of symmetric strength",
"does not provide protection against post-quantum attacks"
]
}
}
@@ -356,6 +382,14 @@
}
},
"warning": {
"chg": {
"kex": [
{
"name": "diffie-hellman-group-exchange-sha256",
"notes": "increase modulus size to 3072 bits or larger"
}
]
},
"del": {
"enc": [
{
@@ -364,9 +398,25 @@
}
],
"kex": [
{
"name": "curve25519-sha256",
"notes": ""
},
{
"name": "curve25519-sha256@libssh.org",
"notes": ""
},
{
"name": "diffie-hellman-group14-sha256",
"notes": ""
},
{
"name": "diffie-hellman-group16-sha512",
"notes": ""
},
{
"name": "diffie-hellman-group18-sha512",
"notes": ""
}
],
"mac": [
@@ -5,24 +5,34 @@
(gen) compression: enabled (zlib@openssh.com)
# key exchange algorithms
(kex) curve25519-sha256 -- [info] available since OpenSSH 7.4, Dropbear SSH 2018.76
 `- [info] default key exchange from OpenSSH 7.4 to 8.9
(kex) curve25519-sha256@libssh.org -- [info] available since OpenSSH 6.4, Dropbear SSH 2013.62
 `- [info] default key exchange from OpenSSH 6.5 to 7.3
(kex) curve25519-sha256 -- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 7.4, Dropbear SSH 2018.76
`- [info] default key exchange from OpenSSH 7.4 to 8.9
(kex) curve25519-sha256@libssh.org -- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 6.4, Dropbear SSH 2013.62
`- [info] default key exchange from OpenSSH 6.5 to 7.3
(kex) ecdh-sha2-nistp256 -- [fail] using elliptic curves that are suspected as being backdoored by the U.S. National Security Agency
 `- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 5.7, Dropbear SSH 2013.62
(kex) ecdh-sha2-nistp384 -- [fail] using elliptic curves that are suspected as being backdoored by the U.S. National Security Agency
 `- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 5.7, Dropbear SSH 2013.62
(kex) ecdh-sha2-nistp521 -- [fail] using elliptic curves that are suspected as being backdoored by the U.S. National Security Agency
 `- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 5.7, Dropbear SSH 2013.62
(kex) diffie-hellman-group-exchange-sha256 (4096-bit) -- [info] available since OpenSSH 4.4
 `- [info] OpenSSH's GEX fallback mechanism was triggered during testing. Very old SSH clients will still be able to create connections using a 2048-bit modulus, though modern clients will use 4096. This can only be disabled by recompiling the code (see https://github.com/openssh/openssh-portable/blob/V_9_4/dh.c#L477).
(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-group-exchange-sha256 (4096-bit) -- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 4.4
`- [info] OpenSSH's GEX fallback mechanism was triggered during testing. Very old SSH clients will still be able to create connections using a 2048-bit modulus, though modern clients will use 4096. This can only be disabled by recompiling the code (see https://github.com/openssh/openssh-portable/blob/V_9_4/dh.c#L477).
(kex) diffie-hellman-group16-sha512 -- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 7.3, Dropbear SSH 2016.73
(kex) diffie-hellman-group18-sha512 -- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 7.3
(kex) diffie-hellman-group14-sha256 -- [warn] 2048-bit modulus only provides 112-bits of symmetric strength
 `- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 7.3, Dropbear SSH 2016.73
(kex) diffie-hellman-group14-sha1 -- [fail] using broken SHA-1 hash algorithm
 `- [warn] 2048-bit modulus only provides 112-bits of symmetric strength
 `- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 3.9, Dropbear SSH 0.53
# host-key algorithms
@@ -72,8 +82,13 @@
(rec) -hmac-sha1-etm@openssh.com -- mac algorithm to remove 
(rec) +rsa-sha2-256 -- key algorithm to append 
(rec) +rsa-sha2-512 -- key algorithm to append 
(rec) !diffie-hellman-group-exchange-sha256 -- kex algorithm to change (increase modulus size to 3072 bits or larger) 
(rec) -chacha20-poly1305@openssh.com -- enc algorithm to remove 
(rec) -curve25519-sha256 -- kex algorithm to remove 
(rec) -curve25519-sha256@libssh.org -- kex algorithm to remove 
(rec) -diffie-hellman-group14-sha256 -- kex algorithm to remove 
(rec) -diffie-hellman-group16-sha512 -- kex algorithm to remove 
(rec) -diffie-hellman-group18-sha512 -- kex 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 
@@ -84,6 +84,9 @@
"info": [
"default key exchange from OpenSSH 7.4 to 8.9",
"available since OpenSSH 7.4, Dropbear SSH 2018.76"
],
"warn": [
"does not provide protection against post-quantum attacks"
]
}
},
@@ -93,6 +96,9 @@
"info": [
"default key exchange from OpenSSH 6.5 to 7.3",
"available since OpenSSH 6.4, Dropbear SSH 2013.62"
],
"warn": [
"does not provide protection against post-quantum attacks"
]
}
},
@@ -103,6 +109,9 @@
"info": [
"OpenSSH's GEX fallback mechanism was triggered during testing. Very old SSH clients will still be able to create connections using a 2048-bit modulus, though modern clients will use 4096. This can only be disabled by recompiling the code (see https://github.com/openssh/openssh-portable/blob/V_9_4/dh.c#L477).",
"available since OpenSSH 4.4"
],
"warn": [
"does not provide protection against post-quantum attacks"
]
}
}
@@ -146,16 +155,6 @@
"recommendations": {
"informational": {
"add": {
"kex": [
{
"name": "diffie-hellman-group16-sha512",
"notes": ""
},
{
"name": "diffie-hellman-group18-sha512",
"notes": ""
}
],
"key": [
{
"name": "rsa-sha2-256",
@@ -169,12 +168,30 @@
}
},
"warning": {
"chg": {
"kex": [
{
"name": "diffie-hellman-group-exchange-sha256",
"notes": "increase modulus size to 3072 bits or larger"
}
]
},
"del": {
"enc": [
{
"name": "chacha20-poly1305@openssh.com",
"notes": ""
}
],
"kex": [
{
"name": "curve25519-sha256",
"notes": ""
},
{
"name": "curve25519-sha256@libssh.org",
"notes": ""
}
]
}
}
@@ -5,12 +5,15 @@
(gen) compression: enabled (zlib@openssh.com)
# key exchange algorithms
(kex) curve25519-sha256 -- [info] available since OpenSSH 7.4, Dropbear SSH 2018.76
 `- [info] default key exchange from OpenSSH 7.4 to 8.9
(kex) curve25519-sha256@libssh.org -- [info] available since OpenSSH 6.4, Dropbear SSH 2013.62
 `- [info] default key exchange from OpenSSH 6.5 to 7.3
(kex) diffie-hellman-group-exchange-sha256 (4096-bit) -- [info] available since OpenSSH 4.4
 `- [info] OpenSSH's GEX fallback mechanism was triggered during testing. Very old SSH clients will still be able to create connections using a 2048-bit modulus, though modern clients will use 4096. This can only be disabled by recompiling the code (see https://github.com/openssh/openssh-portable/blob/V_9_4/dh.c#L477).
(kex) curve25519-sha256 -- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 7.4, Dropbear SSH 2018.76
`- [info] default key exchange from OpenSSH 7.4 to 8.9
(kex) curve25519-sha256@libssh.org -- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 6.4, Dropbear SSH 2013.62
`- [info] default key exchange from OpenSSH 6.5 to 7.3
(kex) diffie-hellman-group-exchange-sha256 (4096-bit) -- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 4.4
`- [info] OpenSSH's GEX fallback mechanism was triggered during testing. Very old SSH clients will still be able to create connections using a 2048-bit modulus, though modern clients will use 4096. This can only be disabled by recompiling the code (see https://github.com/openssh/openssh-portable/blob/V_9_4/dh.c#L477).
# host-key algorithms
(key) ssh-ed25519 -- [info] available since OpenSSH 6.5, Dropbear SSH 2020.79
@@ -34,11 +37,12 @@
(fin) ssh-ed25519: SHA256:UrnXIVH+7dlw8UqYocl48yUEcKrthGDQG2CPCgp7MxU
# algorithm recommendations (for OpenSSH 8.0)
(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) !diffie-hellman-group-exchange-sha256 -- kex algorithm to change (increase modulus size to 3072 bits or larger) 
(rec) -chacha20-poly1305@openssh.com -- enc algorithm to remove 
(rec) -curve25519-sha256 -- kex algorithm to remove 
(rec) -curve25519-sha256@libssh.org -- kex algorithm to remove 
# additional info
(nfo) For hardening guides on common OSes, please see: <https://www.ssh-audit.com/hardening_guides.html>
@@ -43,6 +43,9 @@
"info": [
"default key exchange from OpenSSH 7.4 to 8.9",
"available since OpenSSH 7.4, Dropbear SSH 2018.76"
],
"warn": [
"does not provide protection against post-quantum attacks"
]
}
},
@@ -52,6 +55,9 @@
"info": [
"default key exchange from OpenSSH 6.5 to 7.3",
"available since OpenSSH 6.4, Dropbear SSH 2013.62"
],
"warn": [
"does not provide protection against post-quantum attacks"
]
}
},
@@ -4,10 +4,12 @@
(gen) compression: disabled
# key exchange algorithms
(kex) curve25519-sha256 -- [info] available since OpenSSH 7.4, Dropbear SSH 2018.76
 `- [info] default key exchange from OpenSSH 7.4 to 8.9
(kex) curve25519-sha256@libssh.org -- [info] available since OpenSSH 6.4, Dropbear SSH 2013.62
 `- [info] default key exchange from OpenSSH 6.5 to 7.3
(kex) curve25519-sha256 -- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 7.4, Dropbear SSH 2018.76
`- [info] default key exchange from OpenSSH 7.4 to 8.9
(kex) curve25519-sha256@libssh.org -- [warn] does not provide protection against post-quantum attacks
`- [info] available since OpenSSH 6.4, Dropbear SSH 2013.62
`- [info] default key exchange from OpenSSH 6.5 to 7.3
(kex) sntrup4591761x25519-sha512@tinyssh.org -- [warn] using experimental algorithm
`- [info] available since OpenSSH 8.0
`- [info] the sntrup4591761 algorithm was withdrawn, as it may not provide strong post-quantum security
+3 -14
View File
@@ -8,7 +8,6 @@ class TestAuditConf:
def init(self, ssh_audit):
self.AuditConf = ssh_audit.AuditConf
self.OutputBuffer = ssh_audit.OutputBuffer()
self.usage = ssh_audit.usage
self.process_commandline = process_commandline
@staticmethod
@@ -16,8 +15,6 @@ class TestAuditConf:
options = {
'host': '',
'port': 22,
'ssh1': True,
'ssh2': True,
'batch': False,
'colors': True,
'verbose': False,
@@ -29,8 +26,6 @@ class TestAuditConf:
options[k] = v
assert conf.host == options['host']
assert conf.port == options['port']
assert conf.ssh1 is options['ssh1']
assert conf.ssh2 is options['ssh2']
assert conf.batch is options['batch']
assert conf.colors is options['colors']
assert conf.verbose is options['verbose']
@@ -44,7 +39,7 @@ class TestAuditConf:
def test_audit_conf_booleans(self):
conf = self.AuditConf()
for p in ['ssh1', 'ssh2', 'batch', 'colors', 'verbose']:
for p in ['batch', 'colors', 'verbose']:
for v in [True, 1]:
setattr(conf, p, v)
assert getattr(conf, p) is True
@@ -107,7 +102,7 @@ class TestAuditConf:
def test_audit_conf_process_commandline(self):
# pylint: disable=too-many-statements
c = lambda x: self.process_commandline(self.OutputBuffer, x.split(), self.usage) # noqa
c = lambda x: self.process_commandline(self.OutputBuffer, x.split()) # noqa
with pytest.raises(SystemExit):
conf = c('')
with pytest.raises(SystemExit):
@@ -148,12 +143,6 @@ class TestAuditConf:
conf = c('localhost:99999')
with pytest.raises(SystemExit):
conf = c('-p 99999 localhost')
conf = c('-1 localhost')
self._test_conf(conf, host='localhost', ssh1=True, ssh2=False)
conf = c('-2 localhost')
self._test_conf(conf, host='localhost', ssh1=False, ssh2=True)
conf = c('-12 localhost')
self._test_conf(conf, host='localhost', ssh1=True, ssh2=True)
conf = c('-4 localhost')
self._test_conf(conf, host='localhost', ipv4=True, ipv6=False, ipvo=(4,))
conf = c('-6 localhost')
@@ -163,7 +152,7 @@ class TestAuditConf:
conf = c('-64 localhost')
self._test_conf(conf, host='localhost', ipv4=True, ipv6=True, ipvo=(6, 4))
conf = c('-b localhost')
self._test_conf(conf, host='localhost', batch=True, verbose=True)
self._test_conf(conf, host='localhost', batch=True)
conf = c('-n localhost')
self._test_conf(conf, host='localhost', colors=False)
conf = c('-v localhost')
-1
View File
@@ -166,7 +166,6 @@ class TestErrors:
vsocket.rdata.append(b'SSH-1.3-ssh-audit-test\r\n')
vsocket.rdata.append(b'Protocol major versions differ.\n')
conf = self._conf()
conf.ssh1, conf.ssh2 = True, False
lines = self._audit(output_spy, conf)
assert len(lines) == 4
assert 'error reading packet' in lines[-1]
-174
View File
@@ -1,174 +0,0 @@
import struct
import pytest
from ssh_audit.auditconf import AuditConf
from ssh_audit.fingerprint import Fingerprint
from ssh_audit.outputbuffer import OutputBuffer
from ssh_audit.protocol import Protocol
from ssh_audit.readbuf import ReadBuf
from ssh_audit.ssh1 import SSH1
from ssh_audit.ssh1_publickeymessage import SSH1_PublicKeyMessage
from ssh_audit.ssh_audit import audit
from ssh_audit.writebuf import WriteBuf
# pylint: disable=line-too-long,attribute-defined-outside-init
class TestSSH1:
@pytest.fixture(autouse=True)
def init(self, ssh_audit):
self.OutputBuffer = OutputBuffer
self.protocol = Protocol
self.ssh1 = SSH1
self.PublicKeyMessage = SSH1_PublicKeyMessage
self.rbuf = ReadBuf
self.wbuf = WriteBuf
self.audit = audit
self.AuditConf = AuditConf
self.fingerprint = Fingerprint
def _conf(self):
conf = self.AuditConf('localhost', 22)
conf.colors = False
conf.batch = True
conf.verbose = True
conf.ssh1 = True
conf.ssh2 = False
conf.skip_rate_test = True
return conf
def _create_ssh1_packet(self, payload, valid_crc=True):
padding = -(len(payload) + 4) % 8
plen = len(payload) + 4
pad_bytes = b'\x00' * padding
cksum = self.ssh1.crc32(pad_bytes + payload) if valid_crc else 0
data = struct.pack('>I', plen) + pad_bytes + payload + struct.pack('>I', cksum)
return data
@classmethod
def _server_key(cls):
return (1024, 0x10001, 0xee6552da432e0ac2c422df1a51287507748bfe3b5e3e4fa989a8f49fdc163a17754939ef18ef8a667ea3b71036a151fcd7f5e01ceef1e4439864baf3ac569047582c69d6c128212e0980dcb3168f00d371004039983f6033cd785b8b8f85096c7d9405cbfdc664e27c966356a6b4eb6ee20ad43414b50de18b22829c1880b551)
@classmethod
def _host_key(cls):
return (2048, 0x10001, 0xdfa20cd2a530ccc8c870aa60d9feb3b35deeab81c3215a96557abbd683d21f4600f38e475d87100da9a4404220eeb3bb5584e5a2b5b48ffda58530ea19104a32577d7459d91e76aa711b241050f4cc6d5327ccce254f371acad3be56d46eb5919b73f20dbdb1177b700f00891c5bf4ed128bb90ed541b778288285bcfa28432ab5cbcb8321b6e24760e998e0daa519f093a631e44276d7dd252ce0c08c75e2ab28a7349ead779f97d0f20a6d413bf3623cd216dc35375f6366690bcc41e3b2d5465840ec7ee0dc7e3f1c101d674a0c7dbccbc3942788b111396add2f8153b46a0e4b50d66e57ee92958f1c860dd97cc0e40e32febff915343ed53573142bdf4b)
def _pkm_payload(self):
w = self.wbuf()
w.write(b'\x88\x99\xaa\xbb\xcc\xdd\xee\xff')
b, e, m = self._server_key()
w.write_int(b).write_mpint1(e).write_mpint1(m)
b, e, m = self._host_key()
w.write_int(b).write_mpint1(e).write_mpint1(m)
w.write_int(2)
w.write_int(72)
w.write_int(36)
return w.write_flush()
def test_crc32(self):
assert self.ssh1.crc32(b'') == 0x00
assert self.ssh1.crc32(b'The quick brown fox jumps over the lazy dog') == 0xb9c60808
def test_fingerprint(self):
# pylint: disable=protected-access
b, e, m = self._host_key()
fpd = self.wbuf._create_mpint(m, False)
fpd += self.wbuf._create_mpint(e, False)
fp = self.fingerprint(fpd)
assert b == 2048
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 _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 = hkey
assert pkm.host_key_bits == b
assert pkm.host_key_public_exponent == e
assert pkm.host_key_public_modulus == m
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.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.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.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.PublicKeyMessage(cookie, skey, hkey2, pflags, cmask, amask)
def test_pkm_read(self):
pkm = self.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, hkey = self._server_key(), self._host_key()
pflags, cmask, amask = 2, 72, 36
pkm1 = self.PublicKeyMessage(cookie, skey, hkey, pflags, cmask, amask)
pkm2 = self.PublicKeyMessage.parse(self._pkm_payload())
assert pkm1.payload == pkm2.payload
def test_ssh1_server_simple(self, output_spy, virtual_socket):
vsocket = virtual_socket
w = self.wbuf()
w.write_byte(self.protocol.SMSG_PUBLIC_KEY)
w.write(self._pkm_payload())
vsocket.rdata.append(b'SSH-1.5-OpenSSH_7.2 ssh-audit-test\r\n')
vsocket.rdata.append(self._create_ssh1_packet(w.write_flush()))
output_spy.begin()
out = self.OutputBuffer()
self.audit(out, self._conf())
out.write()
lines = output_spy.flush()
assert len(lines) == 13
def test_ssh1_server_invalid_first_packet(self, output_spy, virtual_socket):
vsocket = virtual_socket
w = self.wbuf()
w.write_byte(self.protocol.SMSG_PUBLIC_KEY + 1)
w.write(self._pkm_payload())
vsocket.rdata.append(b'SSH-1.5-OpenSSH_7.2 ssh-audit-test\r\n')
vsocket.rdata.append(self._create_ssh1_packet(w.write_flush()))
output_spy.begin()
out = self.OutputBuffer()
ret = self.audit(out, self._conf())
out.write()
assert ret != 0
lines = output_spy.flush()
assert len(lines) == 6
assert 'unknown message' in lines[-1]
def test_ssh1_server_invalid_checksum(self, output_spy, virtual_socket):
vsocket = virtual_socket
w = self.wbuf()
w.write_byte(self.protocol.SMSG_PUBLIC_KEY + 1)
w.write(self._pkm_payload())
vsocket.rdata.append(b'SSH-1.5-OpenSSH_7.2 ssh-audit-test\r\n')
vsocket.rdata.append(self._create_ssh1_packet(w.write_flush(), False))
output_spy.begin()
out = self.OutputBuffer()
with pytest.raises(SystemExit):
self.audit(out, self._conf())
out.write()
lines = output_spy.flush()
assert len(lines) == 3
assert ('checksum' in lines[0]) or ('checksum' in lines[1]) or ('checksum' in lines[2])
+1 -1
View File
@@ -165,7 +165,7 @@ class TestSSH2:
self.audit(out, self._conf())
out.write()
lines = output_spy.flush()
assert len(lines) == 78
assert len(lines) == 74
def test_ssh2_server_invalid_first_packet(self, output_spy, virtual_socket):
vsocket = virtual_socket
+38
View File
@@ -8,6 +8,7 @@ class Test_SSH2_KexDB:
@pytest.fixture(autouse=True)
def init(self):
self.db = SSH2_KexDB.get_db()
self.pq_warning = SSH2_KexDB.WARN_NOT_PQ_SAFE
def test_ssh2_kexdb(self):
'''Ensures that the SSH2_KexDB.ALGORITHMS dictionary is in the right format.'''
@@ -33,3 +34,40 @@ class Test_SSH2_KexDB:
# The first entry denotes the versions when this algorithm was added to OpenSSH, Dropbear, and/or libssh, followed by when it was deprecated, and finally when it was removed. Hence it must have between 0 and 3 entries.
added_entry = alg_data[0]
assert 0 <= len(added_entry) <= 3
def test_kex_pq_unsafe(self):
'''Ensures that all key exchange algorithms are marked as post-quantum unsafe, unless they appear in a whitelist.'''
# These algorithms include protections against quantum attacks.
kex_pq_safe = [
"ecdh-nistp256-kyber-512r3-sha256-d00@openquantumsafe.org",
"ecdh-nistp384-kyber-768r3-sha384-d00@openquantumsafe.org",
"ecdh-nistp521-kyber-1024r3-sha512-d00@openquantumsafe.org",
"ext-info-c",
"ext-info-s",
"kex-strict-c-v00@openssh.com",
"kex-strict-s-v00@openssh.com",
"mlkem768x25519-sha256",
"sntrup4591761x25519-sha512@tinyssh.org",
"sntrup761x25519-sha512@openssh.com",
"sntrup761x25519-sha512",
"x25519-kyber-512r3-sha256-d00@amazon.com",
"x25519-kyber512-sha512@aws.amazon.com",
"mlkem768nistp256-sha256", # PQ safe, but has a conventional back-door.
"mlkem1024nistp384-sha384" # PQ safe, but has a conventional back-door.
]
failures = []
for kex_name in self.db['kex']:
# Skip key exchanges that are PQ safe.
if kex_name in kex_pq_safe:
continue
# Ensure all other kex exchanges have the proper PQ unsafe flag set in their warnings list.
alg_data = self.db['kex'][kex_name]
if len(alg_data) < 3 or self.pq_warning not in alg_data[2]:
failures.append(kex_name)
assert failures == []