mirror of
https://github.com/jtesta/ssh-audit.git
synced 2026-05-25 23:41:22 +02:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 58c51e2c2c |
@@ -1 +0,0 @@
|
|||||||
github: jtesta
|
|
||||||
@@ -7,12 +7,12 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13.0-rc.2"]
|
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
# ssh-audit
|
# ssh-audit
|
||||||
[](https://github.com/jtesta/ssh-audit/blob/master/LICENSE)
|
[](https://github.com/jtesta/ssh-audit/blob/master/LICENSE)
|
||||||
|
[](https://pypi.org/project/ssh-audit/)
|
||||||
|
[](https://hub.docker.com/r/positronsecurity/ssh-audit)
|
||||||
[](https://github.com/jtesta/ssh-audit/actions)
|
[](https://github.com/jtesta/ssh-audit/actions)
|
||||||
[](https://github.com/jtesta/ssh-audit/blob/master/CONTRIBUTING.md)
|
[](https://github.com/jtesta/ssh-audit/blob/master/CONTRIBUTING.md)
|
||||||
|
|
||||||
[](https://pypi.org/project/ssh-audit/)
|
|
||||||
[](https://formulae.brew.sh/formula/ssh-audit)
|
|
||||||
[](https://hub.docker.com/r/positronsecurity/ssh-audit)
|
|
||||||
[](https://snapcraft.io/ssh-audit)
|
|
||||||
|
|
||||||
[](https://github.com/sponsors/jtesta)
|
|
||||||
|
|
||||||
**ssh-audit** is a tool for ssh server & client configuration auditing.
|
**ssh-audit** is a tool for ssh server & client configuration auditing.
|
||||||
|
|
||||||
[jtesta/ssh-audit](https://github.com/jtesta/ssh-audit/) (v2.0+) is the updated and maintained version of ssh-audit forked from [arthepsy/ssh-audit](https://github.com/arthepsy/ssh-audit) (v1.x) due to inactivity.
|
[jtesta/ssh-audit](https://github.com/jtesta/ssh-audit/) (v2.0+) is the updated and maintained version of ssh-audit forked from [arthepsy/ssh-audit](https://github.com/arthepsy/ssh-audit) (v1.x) due to inactivity.
|
||||||
@@ -37,7 +32,7 @@
|
|||||||
- historical information from OpenSSH, Dropbear SSH and libssh;
|
- historical information from OpenSSH, Dropbear SSH and libssh;
|
||||||
- policy scans to ensure adherence to a hardened/standard configuration;
|
- policy scans to ensure adherence to a hardened/standard configuration;
|
||||||
- runs on Linux and Windows;
|
- runs on Linux and Windows;
|
||||||
- supports Python 3.8 - 3.13;
|
- supports Python 3.8 - 3.12;
|
||||||
- no dependencies
|
- no dependencies
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
@@ -94,9 +89,7 @@ usage: ssh-audit.py [options] <host>
|
|||||||
-t, --timeout=<secs> timeout (in seconds) for connection and reading
|
-t, --timeout=<secs> timeout (in seconds) for connection and reading
|
||||||
(default: 5)
|
(default: 5)
|
||||||
-T, --targets=<hosts.txt> a file containing a list of target hosts (one
|
-T, --targets=<hosts.txt> a file containing a list of target hosts (one
|
||||||
per line, format HOST[:PORT]). Use -p/--port
|
per line, format HOST[: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
|
--threads=<threads> number of threads to use when scanning multiple
|
||||||
targets (-T/--targets) (default: 32)
|
targets (-T/--targets) (default: 32)
|
||||||
-v, --verbose verbose output
|
-v, --verbose verbose output
|
||||||
@@ -221,7 +214,6 @@ For convenience, a web front-end on top of the command-line tool is available at
|
|||||||
## ChangeLog
|
## ChangeLog
|
||||||
|
|
||||||
### v3.3.0-dev (???)
|
### v3.3.0-dev (???)
|
||||||
- Added Python 3.13 support.
|
|
||||||
- Added built-in policies for Ubuntu 24.04 LTS server & client, OpenSSH 9.8, and OpenSSH 9.9.
|
- Added built-in policies for Ubuntu 24.04 LTS server & client, OpenSSH 9.8, and OpenSSH 9.9.
|
||||||
- Added IPv6 support for DHEat and connection rate tests.
|
- 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 TCP port information to JSON policy scan results; credit [Fabian Malte Kopp](https://github.com/dreizehnutters).
|
||||||
@@ -231,7 +223,6 @@ For convenience, a web front-end on top of the command-line tool is available at
|
|||||||
- 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).
|
- 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).
|
||||||
- Fixed invalid JSON output when a socket error occurs while performing a client audit.
|
- Fixed invalid JSON output when a socket error occurs while performing a client audit.
|
||||||
- When scanning multiple targets (using `-T`/`--targets`), the `-p`/`--port` option will now be used as the default port (set to 22 if `-p`/`--port` is not given). Hosts specified in the file can override this default with an explicit port number (i.e.: "host1:1234"). For example, when using `-T targets.txt -p 222`, all hosts in `targets.txt` that do not explicitly include a port number will default to 222; when using `-T targets.txt` (without `-p`), all hosts will use a default of 22.
|
- When scanning multiple targets (using `-T`/`--targets`), the `-p`/`--port` option will now be used as the default port (set to 22 if `-p`/`--port` is not given). Hosts specified in the file can override this default with an explicit port number (i.e.: "host1:1234"). For example, when using `-T targets.txt -p 222`, all hosts in `targets.txt` that do not explicitly include a port number will default to 222; when using `-T targets.txt` (without `-p`), all hosts will use a default of 22.
|
||||||
- Now reports ECDSA and DSS fingerprints when in verbose mode; partial credit [Daniel Lenski](https://github.com/dlenskiSB).
|
|
||||||
- Added 1 new cipher: `grasshopper-ctr128`.
|
- Added 1 new cipher: `grasshopper-ctr128`.
|
||||||
- Added 2 new key exchanges: `mlkem768x25519-sha256`, `sntrup761x25519-sha512`.
|
- Added 2 new key exchanges: `mlkem768x25519-sha256`, `sntrup761x25519-sha512`.
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ classifiers =
|
|||||||
Programming Language :: Python :: 3.10
|
Programming Language :: Python :: 3.10
|
||||||
Programming Language :: Python :: 3.11
|
Programming Language :: Python :: 3.11
|
||||||
Programming Language :: Python :: 3.12
|
Programming Language :: Python :: 3.12
|
||||||
Programming Language :: Python :: 3.13
|
|
||||||
Programming Language :: Python :: Implementation :: CPython
|
Programming Language :: Python :: Implementation :: CPython
|
||||||
Programming Language :: Python :: Implementation :: PyPy
|
Programming Language :: Python :: Implementation :: PyPy
|
||||||
Topic :: Security
|
Topic :: Security
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (C) 2017-2024 Joe Testa (jtesta@positronsecurity.com)
|
Copyright (C) 2017-2023 Joe Testa (jtesta@positronsecurity.com)
|
||||||
|
|
||||||
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
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
@@ -55,17 +55,6 @@ class HostKeyTest:
|
|||||||
|
|
||||||
'ssh-ed448': {'cert': False, 'variable_key_len': False},
|
'ssh-ed448': {'cert': False, 'variable_key_len': False},
|
||||||
# 'ssh-ed448-cert-v01@openssh.com': {'cert': True, 'variable_key_len': False},
|
# 'ssh-ed448-cert-v01@openssh.com': {'cert': True, 'variable_key_len': False},
|
||||||
|
|
||||||
'ecdsa-sha2-nistp256': {'cert': False, 'variable_key_len': False},
|
|
||||||
'ecdsa-sha2-nistp384': {'cert': False, 'variable_key_len': False},
|
|
||||||
'ecdsa-sha2-nistp521': {'cert': False, 'variable_key_len': False},
|
|
||||||
|
|
||||||
'ecdsa-sha2-nistp256-cert-v01@openssh.com': {'cert': True, 'variable_key_len': False},
|
|
||||||
'ecdsa-sha2-nistp384-cert-v01@openssh.com': {'cert': True, 'variable_key_len': False},
|
|
||||||
'ecdsa-sha2-nistp521-cert-v01@openssh.com': {'cert': True, 'variable_key_len': False},
|
|
||||||
|
|
||||||
'ssh-dss': {'cert': False, 'variable_key_len': True},
|
|
||||||
'ssh-dss-cert-v01@openssh.com': {'cert': True, 'variable_key_len': True},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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'
|
||||||
@@ -151,12 +140,7 @@ class HostKeyTest:
|
|||||||
_, payload = s.read_packet()
|
_, payload = s.read_packet()
|
||||||
SSH2_Kex.parse(out, payload)
|
SSH2_Kex.parse(out, payload)
|
||||||
except Exception:
|
except Exception:
|
||||||
msg = "Failed to parse server's kex."
|
out.v("Failed to parse server's kex. Stack trace:\n%s" % str(traceback.format_exc()), write_now=True)
|
||||||
if not out.debug:
|
|
||||||
msg += " Re-run in debug mode to see stack trace."
|
|
||||||
|
|
||||||
out.v(msg, write_now=True)
|
|
||||||
out.d("Stack trace:\n%s" % str(traceback.format_exc()), write_now=True)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Do the initial DH exchange. The server responds back
|
# Do the initial DH exchange. The server responds back
|
||||||
@@ -167,12 +151,7 @@ class HostKeyTest:
|
|||||||
kex_reply = kex_group.recv_reply(s)
|
kex_reply = kex_group.recv_reply(s)
|
||||||
raw_hostkey_bytes = kex_reply if kex_reply is not None else b''
|
raw_hostkey_bytes = kex_reply if kex_reply is not None else b''
|
||||||
except KexDHException:
|
except KexDHException:
|
||||||
msg = "Failed to parse server's host key."
|
out.v("Failed to parse server's host key. Stack trace:\n%s" % str(traceback.format_exc()), write_now=True)
|
||||||
if not out.debug:
|
|
||||||
msg += " Re-run in debug mode to see stack trace."
|
|
||||||
|
|
||||||
out.v(msg, write_now=True)
|
|
||||||
out.d("Stack trace:\n%s" % str(traceback.format_exc()), write_now=True)
|
|
||||||
|
|
||||||
# Since parsing this host key failed, there's nothing more to do but close the socket and move on to the next host key type.
|
# Since parsing this host key failed, there's nothing more to do but close the socket and move on to the next host key type.
|
||||||
s.close()
|
s.close()
|
||||||
@@ -212,7 +191,7 @@ class HostKeyTest:
|
|||||||
cakey_warn_str = HostKeyTest.SMALL_ECC_MODULUS_WARNING
|
cakey_warn_str = HostKeyTest.SMALL_ECC_MODULUS_WARNING
|
||||||
|
|
||||||
# Keys smaller than 2048 result in a failure. Keys smaller 3072 result in a warning. Update the database accordingly.
|
# Keys smaller than 2048 result in a failure. Keys smaller 3072 result in a warning. Update the database accordingly.
|
||||||
if (cert is False) and (hostkey_modulus_size < hostkey_min_good) and (host_key_type != 'ssh-dss'): # Skip ssh-dss, otherwise we get duplicate failure messages (SSH2_KexDB will always flag it).
|
if (cert is False) and (hostkey_modulus_size < hostkey_min_good):
|
||||||
|
|
||||||
# If the key is under 2048, add to the failure list.
|
# If the key is under 2048, add to the failure list.
|
||||||
if hostkey_modulus_size < hostkey_min_warn:
|
if hostkey_modulus_size < hostkey_min_warn:
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
"""
|
"""
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
import copy
|
import copy
|
||||||
import getopt # pylint: disable=deprecated-module
|
import getopt
|
||||||
import json
|
import json
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
import os
|
import os
|
||||||
@@ -360,19 +360,11 @@ def output_fingerprints(out: OutputBuffer, algs: Algorithms, is_json_output: boo
|
|||||||
fp_types = sorted(fps.keys())
|
fp_types = sorted(fps.keys())
|
||||||
for fp_type in fp_types:
|
for fp_type in fp_types:
|
||||||
fp = fps[fp_type]
|
fp = fps[fp_type]
|
||||||
|
out.good('(fin) {}: {}'.format(fp_type, fp.sha256))
|
||||||
# Don't output any ECDSA or DSS fingerprints unless verbose mode is enabled.
|
|
||||||
if fp_type.startswith("ecdsa-") or (fp_type == "ssh-dss"):
|
|
||||||
if out.verbose:
|
|
||||||
out.warn('(fin) {}: {} -- [info] this fingerprint type is insecure and should not be relied upon'.format(fp_type, fp.sha256))
|
|
||||||
else:
|
|
||||||
continue # If verbose mode is not enabled, skip this type entirely.
|
|
||||||
else:
|
|
||||||
out.good('(fin) {}: {}'.format(fp_type, fp.sha256))
|
|
||||||
|
|
||||||
# Output the MD5 hash too if verbose mode is enabled.
|
# Output the MD5 hash too if verbose mode is enabled.
|
||||||
if out.verbose:
|
if out.verbose:
|
||||||
out.warn('(fin) {}: {} -- [info] do not rely on MD5 fingerprints for server identification; it is insecure for this use case'.format(fp_type, fp.md5))
|
out.info('(fin) {}: {} -- [info] do not rely on MD5 fingerprints for server identification; it is insecure for this use case'.format(fp_type, fp.md5))
|
||||||
|
|
||||||
if not out.is_section_empty() and not is_json_output:
|
if not out.is_section_empty() and not is_json_output:
|
||||||
out.head('# fingerprints')
|
out.head('# fingerprints')
|
||||||
|
|||||||
@@ -78,26 +78,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"fingerprints": [
|
"fingerprints": [
|
||||||
{
|
|
||||||
"hash": "jdUfqoGCDOY1drQcoqIJm/pEix2r09hqwOs9E9GimZQ",
|
|
||||||
"hash_alg": "SHA256",
|
|
||||||
"hostkey": "ecdsa-sha2-nistp256"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hash": "98:27:f3:12:20:f6:23:6d:1a:00:2a:6c:71:7c:1e:6b",
|
|
||||||
"hash_alg": "MD5",
|
|
||||||
"hostkey": "ecdsa-sha2-nistp256"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hash": "NBzry0uMAX8BRsn4mv9CHpeivMOdwzGFEKrf6Hg7tIQ",
|
|
||||||
"hash_alg": "SHA256",
|
|
||||||
"hostkey": "ssh-dss"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hash": "16:60:9e:54:d7:1e:b3:0d:97:60:12:ad:fe:83:a2:40",
|
|
||||||
"hash_alg": "MD5",
|
|
||||||
"hostkey": "ssh-dss"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"hash": "CDfAU12pjQS7/91kg7gYacza0U/6PDbE04Ic3IpYxkM",
|
"hash": "CDfAU12pjQS7/91kg7gYacza0U/6PDbE04Ic3IpYxkM",
|
||||||
"hash_alg": "SHA256",
|
"hash_alg": "SHA256",
|
||||||
|
|||||||
@@ -253,16 +253,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"fingerprints": [
|
"fingerprints": [
|
||||||
{
|
|
||||||
"hash": "sqDDYhzYz7YIQeFDc0WF8SeXtrEz+iwsV7d/FdIgztM",
|
|
||||||
"hash_alg": "SHA256",
|
|
||||||
"hostkey": "ssh-dss"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hash": "5c:de:62:f0:60:c8:93:13:87:71:78:95:56:3f:61:51",
|
|
||||||
"hash_alg": "MD5",
|
|
||||||
"hostkey": "ssh-dss"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"hash": "YZ457EBcJTSxRKI3yXRgtAj3PBf5B9/F36b1SVooml4",
|
"hash": "YZ457EBcJTSxRKI3yXRgtAj3PBf5B9/F36b1SVooml4",
|
||||||
"hash_alg": "SHA256",
|
"hash_alg": "SHA256",
|
||||||
|
|||||||
@@ -240,16 +240,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"fingerprints": [
|
"fingerprints": [
|
||||||
{
|
|
||||||
"hash": "sqDDYhzYz7YIQeFDc0WF8SeXtrEz+iwsV7d/FdIgztM",
|
|
||||||
"hash_alg": "SHA256",
|
|
||||||
"hostkey": "ssh-dss"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hash": "5c:de:62:f0:60:c8:93:13:87:71:78:95:56:3f:61:51",
|
|
||||||
"hash_alg": "MD5",
|
|
||||||
"hostkey": "ssh-dss"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"hash": "YZ457EBcJTSxRKI3yXRgtAj3PBf5B9/F36b1SVooml4",
|
"hash": "YZ457EBcJTSxRKI3yXRgtAj3PBf5B9/F36b1SVooml4",
|
||||||
"hash_alg": "SHA256",
|
"hash_alg": "SHA256",
|
||||||
|
|||||||
@@ -87,16 +87,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"fingerprints": [
|
"fingerprints": [
|
||||||
{
|
|
||||||
"hash": "Q6Llm0o4TrcUen4tnT2h4BDf2f+ina6dIJmVH8c40bg",
|
|
||||||
"hash_alg": "SHA256",
|
|
||||||
"hostkey": "ecdsa-sha2-nistp256"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hash": "cc:e0:80:84:5b:05:98:64:24:43:52:3b:17:c8:94:89",
|
|
||||||
"hash_alg": "MD5",
|
|
||||||
"hostkey": "ecdsa-sha2-nistp256"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"hash": "UrnXIVH+7dlw8UqYocl48yUEcKrthGDQG2CPCgp7MxU",
|
"hash": "UrnXIVH+7dlw8UqYocl48yUEcKrthGDQG2CPCgp7MxU",
|
||||||
"hash_alg": "SHA256",
|
"hash_alg": "SHA256",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[tox]
|
[tox]
|
||||||
envlist =
|
envlist =
|
||||||
py{py3}-{test,pylint,flake8}
|
py{py3}-{test,pylint,flake8}
|
||||||
py{38,39,310,311,312,313}-{test,mypy,pylint,flake8}
|
py{38,39,310,311,312}-{test,mypy,pylint,flake8}
|
||||||
cov
|
cov
|
||||||
skip_missing_interpreters = true
|
skip_missing_interpreters = true
|
||||||
|
|
||||||
@@ -9,10 +9,10 @@ skip_missing_interpreters = true
|
|||||||
deps =
|
deps =
|
||||||
test: pytest
|
test: pytest
|
||||||
test,cov: {[testenv:cov]deps}
|
test,cov: {[testenv:cov]deps}
|
||||||
test,py{38,39,310,311,312,313}-{type,mypy}: colorama
|
test,py{38,39,310,311,312}-{type,mypy}: colorama
|
||||||
py{38,39,310,311,312,313}-{type,mypy}: {[testenv:mypy]deps}
|
py{38,39,310,311,312}-{type,mypy}: {[testenv:mypy]deps}
|
||||||
py{py3,38,39,310,311,312,313}-{lint,pylint},lint: {[testenv:pylint]deps}
|
py{py3,38,39,310,311,312}-{lint,pylint},lint: {[testenv:pylint]deps}
|
||||||
py{py3,38,39,310,311,312,313}-{lint,flake8},lint: {[testenv:flake8]deps}
|
py{py3,38,39,310,311,312}-{lint,flake8},lint: {[testenv:flake8]deps}
|
||||||
setenv =
|
setenv =
|
||||||
SSHAUDIT = {toxinidir}/src
|
SSHAUDIT = {toxinidir}/src
|
||||||
test: COVERAGE_FILE = {toxinidir}/.coverage.{envname}
|
test: COVERAGE_FILE = {toxinidir}/.coverage.{envname}
|
||||||
@@ -24,9 +24,9 @@ commands =
|
|||||||
test: coverage combine
|
test: coverage combine
|
||||||
test: coverage report --show-missing
|
test: coverage report --show-missing
|
||||||
test: coverage html -d {toxinidir}/reports/html/coverage.{envname}
|
test: coverage html -d {toxinidir}/reports/html/coverage.{envname}
|
||||||
py{38,39,310,311,312,313}-{type,mypy}: {[testenv:mypy]commands}
|
py{38,39,310,311,312}-{type,mypy}: {[testenv:mypy]commands}
|
||||||
py{py3,38,39,310,311,312,313}-{lint,pylint},lint: {[testenv:pylint]commands}
|
py{py3,38,39,310,311,312}-{lint,pylint},lint: {[testenv:pylint]commands}
|
||||||
py{py3,38,39,310,311,312,313}-{lint,flake8},lint: {[testenv:flake8]commands}
|
py{py3,38,39,310,311,312}-{lint,flake8},lint: {[testenv:flake8]commands}
|
||||||
|
|
||||||
#ignore_outcome =
|
#ignore_outcome =
|
||||||
# type: true
|
# type: true
|
||||||
|
|||||||
Reference in New Issue
Block a user