mirror of
https://github.com/jtesta/ssh-audit.git
synced 2024-11-16 13:35:39 +01:00
This commit is contained in:
parent
d7f8bf3e6d
commit
8190fe59d0
34
README.md
34
README.md
@ -48,7 +48,22 @@ usage: ssh-audit.py [options] <host>
|
||||
-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]>
|
||||
@ -68,6 +83,9 @@ usage: ssh-audit.py [options] <host>
|
||||
-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
|
||||
@ -132,6 +150,21 @@ To create a policy based on a target server (which can be manually edited):
|
||||
ssh-audit -M new_policy.txt targetserver
|
||||
```
|
||||
|
||||
To run the DHEat CPU exhaustion DoS attack ([CVE-2002-20001](https://nvd.nist.gov/vuln/detail/CVE-2002-20001)) against a target using 10 concurrent sockets:
|
||||
```
|
||||
ssh-audit --dheat=10 targetserver
|
||||
```
|
||||
|
||||
To run the DHEat attack using the `diffie-hellman-group-exchange-sha256` key exchange algorithm:
|
||||
```
|
||||
ssh-audit --dheat=10:diffie-hellman-group-exchange-sha256 targetserver
|
||||
```
|
||||
|
||||
To run the DHEat attack using the `diffie-hellman-group-exchange-sha256` key exchange algorithm along with very small but non-standard packet lengths (this may result in the same CPU exhaustion, but with many less bytes per second being sent):
|
||||
```
|
||||
ssh-audit --dheat=10:diffie-hellman-group-exchange-sha256:4 targetserver
|
||||
```
|
||||
|
||||
## Screenshots
|
||||
|
||||
### Server Standard Audit Example
|
||||
@ -181,6 +214,7 @@ For convenience, a web front-end on top of the command-line tool is available at
|
||||
## ChangeLog
|
||||
|
||||
### v3.2.0-dev (???)
|
||||
- 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.
|
||||
- Color output is disabled if the `NO_COLOR` environment variable is set (see https://no-color.org/).
|
||||
- Fixed parsing of `ecdsa-sha2-nistp*` CA signatures on host keys. Additionally, they are now flagged as potentially back-doored, just as standard host keys are.
|
||||
|
@ -464,7 +464,7 @@ run_test() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
./ssh-audit.py localhost:2222 > "$test_result_stdout"
|
||||
./ssh-audit.py --skip-rate-test localhost:2222 > "$test_result_stdout"
|
||||
actual_retval=$?
|
||||
if [[ $actual_retval != "$expected_retval" ]]; then
|
||||
echo -e "${REDB}Unexpected return value. Expected: ${expected_retval}; Actual: ${actual_retval}${CLR}"
|
||||
@ -478,7 +478,7 @@ run_test() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
./ssh-audit.py -jj localhost:2222 > "$test_result_json"
|
||||
./ssh-audit.py --skip-rate-test -jj localhost:2222 > "$test_result_json"
|
||||
actual_retval=$?
|
||||
if [[ $actual_retval != "$expected_retval" ]]; then
|
||||
echo -e "${REDB}Unexpected return value. Expected: ${expected_retval}; Actual: ${actual_retval}${CLR}"
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (C) 2017-2021 Joe Testa (jtesta@positronsecurity.com)
|
||||
Copyright (C) 2017-2024 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
|
||||
@ -60,10 +60,20 @@ class AuditConf:
|
||||
self.manual = False
|
||||
self.debug = False
|
||||
self.gex_test = ''
|
||||
self.dheat: Optional[str] = None
|
||||
self.dheat_concurrent_connections: int = 0
|
||||
self.dheat_e_length: int = 0
|
||||
self.dheat_target_alg: str = ""
|
||||
self.skip_rate_test = False
|
||||
self.conn_rate_test: str = "1:1"
|
||||
self.conn_rate_test_enabled = False
|
||||
self.conn_rate_test_threads = 0
|
||||
self.conn_rate_test_target_rate = 0
|
||||
|
||||
|
||||
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']:
|
||||
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']:
|
||||
valid, value = True, bool(value)
|
||||
elif name in ['ipv4', 'ipv6']:
|
||||
valid, value = True, bool(value)
|
||||
@ -94,6 +104,89 @@ class AuditConf:
|
||||
if num_threads < 1:
|
||||
raise ValueError('invalid number of threads: {}'.format(value))
|
||||
value = num_threads
|
||||
elif name == "dheat":
|
||||
# Valid values:
|
||||
# * None
|
||||
# * "10" (concurrent-connections)
|
||||
# * "10:diffie-hellman-group18-sha512" (concurrent-connections:target-alg)
|
||||
# * "10:diffie-hellman-group18-sha512:100" (concurrent-connections:target-alg:e-length)
|
||||
valid = True
|
||||
if value is not None:
|
||||
|
||||
def _parse_concurrent_connections(s: str) -> int:
|
||||
if Utils.parse_int(s) < 1:
|
||||
raise ValueError("number of concurrent connections must be 1 or greater: {}".format(s))
|
||||
return int(s)
|
||||
|
||||
def _parse_e_length(s: str) -> int:
|
||||
s_int = Utils.parse_int(s)
|
||||
if s_int < 2:
|
||||
raise ValueError("length of e must not be less than 2: {}".format(s))
|
||||
return s_int
|
||||
|
||||
def _parse_target_alg(s: str) -> str:
|
||||
if len(s) == 0:
|
||||
raise ValueError("target algorithm must not be the empty string.")
|
||||
return s
|
||||
|
||||
value = str(value)
|
||||
fields = value.split(':')
|
||||
|
||||
self.dheat_concurrent_connections = _parse_concurrent_connections(fields[0])
|
||||
|
||||
# Parse the target algorithm if present.
|
||||
if len(fields) >= 2:
|
||||
self.dheat_target_alg = _parse_target_alg(fields[1])
|
||||
|
||||
# Parse the length of e, if present.
|
||||
if len(fields) == 3:
|
||||
self.dheat_e_length = _parse_e_length(fields[2])
|
||||
|
||||
if len(fields) > 3:
|
||||
raise ValueError("only three fields are expected instead of {}: {}".format(len(fields), value))
|
||||
|
||||
elif name in ["dheat_concurrent_connections", "dheat_e_length"]:
|
||||
valid = True
|
||||
if not isinstance(value, int):
|
||||
valid = False
|
||||
|
||||
elif name == "dheat_target_alg":
|
||||
valid = True
|
||||
if not isinstance(value, str):
|
||||
valid = False
|
||||
|
||||
elif name == "conn_rate_test":
|
||||
# Valid values:
|
||||
# * "4" (run rate test with 4 threads)
|
||||
# * "4:100" (run rate test with 4 threads, targeting 100 connections/second)
|
||||
|
||||
error_msg = "valid format for {:s} is \"N\" or \"N:N\", where N is an integer.".format(name)
|
||||
self.conn_rate_test_enabled = True
|
||||
fields = str(value).split(":")
|
||||
|
||||
if len(fields) > 2 or len(fields) == 0:
|
||||
raise ValueError(error_msg)
|
||||
else:
|
||||
self.conn_rate_test_threads = int(fields[0])
|
||||
if self.conn_rate_test_threads < 1:
|
||||
raise ValueError("number of threads must be 1 or greater.")
|
||||
|
||||
self.conn_rate_test_target_rate = 0
|
||||
if len(fields) == 2:
|
||||
self.conn_rate_test_target_rate = int(fields[1])
|
||||
if self.conn_rate_test_target_rate < 1:
|
||||
raise ValueError("rate target must be 1 or greater.")
|
||||
|
||||
elif name == "conn_rate_test_enabled":
|
||||
valid = True
|
||||
if not isinstance(value, bool):
|
||||
valid = False
|
||||
|
||||
elif name in ["conn_rate_test_threads", "conn_rate_test_target_rate"]:
|
||||
valid = True
|
||||
if not isinstance(value, int):
|
||||
valid = False
|
||||
|
||||
|
||||
if valid:
|
||||
object.__setattr__(self, name, value)
|
||||
|
1002
src/ssh_audit/dheat.py
Normal file
1002
src/ssh_audit/dheat.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -27,6 +27,7 @@ import concurrent.futures
|
||||
import copy
|
||||
import getopt
|
||||
import json
|
||||
import multiprocessing
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
@ -44,6 +45,7 @@ from ssh_audit.algorithm import Algorithm
|
||||
from ssh_audit.algorithms import Algorithms
|
||||
from ssh_audit.auditconf import AuditConf
|
||||
from ssh_audit.banner import Banner
|
||||
from ssh_audit.dheat import DHEat
|
||||
from ssh_audit import exitcodes
|
||||
from ssh_audit.fingerprint import Fingerprint
|
||||
from ssh_audit.gextest import GEXTest
|
||||
@ -96,7 +98,22 @@ def usage(uout: OutputBuffer, err: Optional[str] = None) -> None:
|
||||
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]>')
|
||||
@ -111,6 +128,7 @@ def usage(uout: OutputBuffer, err: Optional[str] = None) -> None:
|
||||
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 --threads\n to control concurrent scans.')
|
||||
uout.info(' --threads=<threads> number of threads to use when scanning multiple\n targets (-T/--targets) (default: 32)')
|
||||
@ -430,7 +448,7 @@ def output_recommendations(out: OutputBuffer, algs: Algorithms, algorithm_recomm
|
||||
|
||||
|
||||
# Output additional information & notes.
|
||||
def output_info(out: OutputBuffer, software: Optional['Software'], client_audit: bool, any_problems: bool, is_json_output: bool, additional_notes: str) -> None:
|
||||
def output_info(out: OutputBuffer, software: Optional['Software'], client_audit: bool, any_problems: bool, is_json_output: bool, additional_notes: List[str]) -> None:
|
||||
with out:
|
||||
# Tell user that PuTTY cannot be hardened at the protocol-level.
|
||||
if client_audit and (software is not None) and (software.product == Product.PuTTY):
|
||||
@ -441,8 +459,9 @@ def output_info(out: OutputBuffer, software: Optional['Software'], client_audit:
|
||||
out.warn('(nfo) For hardening guides on common OSes, please see: <https://www.ssh-audit.com/hardening_guides.html>')
|
||||
|
||||
# Add any additional notes.
|
||||
if len(additional_notes) > 0:
|
||||
out.warn("(nfo) %s" % additional_notes)
|
||||
for additional_note in additional_notes:
|
||||
if len(additional_note) > 0:
|
||||
out.warn("(nfo) %s" % additional_note)
|
||||
|
||||
if not out.is_section_empty() and not is_json_output:
|
||||
out.head('# additional info')
|
||||
@ -450,8 +469,8 @@ def output_info(out: OutputBuffer, software: Optional['Software'], client_audit:
|
||||
out.sep()
|
||||
|
||||
|
||||
def post_process_findings(banner: Optional[Banner], algs: Algorithms, client_audit: bool) -> Tuple[List[str], str]:
|
||||
'''Perform post-processing on scan results before reporting them to the user. Returns a list of algorithms that should not be recommended'''
|
||||
def post_process_findings(banner: Optional[Banner], algs: Algorithms, client_audit: bool, dh_rate_test_notes: str) -> Tuple[List[str], List[str]]:
|
||||
'''Perform post-processing on scan results before reporting them to the user. Returns a list of algorithms that should not be recommended and a list of notes.'''
|
||||
|
||||
def _add_terrapin_warning(db: Dict[str, Dict[str, List[List[Optional[str]]]]], category: str, algorithm_name: str) -> None:
|
||||
'''Adds a warning regarding the Terrapin vulnerability for the specified algorithm.'''
|
||||
@ -590,20 +609,24 @@ def post_process_findings(banner: Optional[Banner], algs: Algorithms, client_aud
|
||||
_add_terrapin_warning(db, "mac", mac)
|
||||
|
||||
# Return a note telling the user that, while this target is properly configured, if connected to a vulnerable peer, then a vulnerable connection is still possible.
|
||||
notes = ""
|
||||
additional_notes = []
|
||||
if len(algs_to_note) > 0:
|
||||
notes = "Be aware that, while this target properly supports the strict key exchange method (via the kex-strict-?-v00@openssh.com marker) needed to protect against the Terrapin vulnerability (CVE-2023-48795), all peers must also support this feature as well, otherwise the vulnerability will still be present. The following algorithms would allow an unpatched peer to create vulnerable SSH channels with this target: %s. If any CBC ciphers are in this list, you may remove them while leaving the *-etm@openssh.com MACs in place; these MACs are fine while paired with non-CBC cipher types." % ", ".join(algs_to_note)
|
||||
additional_notes.append("Be aware that, while this target properly supports the strict key exchange method (via the kex-strict-?-v00@openssh.com marker) needed to protect against the Terrapin vulnerability (CVE-2023-48795), all peers must also support this feature as well, otherwise the vulnerability will still be present. The following algorithms would allow an unpatched peer to create vulnerable SSH channels with this target: %s. If any CBC ciphers are in this list, you may remove them while leaving the *-etm@openssh.com MACs in place; these MACs are fine while paired with non-CBC cipher types." % ", ".join(algs_to_note))
|
||||
|
||||
# Add the chacha ciphers, CBC ciphers, and ETM MACs to the recommendation suppression list if they are not enabled on the server. That way they are not recommended to the user to enable if they were explicitly disabled to handle the Terrapin vulnerability. However, they can still be recommended for disabling.
|
||||
algorithm_recommendation_suppress_list += _get_chacha_ciphers_not_enabled(db, algs)
|
||||
algorithm_recommendation_suppress_list += _get_cbc_ciphers_not_enabled(db, algs)
|
||||
algorithm_recommendation_suppress_list += _get_etm_macs_not_enabled(db, algs)
|
||||
|
||||
return algorithm_recommendation_suppress_list, notes
|
||||
# Append any notes related to the DH rate test.
|
||||
if len(dh_rate_test_notes) > 0:
|
||||
additional_notes.append(dh_rate_test_notes)
|
||||
|
||||
return algorithm_recommendation_suppress_list, additional_notes
|
||||
|
||||
|
||||
# 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) -> int:
|
||||
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:
|
||||
|
||||
program_retval = exitcodes.GOOD
|
||||
client_audit = client_host is not None # If set, this is a client audit.
|
||||
@ -611,7 +634,7 @@ def output(out: OutputBuffer, aconf: AuditConf, banner: Optional[Banner], header
|
||||
algs = Algorithms(pkm, 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)
|
||||
algorithm_recommendation_suppress_list, additional_notes = post_process_findings(banner, algs, client_audit, dh_rate_test_notes)
|
||||
|
||||
with out:
|
||||
if print_target:
|
||||
@ -868,7 +891,7 @@ def process_commandline(out: OutputBuffer, args: List[str], usage_cb: Callable[.
|
||||
|
||||
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=']
|
||||
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))
|
||||
@ -956,6 +979,12 @@ def process_commandline(out: OutputBuffer, args: List[str], usage_cb: Callable[.
|
||||
usage_cb(out, '{} {} {} is not valid'.format(o, bits_left_bound, bits_right_bound))
|
||||
|
||||
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
|
||||
|
||||
|
||||
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:
|
||||
@ -1039,7 +1068,7 @@ def process_commandline(out: OutputBuffer, args: List[str], usage_cb: Callable[.
|
||||
return aconf
|
||||
|
||||
|
||||
def build_struct(target_host: str, banner: Optional['Banner'], cves: List[Dict[str, Union[str, float]]], 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: str = "") -> Any: # pylint: disable=too-many-arguments
|
||||
def build_struct(target_host: str, banner: Optional['Banner'], cves: List[Dict[str, Union[str, float]]], 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 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.'''
|
||||
@ -1207,8 +1236,8 @@ def build_struct(target_host: str, banner: Optional['Banner'], cves: List[Dict[s
|
||||
# Add in the recommendations.
|
||||
res['recommendations'] = get_algorithm_recommendations(algorithms, algorithm_recommendation_suppress_list, software, for_server=True)
|
||||
|
||||
# Add in the additional notes. Currently just one string, but in the future this may grow to multiple strings. Hence, an array is needed to prevent future schema breakage.
|
||||
res['additional_notes'] = [additional_notes]
|
||||
# Add in the additional notes.
|
||||
res['additional_notes'] = additional_notes
|
||||
|
||||
return res
|
||||
|
||||
@ -1290,6 +1319,14 @@ def audit(out: OutputBuffer, aconf: AuditConf, sshv: Optional[int] = None, print
|
||||
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
|
||||
|
||||
dh_rate_test_notes = ""
|
||||
if aconf.client_audit is False:
|
||||
HostKeyTest.run(out, s, kex)
|
||||
if aconf.gex_test != '':
|
||||
@ -1297,9 +1334,16 @@ def audit(out: OutputBuffer, aconf: AuditConf, sshv: Optional[int] = None, print
|
||||
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)
|
||||
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):
|
||||
@ -1588,8 +1632,9 @@ def main() -> int:
|
||||
|
||||
|
||||
if __name__ == '__main__': # pragma: nocover
|
||||
exit_code = exitcodes.GOOD
|
||||
multiprocessing.freeze_support() # Needed for PyInstaller (Windows) builds.
|
||||
|
||||
exit_code = exitcodes.GOOD
|
||||
try:
|
||||
exit_code = main()
|
||||
except Exception:
|
||||
|
57
ssh-audit.1
57
ssh-audit.1
@ -1,4 +1,4 @@
|
||||
.TH SSH-AUDIT 1 "March 14, 2024"
|
||||
.TH SSH-AUDIT 1 "April 18, 2024"
|
||||
.SH NAME
|
||||
\fBssh-audit\fP \- SSH server & client configuration auditor
|
||||
.SH SYNOPSIS
|
||||
@ -46,11 +46,21 @@ Enables grepable output.
|
||||
.br
|
||||
Starts a server on port 2222 to audit client software configuration. Use -p/--port=<port> to change port and -t/--timeout=<secs> to change listen timeout.
|
||||
|
||||
.TP
|
||||
.B \-\-conn\-rate\-test=N[:max_rate]
|
||||
.br
|
||||
Performs a connection rate test (useful for collecting metrics related to susceptibility of the DHEat vulnerability [CVE-2002-20001]). A successful connection is counted when the server returns a valid SSH banner. Testing is conducted with N concurrent sockets with an optional maximum rate of connections per second.
|
||||
|
||||
.TP
|
||||
.B -d, \-\-debug
|
||||
.br
|
||||
Enable debug output.
|
||||
|
||||
.TP
|
||||
.B \-\-dheat=N[:kex[:e_len]]
|
||||
.br
|
||||
Run the DHEat DoS attack (CVE-2002-20001) against the target server (which will consume all available CPU resources). The number of concurrent sockets, N, needed to achieve this effect will be highly dependent on the CPU resources available on the target, as well as the latency between the source and target machines. The key exchange is automatically chosen based on which would cause maximum effect, unless explicitly chosen in the second field. Lastly, an (experimental) option allows the length in bytes of the fake e value sent to the server to be specified in the third field. Normally, the length of e is roughly the length of the modulus of the Diffie-Hellman exchange (hence, an 8192-bit / 1024-byte value of e is sent in each connection when targeting the diffie-hellman-group18-sha512 algorithm). Instead, it was observed that many SSH implementations accept small values, such as 4 bytes; this results in a much more network-efficient attack.
|
||||
|
||||
.TP
|
||||
.B -g, \-\-gex-test=<x[,y,...] | min1:pref1:max1[,min2:pref2:max2,...] | x-y[:step]>
|
||||
.br
|
||||
@ -126,6 +136,11 @@ The TCP port to connect to when auditing a server, or the port to listen on when
|
||||
.br
|
||||
Runs a policy audit against a target using the specified policy (see \fBPOLICY AUDIT\fP section for detailed description of this mode of operation). Combine with -c/--client-audit to audit a client configuration instead of a server. Use -L/--list-policies to list all official, built-in policies for common systems.
|
||||
|
||||
.TP
|
||||
.B \-\-skip\-rate\-test
|
||||
.br
|
||||
Skips the connection rate test during standard audits. By default, a few dozen TCP connections are created with the target host to see if connection throttling is implemented (this can safely infer whether the target is vulnerable to the DHEat attack; see CVE-2002-20001).
|
||||
|
||||
.TP
|
||||
.B -t, \-\-timeout=<secs>
|
||||
.br
|
||||
@ -273,6 +288,46 @@ ssh-audit targetserver --gex-test=0-5120:1024
|
||||
.fi
|
||||
.RE
|
||||
|
||||
.LP
|
||||
To run the DHEat DoS attack (monitor the target server's CPU usage to determine the optimal number of concurrent sockets):
|
||||
.RS
|
||||
.nf
|
||||
ssh-audit targetserver --dheat=10
|
||||
.fi
|
||||
.RE
|
||||
|
||||
.LP
|
||||
To run the DHEat attack and manually target the diffie-hellman-group-exchange-sha256 algorithm:
|
||||
.RS
|
||||
.nf
|
||||
ssh-audit targetserver --dheat=10:diffie-hellman-group-exchange-sha256
|
||||
.fi
|
||||
.RE
|
||||
|
||||
.LP
|
||||
To run the DHEat attack and manually target the diffie-hellman-group-exchange-sha256 algorithm with a very small length of e (resulting in the same effect but without having to send large packets):
|
||||
.RS
|
||||
.nf
|
||||
ssh-audit targetserver --dheat=10:diffie-hellman-group-exchange-sha256:4
|
||||
.fi
|
||||
.RE
|
||||
|
||||
.LP
|
||||
To test the number of successful connections per second that can be created with the target using 8 parallel threads (useful for detecting whether connection throttling is implemented by the target):
|
||||
.RS
|
||||
.nf
|
||||
ssh-audit targetserver --conn-rate-test=8
|
||||
.fi
|
||||
.RE
|
||||
|
||||
.LP
|
||||
To use 8 parallel threads to create up to 100 connections per second with the target (useful for understanding how much CPU load is caused on the target simply from handling new connections vs excess modular exponentiation when performing the DHEat attack):
|
||||
.RS
|
||||
.nf
|
||||
ssh-audit targetserver --conn-rate-test=8:100
|
||||
.fi
|
||||
.RE
|
||||
|
||||
.SH RETURN VALUES
|
||||
When a successful connection is made and all algorithms are rated as "good", \fBssh-audit\fP returns 0. Other possible return values are:
|
||||
|
||||
|
17
ssh-audit.py
17
ssh-audit.py
@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""src/ssh_audit/ssh_audit.py wrapper for backwards compatibility"""
|
||||
|
||||
import multiprocessing
|
||||
import sys
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
@ -10,12 +11,14 @@ sys.path.insert(0, str(Path(__file__).resolve().parent / "src"))
|
||||
from ssh_audit.ssh_audit import main # noqa: E402
|
||||
from ssh_audit import exitcodes # noqa: E402
|
||||
|
||||
exit_code = exitcodes.GOOD
|
||||
if __name__ == "__main__":
|
||||
multiprocessing.freeze_support() # Needed for PyInstaller (Windows) builds.
|
||||
|
||||
try:
|
||||
exit_code = main()
|
||||
except Exception:
|
||||
exit_code = exitcodes.UNKNOWN_ERROR
|
||||
print(traceback.format_exc())
|
||||
exit_code = exitcodes.GOOD
|
||||
try:
|
||||
exit_code = main()
|
||||
except Exception:
|
||||
exit_code = exitcodes.UNKNOWN_ERROR
|
||||
print(traceback.format_exc())
|
||||
|
||||
sys.exit(exit_code)
|
||||
sys.exit(exit_code)
|
||||
|
@ -73,6 +73,7 @@ class _VirtualSocket:
|
||||
self.rdata = []
|
||||
self.sdata = []
|
||||
self.errors = {}
|
||||
self.blocking = False
|
||||
self.gsock = _VirtualGlobalSocket(self)
|
||||
|
||||
def _check_err(self, method):
|
||||
@ -83,12 +84,18 @@ class _VirtualSocket:
|
||||
def connect(self, address):
|
||||
return self._connect(address, False)
|
||||
|
||||
def connect_ex(self, address):
|
||||
return self.connect(address)
|
||||
|
||||
def _connect(self, address, ret=True):
|
||||
self.peer_address = address
|
||||
self._connected = True
|
||||
self._check_err('connect')
|
||||
return self if ret else None
|
||||
|
||||
def setblocking(self, r: bool):
|
||||
self.blocking = r
|
||||
|
||||
def settimeout(self, timeout):
|
||||
self.timeout = timeout
|
||||
|
||||
|
@ -1,7 +1,5 @@
|
||||
{
|
||||
"additional_notes": [
|
||||
""
|
||||
],
|
||||
"additional_notes": [],
|
||||
"banner": {
|
||||
"comments": null,
|
||||
"protocol": "2.0",
|
||||
|
@ -1,7 +1,5 @@
|
||||
{
|
||||
"additional_notes": [
|
||||
""
|
||||
],
|
||||
"additional_notes": [],
|
||||
"banner": {
|
||||
"comments": null,
|
||||
"protocol": "1.99",
|
||||
|
@ -1,7 +1,5 @@
|
||||
{
|
||||
"additional_notes": [
|
||||
""
|
||||
],
|
||||
"additional_notes": [],
|
||||
"banner": {
|
||||
"comments": null,
|
||||
"protocol": "2.0",
|
||||
|
@ -1,7 +1,5 @@
|
||||
{
|
||||
"additional_notes": [
|
||||
""
|
||||
],
|
||||
"additional_notes": [],
|
||||
"banner": {
|
||||
"comments": null,
|
||||
"protocol": "2.0",
|
||||
|
@ -1,7 +1,5 @@
|
||||
{
|
||||
"additional_notes": [
|
||||
""
|
||||
],
|
||||
"additional_notes": [],
|
||||
"banner": {
|
||||
"comments": null,
|
||||
"protocol": "2.0",
|
||||
|
@ -1,7 +1,5 @@
|
||||
{
|
||||
"additional_notes": [
|
||||
""
|
||||
],
|
||||
"additional_notes": [],
|
||||
"banner": {
|
||||
"comments": null,
|
||||
"protocol": "2.0",
|
||||
|
@ -1,7 +1,5 @@
|
||||
{
|
||||
"additional_notes": [
|
||||
""
|
||||
],
|
||||
"additional_notes": [],
|
||||
"banner": {
|
||||
"comments": null,
|
||||
"protocol": "2.0",
|
||||
|
@ -1,7 +1,5 @@
|
||||
{
|
||||
"additional_notes": [
|
||||
""
|
||||
],
|
||||
"additional_notes": [],
|
||||
"banner": {
|
||||
"comments": null,
|
||||
"protocol": "2.0",
|
||||
|
@ -1,7 +1,5 @@
|
||||
{
|
||||
"additional_notes": [
|
||||
""
|
||||
],
|
||||
"additional_notes": [],
|
||||
"banner": {
|
||||
"comments": null,
|
||||
"protocol": "2.0",
|
||||
|
@ -1,7 +1,5 @@
|
||||
{
|
||||
"additional_notes": [
|
||||
""
|
||||
],
|
||||
"additional_notes": [],
|
||||
"banner": {
|
||||
"comments": null,
|
||||
"protocol": "2.0",
|
||||
|
@ -1,7 +1,5 @@
|
||||
{
|
||||
"additional_notes": [
|
||||
""
|
||||
],
|
||||
"additional_notes": [],
|
||||
"banner": {
|
||||
"comments": "",
|
||||
"protocol": "2.0",
|
||||
|
29
test/test_dheater.py
Normal file
29
test/test_dheater.py
Normal file
@ -0,0 +1,29 @@
|
||||
import pytest
|
||||
|
||||
from ssh_audit.ssh2_kexdb import SSH2_KexDB
|
||||
from ssh_audit.dheat import DHEat
|
||||
|
||||
|
||||
class TestDHEat:
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def init(self):
|
||||
self.SSH2_KexDB = SSH2_KexDB
|
||||
self.DHEat = DHEat
|
||||
|
||||
def test_kex_definition_completeness(self):
|
||||
alg_db = self.SSH2_KexDB.get_db()
|
||||
kex_db = alg_db['kex']
|
||||
|
||||
# Get all Diffie-Hellman algorithms defined in our database.
|
||||
dh_algs = []
|
||||
for kex in kex_db:
|
||||
if kex.startswith('diffie-hellman-'):
|
||||
dh_algs.append(kex)
|
||||
|
||||
# Ensure that each DH algorithm in our database is in either DHEat's alg_priority or gex_algs list. Also ensure that all non-group exchange algorithms are accounted for in the alg_modulus_sizes dictionary.
|
||||
for dh_alg in dh_algs:
|
||||
assert (dh_alg in self.DHEat.alg_priority) or (dh_alg in self.DHEat.gex_algs)
|
||||
|
||||
if dh_alg.find("group-exchange") == -1:
|
||||
assert dh_alg in self.DHEat.alg_modulus_sizes
|
@ -17,6 +17,7 @@ class TestErrors:
|
||||
conf = self.AuditConf('localhost', 22)
|
||||
conf.colors = False
|
||||
conf.batch = True
|
||||
conf.skip_rate_test = True
|
||||
return conf
|
||||
|
||||
def _audit(self, spy, conf=None, exit_expected=False):
|
||||
|
@ -33,6 +33,7 @@ class TestSSH1:
|
||||
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):
|
||||
|
@ -32,6 +32,7 @@ class TestSSH2:
|
||||
conf.verbose = True
|
||||
conf.ssh1 = False
|
||||
conf.ssh2 = True
|
||||
conf.skip_rate_test = True
|
||||
return conf
|
||||
|
||||
@classmethod
|
||||
|
3
tox.ini
3
tox.ini
@ -101,11 +101,14 @@ disable =
|
||||
no-else-return,
|
||||
super-with-arguments, # Can be re-factored, at some point.
|
||||
too-complex,
|
||||
too-many-arguments,
|
||||
too-many-boolean-expressions,
|
||||
too-many-branches,
|
||||
too-many-instance-attributes,
|
||||
too-many-lines,
|
||||
too-many-locals,
|
||||
too-many-nested-blocks,
|
||||
too-many-return-statements,
|
||||
too-many-statements,
|
||||
consider-using-f-string
|
||||
max-complexity = 15
|
||||
|
Loading…
Reference in New Issue
Block a user