17 Commits

Author SHA1 Message Date
Bandit Pingu ca08928fae Merge b2e621cafc into 06ebdbd0fe 2024-08-27 17:24:28 +01:00
Joe Testa 06ebdbd0fe Updated README. 2024-08-26 16:46:34 -04:00
Drew Noel 7752023dc2 Switch connect_ex result checks to use errno lookups (#289)
* Switch connect_ex result checks to errno lookups

* Return errno strings, clean up comment
2024-08-26 16:38:44 -04:00
Joe Testa a6f02ae8e8 Added debugging output for key exchanges. 2024-08-26 16:25:32 -04:00
Joe Testa 9049c8476a Updated README. 2024-07-06 21:01:19 -04:00
Daniel Lenski bbbdf71e50 Recognize LANcom LCOS software and support ed448 key extraction (#277)
* Include raw hostkey bytes in debug output

* Recognize LANcom LCOS software and support extraction of ssh-ed448 key type

LANcom router devices appear to be primarily used in Germany (see [1]
for examples on the public Internet), and they appear to support the
`ssh-ed448` key type which is documented in [2], but which has never
been supported by any as-yet-released version of OpenSSH.

[1] https://www.shodan.io/search?query=ssh+%22ed448%22
[2] https://datatracker.ietf.org/doc/html/rfc8709#name-public-key-format
2024-07-06 20:56:24 -04:00
Joe Testa 92db5f0138 Updated docker tests and README due to merge of PR #281. 2024-07-05 10:53:00 -04:00
dreizehnutters bc2a89eb11 fix for https://github.com/jtesta/ssh-audit/issues/280 (#281)
* fix for https://github.com/jtesta/ssh-audit/issues/280

* changed json format to min. the damage for a change
2024-07-05 10:49:16 -04:00
Joe Testa ea117b203b Updated README. 2024-07-05 10:16:06 -04:00
Daniel Lenski d8f8b7c57c Make HostKeyTest class reusable (#278)
Because the `HostKeyTest` class was mutating its static/global
`HOST_KEY_TYPES` dict, this class could not actually be used more than once
in a single thread!

Rather than mutate this dict after parsing each key type
(`HOST_KEY_TYPES[host_key_type]['parsed'] = True`), the `perform_test`
method should simple add the parsed key types to a local `set()`.
2024-07-05 10:11:18 -04:00
Joe Testa e42961fa9a Added built-in policy for OpenSSH 9.8. 2024-07-02 21:31:36 -04:00
Joe Testa dcbc43acdf Fixed crash when running with '-P' and '-T' options simultaneously. (#273) 2024-07-02 20:56:11 -04:00
Joe Testa 87e22ae26b Added IPv6 support for DHEat and connection rate tests. (#269) 2024-06-29 19:05:20 -04:00
FlyingFish b2e621cafc Modified OutputBuffer to have an error function to output to stderr. Change .fail with errors to .error 2024-05-01 23:00:25 +01:00
Joe Testa 46ec4e3edc Added built-in policies for Ubuntu 24.04 LTS server and client. 2024-04-29 19:11:47 -04:00
Joe Testa d19b154a46 Bumped version to v3.3.0-dev. 2024-04-22 17:57:26 -04:00
Joe Testa c5d90106e8 Updated docker run command. 2024-04-22 17:54:37 -04:00
33 changed files with 185 additions and 54 deletions
+10 -1
View File
@@ -202,7 +202,7 @@ To install from Dockerhub:
``` ```
$ docker pull positronsecurity/ssh-audit $ docker pull positronsecurity/ssh-audit
``` ```
(Then run with: `docker run -it -p 2222:2222 positronsecurity/ssh-audit 10.1.1.1`) (Then run with: `docker run -it --rm -p 2222:2222 positronsecurity/ssh-audit 10.1.1.1`)
The status of various other platform packages can be found below (via Repology): The status of various other platform packages can be found below (via Repology):
@@ -213,6 +213,15 @@ For convenience, a web front-end on top of the command-line tool is available at
## ChangeLog ## ChangeLog
### v3.3.0-dev (???)
- Added built-in policies for Ubuntu 24.04 LTS server and client, and OpenSSH 9.8.
- Added IPv6 support for DHEat and connection rate tests.
- Added TCP port information to JSON policy scan results; credit [Fabian Malte Kopp](https://github.com/dreizehnutters).
- Added LANcom LCOS server recognition and Ed448 key extraction; credit [Daniel Lenski](https://github.com/dlenskiSB).
- Fixed crash when running with `-P` and `-T` options simultaneously.
- Fixed host key tests from only reporting a key type at most once despite multiple hosts supporting it; credit [Daniel Lenski](https://github.com/dlenskiSB).
- Fixed DHEat connection rate testing on MacOS X and BSD platforms; credit [Drew Noel](https://github.com/drewmnoel) and [Michael Osipov](https://github.com/michael-o).
### v3.2.0 (2024-04-22) ### v3.2.0 (2024-04-22)
- Added implementation of the DHEat denial-of-service attack (see `--dheat` option; [CVE-2002-20001](https://nvd.nist.gov/vuln/detail/CVE-2002-20001)). - Added implementation of the DHEat denial-of-service attack (see `--dheat` option; [CVE-2002-20001](https://nvd.nist.gov/vuln/detail/CVE-2002-20001)).
- Expanded filter of CBC ciphers to flag for the Terrapin vulnerability. It now includes more rarely found ciphers. - Expanded filter of CBC ciphers to flag for the Terrapin vulnerability. It now includes more rarely found ciphers.
+4 -1
View File
@@ -49,6 +49,7 @@ BUILTIN_POLICIES: Dict[str, Dict[str, Union[Optional[str], Optional[List[str]],
'Hardened Ubuntu Server 22.04 LTS (version 5)': {'version': '5', 'changelog': 'Added kex-strict-s-v00@openssh.com to kex list.', 'banner': None, 'compressions': None, 'host_keys': ['rsa-sha2-512', 'rsa-sha2-256', 'ssh-ed25519'], 'optional_host_keys': ['sk-ssh-ed25519@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'sk-ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['sntrup761x25519-sha512@openssh.com', 'curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256', 'kex-strict-s-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': {"rsa-sha2-256": {"hostkey_size": 4096}, "rsa-sha2-256-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "rsa-sha2-512": {"hostkey_size": 4096}, "rsa-sha2-512-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "sk-ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}, "sk-ssh-ed25519@openssh.com": {"hostkey_size": 256}, "ssh-ed25519": {"hostkey_size": 256}, "ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}}, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 3072}, 'server_policy': True}, 'Hardened Ubuntu Server 22.04 LTS (version 5)': {'version': '5', 'changelog': 'Added kex-strict-s-v00@openssh.com to kex list.', 'banner': None, 'compressions': None, 'host_keys': ['rsa-sha2-512', 'rsa-sha2-256', 'ssh-ed25519'], 'optional_host_keys': ['sk-ssh-ed25519@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'sk-ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['sntrup761x25519-sha512@openssh.com', 'curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256', 'kex-strict-s-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': {"rsa-sha2-256": {"hostkey_size": 4096}, "rsa-sha2-256-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "rsa-sha2-512": {"hostkey_size": 4096}, "rsa-sha2-512-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "sk-ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}, "sk-ssh-ed25519@openssh.com": {"hostkey_size": 256}, "ssh-ed25519": {"hostkey_size": 256}, "ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}}, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 3072}, 'server_policy': True},
'Hardened Ubuntu Server 24.04 LTS (version 1)': {'version': '1', 'changelog': 'Initial version.', 'banner': None, 'compressions': None, 'host_keys': ['ssh-ed25519', 'rsa-sha2-512', 'rsa-sha2-256'], 'optional_host_keys': ['sk-ssh-ed25519@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'sk-ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['sntrup761x25519-sha512@openssh.com', 'curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256', 'diffie-hellman-group16-sha512', 'ext-info-s', 'kex-strict-s-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-gcm@openssh.com', 'aes128-ctr'], 'macs': ['hmac-sha2-512-etm@openssh.com', 'hmac-sha2-256-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': {"rsa-sha2-256": {"hostkey_size": 4096}, "rsa-sha2-256-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "rsa-sha2-512": {"hostkey_size": 4096}, "rsa-sha2-512-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "sk-ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}, "sk-ssh-ed25519@openssh.com": {"hostkey_size": 256}, "ssh-ed25519": {"hostkey_size": 256}, "ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}}, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 3072}, 'server_policy': True},
# Generic OpenSSH Server policies # Generic OpenSSH Server policies
@@ -94,6 +95,8 @@ BUILTIN_POLICIES: Dict[str, Dict[str, Union[Optional[str], Optional[List[str]],
'Hardened OpenSSH Server v9.7 (version 1)': {'version': '1', 'changelog': 'Initial version.', 'banner': None, 'compressions': None, 'host_keys': ['rsa-sha2-512', 'rsa-sha2-256', 'ssh-ed25519'], 'optional_host_keys': ['sk-ssh-ed25519@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'sk-ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['sntrup761x25519-sha512@openssh.com', 'curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256', 'ext-info-s', 'kex-strict-s-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': {"rsa-sha2-256": {"hostkey_size": 4096}, "rsa-sha2-256-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "rsa-sha2-512": {"hostkey_size": 4096}, "rsa-sha2-512-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "sk-ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}, "sk-ssh-ed25519@openssh.com": {"hostkey_size": 256}, "ssh-ed25519": {"hostkey_size": 256}, "ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}}, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 3072}, 'server_policy': True}, 'Hardened OpenSSH Server v9.7 (version 1)': {'version': '1', 'changelog': 'Initial version.', 'banner': None, 'compressions': None, 'host_keys': ['rsa-sha2-512', 'rsa-sha2-256', 'ssh-ed25519'], 'optional_host_keys': ['sk-ssh-ed25519@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'sk-ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['sntrup761x25519-sha512@openssh.com', 'curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256', 'ext-info-s', 'kex-strict-s-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': {"rsa-sha2-256": {"hostkey_size": 4096}, "rsa-sha2-256-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "rsa-sha2-512": {"hostkey_size": 4096}, "rsa-sha2-512-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "sk-ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}, "sk-ssh-ed25519@openssh.com": {"hostkey_size": 256}, "ssh-ed25519": {"hostkey_size": 256}, "ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}}, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 3072}, 'server_policy': True},
'Hardened OpenSSH Server v9.8 (version 1)': {'version': '1', 'changelog': 'Initial version.', 'banner': None, 'compressions': None, 'host_keys': ['rsa-sha2-512', 'rsa-sha2-256', 'ssh-ed25519'], 'optional_host_keys': ['sk-ssh-ed25519@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'sk-ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['sntrup761x25519-sha512@openssh.com', 'curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256', 'ext-info-s', 'kex-strict-s-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': {"rsa-sha2-256": {"hostkey_size": 4096}, "rsa-sha2-256-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "rsa-sha2-512": {"hostkey_size": 4096}, "rsa-sha2-512-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "sk-ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}, "sk-ssh-ed25519@openssh.com": {"hostkey_size": 256}, "ssh-ed25519": {"hostkey_size": 256}, "ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}}, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 3072}, 'server_policy': True},
# Amazon Linux Policies # Amazon Linux Policies
@@ -109,7 +112,6 @@ BUILTIN_POLICIES: Dict[str, Dict[str, Union[Optional[str], Optional[List[str]],
'Hardened Rocky Linux Client 9 (version 1)': {'version': '1', 'changelog': 'Initial version.', 'banner': None, 'compressions': None, 'host_keys': ['sk-ssh-ed25519-cert-v01@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'sk-ssh-ed25519@openssh.com', 'ssh-ed25519', 'rsa-sha2-512', 'rsa-sha2-256'], 'optional_host_keys': None, 'kex': ['sntrup761x25519-sha512@openssh.com', 'curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256', 'ext-info-c', 'kex-strict-c-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': None, 'dh_modulus_sizes': None, 'server_policy': False}, 'Hardened Rocky Linux Client 9 (version 1)': {'version': '1', 'changelog': 'Initial version.', 'banner': None, 'compressions': None, 'host_keys': ['sk-ssh-ed25519-cert-v01@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'sk-ssh-ed25519@openssh.com', 'ssh-ed25519', 'rsa-sha2-512', 'rsa-sha2-256'], 'optional_host_keys': None, 'kex': ['sntrup761x25519-sha512@openssh.com', 'curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256', 'ext-info-c', 'kex-strict-c-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': None, 'dh_modulus_sizes': None, 'server_policy': False},
# Ubuntu Client policies # Ubuntu Client policies
'Hardened Ubuntu Client 16.04 LTS (version 2)': {'version': '2', 'changelog': 'No change log available.', 'banner': None, 'compressions': None, 'host_keys': ['ssh-ed25519', 'ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256', 'rsa-sha2-512'], 'optional_host_keys': None, 'kex': ['curve25519-sha256@libssh.org', 'diffie-hellman-group-exchange-sha256', 'ext-info-c'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': None, 'dh_modulus_sizes': None, 'server_policy': False}, 'Hardened Ubuntu Client 16.04 LTS (version 2)': {'version': '2', 'changelog': 'No change log available.', 'banner': None, 'compressions': None, 'host_keys': ['ssh-ed25519', 'ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256', 'rsa-sha2-512'], 'optional_host_keys': None, 'kex': ['curve25519-sha256@libssh.org', 'diffie-hellman-group-exchange-sha256', 'ext-info-c'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': None, 'dh_modulus_sizes': None, 'server_policy': False},
@@ -120,5 +122,6 @@ BUILTIN_POLICIES: Dict[str, Dict[str, Union[Optional[str], Optional[List[str]],
'Hardened Ubuntu Client 22.04 LTS (version 4)': {'version': '4', 'changelog': 'Added kex-strict-c-v00@openssh.com to kex list.', 'banner': None, 'compressions': None, 'host_keys': ['sk-ssh-ed25519-cert-v01@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'sk-ssh-ed25519@openssh.com', 'ssh-ed25519', 'rsa-sha2-512', 'rsa-sha2-256'], 'optional_host_keys': None, 'kex': ['sntrup761x25519-sha512@openssh.com', 'curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256', 'ext-info-c', 'kex-strict-c-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': None, 'dh_modulus_sizes': None, 'server_policy': False}, 'Hardened Ubuntu Client 22.04 LTS (version 4)': {'version': '4', 'changelog': 'Added kex-strict-c-v00@openssh.com to kex list.', 'banner': None, 'compressions': None, 'host_keys': ['sk-ssh-ed25519-cert-v01@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'sk-ssh-ed25519@openssh.com', 'ssh-ed25519', 'rsa-sha2-512', 'rsa-sha2-256'], 'optional_host_keys': None, 'kex': ['sntrup761x25519-sha512@openssh.com', 'curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256', 'ext-info-c', 'kex-strict-c-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': None, 'dh_modulus_sizes': None, 'server_policy': False},
'Hardened Ubuntu Client 24.04 LTS (version 1)': {'version': '1', 'changelog': 'Initial version.', 'banner': None, 'compressions': None, 'host_keys': ['sk-ssh-ed25519-cert-v01@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'sk-ssh-ed25519@openssh.com', 'ssh-ed25519', 'rsa-sha2-512', 'rsa-sha2-256'], 'optional_host_keys': None, 'kex': ['sntrup761x25519-sha512@openssh.com', 'curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256', 'diffie-hellman-group16-sha512', 'ext-info-c', 'kex-strict-c-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-gcm@openssh.com', 'aes128-ctr'], 'macs': ['hmac-sha2-512-etm@openssh.com', 'hmac-sha2-256-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': None, 'dh_modulus_sizes': None, 'server_policy': False},
} }
+38 -10
View File
@@ -21,6 +21,7 @@
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
""" """
import errno
import multiprocessing import multiprocessing
import os import os
import queue import queue
@@ -160,6 +161,11 @@ class DHEat:
# The SSH2_Kex object that we recieved from the server in a prior connection. We'll use it as a template to craft our own kex. # The SSH2_Kex object that we recieved from the server in a prior connection. We'll use it as a template to craft our own kex.
self.kex = kex self.kex = kex
# Resolve the target to an IP address depending on the user preferences (IPv4 or IPv6).
self.debug("Resolving target %s..." % self.target)
self.target_address_family, self.target_ip_address = DHEat._resolve_hostname(self.target, aconf.ip_version_preference)
self.debug("Resolved %s to %s (address family %u)" % (self.target, self.target_ip_address, self.target_address_family))
# The connection and read timeouts. # The connection and read timeouts.
self.connect_timeout = aconf.timeout self.connect_timeout = aconf.timeout
self.read_timeout = aconf.timeout self.read_timeout = aconf.timeout
@@ -324,6 +330,11 @@ class DHEat:
print("\n%sUnfortunately, this feature is not currently functional under Windows.%s This should get fixed in a future release. See: <https://github.com/jtesta/ssh-audit/issues/261>" % (DHEat.YELLOWB, DHEat.CLEAR)) print("\n%sUnfortunately, this feature is not currently functional under Windows.%s This should get fixed in a future release. See: <https://github.com/jtesta/ssh-audit/issues/261>" % (DHEat.YELLOWB, DHEat.CLEAR))
return "" return ""
# Resolve the target into an IP address
out.d("Resolving target %s..." % aconf.host)
target_address_family, target_ip_address = DHEat._resolve_hostname(aconf.host, aconf.ip_version_preference)
out.d("Resolved %s to %s (address family %u)" % (aconf.host, target_ip_address, target_address_family))
spinner = ["-", "\\", "|", "/"] spinner = ["-", "\\", "|", "/"]
spinner_index = 0 spinner_index = 0
@@ -349,7 +360,7 @@ class DHEat:
rate_str = " at a max rate of %s%u%s connections per second" % (DHEat.WHITEB, aconf.conn_rate_test_target_rate, DHEat.CLEAR) rate_str = " at a max rate of %s%u%s connections per second" % (DHEat.WHITEB, aconf.conn_rate_test_target_rate, DHEat.CLEAR)
print() print()
print("Performing non-disruptive rate test against %s[%s]:%u%s with %s%u%s concurrent sockets%s. No Diffie-Hellman requests will be sent." % (DHEat.WHITEB, aconf.host, aconf.port, DHEat.CLEAR, DHEat.WHITEB, concurrent_sockets, DHEat.CLEAR, rate_str)) print("Performing non-disruptive rate test against %s[%s]:%u%s with %s%u%s concurrent sockets%s. No Diffie-Hellman requests will be sent." % (DHEat.WHITEB, target_ip_address, aconf.port, DHEat.CLEAR, DHEat.WHITEB, concurrent_sockets, DHEat.CLEAR, rate_str))
print() print()
# Make room for the multi-line output. # Make room for the multi-line output.
@@ -426,16 +437,16 @@ class DHEat:
# Open new sockets until we've hit the number of concurrent sockets, or if we exceeded the number of maximum connections. # Open new sockets until we've hit the number of concurrent sockets, or if we exceeded the number of maximum connections.
while (len(socket_dict) < concurrent_sockets) and (len(socket_dict) + num_opened_connections < max_connections): while (len(socket_dict) < concurrent_sockets) and (len(socket_dict) + num_opened_connections < max_connections):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s = socket.socket(target_address_family, socket.SOCK_STREAM)
s.setblocking(False) s.setblocking(False)
# out.d("Creating socket (%u of %u already exist)..." % (len(socket_dict), concurrent_sockets), write_now=True) # out.d("Creating socket (%u of %u already exist)..." % (len(socket_dict), concurrent_sockets), write_now=True)
ret = s.connect_ex((aconf.host, aconf.port)) ret = s.connect_ex((target_ip_address, aconf.port))
num_attempted_connections += 1 num_attempted_connections += 1
if ret in [0, 115]: # Check if connection is successful or EINPROGRESS. if ret in [0, errno.EINPROGRESS]:
socket_dict[s] = now socket_dict[s] = now
else: else:
out.d("connect_ex() returned: %d" % ret, write_now=True) out.d("connect_ex() returned: %s (%d)" % (os.strerror(ret), ret), write_now=True)
# out.d("Calling select() on %u sockets..." % len(socket_dict), write_now=True) # out.d("Calling select() on %u sockets..." % len(socket_dict), write_now=True)
socket_list: List[socket.socket] = [*socket_dict] # Get a list of sockets from the dictionary. socket_list: List[socket.socket] = [*socket_dict] # Get a list of sockets from the dictionary.
@@ -743,6 +754,22 @@ class DHEat:
print() print()
@staticmethod
def _resolve_hostname(host: str, ip_version_preference: List[int]) -> Tuple[int, str]:
'''Resolves a hostname to its IPv4 or IPv6 address, depending on user preference.'''
family = socket.AF_UNSPEC
if len(ip_version_preference) == 1:
family = socket.AF_INET if ip_version_preference[0] == 4 else socket.AF_INET6
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 -1, ''
def _run(self) -> bool: def _run(self) -> bool:
'''Where all the magic happens.''' '''Where all the magic happens.'''
@@ -751,7 +778,7 @@ class DHEat:
if sys.platform == "win32": if sys.platform == "win32":
self.output("%sWARNING:%s this feature has not been thoroughly tested on Windows. It may perform worse than on UNIX OSes." % (self.YELLOWB, self.CLEAR)) self.output("%sWARNING:%s this feature has not been thoroughly tested on Windows. It may perform worse than on UNIX OSes." % (self.YELLOWB, self.CLEAR))
self.output("Running DHEat test against %s[%s]:%u%s with %s%u%s concurrent sockets..." % (self.WHITEB, self.target, self.port, self.CLEAR, self.WHITEB, self.concurrent_connections, self.CLEAR)) self.output("Running DHEat test against %s[%s]:%u%s with %s%u%s concurrent sockets..." % (self.WHITEB, self.target_ip_address, self.port, self.CLEAR, self.WHITEB, self.concurrent_connections, self.CLEAR))
# If the user didn't specify an exact kex algorithm to test, check our prioritized list against what the server supports. Larger p-values (such as group18: 8192-bits) cause the most strain on the server. # If the user didn't specify an exact kex algorithm to test, check our prioritized list against what the server supports. Larger p-values (such as group18: 8192-bits) cause the most strain on the server.
chosen_alg = "" chosen_alg = ""
@@ -894,7 +921,8 @@ class DHEat:
# Copy variables from the object (which might exist in another process?). This might cut down on inter-process overhead. # Copy variables from the object (which might exist in another process?). This might cut down on inter-process overhead.
connect_timeout = self.connect_timeout connect_timeout = self.connect_timeout
target = self.target target_ip_address = self.target_ip_address
target_address_family = self.target_address_family
port = self.port port = self.port
# Determine if we are attacking with a GEX. # Determine if we are attacking with a GEX.
@@ -945,17 +973,17 @@ class DHEat:
num_socket_exceptions = 0 num_socket_exceptions = 0
num_openssh_throttled_connections = 0 num_openssh_throttled_connections = 0
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s = socket.socket(target_address_family, socket.SOCK_STREAM)
s.settimeout(connect_timeout) s.settimeout(connect_timeout)
# Loop until a successful TCP connection is made. # Loop until a successful TCP connection is made.
connected = False connected = False
while not connected: while not connected:
# self.debug("Connecting to %s:%d" % (self.target, self.port)) # self.debug("Connecting to %s:%d" % (self.target_ip_address, self.port))
try: try:
num_attempted_tcp_connections += 1 num_attempted_tcp_connections += 1
s.connect((target, port)) s.connect((target_ip_address, port))
connected = True connected = True
except OSError as e: except OSError as e:
self.debug("Failed to connect: %s" % str(e)) self.debug("Failed to connect: %s" % str(e))
+1 -1
View File
@@ -110,7 +110,7 @@ class GEXTest:
# before continuing to issue reconnects. # before continuing to issue reconnects.
modulus_size_returned, reconnect_failed = GEXTest._send_init(out, s, kex_group, kex, gex_alg, bits_min, bits_pref, bits_max) modulus_size_returned, reconnect_failed = GEXTest._send_init(out, s, kex_group, kex, gex_alg, bits_min, bits_pref, bits_max)
if reconnect_failed: if reconnect_failed:
out.fail('Reconnect failed.') out.error('Reconnect failed.')
return exitcodes.FAILURE return exitcodes.FAILURE
if modulus_size_returned > 0: if modulus_size_returned > 0:
+1 -1
View File
@@ -22,7 +22,7 @@
THE SOFTWARE. THE SOFTWARE.
""" """
# The version to display. # The version to display.
VERSION = 'v3.2.0' VERSION = 'v3.3.0-dev'
# SSH software to impersonate # SSH software to impersonate
SSH_HEADER = 'SSH-{0}-OpenSSH_8.2' SSH_HEADER = 'SSH-{0}-OpenSSH_8.2'
+9 -4
View File
@@ -40,7 +40,7 @@ class HostKeyTest:
# Tracks the RSA host key types. As of this writing, testing one in this family yields valid results for the rest. # Tracks the RSA host key types. As of this writing, testing one in this family yields valid results for the rest.
RSA_FAMILY = ['ssh-rsa', 'rsa-sha2-256', 'rsa-sha2-512'] RSA_FAMILY = ['ssh-rsa', 'rsa-sha2-256', 'rsa-sha2-512']
# Dict holding the host key types we should extract & parse. 'cert' is True to denote that a host key type handles certificates (thus requires additional parsing). 'variable_key_len' is True for host key types that can have variable sizes (True only for RSA types, as the rest are of fixed-size). After the host key type is fully parsed, the key 'parsed' is added with a value of True. # Dict holding the host key types we should extract & parse. 'cert' is True to denote that a host key type handles certificates (thus requires additional parsing). 'variable_key_len' is True for host key types that can have variable sizes (True only for RSA types, as the rest are of fixed-size).
HOST_KEY_TYPES = { HOST_KEY_TYPES = {
'ssh-rsa': {'cert': False, 'variable_key_len': True}, 'ssh-rsa': {'cert': False, 'variable_key_len': True},
'rsa-sha2-256': {'cert': False, 'variable_key_len': True}, 'rsa-sha2-256': {'cert': False, 'variable_key_len': True},
@@ -52,6 +52,9 @@ class HostKeyTest:
'ssh-ed25519': {'cert': False, 'variable_key_len': False}, 'ssh-ed25519': {'cert': False, 'variable_key_len': False},
'ssh-ed25519-cert-v01@openssh.com': {'cert': True, 'variable_key_len': False}, 'ssh-ed25519-cert-v01@openssh.com': {'cert': True, 'variable_key_len': False},
'ssh-ed448': {'cert': False, 'variable_key_len': False},
# 'ssh-ed448-cert-v01@openssh.com': {'cert': True, 'variable_key_len': False},
} }
TWO2K_MODULUS_WARNING = '2048-bit modulus only provides 112-bits of symmetric strength' TWO2K_MODULUS_WARNING = '2048-bit modulus only provides 112-bits of symmetric strength'
@@ -93,6 +96,7 @@ class HostKeyTest:
def perform_test(out: 'OutputBuffer', s: 'SSH_Socket', server_kex: 'SSH2_Kex', kex_str: str, kex_group: 'KexDH', host_key_types: Dict[str, Dict[str, bool]]) -> None: def perform_test(out: 'OutputBuffer', s: 'SSH_Socket', server_kex: 'SSH2_Kex', kex_str: str, kex_group: 'KexDH', host_key_types: Dict[str, Dict[str, bool]]) -> None:
hostkey_modulus_size = 0 hostkey_modulus_size = 0
ca_modulus_size = 0 ca_modulus_size = 0
parsed_host_key_types = set()
# If the connection still exists, close it so we can test # If the connection still exists, close it so we can test
# using a clean slate (otherwise it may exist in a non-testable # using a clean slate (otherwise it may exist in a non-testable
@@ -106,7 +110,7 @@ class HostKeyTest:
key_warn_comments = [] key_warn_comments = []
# Skip those already handled (i.e.: those in the RSA family, as testing one tests them all). # Skip those already handled (i.e.: those in the RSA family, as testing one tests them all).
if 'parsed' in host_key_types[host_key_type] and host_key_types[host_key_type]['parsed']: if host_key_type in parsed_host_key_types:
continue continue
# If this host key type is supported by the server, we test it. # If this host key type is supported by the server, we test it.
@@ -157,6 +161,7 @@ class HostKeyTest:
ca_key_type = kex_group.get_ca_type() ca_key_type = kex_group.get_ca_type()
ca_modulus_size = kex_group.get_ca_size() ca_modulus_size = kex_group.get_ca_size()
out.d("Hostkey type: [%s]; hostkey size: %u; CA type: [%s]; CA modulus size: %u" % (host_key_type, hostkey_modulus_size, ca_key_type, ca_modulus_size), write_now=True) out.d("Hostkey type: [%s]; hostkey size: %u; CA type: [%s]; CA modulus size: %u" % (host_key_type, hostkey_modulus_size, ca_key_type, ca_modulus_size), write_now=True)
out.d("Raw hostkey bytes (%d): [%s]" % (len(raw_hostkey_bytes), raw_hostkey_bytes.hex()), write_now=True)
# Record all the host key info. # Record all the host key info.
server_kex.set_host_key(host_key_type, raw_hostkey_bytes, hostkey_modulus_size, ca_key_type, ca_modulus_size) server_kex.set_host_key(host_key_type, raw_hostkey_bytes, hostkey_modulus_size, ca_key_type, ca_modulus_size)
@@ -216,7 +221,7 @@ class HostKeyTest:
# If this host key type is in the RSA family, then mark them all as parsed (since results in one are valid for them all). # If this host key type is in the RSA family, then mark them all as parsed (since results in one are valid for them all).
if host_key_type in HostKeyTest.RSA_FAMILY: if host_key_type in HostKeyTest.RSA_FAMILY:
for rsa_type in HostKeyTest.RSA_FAMILY: for rsa_type in HostKeyTest.RSA_FAMILY:
host_key_types[rsa_type]['parsed'] = True parsed_host_key_types.add(rsa_type)
# If the current key is a member of the RSA family, then populate all RSA family members with the same # If the current key is a member of the RSA family, then populate all RSA family members with the same
# failure and/or warning comments. # failure and/or warning comments.
@@ -228,7 +233,7 @@ class HostKeyTest:
db['key'][rsa_type][2].extend(key_warn_comments) db['key'][rsa_type][2].extend(key_warn_comments)
else: else:
host_key_types[host_key_type]['parsed'] = True parsed_host_key_types.add(host_key_type)
db = SSH2_KexDB.get_db() db = SSH2_KexDB.get_db()
while len(db['key'][host_key_type]) < 3: while len(db['key'][host_key_type]) < 3:
db['key'][host_key_type].append([]) db['key'][host_key_type].append([])
+3
View File
@@ -134,6 +134,9 @@ class KexDH: # pragma: nocover
if self.__hostkey_type == 'ssh-ed25519': if self.__hostkey_type == 'ssh-ed25519':
self.out.d("%s has a fixed host key modulus of 32." % self.__hostkey_type) self.out.d("%s has a fixed host key modulus of 32." % self.__hostkey_type)
self.__hostkey_n_len = 32 self.__hostkey_n_len = 32
elif self.__hostkey_type == 'ssh-ed448':
self.out.d("%s has a fixed host key modulus of 57." % self.__hostkey_type)
self.__hostkey_n_len = 57
else: else:
# Here is the modulus size & actual modulus of the host key public key. # Here is the modulus size & actual modulus of the host key public key.
hostkey_n, self.__hostkey_n_len, ptr = KexDH.__get_bytes(hostkey, ptr) hostkey_n, self.__hostkey_n_len, ptr = KexDH.__get_bytes(hostkey, ptr)
+8
View File
@@ -54,6 +54,14 @@ class OutputBuffer:
self.__is_color_supported = ('colorama' in sys.modules) or (os.name == 'posix') self.__is_color_supported = ('colorama' in sys.modules) or (os.name == 'posix')
self.line_ended = True self.line_ended = True
def error(self, msg, line_ended=True):
"""
Writes an error message to stderr.
"""
end = '' if line_ended else '\n'
sys.stderr.write(f'{msg}{end}')
sys.stderr.flush()
def _print(self, level: str, s: str = '', line_ended: bool = True) -> None: def _print(self, level: str, s: str = '', line_ended: bool = True) -> None:
'''Saves output to buffer (if in buffered mode), or immediately prints to stdout otherwise.''' '''Saves output to buffer (if in buffered mode), or immediately prints to stdout otherwise.'''
+26
View File
@@ -605,3 +605,29 @@ macs = %s
dh_modulus_sizes_str = str(self._dh_modulus_sizes) dh_modulus_sizes_str = str(self._dh_modulus_sizes)
return "Name: %s\nVersion: %s\nAllow Algorithm Subset and/or Reordering: %r\nBanner: %s\nCompressions: %s\nHost Keys: %s\nOptional Host Keys: %s\nKey Exchanges: %s\nCiphers: %s\nMACs: %s\nHost Key Sizes: %s\nDH Modulus Sizes: %s\nServer Policy: %r" % (name, version, self._allow_algorithm_subset_and_reordering, banner, compressions_str, host_keys_str, optional_host_keys_str, kex_str, ciphers_str, macs_str, hostkey_sizes_str, dh_modulus_sizes_str, self._server_policy) return "Name: %s\nVersion: %s\nAllow Algorithm Subset and/or Reordering: %r\nBanner: %s\nCompressions: %s\nHost Keys: %s\nOptional Host Keys: %s\nKey Exchanges: %s\nCiphers: %s\nMACs: %s\nHost Key Sizes: %s\nDH Modulus Sizes: %s\nServer Policy: %r" % (name, version, self._allow_algorithm_subset_and_reordering, banner, compressions_str, host_keys_str, optional_host_keys_str, kex_str, ciphers_str, macs_str, hostkey_sizes_str, dh_modulus_sizes_str, self._server_policy)
def __getstate__(self) -> Dict[str, Any]:
'''Called when pickling this object. The file descriptor isn't serializable, so we'll remove it from the state and include a string representation.'''
state = self.__dict__.copy()
if state['_warning_target'] == sys.stdout:
state['_warning_target_type'] = 'stdout'
else:
state['_warning_target_type'] = 'stderr'
del state['_warning_target']
return state
def __setstate__(self, state: Dict[str, Any]) -> None:
'''Called when unpickling this object. Based on the string representation of the file descriptor, we'll restore the right handle.'''
if state['_warning_target_type'] == 'stdout':
state['_warning_target'] = sys.stdout
else:
state['_warning_target'] = sys.stderr
del state['_warning_target_type']
self.__dict__.update(state)
+4
View File
@@ -224,4 +224,8 @@ class Software:
mx = re.match(r'^PuTTY_Release_(.*)', software) mx = re.match(r'^PuTTY_Release_(.*)', software)
if mx: if mx:
return cls(None, Product.PuTTY, mx.group(1), None, None) return cls(None, Product.PuTTY, mx.group(1), None, None)
mx = re.match(r'^lancom(.*)', software)
if mx:
v, p = 'LANcom', 'LCOS sshd'
return cls(v, p, mx.group(1), None, None)
return None return None
+14 -1
View File
@@ -1,7 +1,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (C) 2017-2020 Joe Testa (jtesta@positronsecurity.com) Copyright (C) 2017-2024 Joe Testa (jtesta@positronsecurity.com)
Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu) Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu)
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -132,3 +132,16 @@ class SSH2_Kex:
srv = SSH2_KexParty(srv_enc, srv_mac, srv_compression, srv_languages) srv = SSH2_KexParty(srv_enc, srv_mac, srv_compression, srv_languages)
kex = cls(outputbuffer, cookie, kex_algs, key_algs, cli, srv, follows, unused) kex = cls(outputbuffer, cookie, kex_algs, key_algs, cli, srv, follows, unused)
return kex return kex
def __str__(self) -> str:
ret = "----\nSSH2_Kex object:"
ret += "\nHost keys: "
ret += ", ".join(self.__key_algs)
ret += "\nKey exchanges: "
ret += ", ".join(self.__kex_algs)
ret += "\nClient SSH2_KexParty:"
ret += "\n" + str(self.__client)
ret += "\nServer SSH2_KexParty:"
ret += "\n" + str(self.__server)
ret += "\n----"
return ret
+8
View File
@@ -1,6 +1,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (C) 2024 Joe Testa (jtesta@positronsecurity.com)
Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu) Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu)
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -48,3 +49,10 @@ class SSH2_KexParty:
@property @property
def languages(self) -> List[str]: def languages(self) -> List[str]:
return self.__languages return self.__languages
def __str__(self) -> str:
ret = "Ciphers: " + ", ".join(self.__enc)
ret += "\nMACs: " + ", ".join(self.__mac)
ret += "\nCompressions: " + ", ".join(self.__compression)
ret += "\nLanguages: " + ", ".join(self.__languages)
return ret
+13 -10
View File
@@ -2,7 +2,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (C) 2017-2023 Joe Testa (jtesta@positronsecurity.com) Copyright (C) 2017-2024 Joe Testa (jtesta@positronsecurity.com)
Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu) Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu)
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -88,7 +88,7 @@ def usage(uout: OutputBuffer, err: Optional[str] = None) -> None:
p = os.path.basename(sys.argv[0]) p = os.path.basename(sys.argv[0])
uout.head('# {} {}, https://github.com/jtesta/ssh-audit\n'.format(p, VERSION)) uout.head('# {} {}, https://github.com/jtesta/ssh-audit\n'.format(p, VERSION))
if err is not None and len(err) > 0: if err is not None and len(err) > 0:
uout.fail(err + '\n') uout.error(err + '\n')
retval = exitcodes.UNKNOWN_ERROR retval = exitcodes.UNKNOWN_ERROR
uout.info('usage: {0} [options] <host>\n'.format(p)) uout.info('usage: {0} [options] <host>\n'.format(p))
uout.info(' -h, --help print this help') uout.info(' -h, --help print this help')
@@ -421,6 +421,8 @@ def output_recommendations(out: OutputBuffer, algs: Algorithms, algorithm_recomm
fn = level_to_output[level] fn = level_to_output[level]
an = '?'
sg = '?'
if action == 'del': if action == 'del':
an, sg = 'remove', '-' an, sg = 'remove', '-'
ret = False ret = False
@@ -733,7 +735,7 @@ def evaluate_policy(out: OutputBuffer, aconf: AuditConf, banner: Optional['Banne
passed, error_struct, error_str = aconf.policy.evaluate(banner, kex) passed, error_struct, error_str = aconf.policy.evaluate(banner, kex)
if aconf.json: if aconf.json:
json_struct = {'host': aconf.host, 'policy': aconf.policy.get_name_and_version(), 'passed': passed, 'errors': error_struct} json_struct = {'host': aconf.host, 'port': aconf.port, 'policy': aconf.policy.get_name_and_version(), 'passed': passed, 'errors': error_struct}
out.info(json.dumps(json_struct, indent=4 if aconf.json_print_indent else None, sort_keys=True)) out.info(json.dumps(json_struct, indent=4 if aconf.json_print_indent else None, sort_keys=True))
else: else:
spacing = '' spacing = ''
@@ -833,7 +835,7 @@ def list_policies(out: OutputBuffer, verbose: bool) -> None:
out.sep() out.sep()
if len(server_policy_names) == 0 and len(client_policy_names) == 0: if len(server_policy_names) == 0 and len(client_policy_names) == 0:
out.fail("Error: no built-in policies found!") out.error("Error: no built-in policies found!")
else: else:
out.info("\nHint: Use -P and provide the full name of a policy to run a policy scan with.\n") out.info("\nHint: Use -P and provide the full name of a policy to run a policy scan with.\n")
out.info("Hint: Use -L -v to also see the change log for each policy.\n") out.info("Hint: Use -L -v to also see the change log for each policy.\n")
@@ -1049,19 +1051,19 @@ def process_commandline(out: OutputBuffer, args: List[str], usage_cb: Callable[.
try: try:
aconf.policy = Policy(policy_file=aconf.policy_file, json_output=aconf.json) aconf.policy = Policy(policy_file=aconf.policy_file, json_output=aconf.json)
except Exception as e: except Exception as e:
out.fail("Error while loading policy file: %s: %s" % (str(e), traceback.format_exc())) out.error("Error while loading policy file: %s: %s" % (str(e), traceback.format_exc()))
out.write() out.write()
sys.exit(exitcodes.UNKNOWN_ERROR) sys.exit(exitcodes.UNKNOWN_ERROR)
# If the user wants to do a client audit, but provided a server policy, terminate. # 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(): if aconf.client_audit and aconf.policy.is_server_policy():
out.fail("Error: client audit selected, but server policy provided.") out.error("Error: client audit selected, but server policy provided.")
out.write() out.write()
sys.exit(exitcodes.UNKNOWN_ERROR) sys.exit(exitcodes.UNKNOWN_ERROR)
# If the user wants to do a server audit, but provided a client policy, terminate. # 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: 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.error("Error: server audit selected, but client policy provided.")
out.write() out.write()
sys.exit(exitcodes.UNKNOWN_ERROR) sys.exit(exitcodes.UNKNOWN_ERROR)
@@ -1260,7 +1262,7 @@ def audit(out: OutputBuffer, aconf: AuditConf, sshv: Optional[int] = None, print
err = s.connect() err = s.connect()
if err is not None: if err is not None:
out.fail(err) out.error(err)
# If we're running against multiple targets, return a connection error to the calling worker thread. Otherwise, write the error message to the console and exit. # If we're running against multiple targets, return a connection error to the calling worker thread. Otherwise, write the error message to the console and exit.
if len(aconf.target_list) > 0: if len(aconf.target_list) > 0:
@@ -1308,15 +1310,16 @@ def audit(out: OutputBuffer, aconf: AuditConf, sshv: Optional[int] = None, print
err = fmt.format(err_pair[0], err_pair[1], packet_type) err = fmt.format(err_pair[0], err_pair[1], packet_type)
if err is not None: if err is not None:
output(out, aconf, banner, header) output(out, aconf, banner, header)
out.fail(err) out.error(err)
return exitcodes.CONNECTION_ERROR return exitcodes.CONNECTION_ERROR
if sshv == 1: if sshv == 1:
program_retval = output(out, aconf, banner, header, pkm=SSH1_PublicKeyMessage.parse(payload)) program_retval = output(out, aconf, banner, header, pkm=SSH1_PublicKeyMessage.parse(payload))
elif sshv == 2: elif sshv == 2:
try: try:
kex = SSH2_Kex.parse(out, payload) kex = SSH2_Kex.parse(out, payload)
out.d(str(kex))
except Exception: except Exception:
out.fail("Failed to parse server's kex. Stack trace:\n%s" % str(traceback.format_exc())) out.error("Failed to parse server's kex. Stack trace:\n%s" % str(traceback.format_exc()))
return exitcodes.CONNECTION_ERROR return exitcodes.CONNECTION_ERROR
if aconf.dheat is not None: if aconf.dheat is not None:
+8 -6
View File
@@ -108,7 +108,8 @@ class SSH_Socket(ReadBuf, WriteBuf):
s.listen() s.listen()
self.__sock_map[s.fileno()] = s self.__sock_map[s.fileno()] = s
except Exception as e: except Exception as e:
print("Warning: failed to listen on any IPv4 interfaces: %s" % str(e)) self.__outputbuffer.error("Warning: failed to listen on any IPv4 interfaces: %s" % str(e))
sys.exit(exitcodes.CONNECTION_ERROR)
try: try:
# Socket to listen on all IPv6 addresses. # Socket to listen on all IPv6 addresses.
@@ -119,11 +120,12 @@ class SSH_Socket(ReadBuf, WriteBuf):
s.listen() s.listen()
self.__sock_map[s.fileno()] = s self.__sock_map[s.fileno()] = s
except Exception as e: except Exception as e:
print("Warning: failed to listen on any IPv6 interfaces: %s" % str(e)) self.__outputbuffer.error("Warning: failed to listen on any IPv6 interfaces: %s" % str(e))
sys.exit(exitcodes.CONNECTION_ERROR)
# If we failed to listen on any interfaces, terminate. # If we failed to listen on any interfaces, terminate.
if len(self.__sock_map.keys()) == 0: if len(self.__sock_map.keys()) == 0:
print("Error: failed to listen on any IPv4 and IPv6 interfaces!") self.__outputbuffer.error("Error: failed to listen on any IPv4 and IPv6 interfaces!")
sys.exit(exitcodes.CONNECTION_ERROR) sys.exit(exitcodes.CONNECTION_ERROR)
# Wait for an incoming connection. If a timeout was explicitly # Wait for an incoming connection. If a timeout was explicitly
@@ -141,7 +143,7 @@ class SSH_Socket(ReadBuf, WriteBuf):
break break
if self.__timeout_set and time_elapsed >= self.__timeout: if self.__timeout_set and time_elapsed >= self.__timeout:
print("Timeout elapsed. Terminating...") self.__outputbuffer.error("Timeout elapsed. Terminating...")
sys.exit(exitcodes.CONNECTION_ERROR) sys.exit(exitcodes.CONNECTION_ERROR)
# Accept the connection. # Accept the connection.
@@ -275,7 +277,7 @@ class SSH_Socket(ReadBuf, WriteBuf):
payload_length = packet_length - padding_length - 1 payload_length = packet_length - padding_length - 1
check_size = 4 + 1 + payload_length + padding_length check_size = 4 + 1 + payload_length + padding_length
if check_size % self.__block_size != 0: if check_size % self.__block_size != 0:
self.__outputbuffer.fail('[exception] invalid ssh packet (block size)').write() self.__outputbuffer.error('[exception] invalid ssh packet (block size)').write()
sys.exit(exitcodes.CONNECTION_ERROR) sys.exit(exitcodes.CONNECTION_ERROR)
self.ensure_read(payload_length) self.ensure_read(payload_length)
if sshv == 1: if sshv == 1:
@@ -290,7 +292,7 @@ class SSH_Socket(ReadBuf, WriteBuf):
if sshv == 1: if sshv == 1:
rcrc = SSH1.crc32(padding + payload) rcrc = SSH1.crc32(padding + payload)
if crc != rcrc: if crc != rcrc:
self.__outputbuffer.fail('[exception] packet checksum CRC32 mismatch.').write() self.__outputbuffer.error('[exception] packet checksum CRC32 mismatch.').write()
sys.exit(exitcodes.CONNECTION_ERROR) sys.exit(exitcodes.CONNECTION_ERROR)
else: else:
self.ensure_read(padding_length) self.ensure_read(padding_length)
@@ -2,5 +2,6 @@
"errors": [], "errors": [],
"host": "localhost", "host": "localhost",
"passed": true, "passed": true,
"policy": "Docker policy: test1 (version 1)" "policy": "Docker policy: test1 (version 1)",
"port": 2222
} }
@@ -27,5 +27,6 @@
], ],
"host": "localhost", "host": "localhost",
"passed": false, "passed": false,
"policy": "Docker poliicy: test10 (version 1)" "policy": "Docker poliicy: test10 (version 1)",
"port": 2222
} }
@@ -19,5 +19,6 @@
], ],
"host": "localhost", "host": "localhost",
"passed": false, "passed": false,
"policy": "Docker policy: test2 (version 1)" "policy": "Docker policy: test2 (version 1)",
"port": 2222
} }
@@ -18,5 +18,6 @@
], ],
"host": "localhost", "host": "localhost",
"passed": false, "passed": false,
"policy": "Docker policy: test3 (version 1)" "policy": "Docker policy: test3 (version 1)",
"port": 2222
} }
@@ -28,5 +28,6 @@
], ],
"host": "localhost", "host": "localhost",
"passed": false, "passed": false,
"policy": "Docker policy: test4 (version 1)" "policy": "Docker policy: test4 (version 1)",
"port": 2222
} }
@@ -27,5 +27,6 @@
], ],
"host": "localhost", "host": "localhost",
"passed": false, "passed": false,
"policy": "Docker policy: test5 (version 1)" "policy": "Docker policy: test5 (version 1)",
"port": 2222
} }
@@ -2,5 +2,6 @@
"errors": [], "errors": [],
"host": "localhost", "host": "localhost",
"passed": true, "passed": true,
"policy": "Docker poliicy: test7 (version 1)" "policy": "Docker poliicy: test7 (version 1)",
"port": 2222
} }
@@ -15,5 +15,6 @@
], ],
"host": "localhost", "host": "localhost",
"passed": false, "passed": false,
"policy": "Docker poliicy: test8 (version 1)" "policy": "Docker poliicy: test8 (version 1)",
"port": 2222
} }
@@ -15,5 +15,6 @@
], ],
"host": "localhost", "host": "localhost",
"passed": false, "passed": false,
"policy": "Docker poliicy: test9 (version 1)" "policy": "Docker poliicy: test9 (version 1)",
"port": 2222
} }
@@ -39,5 +39,6 @@
], ],
"host": "localhost", "host": "localhost",
"passed": false, "passed": false,
"policy": "Hardened OpenSSH Server v8.0 (version 4)" "policy": "Hardened OpenSSH Server v8.0 (version 4)",
"port": 2222
} }
@@ -62,5 +62,6 @@
], ],
"host": "localhost", "host": "localhost",
"passed": false, "passed": false,
"policy": "Hardened OpenSSH Server v8.0 (version 4)" "policy": "Hardened OpenSSH Server v8.0 (version 4)",
"port": 2222
} }
@@ -2,5 +2,6 @@
"errors": [], "errors": [],
"host": "localhost", "host": "localhost",
"passed": true, "passed": true,
"policy": "Docker policy: test11 (version 1)" "policy": "Docker policy: test11 (version 1)",
"port": 2222
} }
@@ -39,5 +39,6 @@
], ],
"host": "localhost", "host": "localhost",
"passed": false, "passed": false,
"policy": "Docker policy: test12 (version 1)" "policy": "Docker policy: test12 (version 1)",
"port": 2222
} }
@@ -2,5 +2,6 @@
"errors": [], "errors": [],
"host": "localhost", "host": "localhost",
"passed": true, "passed": true,
"policy": "Docker policy: test13 (version 1)" "policy": "Docker policy: test13 (version 1)",
"port": 2222
} }
@@ -15,5 +15,6 @@
], ],
"host": "localhost", "host": "localhost",
"passed": false, "passed": false,
"policy": "Docker policy: test14 (version 1)" "policy": "Docker policy: test14 (version 1)",
"port": 2222
} }
@@ -2,5 +2,6 @@
"errors": [], "errors": [],
"host": "localhost", "host": "localhost",
"passed": true, "passed": true,
"policy": "Docker policy: test15 (version 1)" "policy": "Docker policy: test15 (version 1)",
"port": 2222
} }
@@ -82,5 +82,6 @@
], ],
"host": "localhost", "host": "localhost",
"passed": false, "passed": false,
"policy": "Docker policy: test16 (version 1)" "policy": "Docker policy: test16 (version 1)",
"port": 2222
} }
@@ -2,5 +2,6 @@
"errors": [], "errors": [],
"host": "localhost", "host": "localhost",
"passed": true, "passed": true,
"policy": "Docker policy: test17 (version 1)" "policy": "Docker policy: test17 (version 1)",
"port": 2222
} }
@@ -2,5 +2,6 @@
"errors": [], "errors": [],
"host": "localhost", "host": "localhost",
"passed": true, "passed": true,
"policy": "Docker policy: test6 (version 1)" "policy": "Docker policy: test6 (version 1)",
"port": 2222
} }