1
0
mirror of https://github.com/jtesta/ssh-audit.git synced 2025-07-06 14:02:49 -05:00

60 Commits

Author SHA1 Message Date
4f2f995b62 Bumped version to v2.5.0. 2021-08-26 15:24:34 -04:00
134236fa7f Fixed badge link. 2021-08-26 14:39:24 -04:00
a6b658d194 Updated badges. 2021-08-26 13:12:13 -04:00
297a807f88 Added Github Actions support. 2021-08-26 12:47:48 -04:00
20d94df400 Updated Windows packaging instructions. 2021-08-26 12:18:06 -04:00
b76060cf49 Updated Tox test section. 2021-08-26 12:16:39 -04:00
1cf1c874db Added Python 3.10 support. 2021-08-26 10:56:43 -04:00
992d8233c9 Remove cache files created during build. 2021-08-26 10:47:43 -04:00
f377b7cea3 Now prompts user for release version, cleans up cached files from previous invokation, and resets all local changes upon completion. 2021-08-26 10:39:11 -04:00
70d9ab2e6b Check if -dev is in version string. (#106) 2021-08-25 14:24:10 -04:00
e7d320f602 Fixed new pylint warnings. 2021-08-25 13:28:30 -04:00
682cb66f85 Added OpenSSH v8.6 & v8.7 policies. 2021-08-25 12:30:38 -04:00
076681a671 Added 3 new key exchanges: gss-gex-sha1-eipGX3TCiQSrx573bT1o1Q==, gss-group1-sha1-eipGX3TCiQSrx573bT1o1Q==, gss-group14-sha1-eipGX3TCiQSrx573bT1o1Q== 2021-07-08 10:18:25 -04:00
98a1fb0315 Added two new MACs: 'AEAD_AES_128_GCM', and 'AEAD_AES_256_GCM'. 2021-06-28 21:59:41 -04:00
45da9f20ae Added 'rsa-sha2-512' and 'rsa-sha2-256' to OpenSSH 8.1 (and earlier) policies. 2021-05-31 15:49:56 -04:00
aa21df29e7 Now handles exceptions during server KEX parsing more gracefully. 2021-05-24 19:50:25 -04:00
32ed9242af Now prints JSON with indents when is used (useful for debugging). 2021-05-20 19:04:35 -04:00
07862489c4 Added MD5 fingerprint hashes to verbose output. 2021-05-20 18:03:24 -04:00
e508a963e7 Added 1 new MAC: hmac-ripemd160-96. 2021-05-20 14:17:37 -04:00
2f1a2a60b1 Added ToC to README.md (#101) 2021-03-04 18:23:12 -05:00
5eb669e01c Updated README. 2021-03-02 11:27:40 -05:00
8e9fe20fac SSH_Socket's constructor now takes an OutputBuffer for verbose & debugging output. 2021-03-02 11:25:37 -05:00
83bd049486 Debug Logging and visibility of SSH Connection errors (#99)
* Debug Logging and visibility of SSH Connection errors

* Updated date in man page
2021-03-02 11:06:40 -05:00
c483fe1861 Fixed a crash while doing host key tests. 2021-02-26 16:01:30 -05:00
741bd631e2 Updated packaging instructions. 2021-02-24 10:18:12 -05:00
f96c0501e9 Bumped version number. 2021-02-23 20:39:18 -05:00
446a411424 Added build_windows_executable.sh. 2021-02-23 19:54:12 -05:00
b300ad1252 Refactored IPv4/6 preference logic to fix pylint warnings. 2021-02-23 16:05:01 -05:00
1bbc3feb57 Added OpenSSH 8.5 built-in policy. Added sntrup761x25519-sha512@openssh.com kex. 2021-02-23 16:02:20 -05:00
8f9771c4e6 Added markdown to PACKAGING. 2021-02-23 09:46:58 -05:00
8a8c284d9a Colour no longer disabled on older vers of Windows. If ssh-audit invoked with a manual parameter and the colorama library was not imported then colour output is disabled. (#95) 2021-02-18 14:52:08 -05:00
1b7cfbec71 Disable color output on Windows 8 and Windows Server 2012. 2021-02-06 11:03:39 -05:00
3c0fc8ead4 Updated README. 2021-02-05 22:12:27 -05:00
ef831d17e0 When -n/--no-colors is used, strip out color from Windows man page. 2021-02-05 21:45:56 -05:00
36094611ce Fixed unicode errors when printing the man page on Windows. 2021-02-05 20:39:12 -05:00
49cf91a902 No longer ignoring mypy and pylint results. 2021-02-05 16:26:14 -05:00
11e2e77585 Simplified Windows man page processing. Added Cygwin support to update_windows_man_page.sh. 2021-02-05 16:25:04 -05:00
090b5d760b Man Page on Windows (#93)
* Man Page on Windows

* Corrected typo in update_windows_man_page.sh

* Check that the 'sed' (stream editor) binary exists
2021-02-05 15:43:50 -05:00
7878d66a46 Now using Python 3.9 base image. 2021-02-02 13:25:52 -05:00
730d6904c2 Updated README. 2021-02-02 12:22:50 -05:00
e0f0956edc Added extra warnings for SSHv1. (#6) 2021-02-02 12:20:37 -05:00
d42725652f Updated README. 2021-02-02 09:54:10 -05:00
6b67a2efb3 Add your local server config to .gitignore (#84) 2021-02-01 19:26:57 -05:00
c49a0fb22f Upgraded SHA-1 key signatures from warnings to failures. Added deprecation warning to ssh-rsa-cert-v00@openssh.com, ssh-rsa-cert-v01@openssh.com, x509v3-sign-rsa, and x509v3-ssh-rsa host key types. 2021-02-01 19:19:46 -05:00
dbe14a075e Added future deprecation notice of ssh-rsa (#92) 2021-02-01 13:17:46 -05:00
13d15baa2a Added multi-threaded scanning support. 2021-02-01 13:10:06 -05:00
bbb81e24ab Streamlined sending of KEXINIT messages. 2021-01-21 11:23:40 -05:00
bbbd75ee69 Tox will now fail on pylint or typing problems. 2021-01-21 10:47:52 -05:00
60de5e55cb Transformed comment type annotations to variable declaration annotations. 2021-01-21 10:20:48 -05:00
4e2f9da632 Updated README. 2021-01-21 07:53:09 -05:00
287c551ff8 Removed Python 3.5 support. 2021-01-20 20:47:26 -05:00
d9a4b49560 Removed Python 3.5 support. Added ARM64 testing in Travis. 2021-01-20 15:58:48 -05:00
a4c78512d8 Add support to ppc64le (#88) 2021-01-20 15:54:55 -05:00
1ba4c7c7ca Send KEX before reading server's KEX during host key and GEX tests; this prevents deadlock against certain server implementations. 2021-01-20 15:27:38 -05:00
338ffc5adb Fixed crash when receiving unexpected response during host key test. 2020-11-05 20:29:39 -05:00
52d1e8f27b Fixed pylint warning. 2020-11-05 20:28:14 -05:00
00dc22b00b Delete output directory only upon successful run to make debugging easier. 2020-11-05 20:25:34 -05:00
0d9881966c Added version check for OpenSSH user enumeration (CVE-2018-15473). (#83) 2020-11-05 20:24:09 -05:00
5c8dc5105b Bumped version number. 2020-11-05 20:16:35 -05:00
75be333bd2 Updated packaging instructions and merged Windows instructions. 2020-10-28 21:01:47 -04:00
67 changed files with 1314 additions and 699 deletions

24
.github/workflows/tox.yaml vendored Normal file
View File

@ -0,0 +1,24 @@
name: ssh-audit
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.6, 3.7, 3.8, 3.9]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install codecov coveralls flake8 mypy pylint tox vulture
- name: Run Tox
run: |
tox

3
.gitignore vendored
View File

@ -22,3 +22,6 @@ reports/
/snap/ /snap/
/stage/ /stage/
/ssh-audit_*.snap /ssh-audit_*.snap
# Your local server config
servers.txt

View File

@ -1,11 +1,16 @@
language: python language: python
arch:
- arm64
- amd64
- ppc64le
python: python:
- "3.5"
- "3.6" - "3.6"
- "3.7" - "3.7"
- "3.8" - "3.8"
- "3.9" - "3.9"
- "3.10"
cache: cache:
- pip - pip

View File

@ -11,26 +11,8 @@ However, if you can submit patches that pass all of our automated tests, then yo
Tox is used to do unit testing, linting with [pylint](http://pylint.pycqa.org/en/latest/) & [flake8](https://flake8.pycqa.org/en/latest/), and static type-checking with [mypy](https://mypy.readthedocs.io/en/stable/). Tox is used to do unit testing, linting with [pylint](http://pylint.pycqa.org/en/latest/) & [flake8](https://flake8.pycqa.org/en/latest/), and static type-checking with [mypy](https://mypy.readthedocs.io/en/stable/).
### Running tests on Ubuntu 18.04 and later
For Ubuntu 18.04 or later, install tox with `apt install tox`, then simply run `tox` in the top-level directory. Look for any error messages in the (verbose) output. For Ubuntu 18.04 or later, install tox with `apt install tox`, then simply run `tox` in the top-level directory. Look for any error messages in the (verbose) output.
### Running tests on Ubuntu 16.04
For Ubuntu 16.04 (which is still supported until April 2021), a newer version of tox is needed. The easiest way is to use virtualenv:
```
$ sudo apt install python3-virtualenv
$ virtualenv -p /usr/bin/python3 ~/venv_ssh-audit
$ source ~/venv_ssh-audit/bin/activate
$ pip install tox
```
Then, to run the tox tests:
```
$ source ~/venv_ssh-audit/bin/activate
$ cd path/to/ssh-audit
$ tox
```
## Docker Tests ## Docker Tests

View File

@ -1,4 +1,4 @@
FROM python:3.8-slim FROM python:3.9-slim
WORKDIR / WORKDIR /

View File

@ -1,52 +0,0 @@
= PyPI =
To create package and upload to test server:
$ sudo apt install python3-virtualenv
$ make -f Makefile.pypi
$ make -f Makefile.pypi uploadtest
To download from test server and verify:
$ virtualenv -p /usr/bin/python3 /tmp/pypi_test
$ cd /tmp/pypi_test; source bin/activate
$ pip3 install --index-url https://test.pypi.org/simple ssh-audit
To upload to production server (hint: use username '__token__' and API token):
$ make -f Makefile.pypi uploadprod
To download from production server and verify:
$ virtualenv -p /usr/bin/python3 /tmp/pypi_prod
$ cd /tmp/pypi_prod; source bin/activate
$ pip3 install ssh-audit
----
= Snap =
To create the snap package, run a fully-updated Ubuntu Server 20.04 VM.
As root, run (leave all options default):
# lxd init
Bump the version number in snapcraft.yaml. Then run:
# make -f Makefile.snap
----
= Docker =
Build image with:
$ make -f Makefile.docker
Then upload them to Dockerhub with:
$ make -f Makefile.docker upload

97
PACKAGING.md Normal file
View File

@ -0,0 +1,97 @@
# Windows
An executable can only be made on a Windows host because the PyInstaller tool (https://www.pyinstaller.org/) does not support cross-compilation.
1.) Install Python v3.9.x from https://www.python.org/. To make life easier, check the option to add Python to the PATH environment variable.
2.) Using pip, install pyinstaller and colorama:
```
pip install pyinstaller colorama
```
3.) Install Cygwin (https://www.cygwin.com/).
4.) Create the executable with:
```
$ ./build_windows_executable.sh
```
# PyPI
To create package and upload to test server:
```
$ sudo apt install python3-virtualenv
$ make -f Makefile.pypi
$ make -f Makefile.pypi uploadtest
```
To download from test server and verify:
```
$ virtualenv -p /usr/bin/python3 /tmp/pypi_test
$ cd /tmp/pypi_test; source bin/activate
$ pip3 install --index-url https://test.pypi.org/simple ssh-audit
```
To upload to production server (hint: use username '\_\_token\_\_' and API token):
```
$ make -f Makefile.pypi uploadprod
```
To download from production server and verify:
```
$ virtualenv -p /usr/bin/python3 /tmp/pypi_prod
$ cd /tmp/pypi_prod; source bin/activate
$ pip3 install ssh-audit
```
# Snap
To create the snap package, run a fully-updated Ubuntu Server 20.04 VM.
Install pre-requisites with:
```
$ sudo apt install make snapcraft
$ sudo snap install review-tools lxd
```
Initialize LXD (leave all options default):
```
$ sudo lxd init
```
Bump the version number in snapcraft.yaml. Then run:
```
$ make -f Makefile.snap
```
Upload the snap with:
```
$ snapcraft login
$ snapcraft upload --release=stable ssh-audit_*.snap
```
# Docker
Build image with:
```
$ make -f Makefile.docker
```
Then upload it to Dockerhub with:
```
$ make -f Makefile.docker upload
```

View File

@ -1,14 +1,25 @@
# ssh-audit # ssh-audit
[![Build Status](https://travis-ci.org/jtesta/ssh-audit.svg?branch=master)](https://travis-ci.org/jtesta/ssh-audit) [![License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](https://github.com/jtesta/ssh-audit/blob/master/LICENSE)
<!-- [![PyPI Downloads](https://img.shields.io/pypi/dm/ssh-audit)](https://pypi.org/project/ssh-audit/)
[![appveyor build status](https://ci.appveyor.com/api/projects/status/4m5r73m0r023edil/branch/develop?svg=true)](https://ci.appveyor.com/project/arthepsy/ssh-audit) [![Docker Pulls](https://img.shields.io/docker/pulls/positronsecurity/ssh-audit)](https://hub.docker.com/r/positronsecurity/ssh-audit)
[![codecov](https://codecov.io/gh/arthepsy/ssh-audit/branch/develop/graph/badge.svg)](https://codecov.io/gh/arthepsy/ssh-audit) [![Build Status](https://github.com/jtesta/ssh-audit/actions/workflows/tox.yaml/badge.svg)](https://github.com/jtesta/ssh-audit/actions)
[![Quality Gate](https://sonarqube.com/api/badges/gate?key=arthepsy-github%3Assh-audit%3Adevelop&template=ROUNDED)](https://sq.evolutiongaming.com/dashboard?id=arthepsy-github%3Assh-audit%3Adevelop) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/jtesta/ssh-audit/blob/master/CONTRIBUTING.md)
-->
**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.
- [Features](#features)
- [Usage](#usage)
- [Screenshots](#screenshots)
- [Server Standard Audit Example](#server-standard-audit-example)
- [Server Policy Audit Example](#server-policy-audit-example)
- [Client Standard Audit Example](#client-standard-audit-example)
- [Hardening Guides](#hardening-guides)
- [Pre-Built Packages](#pre-built-packages)
- [Web Front-End](#web-front-end)
- [ChangeLog](#changelog)
## Features ## Features
- SSH1 and SSH2 protocol server support; - SSH1 and SSH2 protocol server support;
- analyze SSH client configuration; - analyze SSH client configuration;
@ -21,6 +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.6 - 3.9;
- no dependencies - no dependencies
## Usage ## Usage
@ -36,11 +48,13 @@ usage: ssh-audit.py [options] <host>
-c, --client-audit starts a server on port 2222 to audit client -c, --client-audit starts a server on port 2222 to audit client
software config (use -p to change port; software config (use -p to change port;
use -t to change timeout) use -t to change timeout)
-j, --json JSON output -d, --debug Enable debug output.
-j, --json JSON output (use -jj to enable indents)
-l, --level=<level> minimum output level (info|warn|fail) -l, --level=<level> minimum output level (info|warn|fail)
-L, --list-policies list all the official, built-in policies -L, --list-policies list all the official, built-in policies
--lookup=<alg1,alg2,...> looks up an algorithm(s) without --lookup=<alg1,alg2,...> looks up an algorithm(s) without
connecting to a server connecting to a server
-m, --manual print the man page (Windows only)
-M, --make-policy=<policy.txt> creates a policy based on the target server -M, --make-policy=<policy.txt> creates a policy based on the target server
(i.e.: the target server has the ideal (i.e.: the target server has the ideal
configuration that other servers should configuration that other servers should
@ -53,6 +67,8 @@ usage: ssh-audit.py [options] <host>
(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]) per line, format HOST[:PORT])
--threads=<threads> number of threads to use when scanning multiple
targets (-T/--targets) (default: 32)
-v, --verbose verbose output -v, --verbose verbose output
``` ```
* if both IPv4 and IPv6 are used, order of precedence can be set by using either `-46` or `-64`. * if both IPv4 and IPv6 are used, order of precedence can be set by using either `-46` or `-64`.
@ -86,7 +102,7 @@ To audit a client configuration, with a listener on port 4567:
ssh-audit -c -p 4567 ssh-audit -c -p 4567
``` ```
To list all official built-in policies (hint: use resulting file paths with `-P`/`--policy`): To list all official built-in policies (hint: use resulting policy names with `-P`/`--policy`):
``` ```
ssh-audit -L ssh-audit -L
``` ```
@ -106,17 +122,19 @@ To run a policy audit against many servers:
ssh-audit -T servers.txt -P ["policy name" | path/to/server_policy.txt] ssh-audit -T servers.txt -P ["policy name" | path/to/server_policy.txt]
``` ```
To create a policy based on a target server (which can be manually edited; see official built-in policies for syntax examples): To create a policy based on a target server (which can be manually edited):
``` ```
ssh-audit -M new_policy.txt targetserver ssh-audit -M new_policy.txt targetserver
``` ```
## Screenshots
### Server Standard Audit Example ### Server Standard Audit Example
Below is a screen shot of the standard server-auditing output when connecting to an unhardened OpenSSH v5.3 service: Below is a screen shot of the standard server-auditing output when connecting to an unhardened OpenSSH v5.3 service:
![screenshot](https://user-images.githubusercontent.com/2982011/64388792-317e6f80-d00e-11e9-826e-a4934769bb07.png) ![screenshot](https://user-images.githubusercontent.com/2982011/64388792-317e6f80-d00e-11e9-826e-a4934769bb07.png)
### Server Policy Audit Example ### Server Policy Audit Example
Below is a screen shot of the policy auditing output when connecting to an un-hardened Ubuntu Server 20.04 machine: Below is a screen shot of the policy auditing output when connecting to an un-hardened Ubuntu Server 20.04 machine (hint: use `-L`/`--list-policies` to see names of built-in policies to use with `-P`/`--policy`):
![screenshot](https://user-images.githubusercontent.com/2982011/94370881-95178700-00c0-11eb-8705-3157a4669dc0.png) ![screenshot](https://user-images.githubusercontent.com/2982011/94370881-95178700-00c0-11eb-8705-3157a4669dc0.png)
After applying the steps in the hardening guide (see below), the output changes to the following: After applying the steps in the hardening guide (see below), the output changes to the following:
@ -126,10 +144,10 @@ After applying the steps in the hardening guide (see below), the output changes
Below is a screen shot of the client-auditing output when an unhardened OpenSSH v7.2 client connects: Below is a screen shot of the client-auditing output when an unhardened OpenSSH v7.2 client connects:
![client_screenshot](https://user-images.githubusercontent.com/2982011/68867998-b946c100-06c4-11ea-975f-1f47e4178a74.png) ![client_screenshot](https://user-images.githubusercontent.com/2982011/68867998-b946c100-06c4-11ea-975f-1f47e4178a74.png)
### Hardening Guides ## Hardening Guides
Guides to harden server & client configuration can be found here: [https://www.ssh-audit.com/hardening_guides.html](https://www.ssh-audit.com/hardening_guides.html) Guides to harden server & client configuration can be found here: [https://www.ssh-audit.com/hardening_guides.html](https://www.ssh-audit.com/hardening_guides.html)
### Pre-Built Packages ## Pre-Built Packages
Pre-built packages are available for Windows (see the releases page), on PyPI, Snap, and Homebrew. Pre-built packages are available for Windows (see the releases page), on PyPI, Snap, and Homebrew.
To install from PyPI: To install from PyPI:
@ -153,10 +171,36 @@ $ 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 -p 2222:2222 positronsecurity/ssh-audit 10.1.1.1`)
### Web Front-End ## Web Front-End
For convenience, a web front-end on top of the command-line tool is available at [https://www.ssh-audit.com/](https://www.ssh-audit.com/). For convenience, a web front-end on top of the command-line tool is available at [https://www.ssh-audit.com/](https://www.ssh-audit.com/).
## ChangeLog ## ChangeLog
### v2.5.0 (2021-08-26)
- Fixed crash when running host key tests.
- Handles server connection failures more gracefully.
- Now prints JSON with indents when `-jj` is used (useful for debugging).
- Added MD5 fingerprints to verbose output.
- Added `-d`/`--debug` option for getting debugging output; credit [Adam Russell](https://github.com/thecliguy).
- Updated JSON output to include MD5 fingerprints. Note that this results in a breaking change in the 'fingerprints' dictionary format.
- Updated OpenSSH 8.1 (and earlier) policies to include `rsa-sha2-512` and `rsa-sha2-256`.
- Added OpenSSH v8.6 & v8.7 policies.
- Added 3 new key exchanges: `gss-gex-sha1-eipGX3TCiQSrx573bT1o1Q==`, `gss-group1-sha1-eipGX3TCiQSrx573bT1o1Q==`, and `gss-group14-sha1-eipGX3TCiQSrx573bT1o1Q==`.
- Added 3 new MACs: `hmac-ripemd160-96`, `AEAD_AES_128_GCM`, and `AEAD_AES_256_GCM`.
### v2.4.0 (2021-02-23)
- Added multi-threaded scanning support.
- Added built-in Windows manual page (see `-m`/`--manual`); credit [Adam Russell](https://github.com/thecliguy).
- Added version check for OpenSSH user enumeration (CVE-2018-15473).
- Added deprecation note to host key types based on SHA-1.
- Added extra warnings for SSHv1.
- Added built-in hardened OpenSSH v8.5 policy.
- Upgraded warnings to failures for host key types based on SHA-1.
- Fixed crash when receiving unexpected response during host key test.
- Fixed hang against older Cisco devices during host key test & gex test.
- Fixed improper termination while scanning multiple targets when one target returns an error.
- Dropped support for Python 3.5 (which reached EOL in Sept. 2020).
- Added 1 new key exchange: `sntrup761x25519-sha512@openssh.com`.
### v2.3.1 (2020-10-28) ### v2.3.1 (2020-10-28)
- Now parses public key sizes for `rsa-sha2-256-cert-v01@openssh.com` and `rsa-sha2-512-cert-v01@openssh.com` host key types. - Now parses public key sizes for `rsa-sha2-256-cert-v01@openssh.com` and `rsa-sha2-512-cert-v01@openssh.com` host key types.
- Flag `ssh-rsa-cert-v01@openssh.com` as a failure due to SHA-1 hash. - Flag `ssh-rsa-cert-v01@openssh.com` as a failure due to SHA-1 hash.

130
build_windows_executable.sh Executable file
View File

@ -0,0 +1,130 @@
#!/bin/bash
#
# The MIT License (MIT)
#
# Copyright (C) 2021 Joe Testa (jtesta@positronsecurity.com)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
################################################################################
# build_windows_executable.sh
#
# Builds a Windows executable using PyInstaller.
################################################################################
PLATFORM="$(uname -s)"
# This script is intended for use on Cygwin only.
case "$PLATFORM" in
CYGWIN*) ;;
*)
echo "Platform not supported ($PLATFORM). This must be run in Cygwin only."
exit 1
;;
esac
# Ensure that Python 3.x is installed.
if [[ "$(python -V)" != "Python 3."* ]]; then
echo "Python v3.x not found. Install the latest stable version from: https://www.python.org/"
exit 1
fi
# Ensure that pyinstaller is installed.
command -v pyinstaller >/dev/null 2>&1 || { echo >&2 "pyinstaller not found. Install with: 'pip install pyinstaller'"; exit 1; }
# Ensure that the colorama module is installed.
X=`pip show colorama` 2> /dev/null
if [[ $? != 0 ]]; then
echo "Colorama module not found. Install with: 'pip install colorama'"
exit 1
fi
# Prompt for the version to release.
echo -n "Enter the version to release, using format 'vX.X.X': "
read -r version
# Ensure that entered version fits required format.
if [[ ! $version =~ ^v[0-9]\.[0-9]\.[0-9]$ ]]; then
echo "Error: version string does not match format vX.X.X!"
exit 1
fi
# Verify that version is correct.
echo -n "Version will be set to '${version}'. Is this correct? (y/n): "
read -r yn
echo
if [[ $yn != "y" ]]; then
echo "Build cancelled."
exit 1
fi
# Reset any local changes made to globals.py from a previous run.
git checkout src/ssh_audit/globals.py 2> /dev/null
# Update the man page.
./update_windows_man_page.sh
if [[ $? != 0 ]]; then
echo "Failed to run ./update_windows_man_page.sh"
exit 1
fi
# Do all operations from this point from the main source directory.
pushd src/ssh_audit > /dev/null
# Delete the existing VERSION variable and add the value that the user entered, above.
sed -i '/^VERSION/d' globals.py
echo "VERSION = '$version'" >> globals.py
# Delete cached files if they exist from a prior run.
rm -rf dist/ build/ ssh-audit.spec
# Create a hard link from ssh_audit.py to ssh-audit.py.
if [[ ! -f ssh-audit.py ]]; then
ln ssh_audit.py ssh-audit.py
fi
echo -e "\nRunning pyinstaller...\n"
pyinstaller -F --icon ../../windows_icon.ico ssh-audit.py
if [[ -f dist/ssh-audit.exe ]]; then
echo -e "\nExecutable created in $(pwd)/dist/ssh-audit.exe\n"
else
echo -e "\nFAILED to create $(pwd)/dist/ssh-audit.exe!\n"
exit 1
fi
# Ensure that the version string doesn't have '-dev' in it.
X=`dist/ssh-audit.exe | grep -E 'ssh-audit.exe v.+\-dev'` > /dev/null
if [[ $? == 0 ]]; then
echo -e "\nError: executable's version number includes '-dev'."
exit 1
fi
# Remove the cache files created during the build process, along with the link we created, above.
rm -rf build/ ssh-audit.spec ssh-audit.py
# Reset the changes we made to globals.py.
git checkout globals.py 2> /dev/null
popd > /dev/null
exit 0

View File

@ -443,6 +443,7 @@ function run_test {
fi fi
cid=`docker run -d -p 2222:22 ${IMAGE_NAME}:${IMAGE_VERSION} ${server_exec}` cid=`docker run -d -p 2222:22 ${IMAGE_NAME}:${IMAGE_VERSION} ${server_exec}`
#echo "Running: docker run -d -p 2222:22 ${IMAGE_NAME}:${IMAGE_VERSION} ${server_exec}"
if [[ $? != 0 ]]; then if [[ $? != 0 ]]; then
echo -e "${REDB}Failed to run docker image! (exit code: $?)${CLR}" echo -e "${REDB}Failed to run docker image! (exit code: $?)${CLR}"
exit 1 exit 1
@ -699,17 +700,17 @@ run_custom_policy_test 'config2' 'test13' $PROGRAM_RETVAL_GOOD
run_custom_policy_test 'config2' 'test14' $PROGRAM_RETVAL_FAILURE run_custom_policy_test 'config2' 'test14' $PROGRAM_RETVAL_FAILURE
# Passing test for built-in OpenSSH 8.0p1 server policy. # Passing test for built-in OpenSSH 8.0p1 server policy.
run_builtin_policy_test "Hardened OpenSSH Server v8.0 (version 1)" "8.0p1" "test1" "-o HostKeyAlgorithms=ssh-ed25519 -o KexAlgorithms=curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha256 -o Ciphers=chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr -o MACs=hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,umac-128-etm@openssh.com" $PROGRAM_RETVAL_GOOD run_builtin_policy_test "Hardened OpenSSH Server v8.0 (version 1)" "8.0p1" "test1" "-o HostKeyAlgorithms=rsa-sha2-512,rsa-sha2-256,ssh-ed25519 -o KexAlgorithms=curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha256 -o Ciphers=chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr -o MACs=hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,umac-128-etm@openssh.com" $PROGRAM_RETVAL_GOOD
# Failing test for built-in OpenSSH 8.0p1 server policy (MACs not hardened). # Failing test for built-in OpenSSH 8.0p1 server policy (MACs not hardened).
run_builtin_policy_test "Hardened OpenSSH Server v8.0 (version 1)" "8.0p1" "test2" "-o HostKeyAlgorithms=ssh-ed25519 -o KexAlgorithms=curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha256 -o Ciphers=chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr" $PROGRAM_RETVAL_FAILURE run_builtin_policy_test "Hardened OpenSSH Server v8.0 (version 1)" "8.0p1" "test2" "-o HostKeyAlgorithms=rsa-sha2-512,rsa-sha2-256,ssh-ed25519 -o KexAlgorithms=curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha256 -o Ciphers=chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr" $PROGRAM_RETVAL_FAILURE
if [[ $num_failures == 0 ]]; then if [[ $num_failures == 0 ]]; then
echo -e "\n${GREENB}ALL TESTS PASS!${CLR}\n" echo -e "\n${GREENB}ALL TESTS PASS!${CLR}\n"
rm -rf $TEST_RESULT_DIR
else else
echo -e "\n${REDB}${num_failures} TESTS FAILED!${CLR}\n" echo -e "\n${REDB}${num_failures} TESTS FAILED!${CLR}\n"
fi fi
rm -rf $TEST_RESULT_DIR
exit 0 exit 0

View File

@ -18,7 +18,6 @@ classifiers =
License :: OSI Approved :: MIT License License :: OSI Approved :: MIT License
Operating System :: OS Independent Operating System :: OS Independent
Programming Language :: Python :: 3 Programming Language :: Python :: 3
Programming Language :: Python :: 3.5
Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.8
@ -32,7 +31,7 @@ classifiers =
packages = find: packages = find:
package_dir = package_dir =
= src = src
python_requires = >=3.5,<4 python_requires = >=3.6,<4
[options.packages.find] [options.packages.find]
where = src where = src

View File

@ -1,5 +1,5 @@
name: ssh-audit name: ssh-audit
version: '2.3.1-1' version: '2.4.0-1'
license: 'MIT' license: 'MIT'
summary: ssh-audit summary: ssh-audit
description: | description: |

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-2021 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
@ -55,7 +55,7 @@ class Algorithms:
if self.ssh1kex is None: if self.ssh1kex is None:
return None return None
item = Algorithms.Item(1, SSH1_KexDB.ALGORITHMS) item = Algorithms.Item(1, SSH1_KexDB.ALGORITHMS)
item.add('key', [u'ssh-rsa1']) item.add('key', ['ssh-rsa1'])
item.add('enc', self.ssh1kex.supported_ciphers) item.add('enc', self.ssh1kex.supported_ciphers)
item.add('aut', self.ssh1kex.supported_authentications) item.add('aut', self.ssh1kex.supported_authentications)
return item return item
@ -131,7 +131,7 @@ class Algorithms:
# if version is not None: # if version is not None:
# software = SSH.Software(None, product, version, None, None) # software = SSH.Software(None, product, version, None, None)
# break # break
rec = {} # type: Dict[int, Dict[str, Dict[str, Dict[str, int]]]] rec: Dict[int, Dict[str, Dict[str, Dict[str, int]]]] = {}
if software is None: if software is None:
unknown_software = True unknown_software = True
for alg_pair in self.values: for alg_pair in self.values:
@ -206,7 +206,7 @@ class Algorithms:
def __init__(self, sshv: int, db: Dict[str, Dict[str, List[List[Optional[str]]]]]) -> None: def __init__(self, sshv: int, db: Dict[str, Dict[str, List[List[Optional[str]]]]]) -> None:
self.__sshv = sshv self.__sshv = sshv
self.__db = db self.__db = db
self.__storage = {} # type: Dict[str, List[str]] self.__storage: Dict[str, List[str]] = {}
@property @property
def sshv(self) -> int: def sshv(self) -> int:

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-2021 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
@ -41,45 +41,35 @@ class AuditConf:
self.client_audit = False self.client_audit = False
self.colors = True self.colors = True
self.json = False self.json = False
self.json_print_indent = False
self.verbose = False self.verbose = False
self.level = 'info' self.level = 'info'
self.ipvo = () # type: Sequence[int] self.ip_version_preference: List[int] = [] # Holds only 5 possible values: [] (no preference), [4] (use IPv4 only), [6] (use IPv6 only), [46] (use both IPv4 and IPv6, but prioritize v4), and [64] (use both IPv4 and IPv6, but prioritize v6).
self.ipv4 = False self.ipv4 = False
self.ipv6 = False self.ipv6 = False
self.make_policy = False # When True, creates a policy file from an audit scan. self.make_policy = False # When True, creates a policy file from an audit scan.
self.policy_file = None # type: Optional[str] # File system path to a policy self.policy_file: Optional[str] = None # File system path to a policy
self.policy = None # type: Optional[Policy] # Policy object self.policy: Optional[Policy] = None # Policy object
self.timeout = 5.0 self.timeout = 5.0
self.timeout_set = False # Set to True when the user explicitly sets it. self.timeout_set = False # Set to True when the user explicitly sets it.
self.target_file = None # type: Optional[str] self.target_file: Optional[str] = None
self.target_list = [] # type: List[str] self.target_list: List[str] = []
self.threads = 32
self.list_policies = False self.list_policies = False
self.lookup = '' self.lookup = ''
self.manual = False
self.debug = False
def __setattr__(self, name: str, value: Union[str, int, float, bool, Sequence[int]]) -> None: def __setattr__(self, name: str, value: Union[str, int, float, bool, Sequence[int]]) -> None:
valid = False valid = False
if name in ['ssh1', 'ssh2', 'batch', 'client_audit', 'colors', 'verbose', 'timeout_set', 'json', 'make_policy', 'list_policies']: if name in ['batch', 'client_audit', 'colors', 'json', 'json_print_indent', 'list_policies', 'manual', 'make_policy', 'ssh1', 'ssh2', 'timeout_set', 'verbose', 'debug']:
valid, value = True, bool(value) valid, value = True, bool(value)
elif name in ['ipv4', 'ipv6']: elif name in ['ipv4', 'ipv6']:
valid = False valid, value = True, bool(value)
value = bool(value) if len(self.ip_version_preference) == 2: # Being called more than twice is not valid.
ipv = 4 if name == 'ipv4' else 6 valid = False
if value: elif value:
value = tuple(list(self.ipvo) + [ipv]) self.ip_version_preference.append(4 if name == 'ipv4' else 6)
else: # pylint: disable=else-if-used
if len(self.ipvo) == 0:
value = (6,) if ipv == 4 else (4,)
else:
value = tuple([x for x in self.ipvo if x != ipv])
self.__setattr__('ipvo', value)
elif name == 'ipvo':
if isinstance(value, (tuple, list)):
uniq_value = Utils.unique_seq(value)
value = tuple([x for x in uniq_value if x in (4, 6)])
valid = True
ipv_both = len(value) == 0
object.__setattr__(self, 'ipv4', ipv_both or 4 in value)
object.__setattr__(self, 'ipv6', ipv_both or 6 in value)
elif name == 'port': elif name == 'port':
valid, port = True, Utils.parse_int(value) valid, port = True, Utils.parse_int(value)
if port < 1 or port > 65535: if port < 1 or port > 65535:
@ -96,8 +86,13 @@ class AuditConf:
if value == -1.0: if value == -1.0:
raise ValueError('invalid timeout: {}'.format(value)) raise ValueError('invalid timeout: {}'.format(value))
valid = True valid = True
elif name in ['policy_file', 'policy', 'target_file', 'target_list', 'lookup']: elif name in ['ip_version_preference', 'lookup', 'policy_file', 'policy', 'target_file', 'target_list']:
valid = True valid = True
elif name == "threads":
valid, num_threads = True, Utils.parse_int(value)
if num_threads < 1:
raise ValueError('invalid number of threads: {}'.format(value))
value = num_threads
if valid: if valid:
object.__setattr__(self, name, value) object.__setattr__(self, name, value)

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-2021 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
@ -33,11 +33,11 @@ class Fingerprint:
@property @property
def md5(self) -> str: def md5(self) -> str:
h = hashlib.md5(self.__fpd).hexdigest() h = hashlib.md5(self.__fpd).hexdigest()
r = u':'.join(h[i:i + 2] for i in range(0, len(h), 2)) r = ':'.join(h[i:i + 2] for i in range(0, len(h), 2))
return u'MD5:{}'.format(r) return 'MD5:{}'.format(r)
@property @property
def sha256(self) -> str: def sha256(self) -> str:
h = base64.b64encode(hashlib.sha256(self.__fpd).digest()) h = base64.b64encode(hashlib.sha256(self.__fpd).digest())
r = h.decode('ascii').rstrip('=') r = h.decode('ascii').rstrip('=')
return u'SHA256:{}'.format(r) return 'SHA256:{}'.format(r)

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-2021 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
@ -21,17 +21,18 @@
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 os
# pylint: disable=unused-import # pylint: disable=unused-import
from typing import Dict, List, Set, Sequence, Tuple, Iterable # noqa: F401 from typing import Dict, List, Set, Sequence, Tuple, Iterable # noqa: F401
from typing import Callable, Optional, Union, Any # noqa: F401 from typing import Callable, Optional, Union, Any # noqa: F401
import traceback
from ssh_audit.kexdh import KexGroupExchange_SHA1, KexGroupExchange_SHA256 from ssh_audit.kexdh import KexGroupExchange_SHA1, KexGroupExchange_SHA256
from ssh_audit.protocol import Protocol
from ssh_audit.ssh2_kexdb import SSH2_KexDB from ssh_audit.ssh2_kexdb import SSH2_KexDB
from ssh_audit.ssh2_kex import SSH2_Kex from ssh_audit.ssh2_kex import SSH2_Kex
from ssh_audit.ssh_socket import SSH_Socket from ssh_audit.ssh_socket import SSH_Socket
from ssh_audit.outputbuffer import OutputBuffer
# Performs DH group exchanges to find what moduli are supported, and checks # Performs DH group exchanges to find what moduli are supported, and checks
@ -40,37 +41,38 @@ class GEXTest:
# Creates a new connection to the server. Returns True on success, or False. # Creates a new connection to the server. Returns True on success, or False.
@staticmethod @staticmethod
def reconnect(s: 'SSH_Socket', gex_alg: str) -> bool: def reconnect(out: 'OutputBuffer', s: 'SSH_Socket', kex: 'SSH2_Kex', gex_alg: str) -> bool:
if s.is_connected(): if s.is_connected():
return True return True
err = s.connect() err = s.connect()
if err is not None: if err is not None:
out.v(err, write_now=True)
return False return False
unused = None # pylint: disable=unused-variable _, _, err = s.get_banner()
unused2 = None # pylint: disable=unused-variable
unused, unused2, err = s.get_banner()
if err is not None: if err is not None:
out.v(err, write_now=True)
s.close() s.close()
return False return False
# Parse the server's initial KEX.
packet_type = 0 # pylint: disable=unused-variable
packet_type, payload = s.read_packet(2)
kex = SSH2_Kex.parse(payload)
# Send our KEX using the specified group-exchange and most of the # Send our KEX using the specified group-exchange and most of the
# server's own values. # server's own values.
client_kex = SSH2_Kex(os.urandom(16), [gex_alg], kex.key_algorithms, kex.client, kex.server, False, 0) s.send_kexinit(key_exchanges=[gex_alg], hostkeys=kex.key_algorithms, ciphers=kex.server.encryption, macs=kex.server.mac, compressions=kex.server.compression, languages=kex.server.languages)
s.write_byte(Protocol.MSG_KEXINIT)
client_kex.write(s) try:
s.send_packet() # Parse the server's KEX.
_, payload = s.read_packet(2)
SSH2_Kex.parse(payload)
except Exception:
out.v("Failed to parse server's kex. Stack trace:\n%s" % str(traceback.format_exc()), write_now=True)
return False
return True return True
# Runs the DH moduli test against the specified target. # Runs the DH moduli test against the specified target.
@staticmethod @staticmethod
def run(s: 'SSH_Socket', kex: 'SSH2_Kex') -> None: def run(out: 'OutputBuffer', s: 'SSH_Socket', kex: 'SSH2_Kex') -> None:
GEX_ALGS = { GEX_ALGS = {
'diffie-hellman-group-exchange-sha1': KexGroupExchange_SHA1, 'diffie-hellman-group-exchange-sha1': KexGroupExchange_SHA1,
'diffie-hellman-group-exchange-sha256': KexGroupExchange_SHA256, 'diffie-hellman-group-exchange-sha256': KexGroupExchange_SHA256,
@ -84,13 +86,14 @@ class GEXTest:
# Check if the server supports any of the group-exchange # Check if the server supports any of the group-exchange
# algorithms. If so, test each one. # algorithms. If so, test each one.
for gex_alg in GEX_ALGS: for gex_alg, kex_group_class in GEX_ALGS.items():
if gex_alg in kex.kex_algorithms: if gex_alg in kex.kex_algorithms:
out.d('Preparing to perform DH group exchange using ' + gex_alg + '...', write_now=True)
if GEXTest.reconnect(s, gex_alg) is False: if GEXTest.reconnect(out, s, kex, gex_alg) is False:
break break
kex_group = GEX_ALGS[gex_alg]() kex_group = kex_group_class()
smallest_modulus = -1 smallest_modulus = -1
# First try a range of weak sizes. # First try a range of weak sizes.
@ -117,7 +120,9 @@ class GEXTest:
if bits >= smallest_modulus > 0: if bits >= smallest_modulus > 0:
break break
if GEXTest.reconnect(s, gex_alg) is False: out.d('Preparing to perform DH group exchange using ' + gex_alg + ' with modulus size ' + str(bits) + '...', write_now=True)
if GEXTest.reconnect(out, s, kex, gex_alg) is False:
reconnect_failed = True reconnect_failed = True
break break

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-2021 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
@ -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.
""" """
VERSION = 'v2.3.1' VERSION = 'v2.5.0'
SSH_HEADER = 'SSH-{0}-OpenSSH_8.2' # SSH software to impersonate SSH_HEADER = 'SSH-{0}-OpenSSH_8.2' # SSH software to impersonate
GITHUB_ISSUES_URL = 'https://github.com/jtesta/ssh-audit/issues' # The URL to the Github issues tracker. GITHUB_ISSUES_URL = 'https://github.com/jtesta/ssh-audit/issues' # The URL to the Github issues tracker.
WINDOWS_MAN_PAGE = ''

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-2021 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
@ -21,17 +21,18 @@
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 os
# pylint: disable=unused-import # pylint: disable=unused-import
from typing import Dict, List, Set, Sequence, Tuple, Iterable # noqa: F401 from typing import Dict, List, Set, Sequence, Tuple, Iterable # noqa: F401
from typing import Callable, Optional, Union, Any # noqa: F401 from typing import Callable, Optional, Union, Any # noqa: F401
import traceback
from ssh_audit.kexdh import KexDH, KexGroup1, KexGroup14_SHA1, KexGroup14_SHA256, KexCurve25519_SHA256, KexGroup16_SHA512, KexGroup18_SHA512, KexGroupExchange_SHA1, KexGroupExchange_SHA256, KexNISTP256, KexNISTP384, KexNISTP521 from ssh_audit.kexdh import KexDH, KexGroup1, KexGroup14_SHA1, KexGroup14_SHA256, KexCurve25519_SHA256, KexGroup16_SHA512, KexGroup18_SHA512, KexGroupExchange_SHA1, KexGroupExchange_SHA256, KexNISTP256, KexNISTP384, KexNISTP521
from ssh_audit.protocol import Protocol
from ssh_audit.ssh2_kex import SSH2_Kex from ssh_audit.ssh2_kex import SSH2_Kex
from ssh_audit.ssh2_kexdb import SSH2_KexDB from ssh_audit.ssh2_kexdb import SSH2_KexDB
from ssh_audit.ssh_socket import SSH_Socket from ssh_audit.ssh_socket import SSH_Socket
from ssh_audit.outputbuffer import OutputBuffer
# Obtains host keys, checks their size, and derives their fingerprints. # Obtains host keys, checks their size, and derives their fingerprints.
@ -54,7 +55,7 @@ class HostKeyTest:
} }
@staticmethod @staticmethod
def run(s: 'SSH_Socket', server_kex: 'SSH2_Kex') -> None: def run(out: 'OutputBuffer', s: 'SSH_Socket', server_kex: 'SSH2_Kex') -> None:
KEX_TO_DHGROUP = { KEX_TO_DHGROUP = {
'diffie-hellman-group1-sha1': KexGroup1, 'diffie-hellman-group1-sha1': KexGroup1,
'diffie-hellman-group14-sha1': KexGroup14_SHA1, 'diffie-hellman-group14-sha1': KexGroup14_SHA1,
@ -82,10 +83,10 @@ class HostKeyTest:
break break
if kex_str is not None and kex_group is not None: if kex_str is not None and kex_group is not None:
HostKeyTest.perform_test(s, server_kex, kex_str, kex_group, HostKeyTest.HOST_KEY_TYPES) HostKeyTest.perform_test(out, s, server_kex, kex_str, kex_group, HostKeyTest.HOST_KEY_TYPES)
@staticmethod @staticmethod
def perform_test(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
@ -103,6 +104,8 @@ class HostKeyTest:
# 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.
if host_key_type in server_kex.key_algorithms: if host_key_type in server_kex.key_algorithms:
out.d('Preparing to obtain ' + host_key_type + ' host key...', write_now=True)
cert = host_key_types[host_key_type]['cert'] cert = host_key_types[host_key_type]['cert']
variable_key_len = host_key_types[host_key_type]['variable_key_len'] variable_key_len = host_key_types[host_key_type]['variable_key_len']
@ -110,34 +113,35 @@ class HostKeyTest:
if not s.is_connected(): if not s.is_connected():
err = s.connect() err = s.connect()
if err is not None: if err is not None:
out.v(err, write_now=True)
return return
_, _, err = s.get_banner() _, _, err = s.get_banner()
if err is not None: if err is not None:
out.v(err, write_now=True)
s.close() s.close()
return return
# Parse the server's initial KEX. # Send our KEX using the specified group-exchange and most of the server's own values.
packet_type = 0 # pylint: disable=unused-variable s.send_kexinit(key_exchanges=[kex_str], hostkeys=[host_key_type], ciphers=server_kex.server.encryption, macs=server_kex.server.mac, compressions=server_kex.server.compression, languages=server_kex.server.languages)
packet_type, payload = s.read_packet()
SSH2_Kex.parse(payload)
# Send the server our KEXINIT message, using only our try:
# selected kex and host key type. Send the server's own # Parse the server's KEX.
# list of ciphers and MACs back to it (this doesn't _, payload = s.read_packet()
# matter, really). SSH2_Kex.parse(payload)
client_kex = SSH2_Kex(os.urandom(16), [kex_str], [host_key_type], server_kex.client, server_kex.server, False, 0) except Exception:
out.v("Failed to parse server's kex. Stack trace:\n%s" % str(traceback.format_exc()), write_now=True)
s.write_byte(Protocol.MSG_KEXINIT) return
client_kex.write(s)
s.send_packet()
# Do the initial DH exchange. The server responds back # Do the initial DH exchange. The server responds back
# with the host key and its length. Bingo. We also get back the host key fingerprint. # with the host key and its length. Bingo. We also get back the host key fingerprint.
kex_group.send_init(s) kex_group.send_init(s)
host_key = kex_group.recv_reply(s, variable_key_len) try:
if host_key is not None: host_key = kex_group.recv_reply(s, variable_key_len)
server_kex.set_host_key(host_key_type, host_key) if host_key is not None:
server_kex.set_host_key(host_key_type, host_key)
except Exception:
pass
hostkey_modulus_size = kex_group.get_hostkey_size() hostkey_modulus_size = kex_group.get_hostkey_size()
ca_modulus_size = kex_group.get_ca_size() ca_modulus_size = kex_group.get_ca_size()
@ -161,12 +165,20 @@ class HostKeyTest:
if (cert is False) and (hostkey_modulus_size < 2048): if (cert is False) and (hostkey_modulus_size < 2048):
for rsa_type in HostKeyTest.RSA_FAMILY: for rsa_type in HostKeyTest.RSA_FAMILY:
alg_list = SSH2_KexDB.ALGORITHMS['key'][rsa_type] alg_list = SSH2_KexDB.ALGORITHMS['key'][rsa_type]
alg_list.append(['using small %d-bit modulus' % hostkey_modulus_size])
# If no failure list exists, add an empty failure list.
if len(alg_list) < 2:
alg_list.append([])
alg_list[1].append('using small %d-bit modulus' % hostkey_modulus_size)
elif (cert is True) and ((hostkey_modulus_size < 2048) or (ca_modulus_size > 0 and ca_modulus_size < 2048)): # pylint: disable=chained-comparison elif (cert is True) and ((hostkey_modulus_size < 2048) or (ca_modulus_size > 0 and ca_modulus_size < 2048)): # pylint: disable=chained-comparison
alg_list = SSH2_KexDB.ALGORITHMS['key'][host_key_type] alg_list = SSH2_KexDB.ALGORITHMS['key'][host_key_type]
min_modulus = min(hostkey_modulus_size, ca_modulus_size) min_modulus = min(hostkey_modulus_size, ca_modulus_size)
min_modulus = min_modulus if min_modulus > 0 else max(hostkey_modulus_size, ca_modulus_size) min_modulus = min_modulus if min_modulus > 0 else max(hostkey_modulus_size, ca_modulus_size)
alg_list.append(['using small %d-bit modulus' % min_modulus])
# If no failure list exists, add an empty failure list.
if len(alg_list) < 2:
alg_list.append([])
alg_list[1].append('using small %d-bit modulus' % min_modulus)
# 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:

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-2021 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
@ -37,8 +37,8 @@ from ssh_audit.ssh_socket import SSH_Socket
class KexDH: # pragma: nocover class KexDH: # pragma: nocover
def __init__(self, kex_name: str, hash_alg: str, g: int, p: int) -> None: def __init__(self, kex_name: str, hash_alg: str, g: int, p: int) -> None:
self.__kex_name = kex_name self.__kex_name = kex_name # pylint: disable=unused-private-member
self.__hash_alg = hash_alg self.__hash_alg = hash_alg # pylint: disable=unused-private-member
self.__g = 0 self.__g = 0
self.__p = 0 self.__p = 0
self.__q = 0 self.__q = 0
@ -46,10 +46,10 @@ class KexDH: # pragma: nocover
self.__e = 0 self.__e = 0
self.set_params(g, p) self.set_params(g, p)
self.__ed25519_pubkey = None # type: Optional[bytes] self.__ed25519_pubkey: Optional[bytes] = None # pylint: disable=unused-private-member
self.__hostkey_type = None # type: Optional[bytes] self.__hostkey_type: Optional[bytes] = None
self.__hostkey_e = 0 self.__hostkey_e = 0 # pylint: disable=unused-private-member
self.__hostkey_n = 0 self.__hostkey_n = 0 # pylint: disable=unused-private-member
self.__hostkey_n_len = 0 # Length of the host key modulus. self.__hostkey_n_len = 0 # Length of the host key modulus.
self.__ca_n_len = 0 # Length of the CA key modulus (if hostkey is a cert). self.__ca_n_len = 0 # Length of the CA key modulus (if hostkey is a cert).
@ -121,11 +121,11 @@ class KexDH: # pragma: nocover
# The public key exponent. # The public key exponent.
hostkey_e, hostkey_e_len, ptr = KexDH.__get_bytes(hostkey, ptr) hostkey_e, hostkey_e_len, ptr = KexDH.__get_bytes(hostkey, ptr)
self.__hostkey_e = int(binascii.hexlify(hostkey_e), 16) self.__hostkey_e = int(binascii.hexlify(hostkey_e), 16) # pylint: disable=unused-private-member
# 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)
self.__hostkey_n = int(binascii.hexlify(hostkey_n), 16) self.__hostkey_n = int(binascii.hexlify(hostkey_n), 16) # pylint: disable=unused-private-member
# If this is an RSA certificate, continue parsing to extract the CA # If this is an RSA certificate, continue parsing to extract the CA
# key. # key.
@ -327,7 +327,7 @@ class KexGroupExchange(KexDH):
s.send_packet() s.send_packet()
packet_type, payload = s.read_packet(2) packet_type, payload = s.read_packet(2)
if (packet_type != Protocol.MSG_KEXDH_GEX_GROUP) and (packet_type != Protocol.MSG_DEBUG): # pylint: disable=consider-using-in if packet_type not in [Protocol.MSG_KEXDH_GEX_GROUP, Protocol.MSG_DEBUG]:
# TODO: replace with a better exception type. # TODO: replace with a better exception type.
raise Exception('Expected MSG_KEXDH_GEX_REPLY (%d), but got %d instead.' % (Protocol.MSG_KEXDH_GEX_REPLY, packet_type)) raise Exception('Expected MSG_KEXDH_GEX_REPLY (%d), but got %d instead.' % (Protocol.MSG_KEXDH_GEX_REPLY, packet_type))

View File

@ -1,87 +0,0 @@
"""
The MIT License (MIT)
Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""
import os
import sys
# pylint: disable=unused-import
from typing import Dict, List, Set, Sequence, Tuple, Iterable # noqa: F401
from typing import Callable, Optional, Union, Any # noqa: F401
from ssh_audit.utils import Utils
class Output:
LEVELS = ('info', 'warn', 'fail') # type: Sequence[str]
COLORS = {'head': 36, 'good': 32, 'warn': 33, 'fail': 31}
# Use brighter colors on Windows for better readability.
if Utils.is_windows():
COLORS = {'head': 96, 'good': 92, 'warn': 93, 'fail': 91}
def __init__(self) -> None:
self.batch = False
self.verbose = False
self.use_colors = True
self.json = False
self.__level = 0
self.__colsupport = 'colorama' in sys.modules or os.name == 'posix'
@property
def level(self) -> str:
if self.__level < len(self.LEVELS):
return self.LEVELS[self.__level]
return 'unknown'
@level.setter
def level(self, name: str) -> None:
self.__level = self.get_level(name)
def get_level(self, name: str) -> int:
cname = 'info' if name == 'good' else name
if cname not in self.LEVELS:
return sys.maxsize
return self.LEVELS.index(cname)
def sep(self) -> None:
if not self.batch:
print()
@property
def colors_supported(self) -> bool:
return self.__colsupport
@staticmethod
def _colorized(color: str) -> Callable[[str], None]:
return lambda x: print(u'{}{}\033[0m'.format(color, x))
def __getattr__(self, name: str) -> Callable[[str], None]:
if name == 'head' and self.batch:
return lambda x: None
if not self.get_level(name) >= self.__level:
return lambda x: None
if self.use_colors and self.colors_supported and name in self.COLORS:
color = '\033[0;{}m'.format(self.COLORS[name])
return self._colorized(color)
else:
return lambda x: print(u'{}'.format(x))

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) 2021 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
@ -22,29 +22,164 @@
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 io import os
import sys import sys
# pylint: disable=unused-import # pylint: disable=unused-import
from typing import Dict, List, Set, Sequence, Tuple, Iterable # noqa: F401 from typing import Dict, List, Set, Sequence, Tuple, Iterable # noqa: F401
from typing import Callable, Optional, Union, Any # noqa: F401 from typing import Callable, Optional, Union, Any # noqa: F401
from ssh_audit.utils import Utils
class OutputBuffer(List[str]):
class OutputBuffer:
LEVELS: Sequence[str] = ('info', 'warn', 'fail')
COLORS = {'head': 36, 'good': 32, 'warn': 33, 'fail': 31}
# Use brighter colors on Windows for better readability.
if Utils.is_windows():
COLORS = {'head': 96, 'good': 92, 'warn': 93, 'fail': 91}
def __init__(self, buffer_output: bool = True) -> None:
self.buffer_output = buffer_output
self.buffer: List[str] = []
self.in_section = False
self.section: List[str] = []
self.batch = False
self.verbose = False
self.debug = False
self.use_colors = True
self.json = False
self.__level = 0
self.__is_color_supported = ('colorama' in sys.modules) or (os.name == 'posix')
self.line_ended = True
def _print(self, level: str, s: str = '', line_ended: bool = True) -> None:
'''Saves output to buffer (if in buffered mode), or immediately prints to stdout otherwise.'''
# If we're logging only 'warn' or above, and this is an 'info', ignore message.
if self.get_level(level) < self.__level:
return
if self.use_colors and self.colors_supported and len(s) > 0 and level != 'info':
s = "\033[0;%dm%s\033[0m" % (self.COLORS[level], s)
if self.buffer_output:
# Select which list to add to. If we are in a 'with' statement, then this goes in the section buffer, otherwise the general buffer.
buf = self.section if self.in_section else self.buffer
# Determine if a new line should be added, or if the last line should be appended.
if not self.line_ended:
last_entry = -1 if len(buf) > 0 else 0
buf[last_entry] = buf[last_entry] + s
else:
buf.append(s)
# When False, this tells the next call to append to the last line we just added.
self.line_ended = line_ended
else:
print(s)
def get_buffer(self) -> str:
'''Returns all buffered output, then clears the buffer.'''
self.flush_section()
buffer_str = "\n".join(self.buffer)
self.buffer = []
return buffer_str
def write(self) -> None:
'''Writes the output to stdout.'''
self.flush_section()
print(self.get_buffer(), flush=True)
def reset(self) -> None:
self.flush_section()
self.get_buffer()
@property
def level(self) -> str:
'''Returns the minimum level for output.'''
if self.__level < len(self.LEVELS):
return self.LEVELS[self.__level]
return 'unknown'
@level.setter
def level(self, name: str) -> None:
'''Sets the minimum level for output (one of: 'info', 'warn', 'fail').'''
self.__level = self.get_level(name)
def get_level(self, name: str) -> int:
cname = 'info' if name == 'good' else name
if cname not in self.LEVELS:
return sys.maxsize
return self.LEVELS.index(cname)
@property
def colors_supported(self) -> bool:
'''Returns True if the system supports color output.'''
return self.__is_color_supported
# When used in a 'with' block, the output to goes into a section; this can be sorted separately when add_section_to_buffer() is later called.
def __enter__(self) -> 'OutputBuffer': def __enter__(self) -> 'OutputBuffer':
# pylint: disable=attribute-defined-outside-init self.in_section = True
self.__buf = io.StringIO()
self.__stdout = sys.stdout
sys.stdout = self.__buf
return self return self
def flush(self, sort_lines: bool = False) -> None:
# Lines must be sorted in some cases to ensure consistent testing.
if sort_lines:
self.sort() # pylint: disable=no-member
for line in self: # pylint: disable=not-an-iterable
print(line)
def __exit__(self, *args: Any) -> None: def __exit__(self, *args: Any) -> None:
self.extend(self.__buf.getvalue().splitlines()) # pylint: disable=no-member self.in_section = False
sys.stdout = self.__stdout
def flush_section(self, sort_section: bool = False) -> None:
'''Appends section output (optionally sorting it first) to the end of the buffer, then clears the section output.'''
if sort_section:
self.section.sort()
self.buffer.extend(self.section)
self.section = []
def is_section_empty(self) -> bool:
'''Returns True if the section buffer is empty, otherwise False.'''
return len(self.section) == 0
def head(self, s: str, line_ended: bool = True) -> 'OutputBuffer':
if not self.batch:
self._print('head', s, line_ended)
return self
def fail(self, s: str, line_ended: bool = True) -> 'OutputBuffer':
self._print('fail', s, line_ended)
return self
def warn(self, s: str, line_ended: bool = True) -> 'OutputBuffer':
self._print('warn', s, line_ended)
return self
def info(self, s: str, line_ended: bool = True) -> 'OutputBuffer':
self._print('info', s, line_ended)
return self
def good(self, s: str, line_ended: bool = True) -> 'OutputBuffer':
self._print('good', s, line_ended)
return self
def sep(self) -> 'OutputBuffer':
if not self.batch:
self._print('info')
return self
def v(self, s: str, write_now: bool = False) -> 'OutputBuffer':
'''Prints a message if verbose output is enabled.'''
if self.verbose or self.debug:
self.info(s)
if write_now:
self.write()
return self
def d(self, s: str, write_now: bool = False) -> 'OutputBuffer':
'''Prints a message if verbose output is enabled.'''
if self.debug:
self.info(s)
if write_now:
self.write()
return self

View File

@ -1,7 +1,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (C) 2020 Joe Testa (jtesta@positronsecurity.com) Copyright (C) 2020-2021 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
@ -36,7 +36,7 @@ from ssh_audit.banner import Banner # pylint: disable=unused-import
class Policy: class Policy:
# Each field maps directly to a private member variable of the Policy class. # Each field maps directly to a private member variable of the Policy class.
BUILTIN_POLICIES = { BUILTIN_POLICIES: Dict[str, Dict[str, Union[Optional[str], Optional[List[str]], bool, Dict[str, int]]]] = {
# Ubuntu Server policies # Ubuntu Server policies
@ -49,15 +49,15 @@ class Policy:
# Generic OpenSSH Server policies # Generic OpenSSH Server policies
'Hardened OpenSSH Server v7.7 (version 1)': {'version': '1', 'banner': None, 'compressions': None, 'host_keys': ['ssh-ed25519'], 'optional_host_keys': ['ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256'], '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, 'cakey_sizes': None, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 2048}, 'server_policy': True}, 'Hardened OpenSSH Server v7.7 (version 1)': {'version': '1', 'banner': None, 'compressions': None, 'host_keys': ['rsa-sha2-512', 'rsa-sha2-256', 'ssh-ed25519'], 'optional_host_keys': ['ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256'], '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, 'cakey_sizes': None, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 2048}, 'server_policy': True},
'Hardened OpenSSH Server v7.8 (version 1)': {'version': '1', 'banner': None, 'compressions': None, 'host_keys': ['ssh-ed25519'], 'optional_host_keys': ['ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256'], '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, 'cakey_sizes': None, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 2048}, 'server_policy': True}, 'Hardened OpenSSH Server v7.8 (version 1)': {'version': '1', 'banner': None, 'compressions': None, 'host_keys': ['rsa-sha2-512', 'rsa-sha2-256', 'ssh-ed25519'], 'optional_host_keys': ['ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256'], '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, 'cakey_sizes': None, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 2048}, 'server_policy': True},
'Hardened OpenSSH Server v7.9 (version 1)': {'version': '1', 'banner': None, 'compressions': None, 'host_keys': ['ssh-ed25519'], 'optional_host_keys': ['ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256'], '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, 'cakey_sizes': None, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 2048}, 'server_policy': True}, 'Hardened OpenSSH Server v7.9 (version 1)': {'version': '1', 'banner': None, 'compressions': None, 'host_keys': ['rsa-sha2-512', 'rsa-sha2-256', 'ssh-ed25519'], 'optional_host_keys': ['ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256'], '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, 'cakey_sizes': None, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 2048}, 'server_policy': True},
'Hardened OpenSSH Server v8.0 (version 1)': {'version': '1', 'banner': None, 'compressions': None, 'host_keys': ['ssh-ed25519'], 'optional_host_keys': ['ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256'], '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, 'cakey_sizes': None, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 2048}, 'server_policy': True}, 'Hardened OpenSSH Server v8.0 (version 1)': {'version': '1', 'banner': None, 'compressions': None, 'host_keys': ['rsa-sha2-512', 'rsa-sha2-256', 'ssh-ed25519'], 'optional_host_keys': ['ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256'], '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, 'cakey_sizes': None, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 2048}, 'server_policy': True},
'Hardened OpenSSH Server v8.1 (version 1)': {'version': '1', 'banner': None, 'compressions': None, 'host_keys': ['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': ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256'], '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, 'cakey_sizes': None, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 2048}, 'server_policy': True}, 'Hardened OpenSSH Server v8.1 (version 1)': {'version': '1', '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': ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256'], '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, 'cakey_sizes': None, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 2048}, 'server_policy': True},
'Hardened OpenSSH Server v8.2 (version 1)': {'version': '1', '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': ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256'], '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': 4096, 'rsa-sha2-512': 4096}, 'cakey_sizes': {'rsa-sha2-256-cert-v01@openssh.com': 4096, 'rsa-sha2-512-cert-v01@openssh.com': 4096}, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 2048}, 'server_policy': True}, 'Hardened OpenSSH Server v8.2 (version 1)': {'version': '1', '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': ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256'], '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': 4096, 'rsa-sha2-512': 4096}, 'cakey_sizes': {'rsa-sha2-256-cert-v01@openssh.com': 4096, 'rsa-sha2-512-cert-v01@openssh.com': 4096}, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 2048}, 'server_policy': True},
@ -65,6 +65,12 @@ class Policy:
'Hardened OpenSSH Server v8.4 (version 1)': {'version': '1', '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': ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256'], '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': 4096, 'rsa-sha2-512': 4096}, 'cakey_sizes': {'rsa-sha2-256-cert-v01@openssh.com': 4096, 'rsa-sha2-512-cert-v01@openssh.com': 4096}, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 2048}, 'server_policy': True}, 'Hardened OpenSSH Server v8.4 (version 1)': {'version': '1', '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': ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256'], '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': 4096, 'rsa-sha2-512': 4096}, 'cakey_sizes': {'rsa-sha2-256-cert-v01@openssh.com': 4096, 'rsa-sha2-512-cert-v01@openssh.com': 4096}, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 2048}, 'server_policy': True},
'Hardened OpenSSH Server v8.5 (version 1)': {'version': '1', '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': ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256'], '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': 4096, 'rsa-sha2-512': 4096}, 'cakey_sizes': {'rsa-sha2-256-cert-v01@openssh.com': 4096, 'rsa-sha2-512-cert-v01@openssh.com': 4096}, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 2048}, 'server_policy': True},
'Hardened OpenSSH Server v8.6 (version 1)': {'version': '1', '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': ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256'], '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': 4096, 'rsa-sha2-512': 4096}, 'cakey_sizes': {'rsa-sha2-256-cert-v01@openssh.com': 4096, 'rsa-sha2-512-cert-v01@openssh.com': 4096}, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 2048}, 'server_policy': True},
'Hardened OpenSSH Server v8.7 (version 1)': {'version': '1', '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': ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256'], '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': 4096, 'rsa-sha2-512': 4096}, 'cakey_sizes': {'rsa-sha2-256-cert-v01@openssh.com': 4096, 'rsa-sha2-512-cert-v01@openssh.com': 4096}, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 2048}, 'server_policy': True},
# Ubuntu Client policies # Ubuntu Client policies
@ -74,25 +80,25 @@ class Policy:
'Hardened Ubuntu Client 20.04 LTS (version 2)': {'version': '2', 'banner': None, 'compressions': None, 'host_keys': ['ssh-ed25519', 'ssh-ed25519-cert-v01@openssh.com', 'sk-ssh-ed25519@openssh.com', 'sk-ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512', 'rsa-sha2-512-cert-v01@openssh.com'], 'optional_host_keys': None, 'kex': ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', '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, 'cakey_sizes': None, 'dh_modulus_sizes': None, 'server_policy': False}, 'Hardened Ubuntu Client 20.04 LTS (version 2)': {'version': '2', 'banner': None, 'compressions': None, 'host_keys': ['ssh-ed25519', 'ssh-ed25519-cert-v01@openssh.com', 'sk-ssh-ed25519@openssh.com', 'sk-ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512', 'rsa-sha2-512-cert-v01@openssh.com'], 'optional_host_keys': None, 'kex': ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', '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, 'cakey_sizes': None, 'dh_modulus_sizes': None, 'server_policy': False},
} # type: Dict[str, Dict[str, Union[Optional[str], Optional[List[str]], bool, Dict[str, int]]]] }
def __init__(self, policy_file: Optional[str] = None, policy_data: Optional[str] = None, manual_load: bool = False) -> None: def __init__(self, policy_file: Optional[str] = None, policy_data: Optional[str] = None, manual_load: bool = False) -> None:
self._name = None # type: Optional[str] self._name: Optional[str] = None
self._version = None # type: Optional[str] self._version: Optional[str] = None
self._banner = None # type: Optional[str] self._banner: Optional[str] = None
self._compressions = None # type: Optional[List[str]] self._compressions: Optional[List[str]] = None
self._host_keys = None # type: Optional[List[str]] self._host_keys: Optional[List[str]] = None
self._optional_host_keys = None # type: Optional[List[str]] self._optional_host_keys: Optional[List[str]] = None
self._kex = None # type: Optional[List[str]] self._kex: Optional[List[str]] = None
self._ciphers = None # type: Optional[List[str]] self._ciphers: Optional[List[str]] = None
self._macs = None # type: Optional[List[str]] self._macs: Optional[List[str]] = None
self._hostkey_sizes = None # type: Optional[Dict[str, int]] self._hostkey_sizes: Optional[Dict[str, int]] = None
self._cakey_sizes = None # type: Optional[Dict[str, int]] self._cakey_sizes: Optional[Dict[str, int]] = None
self._dh_modulus_sizes = None # type: Optional[Dict[str, int]] self._dh_modulus_sizes: Optional[Dict[str, int]] = None
self._server_policy = True self._server_policy = True
self._name_and_version = '' # type: str self._name_and_version: str = ''
# Ensure that only one mode was specified. # Ensure that only one mode was specified.
num_modes = 0 num_modes = 0
@ -111,7 +117,7 @@ class Policy:
if policy_file is not None: if policy_file is not None:
try: try:
with open(policy_file, "r") as f: with open(policy_file, "r", encoding='utf-8') as f:
policy_data = f.read() policy_data = f.read()
except FileNotFoundError: except FileNotFoundError:
print("Error: policy file not found: %s" % policy_file) print("Error: policy file not found: %s" % policy_file)
@ -305,7 +311,7 @@ macs = %s
'''Evaluates a server configuration against this policy. Returns a tuple of a boolean (True if server adheres to policy) and an array of strings that holds error messages.''' '''Evaluates a server configuration against this policy. Returns a tuple of a boolean (True if server adheres to policy) and an array of strings that holds error messages.'''
ret = True ret = True
errors = [] # type: List[Any] errors: List[Any] = []
banner_str = str(banner) banner_str = str(banner)
if (self._banner is not None) and (banner_str != self._banner): if (self._banner is not None) and (banner_str != self._banner):
@ -419,8 +425,8 @@ macs = %s
server_policy_names = [] server_policy_names = []
client_policy_names = [] client_policy_names = []
for policy_name in Policy.BUILTIN_POLICIES: for policy_name, policy in Policy.BUILTIN_POLICIES.items():
if Policy.BUILTIN_POLICIES[policy_name]['server_policy']: if policy['server_policy']:
server_policy_names.append(policy_name) server_policy_names.append(policy_name)
else: else:
client_policy_names.append(policy_name) client_policy_names.append(policy_name)

View File

@ -43,14 +43,14 @@ class ReadBuf:
return self._buf.read(size) return self._buf.read(size)
def read_byte(self) -> int: def read_byte(self) -> int:
v = struct.unpack('B', self.read(1))[0] # type: int v: int = struct.unpack('B', self.read(1))[0]
return v return v
def read_bool(self) -> bool: def read_bool(self) -> bool:
return self.read_byte() != 0 return self.read_byte() != 0
def read_int(self) -> int: def read_int(self) -> int:
v = struct.unpack('>I', self.read(4))[0] # type: int v: int = struct.unpack('>I', self.read(4))[0]
return v return v
def read_list(self) -> List[str]: def read_list(self) -> List[str]:

View File

@ -180,7 +180,7 @@ class Software:
# pylint: disable=too-many-return-statements # pylint: disable=too-many-return-statements
software = str(banner.software) software = str(banner.software)
mx = re.match(r'^dropbear_([\d\.]+\d+)(.*)', software) mx = re.match(r'^dropbear_([\d\.]+\d+)(.*)', software)
v = None # type: Optional[str] v: Optional[str] = None
if mx is not None: if mx is not None:
patch = cls._fix_patch(mx.group(2)) patch = cls._fix_patch(mx.group(2))
v, p = 'Matt Johnston', Product.DropbearSSH v, p = 'Matt Johnston', Product.DropbearSSH

View File

@ -29,7 +29,7 @@ from ssh_audit.ssh1_crc32 import SSH1_CRC32
class SSH1: class SSH1:
_crc32 = None # type: Optional[SSH1_CRC32] _crc32: Optional[SSH1_CRC32] = None
CIPHERS = ['none', 'idea', 'des', '3des', 'tss', 'rc4', 'blowfish'] CIPHERS = ['none', 'idea', 'des', '3des', 'tss', 'rc4', 'blowfish']
AUTHS = ['none', 'rhosts', 'rsa', 'password', 'rhosts_rsa', 'tis', 'kerberos'] AUTHS = ['none', 'rhosts', 'rsa', 'password', 'rhosts_rsa', 'tis', 'kerberos']

View File

@ -34,7 +34,7 @@ class SSH1_KexDB: # pylint: disable=too-few-public-methods
FAIL_NA_UNSAFE = 'not implemented in OpenSSH (server), unsafe algorithm' FAIL_NA_UNSAFE = 'not implemented in OpenSSH (server), unsafe algorithm'
TEXT_CIPHER_IDEA = 'cipher used by commercial SSH' TEXT_CIPHER_IDEA = 'cipher used by commercial SSH'
ALGORITHMS = { ALGORITHMS: Dict[str, Dict[str, List[List[Optional[str]]]]] = {
'key': { 'key': {
'ssh-rsa1': [['1.2.2']], 'ssh-rsa1': [['1.2.2']],
}, },
@ -55,4 +55,4 @@ class SSH1_KexDB: # pylint: disable=too-few-public-methods
'tis': [['1.2.2']], 'tis': [['1.2.2']],
'kerberos': [['1.2.2', '3.6'], [FAIL_OPENSSH37_REMOVE]], 'kerberos': [['1.2.2', '3.6'], [FAIL_OPENSSH37_REMOVE]],
} }
} # type: Dict[str, Dict[str, List[List[Optional[str]]]]] }

View File

@ -90,7 +90,7 @@ class SSH1_PublicKeyMessage:
@property @property
def supported_ciphers(self) -> List[str]: def supported_ciphers(self) -> List[str]:
ciphers = [] ciphers = []
for i in range(len(SSH1.CIPHERS)): for i in range(len(SSH1.CIPHERS)): # pylint: disable=consider-using-enumerate
if self.__supported_ciphers_mask & (1 << i) != 0: if self.__supported_ciphers_mask & (1 << i) != 0:
ciphers.append(Utils.to_text(SSH1.CIPHERS[i])) ciphers.append(Utils.to_text(SSH1.CIPHERS[i]))
return ciphers return ciphers

View File

@ -41,9 +41,9 @@ class SSH2_Kex:
self.__follows = follows self.__follows = follows
self.__unused = unused self.__unused = unused
self.__rsa_key_sizes = {} # type: Dict[str, Tuple[int, int]] self.__rsa_key_sizes: Dict[str, Tuple[int, int]] = {}
self.__dh_modulus_sizes = {} # type: Dict[str, Tuple[int, int]] self.__dh_modulus_sizes: Dict[str, Tuple[int, int]] = {}
self.__host_keys = {} # type: Dict[str, bytes] self.__host_keys: Dict[str, bytes] = {}
@property @property
def cookie(self) -> bytes: def cookie(self) -> bytes:

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-2021 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
@ -37,6 +37,7 @@ class SSH2_KexDB: # pylint: disable=too-few-public-methods
FAIL_OPENSSH67_UNSAFE = 'removed (in server) since OpenSSH 6.7, unsafe algorithm' FAIL_OPENSSH67_UNSAFE = 'removed (in server) since OpenSSH 6.7, unsafe algorithm'
FAIL_OPENSSH61_REMOVE = 'removed since OpenSSH 6.1, removed from specification' FAIL_OPENSSH61_REMOVE = 'removed since OpenSSH 6.1, removed from specification'
FAIL_OPENSSH31_REMOVE = 'removed since OpenSSH 3.1' FAIL_OPENSSH31_REMOVE = 'removed since OpenSSH 3.1'
INFO_OPENSSH82_FUTURE_DEPRECATION = 'a future deprecation notice has been issued in OpenSSH 8.2: https://www.openssh.com/txt/release-8.2'
FAIL_DBEAR67_DISABLED = 'disabled since Dropbear SSH 2015.67' FAIL_DBEAR67_DISABLED = 'disabled since Dropbear SSH 2015.67'
FAIL_DBEAR53_DISABLED = 'disabled since Dropbear SSH 0.53' FAIL_DBEAR53_DISABLED = 'disabled since Dropbear SSH 0.53'
FAIL_DEPRECATED_CIPHER = 'deprecated cipher' FAIL_DEPRECATED_CIPHER = 'deprecated cipher'
@ -46,6 +47,7 @@ class SSH2_KexDB: # pylint: disable=too-few-public-methods
FAIL_DEPRECATED_MAC = 'deprecated MAC' FAIL_DEPRECATED_MAC = 'deprecated MAC'
FAIL_1024BIT_MODULUS = 'using small 1024-bit modulus' FAIL_1024BIT_MODULUS = 'using small 1024-bit modulus'
FAIL_UNPROVEN = 'using unproven algorithm' FAIL_UNPROVEN = 'using unproven algorithm'
FAIL_HASH_WEAK = 'using weak hashing algorithm'
WARN_CURVES_WEAK = 'using weak elliptic curves' WARN_CURVES_WEAK = 'using weak elliptic curves'
WARN_RNDSIG_KEY = 'using weak random number generator could reveal the key' WARN_RNDSIG_KEY = 'using weak random number generator could reveal the key'
WARN_HASH_WEAK = 'using weak hashing algorithm' WARN_HASH_WEAK = 'using weak hashing algorithm'
@ -59,15 +61,18 @@ class SSH2_KexDB: # pylint: disable=too-few-public-methods
WARN_OBSOLETE = 'using obsolete algorithm' WARN_OBSOLETE = 'using obsolete algorithm'
WARN_UNTRUSTED = 'using untrusted algorithm' WARN_UNTRUSTED = 'using untrusted algorithm'
ALGORITHMS = { ALGORITHMS: Dict[str, Dict[str, List[List[Optional[str]]]]] = {
# Format: 'algorithm_name': [['version_first_appeared_in'], [reason_for_failure1, reason_for_failure2, ...], [warning1, warning2, ...]] # Format: 'algorithm_name': [['version_first_appeared_in'], [reason_for_failure1, reason_for_failure2, ...], [warning1, warning2, ...]]
'kex': { 'kex': {
'diffie-hellman-group1-sha1': [['2.3.0,d0.28,l10.2', '6.6', '6.9'], [FAIL_1024BIT_MODULUS, FAIL_OPENSSH67_UNSAFE, FAIL_OPENSSH70_LOGJAM], [WARN_HASH_WEAK]], 'diffie-hellman-group1-sha1': [['2.3.0,d0.28,l10.2', '6.6', '6.9'], [FAIL_1024BIT_MODULUS, FAIL_OPENSSH67_UNSAFE, FAIL_OPENSSH70_LOGJAM], [WARN_HASH_WEAK]],
'gss-group1-sha1-toWM5Slw5Ew8Mqkay+al2g==': [[], [FAIL_1024BIT_MODULUS, FAIL_OPENSSH67_UNSAFE, FAIL_OPENSSH70_LOGJAM], [WARN_HASH_WEAK]], 'gss-group1-sha1-toWM5Slw5Ew8Mqkay+al2g==': [[], [FAIL_1024BIT_MODULUS, FAIL_OPENSSH67_UNSAFE, FAIL_OPENSSH70_LOGJAM], [WARN_HASH_WEAK]],
'gss-gex-sha1-eipGX3TCiQSrx573bT1o1Q==': [[], [], [WARN_HASH_WEAK]],
'gss-gex-sha1-toWM5Slw5Ew8Mqkay+al2g==': [[], [], [WARN_HASH_WEAK]], 'gss-gex-sha1-toWM5Slw5Ew8Mqkay+al2g==': [[], [], [WARN_HASH_WEAK]],
'gss-gex-sha1-': [[], [], [WARN_HASH_WEAK]], 'gss-gex-sha1-': [[], [], [WARN_HASH_WEAK]],
'gss-group1-sha1-eipGX3TCiQSrx573bT1o1Q==': [[], [FAIL_1024BIT_MODULUS], [WARN_HASH_WEAK]],
'gss-group1-sha1-': [[], [FAIL_1024BIT_MODULUS], [WARN_HASH_WEAK]], 'gss-group1-sha1-': [[], [FAIL_1024BIT_MODULUS], [WARN_HASH_WEAK]],
'gss-group14-sha1-': [[], [], [WARN_HASH_WEAK]], 'gss-group14-sha1-': [[], [], [WARN_HASH_WEAK]],
'gss-group14-sha1-eipGX3TCiQSrx573bT1o1Q==': [[], [], [WARN_HASH_WEAK]],
'gss-group14-sha1-toWM5Slw5Ew8Mqkay+al2g==': [[], [], [WARN_HASH_WEAK]], 'gss-group14-sha1-toWM5Slw5Ew8Mqkay+al2g==': [[], [], [WARN_HASH_WEAK]],
'gss-group14-sha256-': [[]], 'gss-group14-sha256-': [[]],
'gss-group14-sha256-toWM5Slw5Ew8Mqkay+al2g==': [[]], 'gss-group14-sha256-toWM5Slw5Ew8Mqkay+al2g==': [[]],
@ -115,7 +120,8 @@ class SSH2_KexDB: # pylint: disable=too-few-public-methods
'kexguess2@matt.ucc.asn.au': [['d2013.57']], 'kexguess2@matt.ucc.asn.au': [['d2013.57']],
'rsa1024-sha1': [[], [FAIL_1024BIT_MODULUS], [WARN_HASH_WEAK]], 'rsa1024-sha1': [[], [FAIL_1024BIT_MODULUS], [WARN_HASH_WEAK]],
'rsa2048-sha256': [[]], 'rsa2048-sha256': [[]],
'sntrup4591761x25519-sha512@tinyssh.org': [['8.0'], [], [WARN_EXPERIMENTAL]], 'sntrup4591761x25519-sha512@tinyssh.org': [['8.0', '8.4'], [], [WARN_EXPERIMENTAL]],
'sntrup761x25519-sha512@openssh.com': [['8.5'], [], [WARN_EXPERIMENTAL]],
'kexAlgoCurve25519SHA256': [[]], 'kexAlgoCurve25519SHA256': [[]],
'Curve25519SHA256': [[]], 'Curve25519SHA256': [[]],
'ext-info-c': [[]], # Extension negotiation (RFC 8308) 'ext-info-c': [[]], # Extension negotiation (RFC 8308)
@ -127,20 +133,20 @@ class SSH2_KexDB: # pylint: disable=too-few-public-methods
'rsa-sha2-512': [['7.2']], 'rsa-sha2-512': [['7.2']],
'ssh-ed25519': [['6.5,l10.7.0']], 'ssh-ed25519': [['6.5,l10.7.0']],
'ssh-ed25519-cert-v01@openssh.com': [['6.5']], 'ssh-ed25519-cert-v01@openssh.com': [['6.5']],
'ssh-rsa': [['2.5.0,d0.28,l10.2'], [WARN_HASH_WEAK]], 'ssh-rsa': [['2.5.0,d0.28,l10.2'], [FAIL_HASH_WEAK], [], [INFO_OPENSSH82_FUTURE_DEPRECATION]],
'ssh-dss': [['2.1.0,d0.28,l10.2', '6.9'], [FAIL_1024BIT_MODULUS, FAIL_OPENSSH70_WEAK], [WARN_RNDSIG_KEY]], 'ssh-dss': [['2.1.0,d0.28,l10.2', '6.9'], [FAIL_1024BIT_MODULUS, FAIL_OPENSSH70_WEAK], [WARN_RNDSIG_KEY]],
'ecdsa-sha2-nistp256': [['5.7,d2013.62,l10.6.4'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]], 'ecdsa-sha2-nistp256': [['5.7,d2013.62,l10.6.4'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]],
'ecdsa-sha2-nistp384': [['5.7,d2013.62,l10.6.4'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]], 'ecdsa-sha2-nistp384': [['5.7,d2013.62,l10.6.4'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]],
'ecdsa-sha2-nistp521': [['5.7,d2013.62,l10.6.4'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]], 'ecdsa-sha2-nistp521': [['5.7,d2013.62,l10.6.4'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]],
'ecdsa-sha2-1.3.132.0.10': [[], [], [WARN_RNDSIG_KEY]], # ECDSA over secp256k1 (i.e.: the Bitcoin curve) 'ecdsa-sha2-1.3.132.0.10': [[], [], [WARN_RNDSIG_KEY]], # ECDSA over secp256k1 (i.e.: the Bitcoin curve)
'x509v3-sign-dss': [[], [FAIL_1024BIT_MODULUS, FAIL_OPENSSH70_WEAK], [WARN_RNDSIG_KEY]], 'x509v3-sign-dss': [[], [FAIL_1024BIT_MODULUS, FAIL_OPENSSH70_WEAK], [WARN_RNDSIG_KEY]],
'x509v3-sign-rsa': [[], [], [WARN_HASH_WEAK]], 'x509v3-sign-rsa': [[], [FAIL_HASH_WEAK], [], [INFO_OPENSSH82_FUTURE_DEPRECATION]],
'x509v3-sign-rsa-sha256@ssh.com': [[]], 'x509v3-sign-rsa-sha256@ssh.com': [[]],
'x509v3-ssh-dss': [[], [FAIL_1024BIT_MODULUS, FAIL_OPENSSH70_WEAK], [WARN_RNDSIG_KEY]], 'x509v3-ssh-dss': [[], [FAIL_1024BIT_MODULUS, FAIL_OPENSSH70_WEAK], [WARN_RNDSIG_KEY]],
'x509v3-ssh-rsa': [[], [], [WARN_HASH_WEAK]], 'x509v3-ssh-rsa': [[], [FAIL_HASH_WEAK], [], [INFO_OPENSSH82_FUTURE_DEPRECATION]],
'ssh-rsa-cert-v00@openssh.com': [['5.4', '6.9'], [FAIL_OPENSSH70_LEGACY], []], 'ssh-rsa-cert-v00@openssh.com': [['5.4', '6.9'], [FAIL_OPENSSH70_LEGACY, FAIL_HASH_WEAK], [], [INFO_OPENSSH82_FUTURE_DEPRECATION]],
'ssh-dss-cert-v00@openssh.com': [['5.4', '6.9'], [FAIL_1024BIT_MODULUS, FAIL_OPENSSH70_LEGACY], [WARN_RNDSIG_KEY]], 'ssh-dss-cert-v00@openssh.com': [['5.4', '6.9'], [FAIL_1024BIT_MODULUS, FAIL_OPENSSH70_LEGACY], [WARN_RNDSIG_KEY]],
'ssh-rsa-cert-v01@openssh.com': [['5.6'], [WARN_HASH_WEAK]], 'ssh-rsa-cert-v01@openssh.com': [['5.6'], [FAIL_HASH_WEAK], [], [INFO_OPENSSH82_FUTURE_DEPRECATION]],
'ssh-dss-cert-v01@openssh.com': [['5.6', '6.9'], [FAIL_1024BIT_MODULUS, FAIL_OPENSSH70_WEAK], [WARN_RNDSIG_KEY]], 'ssh-dss-cert-v01@openssh.com': [['5.6', '6.9'], [FAIL_1024BIT_MODULUS, FAIL_OPENSSH70_WEAK], [WARN_RNDSIG_KEY]],
'ecdsa-sha2-nistp256-cert-v01@openssh.com': [['5.7'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]], 'ecdsa-sha2-nistp256-cert-v01@openssh.com': [['5.7'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]],
'ecdsa-sha2-nistp384-cert-v01@openssh.com': [['5.7'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]], 'ecdsa-sha2-nistp384-cert-v01@openssh.com': [['5.7'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]],
@ -247,6 +253,7 @@ class SSH2_KexDB: # pylint: disable=too-few-public-methods
'hmac-md5-96': [['2.5.0', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC, WARN_HASH_WEAK]], 'hmac-md5-96': [['2.5.0', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC, WARN_HASH_WEAK]],
'hmac-ripemd': [[], [FAIL_DEPRECATED_MAC], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC]], 'hmac-ripemd': [[], [FAIL_DEPRECATED_MAC], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC]],
'hmac-ripemd160': [['2.5.0', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC]], 'hmac-ripemd160': [['2.5.0', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC]],
'hmac-ripemd160-96': [[], [FAIL_DEPRECATED_MAC], [WARN_ENCRYPT_AND_MAC, WARN_TAG_SIZE]],
'hmac-ripemd160@openssh.com': [['2.1.0', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC]], 'hmac-ripemd160@openssh.com': [['2.1.0', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC]],
'umac-64@openssh.com': [['4.7'], [], [WARN_ENCRYPT_AND_MAC, WARN_TAG_SIZE]], 'umac-64@openssh.com': [['4.7'], [], [WARN_ENCRYPT_AND_MAC, WARN_TAG_SIZE]],
'umac-128@openssh.com': [['6.2'], [], [WARN_ENCRYPT_AND_MAC]], 'umac-128@openssh.com': [['6.2'], [], [WARN_ENCRYPT_AND_MAC]],
@ -267,5 +274,7 @@ class SSH2_KexDB: # pylint: disable=too-few-public-methods
'aes256-gcm': [[]], 'aes256-gcm': [[]],
'chacha20-poly1305@openssh.com': [[]], # Despite the @openssh.com tag, this was never shipped as a MAC in OpenSSH (only as a cipher); it is only implemented as a MAC in Syncplify. 'chacha20-poly1305@openssh.com': [[]], # Despite the @openssh.com tag, this was never shipped as a MAC in OpenSSH (only as a cipher); it is only implemented as a MAC in Syncplify.
'crypticore-mac@ssh.com': [[], [FAIL_UNPROVEN]], 'crypticore-mac@ssh.com': [[], [FAIL_UNPROVEN]],
'AEAD_AES_128_GCM': [[]],
'AEAD_AES_256_GCM': [[]],
} }
} # type: Dict[str, Dict[str, List[List[Optional[str]]]]] }

View File

@ -2,7 +2,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (C) 2017-2020 Joe Testa (jtesta@positronsecurity.com) Copyright (C) 2017-2021 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
@ -23,9 +23,12 @@
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 concurrent.futures
import copy
import getopt import getopt
import json import json
import os import os
import re
import sys import sys
import traceback import traceback
@ -34,6 +37,7 @@ from typing import Dict, List, Set, Sequence, Tuple, Iterable # noqa: F401
from typing import Callable, Optional, Union, Any # noqa: F401 from typing import Callable, Optional, Union, Any # noqa: F401
from ssh_audit.globals import VERSION from ssh_audit.globals import VERSION
from ssh_audit.globals import WINDOWS_MAN_PAGE
from ssh_audit.algorithm import Algorithm from ssh_audit.algorithm import Algorithm
from ssh_audit.algorithms import Algorithms from ssh_audit.algorithms import Algorithms
from ssh_audit.auditconf import AuditConf from ssh_audit.auditconf import AuditConf
@ -42,7 +46,6 @@ from ssh_audit import exitcodes
from ssh_audit.fingerprint import Fingerprint from ssh_audit.fingerprint import Fingerprint
from ssh_audit.gextest import GEXTest from ssh_audit.gextest import GEXTest
from ssh_audit.hostkeytest import HostKeyTest from ssh_audit.hostkeytest import HostKeyTest
from ssh_audit.output import Output
from ssh_audit.outputbuffer import OutputBuffer from ssh_audit.outputbuffer import OutputBuffer
from ssh_audit.policy import Policy from ssh_audit.policy import Policy
from ssh_audit.product import Product from ssh_audit.product import Product
@ -56,17 +59,18 @@ from ssh_audit.ssh_socket import SSH_Socket
from ssh_audit.utils import Utils from ssh_audit.utils import Utils
from ssh_audit.versionvulnerabilitydb import VersionVulnerabilityDB from ssh_audit.versionvulnerabilitydb import VersionVulnerabilityDB
# Only import colorama under Windows. Other OSes can natively handle terminal colors.
try: # pragma: nocover if sys.platform == 'win32':
from colorama import init as colorama_init try:
colorama_init(strip=False) # pragma: nocover from colorama import init as colorama_init
except ImportError: # pragma: nocover colorama_init()
pass except ImportError:
pass
def usage(err: Optional[str] = None) -> None: def usage(err: Optional[str] = None) -> None:
retval = exitcodes.GOOD retval = exitcodes.GOOD
uout = Output() uout = OutputBuffer()
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:
@ -80,34 +84,38 @@ def usage(err: Optional[str] = None) -> None:
uout.info(' -6, --ipv6 enable IPv6 (order of precedence)') uout.info(' -6, --ipv6 enable IPv6 (order of precedence)')
uout.info(' -b, --batch batch output') 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(' -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(' -j, --json JSON output') uout.info(' -d, --debug debug output')
uout.info(' -j, --json JSON output (use -jj to enable indents)')
uout.info(' -l, --level=<level> minimum output level (info|warn|fail)') uout.info(' -l, --level=<level> minimum output level (info|warn|fail)')
uout.info(' -L, --list-policies list all the official, built-in policies') uout.info(' -L, --list-policies list all the official, built-in policies')
uout.info(' --lookup=<alg1,alg2,...> looks up an algorithm(s) without\n connecting to a server') uout.info(' --lookup=<alg1,alg2,...> looks up an algorithm(s) without\n connecting to a server')
uout.info(' -M, --make-policy=<policy.txt> creates a policy based on the target server\n (i.e.: the target server has the ideal\n configuration that other servers should\n adhere to)') uout.info(' -M, --make-policy=<policy.txt> creates a policy based on the target server\n (i.e.: the target server has the ideal\n configuration that other servers should\n adhere to)')
uout.info(' -m, --manual print the man page (Windows only)')
uout.info(' -n, --no-colors disable colors') uout.info(' -n, --no-colors disable colors')
uout.info(' -p, --port=<port> port to connect') uout.info(' -p, --port=<port> port to connect')
uout.info(' -P, --policy=<policy.txt> run a policy test using the specified policy') uout.info(' -P, --policy=<policy.txt> run a policy test using the specified policy')
uout.info(' -t, --timeout=<secs> timeout (in seconds) for connection and reading\n (default: 5)') 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])') 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)')
uout.info(' -v, --verbose verbose output') uout.info(' -v, --verbose verbose output')
uout.sep() uout.sep()
uout.write()
sys.exit(retval) sys.exit(retval)
def output_algorithms(title: str, alg_db: Dict[str, Dict[str, List[List[Optional[str]]]]], alg_type: str, algorithms: List[str], unknown_algs: List[str], is_json_output: bool, program_retval: int, maxlen: int = 0, alg_sizes: Optional[Dict[str, Tuple[int, int]]] = None) -> int: # pylint: disable=too-many-arguments def output_algorithms(out: OutputBuffer, title: str, alg_db: Dict[str, Dict[str, List[List[Optional[str]]]]], alg_type: str, algorithms: List[str], unknown_algs: List[str], is_json_output: bool, program_retval: int, maxlen: int = 0, alg_sizes: Optional[Dict[str, Tuple[int, int]]] = None) -> int: # pylint: disable=too-many-arguments
with OutputBuffer() as obuf: with out:
for algorithm in algorithms: for algorithm in algorithms:
program_retval = output_algorithm(alg_db, alg_type, algorithm, unknown_algs, program_retval, maxlen, alg_sizes) program_retval = output_algorithm(out, alg_db, alg_type, algorithm, unknown_algs, program_retval, maxlen, alg_sizes)
if len(obuf) > 0 and not is_json_output: if not out.is_section_empty() and not is_json_output:
out.head('# ' + title) out.head('# ' + title)
obuf.flush() out.flush_section()
out.sep() out.sep()
return program_retval return program_retval
def output_algorithm(alg_db: Dict[str, Dict[str, List[List[Optional[str]]]]], alg_type: str, alg_name: str, unknown_algs: List[str], program_retval: int, alg_max_len: int = 0, alg_sizes: Optional[Dict[str, Tuple[int, int]]] = None) -> int: def output_algorithm(out: OutputBuffer, alg_db: Dict[str, Dict[str, List[List[Optional[str]]]]], alg_type: str, alg_name: str, unknown_algs: List[str], program_retval: int, alg_max_len: int = 0, alg_sizes: Optional[Dict[str, Tuple[int, int]]] = None) -> int:
prefix = '(' + alg_type + ') ' prefix = '(' + alg_type + ') '
if alg_max_len == 0: if alg_max_len == 0:
alg_max_len = len(alg_name) alg_max_len = len(alg_name)
@ -175,7 +183,7 @@ def output_algorithm(alg_db: Dict[str, Dict[str, List[List[Optional[str]]]]], al
return program_retval return program_retval
def output_compatibility(algs: Algorithms, client_audit: bool, for_server: bool = True) -> None: def output_compatibility(out: OutputBuffer, algs: Algorithms, client_audit: bool, for_server: bool = True) -> None:
# Don't output any compatibility info if we're doing a client audit. # Don't output any compatibility info if we're doing a client audit.
if client_audit: if client_audit:
@ -205,18 +213,18 @@ def output_compatibility(algs: Algorithms, client_audit: bool, for_server: bool
out.good('(gen) compatibility: ' + ', '.join(comp_text)) out.good('(gen) compatibility: ' + ', '.join(comp_text))
def output_security_sub(sub: str, software: Optional[Software], client_audit: bool, padlen: int) -> None: def output_security_sub(out: OutputBuffer, sub: str, software: Optional[Software], client_audit: bool, padlen: int) -> None:
secdb = VersionVulnerabilityDB.CVE if sub == 'cve' else VersionVulnerabilityDB.TXT secdb = VersionVulnerabilityDB.CVE if sub == 'cve' else VersionVulnerabilityDB.TXT
if software is None or software.product not in secdb: if software is None or software.product not in secdb:
return return
for line in secdb[software.product]: for line in secdb[software.product]:
vfrom = '' # type: str vfrom: str = ''
vtill = '' # type: str vtill: str = ''
vfrom, vtill = line[0:2] vfrom, vtill = line[0:2]
if not software.between_versions(vfrom, vtill): if not software.between_versions(vfrom, vtill):
continue continue
target = 0 # type: int target: int = 0
name = '' # type: str name: str = ''
target, name = line[2:4] target, name = line[2:4]
is_server = target & 1 == 1 is_server = target & 1 == 1
is_client = target & 2 == 2 is_client = target & 2 == 2
@ -227,8 +235,8 @@ def output_security_sub(sub: str, software: Optional[Software], client_audit: bo
continue continue
p = '' if out.batch else ' ' * (padlen - len(name)) p = '' if out.batch else ' ' * (padlen - len(name))
if sub == 'cve': if sub == 'cve':
cvss = 0.0 # type: float cvss: float = 0.0
descr = '' # type: str descr: str = ''
cvss, descr = line[4:6] cvss, descr = line[4:6]
# Critical CVSS scores (>= 8.0) are printed as a fail, otherwise they are printed as a warning. # Critical CVSS scores (>= 8.0) are printed as a fail, otherwise they are printed as a warning.
@ -241,20 +249,23 @@ def output_security_sub(sub: str, software: Optional[Software], client_audit: bo
out.fail('(sec) {}{} -- {}'.format(name, p, descr)) out.fail('(sec) {}{} -- {}'.format(name, p, descr))
def output_security(banner: Optional[Banner], client_audit: bool, padlen: int, is_json_output: bool) -> None: def output_security(out: OutputBuffer, banner: Optional[Banner], client_audit: bool, padlen: int, is_json_output: bool) -> None:
with OutputBuffer() as obuf: with out:
if banner is not None: if banner is not None:
software = Software.parse(banner) software = Software.parse(banner)
output_security_sub('cve', software, client_audit, padlen) output_security_sub(out, 'cve', software, client_audit, padlen)
output_security_sub('txt', software, client_audit, padlen) output_security_sub(out, 'txt', software, client_audit, padlen)
if len(obuf) > 0 and not is_json_output: if banner.protocol[0] == 1:
p = '' if out.batch else ' ' * (padlen - 14)
out.fail('(sec) SSH v1 enabled{} -- SSH v1 can be exploited to recover plaintext passwords'.format(p))
if not out.is_section_empty() and not is_json_output:
out.head('# security') out.head('# security')
obuf.flush() out.flush_section()
out.sep() out.sep()
def output_fingerprints(algs: Algorithms, is_json_output: bool, sha256: bool = True) -> None: def output_fingerprints(out: OutputBuffer, algs: Algorithms, is_json_output: bool) -> None:
with OutputBuffer() as obuf: with out:
fps = [] fps = []
if algs.ssh1kex is not None: if algs.ssh1kex is not None:
name = 'ssh-rsa1' name = 'ssh-rsa1'
@ -280,18 +291,20 @@ def output_fingerprints(algs: Algorithms, is_json_output: bool, sha256: bool = T
fps = sorted(fps) fps = sorted(fps)
for fpp in fps: for fpp in fps:
name, fp = fpp name, fp = fpp
fpo = fp.sha256 if sha256 else fp.md5 out.good('(fin) {}: {}'.format(name, fp.sha256))
# p = '' if out.batch else ' ' * (padlen - len(name))
# out.good('(fin) {0}{1} -- {2} {3}'.format(name, p, bits, fpo)) # Output the MD5 hash too if verbose mode is enabled.
out.good('(fin) {}: {}'.format(name, fpo)) if out.verbose:
if len(obuf) > 0 and not is_json_output: out.info('(fin) {}: {} -- [info] do not rely on MD5 fingerprints for server identification; it is insecure for this use case'.format(name, fp.md5))
if not out.is_section_empty() and not is_json_output:
out.head('# fingerprints') out.head('# fingerprints')
obuf.flush() out.flush_section()
out.sep() out.sep()
# Returns True if no warnings or failures encountered in configuration. # Returns True if no warnings or failures encountered in configuration.
def output_recommendations(algs: Algorithms, software: Optional[Software], is_json_output: bool, padlen: int = 0) -> bool: def output_recommendations(out: OutputBuffer, algs: Algorithms, software: Optional[Software], is_json_output: bool, padlen: int = 0) -> bool:
ret = True ret = True
# PuTTY's algorithms cannot be modified, so there's no point in issuing recommendations. # PuTTY's algorithms cannot be modified, so there's no point in issuing recommendations.
@ -323,7 +336,7 @@ def output_recommendations(algs: Algorithms, software: Optional[Software], is_js
return ret return ret
for_server = True for_server = True
with OutputBuffer() as obuf: with out:
software, alg_rec = algs.get_recommendations(software, for_server) software, alg_rec = algs.get_recommendations(software, for_server)
for sshv in range(2, 0, -1): for sshv in range(2, 0, -1):
if sshv not in alg_rec: if sshv not in alg_rec:
@ -351,20 +364,20 @@ def output_recommendations(algs: Algorithms, software: Optional[Software], is_js
b = '(SSH{})'.format(sshv) if sshv == 1 else '' b = '(SSH{})'.format(sshv) if sshv == 1 else ''
fm = '(rec) {0}{1}{2}-- {3} algorithm to {4}{5} {6}' fm = '(rec) {0}{1}{2}-- {3} algorithm to {4}{5} {6}'
fn(fm.format(sg, name, p, alg_type, an, chg_additional_info, b)) fn(fm.format(sg, name, p, alg_type, an, chg_additional_info, b))
if len(obuf) > 0 and not is_json_output: if not out.is_section_empty() and not is_json_output:
if software is not None: if software is not None:
title = '(for {})'.format(software.display(False)) title = '(for {})'.format(software.display(False))
else: else:
title = '' title = ''
out.head('# algorithm recommendations {}'.format(title)) out.head('# algorithm recommendations {}'.format(title))
obuf.flush(True) # Sort the output so that it is always stable (needed for repeatable testing). out.flush_section(sort_section=True) # Sort the output so that it is always stable (needed for repeatable testing).
out.sep() out.sep()
return ret return ret
# Output additional information & notes. # Output additional information & notes.
def output_info(software: Optional['Software'], client_audit: bool, any_problems: bool, is_json_output: bool) -> None: def output_info(out: OutputBuffer, software: Optional['Software'], client_audit: bool, any_problems: bool, is_json_output: bool) -> None:
with OutputBuffer() as obuf: with out:
# Tell user that PuTTY cannot be hardened at the protocol-level. # Tell user that PuTTY cannot be hardened at the protocol-level.
if client_audit and (software is not None) and (software.product == Product.PuTTY): if client_audit and (software is not None) and (software.product == Product.PuTTY):
out.warn('(nfo) PuTTY does not have the option of restricting any algorithms during the SSH handshake.') out.warn('(nfo) PuTTY does not have the option of restricting any algorithms during the SSH handshake.')
@ -373,20 +386,20 @@ def output_info(software: Optional['Software'], client_audit: bool, any_problems
if any_problems: if any_problems:
out.warn('(nfo) For hardening guides on common OSes, please see: <https://www.ssh-audit.com/hardening_guides.html>') out.warn('(nfo) For hardening guides on common OSes, please see: <https://www.ssh-audit.com/hardening_guides.html>')
if len(obuf) > 0 and not is_json_output: if not out.is_section_empty() and not is_json_output:
out.head('# additional info') out.head('# additional info')
obuf.flush() out.flush_section()
out.sep() out.sep()
# Returns a exitcodes.* flag to denote if any failures or warnings were encountered. # Returns a exitcodes.* flag to denote if any failures or warnings were encountered.
def output(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) -> int:
program_retval = exitcodes.GOOD program_retval = exitcodes.GOOD
client_audit = client_host is not None # If set, this is a client audit. client_audit = client_host is not None # If set, this is a client audit.
sshv = 1 if pkm is not None else 2 sshv = 1 if pkm is not None else 2
algs = Algorithms(pkm, kex) algs = Algorithms(pkm, kex)
with OutputBuffer() as obuf: with out:
if print_target: if print_target:
host = aconf.host host = aconf.host
@ -405,18 +418,23 @@ def output(aconf: AuditConf, banner: Optional[Banner], header: List[str], client
if len(header) > 0: if len(header) > 0:
out.info('(gen) header: ' + '\n'.join(header)) out.info('(gen) header: ' + '\n'.join(header))
if banner is not None: if banner is not None:
out.good('(gen) banner: {}'.format(banner)) banner_line = '(gen) banner: {}'.format(banner)
if sshv == 1 or banner.protocol[0] == 1:
out.fail(banner_line)
out.fail('(gen) protocol SSH1 enabled')
else:
out.good(banner_line)
if not banner.valid_ascii: if not banner.valid_ascii:
# NOTE: RFC 4253, Section 4.2 # NOTE: RFC 4253, Section 4.2
out.warn('(gen) banner contains non-printable ASCII') out.warn('(gen) banner contains non-printable ASCII')
if sshv == 1 or banner.protocol[0] == 1:
out.fail('(gen) protocol SSH1 enabled')
software = Software.parse(banner) software = Software.parse(banner)
if software is not None: if software is not None:
out.good('(gen) software: {}'.format(software)) out.good('(gen) software: {}'.format(software))
else: else:
software = None software = None
output_compatibility(algs, client_audit) output_compatibility(out, algs, client_audit)
if kex is not None: if kex is not None:
compressions = [x for x in kex.server.compression if x != 'none'] compressions = [x for x in kex.server.compression if x != 'none']
if len(compressions) > 0: if len(compressions) > 0:
@ -424,47 +442,49 @@ def output(aconf: AuditConf, banner: Optional[Banner], header: List[str], client
else: else:
cmptxt = 'disabled' cmptxt = 'disabled'
out.good('(gen) compression: {}'.format(cmptxt)) out.good('(gen) compression: {}'.format(cmptxt))
if len(obuf) > 0 and not aconf.json: # Print output when it exists and JSON output isn't requested. if not out.is_section_empty() and not aconf.json: # Print output when it exists and JSON output isn't requested.
out.head('# general') out.head('# general')
obuf.flush() out.flush_section()
out.sep() out.sep()
maxlen = algs.maxlen + 1 maxlen = algs.maxlen + 1
output_security(banner, client_audit, maxlen, aconf.json) output_security(out, banner, client_audit, maxlen, aconf.json)
# Filled in by output_algorithms() with unidentified algs. # Filled in by output_algorithms() with unidentified algs.
unknown_algorithms = [] # type: List[str] unknown_algorithms: List[str] = []
if pkm is not None: if pkm is not None:
adb = SSH1_KexDB.ALGORITHMS adb = SSH1_KexDB.ALGORITHMS
ciphers = pkm.supported_ciphers ciphers = pkm.supported_ciphers
auths = pkm.supported_authentications auths = pkm.supported_authentications
title, atype = 'SSH1 host-key algorithms', 'key' title, atype = 'SSH1 host-key algorithms', 'key'
program_retval = output_algorithms(title, adb, atype, ['ssh-rsa1'], unknown_algorithms, aconf.json, program_retval, maxlen) program_retval = output_algorithms(out, title, adb, atype, ['ssh-rsa1'], unknown_algorithms, aconf.json, program_retval, maxlen)
title, atype = 'SSH1 encryption algorithms (ciphers)', 'enc' title, atype = 'SSH1 encryption algorithms (ciphers)', 'enc'
program_retval = output_algorithms(title, adb, atype, ciphers, unknown_algorithms, aconf.json, program_retval, maxlen) program_retval = output_algorithms(out, title, adb, atype, ciphers, unknown_algorithms, aconf.json, program_retval, maxlen)
title, atype = 'SSH1 authentication types', 'aut' title, atype = 'SSH1 authentication types', 'aut'
program_retval = output_algorithms(title, adb, atype, auths, unknown_algorithms, aconf.json, program_retval, maxlen) program_retval = output_algorithms(out, title, adb, atype, auths, unknown_algorithms, aconf.json, program_retval, maxlen)
if kex is not None: if kex is not None:
adb = SSH2_KexDB.ALGORITHMS adb = SSH2_KexDB.ALGORITHMS
title, atype = 'key exchange algorithms', 'kex' title, atype = 'key exchange algorithms', 'kex'
program_retval = output_algorithms(title, adb, atype, kex.kex_algorithms, unknown_algorithms, aconf.json, program_retval, maxlen, kex.dh_modulus_sizes()) program_retval = output_algorithms(out, title, adb, atype, kex.kex_algorithms, unknown_algorithms, aconf.json, program_retval, maxlen, kex.dh_modulus_sizes())
title, atype = 'host-key algorithms', 'key' title, atype = 'host-key algorithms', 'key'
program_retval = output_algorithms(title, adb, atype, kex.key_algorithms, unknown_algorithms, aconf.json, program_retval, maxlen, kex.rsa_key_sizes()) program_retval = output_algorithms(out, title, adb, atype, kex.key_algorithms, unknown_algorithms, aconf.json, program_retval, maxlen, kex.rsa_key_sizes())
title, atype = 'encryption algorithms (ciphers)', 'enc' title, atype = 'encryption algorithms (ciphers)', 'enc'
program_retval = output_algorithms(title, adb, atype, kex.server.encryption, unknown_algorithms, aconf.json, program_retval, maxlen) program_retval = output_algorithms(out, title, adb, atype, kex.server.encryption, unknown_algorithms, aconf.json, program_retval, maxlen)
title, atype = 'message authentication code algorithms', 'mac' title, atype = 'message authentication code algorithms', 'mac'
program_retval = output_algorithms(title, adb, atype, kex.server.mac, unknown_algorithms, aconf.json, program_retval, maxlen) program_retval = output_algorithms(out, title, adb, atype, kex.server.mac, unknown_algorithms, aconf.json, program_retval, maxlen)
output_fingerprints(algs, aconf.json, True) output_fingerprints(out, algs, aconf.json)
perfect_config = output_recommendations(algs, software, aconf.json, maxlen) perfect_config = output_recommendations(out, algs, software, aconf.json, maxlen)
output_info(software, client_audit, not perfect_config, aconf.json) output_info(out, software, client_audit, not perfect_config, aconf.json)
if aconf.json: if aconf.json:
print(json.dumps(build_struct(banner, kex=kex, client_host=client_host), sort_keys=True), end='' if len(aconf.target_list) > 0 else "\n") # Print the JSON of the audit info. Skip the newline at the end if multiple targets were given (since each audit dump will go into its own list entry). out.reset()
# Build & write the JSON struct.
out.info(json.dumps(build_struct(aconf.host, banner, kex=kex, client_host=client_host), indent=4 if aconf.json_print_indent else None, sort_keys=True))
elif len(unknown_algorithms) > 0: # If we encountered any unknown algorithms, ask the user to report them. elif len(unknown_algorithms) > 0: # If we encountered any unknown algorithms, ask the user to report them.
out.warn("\n\n!!! WARNING: unknown algorithm(s) found!: %s. Please email the full output above to the maintainer (jtesta@positronsecurity.com), or create a Github issue at <https://github.com/jtesta/ssh-audit/issues>.\n" % ','.join(unknown_algorithms)) out.warn("\n\n!!! WARNING: unknown algorithm(s) found!: %s. Please email the full output above to the maintainer (jtesta@positronsecurity.com), or create a Github issue at <https://github.com/jtesta/ssh-audit/issues>.\n" % ','.join(unknown_algorithms))
return program_retval return program_retval
def evaluate_policy(aconf: AuditConf, banner: Optional['Banner'], client_host: Optional[str], kex: Optional['SSH2_Kex'] = None) -> bool: def evaluate_policy(out: OutputBuffer, aconf: AuditConf, banner: Optional['Banner'], client_host: Optional[str], kex: Optional['SSH2_Kex'] = None) -> bool:
if aconf.policy is None: if aconf.policy is None:
raise RuntimeError('Internal error: cannot evaluate against null Policy!') raise RuntimeError('Internal error: cannot evaluate against null Policy!')
@ -472,11 +492,11 @@ def evaluate_policy(aconf: AuditConf, banner: Optional['Banner'], client_host: O
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, 'policy': aconf.policy.get_name_and_version(), 'passed': passed, 'errors': error_struct}
print(json.dumps(json_struct, 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 = ''
if aconf.client_audit: if aconf.client_audit:
print("Client IP: %s" % client_host) out.info("Client IP: %s" % client_host)
spacing = " " # So the fields below line up with 'Client IP: '. spacing = " " # So the fields below line up with 'Client IP: '.
else: else:
host = aconf.host host = aconf.host
@ -487,9 +507,9 @@ def evaluate_policy(aconf: AuditConf, banner: Optional['Banner'], client_host: O
else: else:
host = '%s:%d' % (aconf.host, aconf.port) host = '%s:%d' % (aconf.host, aconf.port)
print("Host: %s" % host) out.info("Host: %s" % host)
print("Policy: %s%s" % (spacing, aconf.policy.get_name_and_version())) out.info("Policy: %s%s" % (spacing, aconf.policy.get_name_and_version()))
print("Result: %s" % spacing, end='') out.info("Result: %s" % spacing, line_ended=False)
# Use these nice unicode characters in the result message, unless we're on Windows (the cmd.exe terminal doesn't display them properly). # Use these nice unicode characters in the result message, unless we're on Windows (the cmd.exe terminal doesn't display them properly).
icon_good = "" icon_good = ""
@ -507,29 +527,31 @@ def evaluate_policy(aconf: AuditConf, banner: Optional['Banner'], client_host: O
return passed return passed
def list_policies() -> None: def list_policies(out: OutputBuffer) -> None:
'''Prints a list of server & client policies.''' '''Prints a list of server & client policies.'''
server_policy_names, client_policy_names = Policy.list_builtin_policies() server_policy_names, client_policy_names = Policy.list_builtin_policies()
if len(server_policy_names) > 0: if len(server_policy_names) > 0:
out.head('\nServer policies:\n') out.head('\nServer policies:\n')
print(" * \"%s\"" % "\"\n * \"".join(server_policy_names)) out.info(" * \"%s\"" % "\"\n * \"".join(server_policy_names))
if len(client_policy_names) > 0: if len(client_policy_names) > 0:
out.head('\nClient policies:\n') out.head('\nClient policies:\n')
print(" * \"%s\"" % "\"\n * \"".join(client_policy_names)) out.info(" * \"%s\"" % "\"\n * \"".join(client_policy_names))
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:
print("Error: no built-in policies found!") out.fail("Error: no built-in policies found!")
else: else:
print("\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.write()
def make_policy(aconf: AuditConf, banner: Optional['Banner'], kex: Optional['SSH2_Kex'], client_host: Optional[str]) -> None: def make_policy(aconf: AuditConf, banner: Optional['Banner'], kex: Optional['SSH2_Kex'], client_host: Optional[str]) -> None:
# Set the source of this policy to the server host if this is a server audit, otherwise set it to the client address. # Set the source of this policy to the server host if this is a server audit, otherwise set it to the client address.
source = aconf.host # type: Optional[str] source: Optional[str] = aconf.host
if aconf.client_audit: if aconf.client_audit:
source = client_host source = client_host
@ -541,7 +563,7 @@ def make_policy(aconf: AuditConf, banner: Optional['Banner'], kex: Optional['SSH
# Open with mode 'x' (creates the file, or fails if it already exist). # Open with mode 'x' (creates the file, or fails if it already exist).
succeeded = True succeeded = True
try: try:
with open(aconf.policy_file, 'x') as f: with open(aconf.policy_file, 'x', encoding='utf-8') as f:
f.write(policy_data) f.write(policy_data)
except FileExistsError: except FileExistsError:
succeeded = False succeeded = False
@ -552,19 +574,19 @@ def make_policy(aconf: AuditConf, banner: Optional['Banner'], kex: Optional['SSH
print("Error: file already exists: %s" % aconf.policy_file) print("Error: file already exists: %s" % aconf.policy_file)
def process_commandline(args: List[str], usage_cb: Callable[..., None]) -> 'AuditConf': # pylint: disable=too-many-statements def process_commandline(out: OutputBuffer, args: List[str], usage_cb: Callable[..., None]) -> 'AuditConf': # pylint: disable=too-many-statements
# pylint: disable=too-many-branches # pylint: disable=too-many-branches
aconf = AuditConf() aconf = AuditConf()
try: try:
sopts = 'h1246M:p:P:jbcnvl:t:T:L' sopts = 'h1246M:p:P:jbcnvl:t:T:Lmd'
lopts = ['help', 'ssh1', 'ssh2', 'ipv4', 'ipv6', 'make-policy=', 'port=', 'policy=', 'json', 'batch', 'client-audit', 'no-colors', 'verbose', 'level=', 'timeout=', 'targets=', 'list-policies', 'lookup='] 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']
opts, args = getopt.gnu_getopt(args, sopts, lopts) opts, args = getopt.gnu_getopt(args, sopts, lopts)
except getopt.GetoptError as err: except getopt.GetoptError as err:
usage_cb(str(err)) usage_cb(str(err))
aconf.ssh1, aconf.ssh2 = False, False aconf.ssh1, aconf.ssh2 = False, False
host = '' # type: str host: str = ''
oport = None # type: Optional[str] oport: Optional[str] = None
port = 0 # type: int port: int = 0
for o, a in opts: for o, a in opts:
if o in ('-h', '--help'): if o in ('-h', '--help'):
usage_cb() usage_cb()
@ -585,10 +607,15 @@ def process_commandline(args: List[str], usage_cb: Callable[..., None]) -> 'Audi
aconf.client_audit = True aconf.client_audit = True
elif o in ('-n', '--no-colors'): elif o in ('-n', '--no-colors'):
aconf.colors = False aconf.colors = False
out.use_colors = False
elif o in ('-j', '--json'): elif o in ('-j', '--json'):
aconf.json = True if aconf.json: # If specified twice, enable indent printing.
aconf.json_print_indent = True
else:
aconf.json = True
elif o in ('-v', '--verbose'): elif o in ('-v', '--verbose'):
aconf.verbose = True aconf.verbose = True
out.verbose = True
elif o in ('-l', '--level'): elif o in ('-l', '--level'):
if a not in ('info', 'warn', 'fail'): if a not in ('info', 'warn', 'fail'):
usage_cb('level {} is not valid'.format(a)) usage_cb('level {} is not valid'.format(a))
@ -603,19 +630,29 @@ def process_commandline(args: List[str], usage_cb: Callable[..., None]) -> 'Audi
aconf.policy_file = a aconf.policy_file = a
elif o in ('-T', '--targets'): elif o in ('-T', '--targets'):
aconf.target_file = a aconf.target_file = a
elif o == '--threads':
aconf.threads = int(a)
elif o in ('-L', '--list-policies'): elif o in ('-L', '--list-policies'):
aconf.list_policies = True aconf.list_policies = True
elif o == '--lookup': elif o == '--lookup':
aconf.lookup = a aconf.lookup = a
elif o in ('-m', '--manual'):
aconf.manual = True
elif o in ('-d', '--debug'):
aconf.debug = True
out.debug = True
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 == '': if len(args) == 0 and aconf.client_audit is False and aconf.target_file is None and aconf.list_policies is False and aconf.lookup == '' and aconf.manual is False:
usage_cb() usage_cb()
if aconf.manual:
return aconf
if aconf.lookup != '': if aconf.lookup != '':
return aconf return aconf
if aconf.list_policies: if aconf.list_policies:
list_policies() list_policies(out)
sys.exit(exitcodes.GOOD) sys.exit(exitcodes.GOOD)
if aconf.client_audit is False and aconf.target_file is None: if aconf.client_audit is False and aconf.target_file is None:
@ -644,7 +681,7 @@ def process_commandline(args: List[str], usage_cb: Callable[..., None]) -> 'Audi
# If a file containing a list of targets was given, read it. # If a file containing a list of targets was given, read it.
if aconf.target_file is not None: if aconf.target_file is not None:
with open(aconf.target_file, 'r') as f: with open(aconf.target_file, 'r', encoding='utf-8') as f:
aconf.target_list = f.readlines() aconf.target_list = f.readlines()
# Strip out whitespace from each line in target file, and skip empty lines. # Strip out whitespace from each line in target file, and skip empty lines.
@ -659,23 +696,26 @@ def process_commandline(args: List[str], usage_cb: Callable[..., None]) -> 'Audi
try: try:
aconf.policy = Policy(policy_file=aconf.policy_file) aconf.policy = Policy(policy_file=aconf.policy_file)
except Exception as e: except Exception as e:
print("Error while loading policy file: %s: %s" % (str(e), traceback.format_exc())) out.fail("Error while loading policy file: %s: %s" % (str(e), traceback.format_exc()))
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():
print("Error: client audit selected, but server policy provided.") out.fail("Error: client audit selected, but server policy provided.")
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:
print("Error: server audit selected, but client policy provided.") out.fail("Error: server audit selected, but client policy provided.")
out.write()
sys.exit(exitcodes.UNKNOWN_ERROR) sys.exit(exitcodes.UNKNOWN_ERROR)
return aconf return aconf
def build_struct(banner: Optional['Banner'], kex: Optional['SSH2_Kex'] = None, pkm: Optional['SSH1_PublicKeyMessage'] = None, client_host: Optional[str] = None) -> Any: def build_struct(target_host: str, banner: Optional['Banner'], kex: Optional['SSH2_Kex'] = None, pkm: Optional['SSH1_PublicKeyMessage'] = None, client_host: Optional[str] = None) -> Any:
banner_str = '' banner_str = ''
banner_protocol = None banner_protocol = None
@ -687,25 +727,30 @@ def build_struct(banner: Optional['Banner'], kex: Optional['SSH2_Kex'] = None, p
banner_software = banner.software banner_software = banner.software
banner_comments = banner.comments banner_comments = banner.comments
res = { res: Any = {
"banner": { "banner": {
"raw": banner_str, "raw": banner_str,
"protocol": banner_protocol, "protocol": banner_protocol,
"software": banner_software, "software": banner_software,
"comments": banner_comments, "comments": banner_comments,
}, },
} # type: Any }
# If we're scanning a client host, put the client's IP into the results. Otherwise, include the target host.
if client_host is not None: if client_host is not None:
res['client_ip'] = client_host res['client_ip'] = client_host
else:
res['target'] = target_host
if kex is not None: if kex is not None:
res['compression'] = kex.server.compression res['compression'] = kex.server.compression
res['kex'] = [] res['kex'] = []
alg_sizes = kex.dh_modulus_sizes() alg_sizes = kex.dh_modulus_sizes()
for algorithm in kex.kex_algorithms: for algorithm in kex.kex_algorithms:
entry = { entry: Any = {
'algorithm': algorithm, 'algorithm': algorithm,
} # type: Any }
if algorithm in alg_sizes: if algorithm in alg_sizes:
hostkey_size, ca_size = alg_sizes[algorithm] hostkey_size, ca_size = alg_sizes[algorithm]
entry['keysize'] = hostkey_size entry['keysize'] = hostkey_size
@ -747,11 +792,18 @@ def build_struct(banner: Optional['Banner'], kex: Optional['SSH2_Kex'] = None, p
# Skip over certificate host types (or we would return invalid fingerprints). # Skip over certificate host types (or we would return invalid fingerprints).
if '-cert-' in host_key_type: if '-cert-' in host_key_type:
continue continue
entry = {
'type': host_key_type, # Add the SHA256 and MD5 fingerprints.
'fp': fp.sha256, res['fingerprints'].append({
} 'hostkey': host_key_type,
res['fingerprints'].append(entry) 'hash_alg': 'SHA256',
'hash': fp.sha256[7:]
})
res['fingerprints'].append({
'hostkey': host_key_type,
'hash_alg': 'MD5',
'hash': fp.md5[4:]
})
else: else:
pkm_supported_ciphers = None pkm_supported_ciphers = None
pkm_supported_authentications = None pkm_supported_authentications = None
@ -773,20 +825,31 @@ def build_struct(banner: Optional['Banner'], kex: Optional['SSH2_Kex'] = None, p
# Returns one of the exitcodes.* flags. # Returns one of the exitcodes.* flags.
def audit(aconf: AuditConf, sshv: Optional[int] = None, print_target: bool = False) -> int: def audit(out: OutputBuffer, aconf: AuditConf, sshv: Optional[int] = None, print_target: bool = False) -> int:
program_retval = exitcodes.GOOD program_retval = exitcodes.GOOD
out.batch = aconf.batch out.batch = aconf.batch
out.verbose = aconf.verbose out.verbose = aconf.verbose
out.debug = aconf.debug
out.level = aconf.level out.level = aconf.level
out.use_colors = aconf.colors out.use_colors = aconf.colors
s = SSH_Socket(aconf.host, aconf.port, aconf.ipvo, aconf.timeout, aconf.timeout_set) s = SSH_Socket(out, aconf.host, aconf.port, aconf.ip_version_preference, aconf.timeout, aconf.timeout_set)
if aconf.client_audit: if aconf.client_audit:
out.v("Listening for client connection on port %d..." % aconf.port, write_now=True)
s.listen_and_accept() s.listen_and_accept()
else: else:
out.v("Starting audit of %s:%d..." % ('[%s]' % aconf.host if Utils.is_ipv6_address(aconf.host) else aconf.host, aconf.port), write_now=True)
err = s.connect() err = s.connect()
if err is not None: if err is not None:
out.fail(err) out.fail(err)
sys.exit(exitcodes.CONNECTION_ERROR)
# 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:
return exitcodes.CONNECTION_ERROR
else:
out.write()
sys.exit(exitcodes.CONNECTION_ERROR)
if sshv is None: if sshv is None:
sshv = 2 if aconf.ssh2 else 1 sshv = 2 if aconf.ssh2 else 1
@ -798,7 +861,7 @@ def audit(aconf: AuditConf, sshv: Optional[int] = None, print_target: bool = Fal
else: else:
err = '[exception] did not receive banner: {}'.format(err) err = '[exception] did not receive banner: {}'.format(err)
if err is None: if err is None:
s.send_algorithms() # Send the algorithms we support (except we don't since this isn't a real SSH connection). s.send_kexinit() # Send the algorithms we support (except we don't since this isn't a real SSH connection).
packet_type, payload = s.read_packet(sshv) packet_type, payload = s.read_packet(sshv)
if packet_type < 0: if packet_type < 0:
@ -806,12 +869,14 @@ def audit(aconf: AuditConf, sshv: Optional[int] = None, print_target: bool = Fal
if len(payload) > 0: if len(payload) > 0:
payload_txt = payload.decode('utf-8') payload_txt = payload.decode('utf-8')
else: else:
payload_txt = u'empty' payload_txt = 'empty'
except UnicodeDecodeError: except UnicodeDecodeError:
payload_txt = u'"{}"'.format(repr(payload).lstrip('b')[1:-1]) payload_txt = '"{}"'.format(repr(payload).lstrip('b')[1:-1])
if payload_txt == u'Protocol major versions differ.': if payload_txt == 'Protocol major versions differ.':
if sshv == 2 and aconf.ssh1: if sshv == 2 and aconf.ssh1:
return audit(aconf, 1) ret = audit(out, aconf, 1)
out.write()
return ret
err = '[exception] error reading packet ({})'.format(payload_txt) err = '[exception] error reading packet ({})'.format(payload_txt)
else: else:
err_pair = None err_pair = None
@ -824,24 +889,29 @@ def audit(aconf: AuditConf, sshv: Optional[int] = None, print_target: bool = Fal
'instead received unknown message ({2})' 'instead received unknown message ({2})'
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(aconf, banner, header) output(out, aconf, banner, header)
out.fail(err) out.fail(err)
return exitcodes.CONNECTION_ERROR return exitcodes.CONNECTION_ERROR
if sshv == 1: if sshv == 1:
program_retval = output(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:
kex = SSH2_Kex.parse(payload) try:
kex = SSH2_Kex.parse(payload)
except Exception:
out.fail("Failed to parse server's kex. Stack trace:\n%s" % str(traceback.format_exc()))
return exitcodes.CONNECTION_ERROR
if aconf.client_audit is False: if aconf.client_audit is False:
HostKeyTest.run(s, kex) HostKeyTest.run(out, s, kex)
GEXTest.run(s, kex) GEXTest.run(out, s, kex)
# This is a standard audit scan. # This is a standard audit scan.
if (aconf.policy is None) and (aconf.make_policy is False): if (aconf.policy is None) and (aconf.make_policy is False):
program_retval = output(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)
# This is a policy test. # This is a policy test.
elif (aconf.policy is not None) and (aconf.make_policy is False): elif (aconf.policy is not None) and (aconf.make_policy is False):
program_retval = exitcodes.GOOD if evaluate_policy(aconf, banner, s.client_host, kex=kex) else exitcodes.FAILURE program_retval = exitcodes.GOOD if evaluate_policy(out, aconf, banner, s.client_host, kex=kex) else exitcodes.FAILURE
# A new policy should be made from this scan. # A new policy should be made from this scan.
elif (aconf.policy is None) and (aconf.make_policy is True): elif (aconf.policy is None) and (aconf.make_policy is True):
@ -853,7 +923,7 @@ def audit(aconf: AuditConf, sshv: Optional[int] = None, print_target: bool = Fal
return program_retval return program_retval
def algorithm_lookup(alg_names: str) -> int: def algorithm_lookup(out: OutputBuffer, alg_names: str) -> int:
'''Looks up a comma-separated list of algorithms and outputs their security properties. Returns an exitcodes.* flag.''' '''Looks up a comma-separated list of algorithms and outputs their security properties. Returns an exitcodes.* flag.'''
retval = exitcodes.GOOD retval = exitcodes.GOOD
alg_types = { alg_types = {
@ -879,13 +949,13 @@ def algorithm_lookup(alg_names: str) -> int:
for (outer_k, outer_v) in adb.items() for (outer_k, outer_v) in adb.items()
} }
unknown_algorithms = [] # type: List[str] unknown_algorithms: List[str] = []
padding = len(max(algorithm_names, key=len)) padding = len(max(algorithm_names, key=len))
for alg_type in alg_types: for alg_type in alg_types:
if len(algorithms_dict[alg_type]) > 0: if len(algorithms_dict[alg_type]) > 0:
title = str(alg_types.get(alg_type)) title = str(alg_types.get(alg_type))
retval = output_algorithms(title, adb, alg_type, list(algorithms_dict[alg_type]), unknown_algorithms, False, retval, padding) retval = output_algorithms(out, title, adb, alg_type, list(algorithms_dict[alg_type]), unknown_algorithms, False, retval, padding)
algorithms_dict_flattened = [ algorithms_dict_flattened = [
alg_name alg_name
@ -902,8 +972,8 @@ def algorithm_lookup(alg_names: str) -> int:
similar_algorithms = [ similar_algorithms = [
alg_unknown + " --> (" + alg_type + ") " + alg_name alg_unknown + " --> (" + alg_type + ") " + alg_name
for alg_unknown in algorithms_not_found for alg_unknown in algorithms_not_found
for alg_type in adb.keys() for alg_type, alg_names in adb.items()
for alg_name in adb[alg_type] for alg_name in alg_names
# Perform a case-insensitive comparison using 'casefold' # Perform a case-insensitive comparison using 'casefold'
# and match substrings using the 'in' operator. # and match substrings using the 'in' operator.
if alg_unknown.casefold() in alg_name.casefold() if alg_unknown.casefold() in alg_name.casefold()
@ -915,7 +985,7 @@ def algorithm_lookup(alg_names: str) -> int:
for algorithm_not_found in algorithms_not_found: for algorithm_not_found in algorithms_not_found:
out.fail(algorithm_not_found) out.fail(algorithm_not_found)
print() out.sep()
if len(similar_algorithms) > 0: if len(similar_algorithms) > 0:
retval = exitcodes.FAILURE retval = exitcodes.FAILURE
@ -926,14 +996,77 @@ def algorithm_lookup(alg_names: str) -> int:
return retval return retval
out = Output() # Worker thread for scanning multiple targets concurrently.
def target_worker_thread(host: str, port: int, shared_aconf: AuditConf) -> Tuple[int, str]:
ret = -1
string_output = ''
out = OutputBuffer()
out.verbose = shared_aconf.verbose
my_aconf = copy.deepcopy(shared_aconf)
my_aconf.host = host
my_aconf.port = port
# If we're outputting JSON, turn off colors and ensure 'info' level messages go through.
if my_aconf.json:
out.json = True
out.use_colors = False
out.v("Running against: %s:%d..." % (my_aconf.host, my_aconf.port), write_now=True)
try:
ret = audit(out, my_aconf, print_target=True)
string_output = out.get_buffer()
except Exception:
ret = -1
string_output = "An exception occurred while scanning %s:%d:\n%s" % (host, port, str(traceback.format_exc()))
return ret, string_output
def windows_manual(out: OutputBuffer) -> int:
'''Prints the man page on Windows. Returns an exitcodes.* flag.'''
retval = exitcodes.GOOD
if sys.platform != 'win32':
out.fail("The '-m' and '--manual' parameters are reserved for use on Windows only.\nUsers of other operating systems should read the man page.")
retval = exitcodes.FAILURE
return retval
# If colors are disabled, strip the ANSI color codes from the man page.
windows_man_page = WINDOWS_MAN_PAGE
if not out.use_colors:
windows_man_page = re.sub(r'\x1b\[\d+?m', '', windows_man_page)
out.info(windows_man_page)
return retval
def main() -> int: def main() -> int:
aconf = process_commandline(sys.argv[1:], usage) out = OutputBuffer()
aconf = process_commandline(out, sys.argv[1:], usage)
# If we're on Windows, but the colorama module could not be imported, print a warning if we're in verbose mode.
if (sys.platform == 'win32') and ('colorama' not in sys.modules):
out.v("WARNING: colorama module not found. Colorized output will be disabled.", write_now=True)
# If we're outputting JSON, turn off colors and ensure 'info' level messages go through.
if aconf.json:
out.json = True
out.use_colors = False
if aconf.manual:
# If the colorama module was not be imported, turn off colors in order
# to output a plain text version of the man page.
if (sys.platform == 'win32') and ('colorama' not in sys.modules):
out.use_colors = False
retval = windows_manual(out)
out.write()
sys.exit(retval)
if aconf.lookup != '': if aconf.lookup != '':
retval = algorithm_lookup(aconf.lookup) retval = algorithm_lookup(out, aconf.lookup)
out.write()
sys.exit(retval) sys.exit(retval)
# If multiple targets were specified... # If multiple targets were specified...
@ -945,31 +1078,46 @@ def main() -> int:
print('[', end='') print('[', end='')
# Loop through each target in the list. # Loop through each target in the list.
for i, target in enumerate(aconf.target_list): target_servers = []
aconf.host, port = Utils.parse_host_and_port(target) for _, target in enumerate(aconf.target_list):
if port == 0: host, port = Utils.parse_host_and_port(target, default_port=22)
port = 22 target_servers.append((host, port))
aconf.port = port
new_ret = audit(aconf, print_target=True) # A ranked list of return codes. Those with higher indices will take precendence over lower ones. For example, if three servers are scanned, yielding WARNING, GOOD, and UNKNOWN_ERROR, the overall result will be UNKNOWN_ERROR, since its index is the highest. Errors have highest priority, followed by failures, then warnings.
ranked_return_codes = [exitcodes.GOOD, exitcodes.WARNING, exitcodes.FAILURE, exitcodes.CONNECTION_ERROR, exitcodes.UNKNOWN_ERROR]
# Set the return value only if an unknown error occurred, a failure occurred, or if a warning occurred and the previous value was good. # Queue all worker threads.
if (new_ret == exitcodes.UNKNOWN_ERROR) or (new_ret == exitcodes.FAILURE) or ((new_ret == exitcodes.WARNING) and (ret == exitcodes.GOOD)): num_target_servers = len(target_servers)
ret = new_ret num_processed = 0
out.v("Scanning %u targets with %s%u threads..." % (num_target_servers, '(at most) ' if aconf.threads > num_target_servers else '', aconf.threads), write_now=True)
with concurrent.futures.ThreadPoolExecutor(max_workers=aconf.threads) as executor:
future_to_server = {executor.submit(target_worker_thread, target_server[0], target_server[1], aconf): target_server for target_server in target_servers}
for future in concurrent.futures.as_completed(future_to_server):
worker_ret, worker_output = future.result()
# Don't print a delimiter after the last target was handled. # If this worker's return code is ranked higher that what we've cached so far, update our cache.
if i + 1 != len(aconf.target_list): if ranked_return_codes.index(worker_ret) > ranked_return_codes.index(ret):
if aconf.json: ret = worker_ret
print(", ", end='')
else: # print("Worker for %s:%d returned %d: [%s]" % (target_server[0], target_server[1], worker_ret, worker_output))
print(("-" * 80) + "\n") print(worker_output, end='' if aconf.json else "\n")
# Don't print a delimiter after the last target was handled.
num_processed += 1
if num_processed < num_target_servers:
if aconf.json:
print(", ", end='')
else:
print(("-" * 80) + "\n")
if aconf.json: if aconf.json:
print(']') print(']')
return ret else: # Just a scan against a single target.
else: ret = audit(out, aconf)
return audit(aconf) out.write()
return ret
if __name__ == '__main__': # pragma: nocover if __name__ == '__main__': # pragma: nocover

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-2021 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
@ -36,7 +36,7 @@ from typing import Callable, Optional, Union, Any # noqa: F401
from ssh_audit import exitcodes from ssh_audit import exitcodes
from ssh_audit.banner import Banner from ssh_audit.banner import Banner
from ssh_audit.globals import SSH_HEADER from ssh_audit.globals import SSH_HEADER
from ssh_audit.output import Output from ssh_audit.outputbuffer import OutputBuffer
from ssh_audit.protocol import Protocol from ssh_audit.protocol import Protocol
from ssh_audit.readbuf import ReadBuf from ssh_audit.readbuf import ReadBuf
from ssh_audit.ssh1 import SSH1 from ssh_audit.ssh1 import SSH1
@ -52,14 +52,15 @@ class SSH_Socket(ReadBuf, WriteBuf):
SM_BANNER_SENT = 1 SM_BANNER_SENT = 1
def __init__(self, host: Optional[str], port: int, ipvo: Optional[Sequence[int]] = None, timeout: Union[int, float] = 5, timeout_set: bool = False) -> None: def __init__(self, outputbuffer: 'OutputBuffer', host: Optional[str], port: int, ip_version_preference: List[int] = [], timeout: Union[int, float] = 5, timeout_set: bool = False) -> None: # pylint: disable=dangerous-default-value
super(SSH_Socket, self).__init__() super(SSH_Socket, self).__init__()
self.__sock = None # type: Optional[socket.socket] self.__outputbuffer = outputbuffer
self.__sock_map = {} # type: Dict[int, socket.socket] self.__sock: Optional[socket.socket] = None
self.__sock_map: Dict[int, socket.socket] = {}
self.__block_size = 8 self.__block_size = 8
self.__state = 0 self.__state = 0
self.__header = [] # type: List[str] self.__header: List[str] = []
self.__banner = None # type: Optional[Banner] self.__banner: Optional[Banner] = None
if host is None: if host is None:
raise ValueError('undefined host') raise ValueError('undefined host')
nport = Utils.parse_int(port) nport = Utils.parse_int(port)
@ -67,35 +68,30 @@ class SSH_Socket(ReadBuf, WriteBuf):
raise ValueError('invalid port: {}'.format(port)) raise ValueError('invalid port: {}'.format(port))
self.__host = host self.__host = host
self.__port = nport self.__port = nport
if ipvo is not None: self.__ip_version_preference = ip_version_preference # Holds only 5 possible values: [] (no preference), [4] (use IPv4 only), [6] (use IPv6 only), [46] (use both IPv4 and IPv6, but prioritize v4), and [64] (use both IPv4 and IPv6, but prioritize v6).
self.__ipvo = ipvo
else:
self.__ipvo = ()
self.__timeout = timeout self.__timeout = timeout
self.__timeout_set = timeout_set self.__timeout_set = timeout_set
self.client_host = None # type: Optional[str] self.client_host: Optional[str] = None
self.client_port = None self.client_port = None
def _resolve(self, ipvo: Sequence[int]) -> Iterable[Tuple[int, Tuple[Any, ...]]]: def _resolve(self) -> Iterable[Tuple[int, Tuple[Any, ...]]]:
ipvo = tuple([x for x in Utils.unique_seq(ipvo) if x in (4, 6)]) # If __ip_version_preference has only one entry, then it means that ONLY that IP version should be used.
ipvo_len = len(ipvo) if len(self.__ip_version_preference) == 1:
prefer_ipvo = ipvo_len > 0 family = socket.AF_INET if self.__ip_version_preference[0] == 4 else socket.AF_INET6
prefer_ipv4 = prefer_ipvo and ipvo[0] == 4
if ipvo_len == 1:
family = socket.AF_INET if ipvo[0] == 4 else socket.AF_INET6
else: else:
family = socket.AF_UNSPEC family = socket.AF_UNSPEC
try: try:
stype = socket.SOCK_STREAM stype = socket.SOCK_STREAM
r = socket.getaddrinfo(self.__host, self.__port, family, stype) r = socket.getaddrinfo(self.__host, self.__port, family, stype)
if prefer_ipvo:
r = sorted(r, key=lambda x: x[0], reverse=not prefer_ipv4) # If the user has a preference for using IPv4 over IPv6 (or vice-versa), then sort the list returned by getaddrinfo() so that the preferred address type comes first.
check = any(stype == rline[2] for rline in r) if len(self.__ip_version_preference) == 2:
r = sorted(r, key=lambda x: x[0], reverse=(self.__ip_version_preference[0] == 6))
for af, socktype, _proto, _canonname, addr in r: for af, socktype, _proto, _canonname, addr in r:
if not check or socktype == socket.SOCK_STREAM: if socktype == socket.SOCK_STREAM:
yield af, addr yield af, addr
except socket.error as e: except socket.error as e:
Output().fail('[exception] {}'.format(e)) self.__outputbuffer.fail('[exception] {}'.format(e)).write()
sys.exit(exitcodes.CONNECTION_ERROR) sys.exit(exitcodes.CONNECTION_ERROR)
# Listens on a server socket and accepts one connection (used for # Listens on a server socket and accepts one connection (used for
@ -156,11 +152,12 @@ class SSH_Socket(ReadBuf, WriteBuf):
def connect(self) -> Optional[str]: def connect(self) -> Optional[str]:
'''Returns None on success, or an error string.''' '''Returns None on success, or an error string.'''
err = None err = None
for af, addr in self._resolve(self.__ipvo): for af, addr in self._resolve():
s = None s = None
try: try:
s = socket.socket(af, socket.SOCK_STREAM) s = socket.socket(af, socket.SOCK_STREAM)
s.settimeout(self.__timeout) s.settimeout(self.__timeout)
self.__outputbuffer.d(("Connecting to %s:%d..." % ('[%s]' % addr[0] if Utils.is_ipv6_address(addr[0]) else addr[0], addr[1])), write_now=True)
s.connect(addr) s.connect(addr)
self.__sock = s self.__sock = s
return None return None
@ -175,6 +172,8 @@ class SSH_Socket(ReadBuf, WriteBuf):
return '[exception] {}'.format(errm) return '[exception] {}'.format(errm)
def get_banner(self, sshv: int = 2) -> Tuple[Optional['Banner'], List[str], Optional[str]]: def get_banner(self, sshv: int = 2) -> Tuple[Optional['Banner'], List[str], Optional[str]]:
self.__outputbuffer.d('Getting banner...', write_now=True)
if self.__sock is None: if self.__sock is None:
return self.__banner, self.__header, 'not connected' return self.__banner, self.__header, 'not connected'
if self.__banner is not None: if self.__banner is not None:
@ -230,15 +229,11 @@ class SSH_Socket(ReadBuf, WriteBuf):
except socket.error as e: except socket.error as e:
return -1, str(e.args[-1]) return -1, str(e.args[-1])
def send_algorithms(self) -> None: # Send a KEXINIT with the lists of key exchanges, hostkeys, ciphers, MACs, compressions, and languages that we "support".
def send_kexinit(self, key_exchanges: List[str] = ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'ecdh-sha2-nistp256', 'ecdh-sha2-nistp384', 'ecdh-sha2-nistp521', 'diffie-hellman-group-exchange-sha256', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group14-sha256'], hostkeys: List[str] = ['rsa-sha2-512', 'rsa-sha2-256', 'ssh-rsa', 'ecdsa-sha2-nistp256', 'ssh-ed25519'], ciphers: List[str] = ['chacha20-poly1305@openssh.com', 'aes128-ctr', 'aes192-ctr', 'aes256-ctr', 'aes128-gcm@openssh.com', 'aes256-gcm@openssh.com'], macs: List[str] = ['umac-64-etm@openssh.com', 'umac-128-etm@openssh.com', 'hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'hmac-sha1-etm@openssh.com', 'umac-64@openssh.com', 'umac-128@openssh.com', 'hmac-sha2-256', 'hmac-sha2-512', 'hmac-sha1'], compressions: List[str] = ['none', 'zlib@openssh.com'], languages: List[str] = ['']) -> None: # pylint: disable=dangerous-default-value
'''Sends the list of supported host keys, key exchanges, ciphers, and MACs. Emulates OpenSSH v8.2.''' '''Sends the list of supported host keys, key exchanges, ciphers, and MACs. Emulates OpenSSH v8.2.'''
key_exchanges = ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'ecdh-sha2-nistp256', 'ecdh-sha2-nistp384', 'ecdh-sha2-nistp521', 'diffie-hellman-group-exchange-sha256', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group14-sha256'] self.__outputbuffer.d('KEX initialisation...', write_now=True)
hostkeys = ['rsa-sha2-512', 'rsa-sha2-256', 'ssh-rsa', 'ecdsa-sha2-nistp256', 'ssh-ed25519']
ciphers = ['chacha20-poly1305@openssh.com', 'aes128-ctr', 'aes192-ctr', 'aes256-ctr', 'aes128-gcm@openssh.com', 'aes256-gcm@openssh.com']
macs = ['umac-64-etm@openssh.com', 'umac-128-etm@openssh.com', 'hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'hmac-sha1-etm@openssh.com', 'umac-64@openssh.com', 'umac-128@openssh.com', 'hmac-sha2-256', 'hmac-sha2-512', 'hmac-sha1']
compressions = ['none', 'zlib@openssh.com']
languages = ['']
kexparty = SSH2_KexParty(ciphers, macs, compressions, languages) kexparty = SSH2_KexParty(ciphers, macs, compressions, languages)
kex = SSH2_Kex(os.urandom(16), key_exchanges, hostkeys, kexparty, kexparty, False, 0) kex = SSH2_Kex(os.urandom(16), key_exchanges, hostkeys, kexparty, kexparty, False, 0)
@ -279,7 +274,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:
Output().fail('[exception] invalid ssh packet (block size)') self.__outputbuffer.fail('[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:
@ -294,7 +289,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:
Output().fail('[exception] packet checksum CRC32 mismatch.') self.__outputbuffer.fail('[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)
@ -343,6 +338,6 @@ class SSH_Socket(ReadBuf, WriteBuf):
def __cleanup(self) -> None: def __cleanup(self) -> None:
self._close_socket(self.__sock) self._close_socket(self.__sock)
for fd in self.__sock_map: for sock in self.__sock_map.values():
self._close_socket(self.__sock_map[fd]) self._close_socket(sock)
self.__sock = None self.__sock = None

View File

@ -30,12 +30,12 @@ from ssh_audit.algorithm import Algorithm
class Timeframe: class Timeframe:
def __init__(self) -> None: def __init__(self) -> None:
self.__storage = {} # type: Dict[str, List[Optional[str]]] self.__storage: Dict[str, List[Optional[str]]] = {}
def __contains__(self, product: str) -> bool: def __contains__(self, product: str) -> bool:
return product in self.__storage return product in self.__storage
def __getitem__(self, product): # type: (str) -> Sequence[Optional[str]] def __getitem__(self, product: str) -> Sequence[Optional[str]]:
return tuple(self.__storage.get(product, [None] * 4)) return tuple(self.__storage.get(product, [None] * 4))
def __str__(self) -> str: def __str__(self) -> str:
@ -51,7 +51,7 @@ class Timeframe:
return self[product][1 if bool(for_server) else 3] return self[product][1 if bool(for_server) else 3]
def _update(self, versions: Optional[str], pos: int) -> None: def _update(self, versions: Optional[str], pos: int) -> None:
ssh_versions = {} # type: Dict[str, str] ssh_versions: Dict[str, str] = {}
for_srv, for_cli = pos < 2, pos > 1 for_srv, for_cli = pos < 2, pos > 1
for v in (versions or '').split(','): for v in (versions or '').split(','):
ssh_prod, ssh_ver, is_cli = Algorithm.get_ssh_version(v) ssh_prod, ssh_ver, is_cli = Algorithm.get_ssh_version(v)

View File

@ -96,7 +96,7 @@ class Utils:
@classmethod @classmethod
def unique_seq(cls, seq: Sequence[Any]) -> Sequence[Any]: def unique_seq(cls, seq: Sequence[Any]) -> Sequence[Any]:
seen = set() # type: Set[Any] seen: Set[Any] = set()
def _seen_add(x: Any) -> bool: def _seen_add(x: Any) -> bool:
seen.add(x) seen.add(x)
@ -129,10 +129,10 @@ class Utils:
return -1.0 return -1.0
@staticmethod @staticmethod
def parse_host_and_port(host_and_port: str) -> Tuple[str, int]: def parse_host_and_port(host_and_port: str, default_port: int = 0) -> Tuple[str, int]:
'''Parses a string into a tuple of its host and port. The port is 0 if not specified.''' '''Parses a string into a tuple of its host and port. The port is 0 if not specified.'''
host = host_and_port host = host_and_port
port = 0 port = default_port
mx = re.match(r'^\[([^\]]+)\](?::(\d+))?$', host_and_port) mx = re.match(r'^\[([^\]]+)\](?::(\d+))?$', host_and_port)
if mx is not None: if mx is not None:

View File

@ -33,7 +33,7 @@ class VersionVulnerabilityDB: # pylint: disable=too-few-public-methods
# Example: if it affects servers, both remote & local, then affected # Example: if it affects servers, both remote & local, then affected
# = 1. If it affects servers, but is a local issue only, # = 1. If it affects servers, but is a local issue only,
# then affected = 1 + 4 = 5. # then affected = 1 + 4 = 5.
CVE = { CVE: Dict[str, List[List[Any]]] = {
'Dropbear SSH': [ 'Dropbear SSH': [
['0.0', '2018.76', 1, 'CVE-2018-15599', 5.0, 'remote users may enumerate users on the system'], ['0.0', '2018.76', 1, 'CVE-2018-15599', 5.0, 'remote users may enumerate users on the system'],
['0.0', '2017.74', 5, 'CVE-2017-9079', 4.7, 'local users can read certain files as root'], ['0.0', '2017.74', 5, 'CVE-2017-9079', 4.7, 'local users can read certain files as root'],
@ -66,6 +66,7 @@ class VersionVulnerabilityDB: # pylint: disable=too-few-public-methods
['0.4.7', '0.5.2', 1, 'CVE-2012-4560', 7.5, 'cause DoS or execute arbitrary code (buffer overflow)'], ['0.4.7', '0.5.2', 1, 'CVE-2012-4560', 7.5, 'cause DoS or execute arbitrary code (buffer overflow)'],
['0.4.7', '0.5.2', 1, 'CVE-2012-4559', 6.8, 'cause DoS or execute arbitrary code (double free)']], ['0.4.7', '0.5.2', 1, 'CVE-2012-4559', 6.8, 'cause DoS or execute arbitrary code (double free)']],
'OpenSSH': [ 'OpenSSH': [
['1.0', '7.7', 1, 'CVE-2018-15473', 5.3, 'enumerate usernames due to timing discrepencies'],
['7.2', '7.2p2', 1, 'CVE-2016-6515', 7.8, 'cause DoS via long password string (crypt CPU consumption)'], ['7.2', '7.2p2', 1, 'CVE-2016-6515', 7.8, 'cause DoS via long password string (crypt CPU consumption)'],
['1.2.2', '7.2', 1, 'CVE-2016-3115', 5.5, 'bypass command restrictions via crafted X11 forwarding data'], ['1.2.2', '7.2', 1, 'CVE-2016-3115', 5.5, 'bypass command restrictions via crafted X11 forwarding data'],
['5.4', '7.1', 1, 'CVE-2016-1907', 5.0, 'cause DoS via crafted network traffic (out of bounds read)'], ['5.4', '7.1', 1, 'CVE-2016-1907', 5.0, 'cause DoS via crafted network traffic (out of bounds read)'],
@ -139,12 +140,12 @@ class VersionVulnerabilityDB: # pylint: disable=too-few-public-methods
['0.0', '0.66', 2, 'CVE-2016-2563', 7.5, 'buffer overflow in SCP command-line utility'], ['0.0', '0.66', 2, 'CVE-2016-2563', 7.5, 'buffer overflow in SCP command-line utility'],
['0.0', '0.65', 2, 'CVE-2015-5309', 4.3, 'integer overflow in terminal-handling code'], ['0.0', '0.65', 2, 'CVE-2015-5309', 4.3, 'integer overflow in terminal-handling code'],
] ]
} # type: Dict[str, List[List[Any]]] }
TXT = { TXT: Dict[str, List[List[Any]]] = {
'Dropbear SSH': [ 'Dropbear SSH': [
['0.28', '0.34', 1, 'remote root exploit', 'remote format string buffer overflow exploit (exploit-db#387)']], ['0.28', '0.34', 1, 'remote root exploit', 'remote format string buffer overflow exploit (exploit-db#387)']],
'libssh': [ 'libssh': [
['0.3.3', '0.3.3', 1, 'null pointer check', 'missing null pointer check in "crypt_set_algorithms_server"'], ['0.3.3', '0.3.3', 1, 'null pointer check', 'missing null pointer check in "crypt_set_algorithms_server"'],
['0.3.3', '0.3.3', 1, 'integer overflow', 'integer overflow in "buffer_get_data"'], ['0.3.3', '0.3.3', 1, 'integer overflow', 'integer overflow in "buffer_get_data"'],
['0.3.3', '0.3.3', 3, 'heap overflow', 'heap overflow in "packet_decrypt"']] ['0.3.3', '0.3.3', 3, 'heap overflow', 'heap overflow in "packet_decrypt"']]
} # type: Dict[str, List[List[Any]]] }

View File

@ -54,7 +54,7 @@ class WriteBuf:
return self.write(v) return self.write(v)
def write_list(self, v: List[str]) -> 'WriteBuf': def write_list(self, v: List[str]) -> 'WriteBuf':
return self.write_string(u','.join(v)) return self.write_string(','.join(v))
@classmethod @classmethod
def _bitlength(cls, n: int) -> int: def _bitlength(cls, n: int) -> int:

View File

@ -1,4 +1,4 @@
.TH SSH-AUDIT 1 "October 19, 2020" .TH SSH-AUDIT 1 "March 2, 2021"
.SH NAME .SH NAME
\fBssh-audit\fP \- SSH server & client configuration auditor \fBssh-audit\fP \- SSH server & client configuration auditor
.SH SYNOPSIS .SH SYNOPSIS
@ -46,10 +46,15 @@ Enables grepable output.
.br .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. 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 -d, \-\-debug
.br
Enable debug output.
.TP .TP
.B -j, \-\-json .B -j, \-\-json
.br .br
Output results in JSON format. Output results in JSON format. Specify twice (-jj) to enable indent printing (useful for debugging).
.TP .TP
.B -l, \-\-level=<info|warn|fail> .B -l, \-\-level=<info|warn|fail>
@ -66,6 +71,11 @@ List all official, built-in policies for common systems. Their full names can t
.br .br
Look up the security information of an algorithm(s) in the internal database. Does not connect to a server. Look up the security information of an algorithm(s) in the internal database. Does not connect to a server.
.TP
.B -m, \-\-manual
.br
Print the man page (Windows only).
.TP .TP
.B -M, \-\-make-policy=<custom_policy.txt> .B -M, \-\-make-policy=<custom_policy.txt>
.br .br
@ -94,7 +104,12 @@ The timeout, in seconds, for creating connections and reading data from the sock
.TP .TP
.B -T, \-\-targets=<hosts.txt> .B -T, \-\-targets=<hosts.txt>
.br .br
A file containing a list of target hosts. Each line must have one host, in the format of HOST[:PORT]. A file containing a list of target hosts. Each line must have one host, in the format of HOST[:PORT]. Use --threads to control concurrent scans.
.TP
.B \-\-threads=<threads>
.br
The number of threads to use when scanning multiple targets (with -T/--targets). Default is 32.
.TP .TP
.B -v, \-\-verbose .B -v, \-\-verbose

View File

@ -1 +1 @@
{"banner": {"comments": null, "protocol": [2, 0], "raw": "SSH-2.0-dropbear_2019.78", "software": "dropbear_2019.78"}, "compression": ["zlib@openssh.com", "none"], "enc": ["aes128-ctr", "aes256-ctr", "aes128-cbc", "aes256-cbc", "3des-ctr", "3des-cbc"], "fingerprints": [{"fp": "SHA256:CDfAU12pjQS7/91kg7gYacza0U/6PDbE04Ic3IpYxkM", "type": "ssh-rsa"}], "kex": [{"algorithm": "curve25519-sha256"}, {"algorithm": "curve25519-sha256@libssh.org"}, {"algorithm": "ecdh-sha2-nistp521"}, {"algorithm": "ecdh-sha2-nistp384"}, {"algorithm": "ecdh-sha2-nistp256"}, {"algorithm": "diffie-hellman-group14-sha256"}, {"algorithm": "diffie-hellman-group14-sha1"}, {"algorithm": "kexguess2@matt.ucc.asn.au"}], "key": [{"algorithm": "ecdsa-sha2-nistp256"}, {"algorithm": "ssh-rsa", "keysize": 1024}, {"algorithm": "ssh-dss"}], "mac": ["hmac-sha1-96", "hmac-sha1", "hmac-sha2-256"]} {"banner": {"comments": null, "protocol": [2, 0], "raw": "SSH-2.0-dropbear_2019.78", "software": "dropbear_2019.78"}, "compression": ["zlib@openssh.com", "none"], "enc": ["aes128-ctr", "aes256-ctr", "aes128-cbc", "aes256-cbc", "3des-ctr", "3des-cbc"], "fingerprints": [{"hash": "CDfAU12pjQS7/91kg7gYacza0U/6PDbE04Ic3IpYxkM", "hash_alg": "SHA256", "hostkey": "ssh-rsa"}, {"hash": "63:7f:54:f7:0a:28:7f:75:0b:f4:07:0b:fc:66:51:a2", "hash_alg": "MD5", "hostkey": "ssh-rsa"}], "kex": [{"algorithm": "curve25519-sha256"}, {"algorithm": "curve25519-sha256@libssh.org"}, {"algorithm": "ecdh-sha2-nistp521"}, {"algorithm": "ecdh-sha2-nistp384"}, {"algorithm": "ecdh-sha2-nistp256"}, {"algorithm": "diffie-hellman-group14-sha256"}, {"algorithm": "diffie-hellman-group14-sha1"}, {"algorithm": "kexguess2@matt.ucc.asn.au"}], "key": [{"algorithm": "ecdsa-sha2-nistp256"}, {"algorithm": "ssh-rsa", "keysize": 1024}, {"algorithm": "ssh-dss"}], "mac": ["hmac-sha1-96", "hmac-sha1", "hmac-sha2-256"], "target": "localhost"}

View File

@ -23,8 +23,9 @@
 `- [warn] using weak random number generator could reveal the key  `- [warn] using weak random number generator could reveal the key
`- [info] available since OpenSSH 5.7, Dropbear SSH 2013.62 `- [info] available since OpenSSH 5.7, Dropbear SSH 2013.62
(key) ssh-rsa (1024-bit) -- [fail] using weak hashing algorithm (key) ssh-rsa (1024-bit) -- [fail] using weak hashing algorithm
 `- [warn] using small 1024-bit modulus  `- [fail] using small 1024-bit modulus
`- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28 `- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28
`- [info] a future deprecation notice has been issued in OpenSSH 8.2: https://www.openssh.com/txt/release-8.2
(key) ssh-dss -- [fail] using small 1024-bit modulus (key) ssh-dss -- [fail] using small 1024-bit modulus
 `- [fail] removed (in server) and disabled (in client) since OpenSSH 7.0, weak algorithm  `- [fail] removed (in server) and disabled (in client) since OpenSSH 7.0, weak algorithm
 `- [warn] using weak random number generator could reveal the key  `- [warn] using weak random number generator could reveal the key

View File

@ -1 +1 @@
{"banner": {"comments": null, "protocol": [1, 99], "raw": "SSH-1.99-OpenSSH_4.0", "software": "OpenSSH_4.0"}, "compression": ["none", "zlib"], "enc": ["aes128-cbc", "3des-cbc", "blowfish-cbc", "cast128-cbc", "arcfour", "aes192-cbc", "aes256-cbc", "rijndael-cbc@lysator.liu.se", "aes128-ctr", "aes192-ctr", "aes256-ctr"], "fingerprints": [{"fp": "SHA256:YZ457EBcJTSxRKI3yXRgtAj3PBf5B9/F36b1SVooml4", "type": "ssh-rsa"}], "kex": [{"algorithm": "diffie-hellman-group-exchange-sha1", "keysize": 1024}, {"algorithm": "diffie-hellman-group14-sha1"}, {"algorithm": "diffie-hellman-group1-sha1"}], "key": [{"algorithm": "ssh-rsa", "keysize": 1024}, {"algorithm": "ssh-dss"}], "mac": ["hmac-md5", "hmac-sha1", "hmac-ripemd160", "hmac-ripemd160@openssh.com", "hmac-sha1-96", "hmac-md5-96"]} {"banner": {"comments": null, "protocol": [1, 99], "raw": "SSH-1.99-OpenSSH_4.0", "software": "OpenSSH_4.0"}, "compression": ["none", "zlib"], "enc": ["aes128-cbc", "3des-cbc", "blowfish-cbc", "cast128-cbc", "arcfour", "aes192-cbc", "aes256-cbc", "rijndael-cbc@lysator.liu.se", "aes128-ctr", "aes192-ctr", "aes256-ctr"], "fingerprints": [{"hash": "YZ457EBcJTSxRKI3yXRgtAj3PBf5B9/F36b1SVooml4", "hash_alg": "SHA256", "hostkey": "ssh-rsa"}, {"hash": "3c:c3:38:f8:55:39:c0:4a:5a:17:89:60:2c:a1:fc:6a", "hash_alg": "MD5", "hostkey": "ssh-rsa"}], "kex": [{"algorithm": "diffie-hellman-group-exchange-sha1", "keysize": 1024}, {"algorithm": "diffie-hellman-group14-sha1"}, {"algorithm": "diffie-hellman-group1-sha1"}], "key": [{"algorithm": "ssh-rsa", "keysize": 1024}, {"algorithm": "ssh-dss"}], "mac": ["hmac-md5", "hmac-sha1", "hmac-ripemd160", "hmac-ripemd160@openssh.com", "hmac-sha1-96", "hmac-md5-96"], "target": "localhost"}

View File

@ -1,11 +1,12 @@
# general # general
(gen) banner: SSH-1.99-OpenSSH_4.0 (gen) banner: SSH-1.99-OpenSSH_4.0
(gen) protocol SSH1 enabled (gen) protocol SSH1 enabled
(gen) software: OpenSSH 4.0 (gen) software: OpenSSH 4.0
(gen) compatibility: OpenSSH 3.9-6.6, Dropbear SSH 0.53+ (some functionality from 0.52) (gen) compatibility: OpenSSH 3.9-6.6, Dropbear SSH 0.53+ (some functionality from 0.52)
(gen) compression: enabled (zlib) (gen) compression: enabled (zlib)
# security # security
(cve) CVE-2018-15473 -- (CVSSv2: 5.3) enumerate usernames due to timing discrepencies
(cve) CVE-2016-3115 -- (CVSSv2: 5.5) bypass command restrictions via crafted X11 forwarding data (cve) CVE-2016-3115 -- (CVSSv2: 5.5) bypass command restrictions via crafted X11 forwarding data
(cve) CVE-2014-1692 -- (CVSSv2: 7.5) cause DoS via triggering error condition (memory corruption) (cve) CVE-2014-1692 -- (CVSSv2: 7.5) cause DoS via triggering error condition (memory corruption)
(cve) CVE-2012-0814 -- (CVSSv2: 3.5) leak data via debug messages (cve) CVE-2012-0814 -- (CVSSv2: 3.5) leak data via debug messages
@ -24,6 +25,7 @@
(cve) CVE-2006-4924 -- (CVSSv2: 7.8) cause DoS via crafted packet (CPU consumption) (cve) CVE-2006-4924 -- (CVSSv2: 7.8) cause DoS via crafted packet (CPU consumption)
(cve) CVE-2006-0225 -- (CVSSv2: 4.6) execute arbitrary code (cve) CVE-2006-0225 -- (CVSSv2: 4.6) execute arbitrary code
(cve) CVE-2005-2798 -- (CVSSv2: 5.0) leak data about authentication credentials (cve) CVE-2005-2798 -- (CVSSv2: 5.0) leak data about authentication credentials
(sec) SSH v1 enabled -- SSH v1 can be exploited to recover plaintext passwords
# key exchange algorithms # key exchange algorithms
(kex) diffie-hellman-group-exchange-sha1 (1024-bit) -- [fail] using small 1024-bit modulus (kex) diffie-hellman-group-exchange-sha1 (1024-bit) -- [fail] using small 1024-bit modulus
@ -39,8 +41,9 @@
# host-key algorithms # host-key algorithms
(key) ssh-rsa (1024-bit) -- [fail] using weak hashing algorithm (key) ssh-rsa (1024-bit) -- [fail] using weak hashing algorithm
 `- [warn] using small 1024-bit modulus  `- [fail] using small 1024-bit modulus
`- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28 `- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28
`- [info] a future deprecation notice has been issued in OpenSSH 8.2: https://www.openssh.com/txt/release-8.2
(key) ssh-dss -- [fail] using small 1024-bit modulus (key) ssh-dss -- [fail] using small 1024-bit modulus
 `- [fail] removed (in server) and disabled (in client) since OpenSSH 7.0, weak algorithm  `- [fail] removed (in server) and disabled (in client) since OpenSSH 7.0, weak algorithm
 `- [warn] using weak random number generator could reveal the key  `- [warn] using weak random number generator could reveal the key

View File

@ -1 +1 @@
{"banner": {"comments": null, "protocol": [2, 0], "raw": "SSH-2.0-OpenSSH_5.6", "software": "OpenSSH_5.6"}, "compression": ["none", "zlib@openssh.com"], "enc": ["aes128-ctr", "aes192-ctr", "aes256-ctr", "arcfour256", "arcfour128", "aes128-cbc", "3des-cbc", "blowfish-cbc", "cast128-cbc", "aes192-cbc", "aes256-cbc", "arcfour", "rijndael-cbc@lysator.liu.se"], "fingerprints": [{"fp": "SHA256:YZ457EBcJTSxRKI3yXRgtAj3PBf5B9/F36b1SVooml4", "type": "ssh-rsa"}], "kex": [{"algorithm": "diffie-hellman-group-exchange-sha256", "keysize": 1024}, {"algorithm": "diffie-hellman-group-exchange-sha1", "keysize": 1024}, {"algorithm": "diffie-hellman-group14-sha1"}, {"algorithm": "diffie-hellman-group1-sha1"}], "key": [{"algorithm": "ssh-rsa", "keysize": 1024}, {"algorithm": "ssh-dss"}], "mac": ["hmac-md5", "hmac-sha1", "umac-64@openssh.com", "hmac-ripemd160", "hmac-ripemd160@openssh.com", "hmac-sha1-96", "hmac-md5-96"]} {"banner": {"comments": null, "protocol": [2, 0], "raw": "SSH-2.0-OpenSSH_5.6", "software": "OpenSSH_5.6"}, "compression": ["none", "zlib@openssh.com"], "enc": ["aes128-ctr", "aes192-ctr", "aes256-ctr", "arcfour256", "arcfour128", "aes128-cbc", "3des-cbc", "blowfish-cbc", "cast128-cbc", "aes192-cbc", "aes256-cbc", "arcfour", "rijndael-cbc@lysator.liu.se"], "fingerprints": [{"hash": "YZ457EBcJTSxRKI3yXRgtAj3PBf5B9/F36b1SVooml4", "hash_alg": "SHA256", "hostkey": "ssh-rsa"}, {"hash": "3c:c3:38:f8:55:39:c0:4a:5a:17:89:60:2c:a1:fc:6a", "hash_alg": "MD5", "hostkey": "ssh-rsa"}], "kex": [{"algorithm": "diffie-hellman-group-exchange-sha256", "keysize": 1024}, {"algorithm": "diffie-hellman-group-exchange-sha1", "keysize": 1024}, {"algorithm": "diffie-hellman-group14-sha1"}, {"algorithm": "diffie-hellman-group1-sha1"}], "key": [{"algorithm": "ssh-rsa", "keysize": 1024}, {"algorithm": "ssh-dss"}], "mac": ["hmac-md5", "hmac-sha1", "umac-64@openssh.com", "hmac-ripemd160", "hmac-ripemd160@openssh.com", "hmac-sha1-96", "hmac-md5-96"], "target": "localhost"}

View File

@ -5,6 +5,7 @@
(gen) compression: enabled (zlib@openssh.com) (gen) compression: enabled (zlib@openssh.com)
# security # security
(cve) CVE-2018-15473 -- (CVSSv2: 5.3) enumerate usernames due to timing discrepencies
(cve) CVE-2016-3115 -- (CVSSv2: 5.5) bypass command restrictions via crafted X11 forwarding data (cve) CVE-2016-3115 -- (CVSSv2: 5.5) bypass command restrictions via crafted X11 forwarding data
(cve) CVE-2016-1907 -- (CVSSv2: 5.0) cause DoS via crafted network traffic (out of bounds read) (cve) CVE-2016-1907 -- (CVSSv2: 5.0) cause DoS via crafted network traffic (out of bounds read)
(cve) CVE-2015-6564 -- (CVSSv2: 6.9) privilege escalation via leveraging sshd uid (cve) CVE-2015-6564 -- (CVSSv2: 6.9) privilege escalation via leveraging sshd uid
@ -33,8 +34,9 @@
# host-key algorithms # host-key algorithms
(key) ssh-rsa (1024-bit) -- [fail] using weak hashing algorithm (key) ssh-rsa (1024-bit) -- [fail] using weak hashing algorithm
 `- [warn] using small 1024-bit modulus  `- [fail] using small 1024-bit modulus
`- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28 `- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28
`- [info] a future deprecation notice has been issued in OpenSSH 8.2: https://www.openssh.com/txt/release-8.2
(key) ssh-dss -- [fail] using small 1024-bit modulus (key) ssh-dss -- [fail] using small 1024-bit modulus
 `- [fail] removed (in server) and disabled (in client) since OpenSSH 7.0, weak algorithm  `- [fail] removed (in server) and disabled (in client) since OpenSSH 7.0, weak algorithm
 `- [warn] using weak random number generator could reveal the key  `- [warn] using weak random number generator could reveal the key

View File

@ -1 +1 @@
{"banner": {"comments": null, "protocol": [2, 0], "raw": "SSH-2.0-OpenSSH_5.6", "software": "OpenSSH_5.6"}, "compression": ["none", "zlib@openssh.com"], "enc": ["aes128-ctr", "aes192-ctr", "aes256-ctr", "arcfour256", "arcfour128", "aes128-cbc", "3des-cbc", "blowfish-cbc", "cast128-cbc", "aes192-cbc", "aes256-cbc", "arcfour", "rijndael-cbc@lysator.liu.se"], "fingerprints": [{"fp": "SHA256:YZ457EBcJTSxRKI3yXRgtAj3PBf5B9/F36b1SVooml4", "type": "ssh-rsa"}], "kex": [{"algorithm": "diffie-hellman-group-exchange-sha256", "keysize": 1024}, {"algorithm": "diffie-hellman-group-exchange-sha1", "keysize": 1024}, {"algorithm": "diffie-hellman-group14-sha1"}, {"algorithm": "diffie-hellman-group1-sha1"}], "key": [{"algorithm": "ssh-rsa", "keysize": 1024}, {"algorithm": "ssh-rsa-cert-v01@openssh.com", "casize": 1024, "keysize": 1024}], "mac": ["hmac-md5", "hmac-sha1", "umac-64@openssh.com", "hmac-ripemd160", "hmac-ripemd160@openssh.com", "hmac-sha1-96", "hmac-md5-96"]} {"banner": {"comments": null, "protocol": [2, 0], "raw": "SSH-2.0-OpenSSH_5.6", "software": "OpenSSH_5.6"}, "compression": ["none", "zlib@openssh.com"], "enc": ["aes128-ctr", "aes192-ctr", "aes256-ctr", "arcfour256", "arcfour128", "aes128-cbc", "3des-cbc", "blowfish-cbc", "cast128-cbc", "aes192-cbc", "aes256-cbc", "arcfour", "rijndael-cbc@lysator.liu.se"], "fingerprints": [{"hash": "YZ457EBcJTSxRKI3yXRgtAj3PBf5B9/F36b1SVooml4", "hash_alg": "SHA256", "hostkey": "ssh-rsa"}, {"hash": "3c:c3:38:f8:55:39:c0:4a:5a:17:89:60:2c:a1:fc:6a", "hash_alg": "MD5", "hostkey": "ssh-rsa"}], "kex": [{"algorithm": "diffie-hellman-group-exchange-sha256", "keysize": 1024}, {"algorithm": "diffie-hellman-group-exchange-sha1", "keysize": 1024}, {"algorithm": "diffie-hellman-group14-sha1"}, {"algorithm": "diffie-hellman-group1-sha1"}], "key": [{"algorithm": "ssh-rsa", "keysize": 1024}, {"algorithm": "ssh-rsa-cert-v01@openssh.com", "casize": 1024, "keysize": 1024}], "mac": ["hmac-md5", "hmac-sha1", "umac-64@openssh.com", "hmac-ripemd160", "hmac-ripemd160@openssh.com", "hmac-sha1-96", "hmac-md5-96"], "target": "localhost"}

View File

@ -5,6 +5,7 @@
(gen) compression: enabled (zlib@openssh.com) (gen) compression: enabled (zlib@openssh.com)
# security # security
(cve) CVE-2018-15473 -- (CVSSv2: 5.3) enumerate usernames due to timing discrepencies
(cve) CVE-2016-3115 -- (CVSSv2: 5.5) bypass command restrictions via crafted X11 forwarding data (cve) CVE-2016-3115 -- (CVSSv2: 5.5) bypass command restrictions via crafted X11 forwarding data
(cve) CVE-2016-1907 -- (CVSSv2: 5.0) cause DoS via crafted network traffic (out of bounds read) (cve) CVE-2016-1907 -- (CVSSv2: 5.0) cause DoS via crafted network traffic (out of bounds read)
(cve) CVE-2015-6564 -- (CVSSv2: 6.9) privilege escalation via leveraging sshd uid (cve) CVE-2015-6564 -- (CVSSv2: 6.9) privilege escalation via leveraging sshd uid
@ -33,11 +34,13 @@
# host-key algorithms # host-key algorithms
(key) ssh-rsa (1024-bit) -- [fail] using weak hashing algorithm (key) ssh-rsa (1024-bit) -- [fail] using weak hashing algorithm
 `- [warn] using small 1024-bit modulus  `- [fail] using small 1024-bit modulus
`- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28 `- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28
`- [info] a future deprecation notice has been issued in OpenSSH 8.2: https://www.openssh.com/txt/release-8.2
(key) ssh-rsa-cert-v01@openssh.com (1024-bit cert/1024-bit CA) -- [fail] using weak hashing algorithm (key) ssh-rsa-cert-v01@openssh.com (1024-bit cert/1024-bit CA) -- [fail] using weak hashing algorithm
 `- [warn] using small 1024-bit modulus  `- [fail] using small 1024-bit modulus
`- [info] available since OpenSSH 5.6 `- [info] available since OpenSSH 5.6
`- [info] a future deprecation notice has been issued in OpenSSH 8.2: https://www.openssh.com/txt/release-8.2
# encryption algorithms (ciphers) # encryption algorithms (ciphers)
(enc) aes128-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 (enc) aes128-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52

View File

@ -1 +1 @@
{"banner": {"comments": null, "protocol": [2, 0], "raw": "SSH-2.0-OpenSSH_5.6", "software": "OpenSSH_5.6"}, "compression": ["none", "zlib@openssh.com"], "enc": ["aes128-ctr", "aes192-ctr", "aes256-ctr", "arcfour256", "arcfour128", "aes128-cbc", "3des-cbc", "blowfish-cbc", "cast128-cbc", "aes192-cbc", "aes256-cbc", "arcfour", "rijndael-cbc@lysator.liu.se"], "fingerprints": [{"fp": "SHA256:YZ457EBcJTSxRKI3yXRgtAj3PBf5B9/F36b1SVooml4", "type": "ssh-rsa"}], "kex": [{"algorithm": "diffie-hellman-group-exchange-sha256", "keysize": 1024}, {"algorithm": "diffie-hellman-group-exchange-sha1", "keysize": 1024}, {"algorithm": "diffie-hellman-group14-sha1"}, {"algorithm": "diffie-hellman-group1-sha1"}], "key": [{"algorithm": "ssh-rsa", "keysize": 1024}, {"algorithm": "ssh-rsa-cert-v01@openssh.com", "casize": 3072, "keysize": 1024}], "mac": ["hmac-md5", "hmac-sha1", "umac-64@openssh.com", "hmac-ripemd160", "hmac-ripemd160@openssh.com", "hmac-sha1-96", "hmac-md5-96"]} {"banner": {"comments": null, "protocol": [2, 0], "raw": "SSH-2.0-OpenSSH_5.6", "software": "OpenSSH_5.6"}, "compression": ["none", "zlib@openssh.com"], "enc": ["aes128-ctr", "aes192-ctr", "aes256-ctr", "arcfour256", "arcfour128", "aes128-cbc", "3des-cbc", "blowfish-cbc", "cast128-cbc", "aes192-cbc", "aes256-cbc", "arcfour", "rijndael-cbc@lysator.liu.se"], "fingerprints": [{"hash": "YZ457EBcJTSxRKI3yXRgtAj3PBf5B9/F36b1SVooml4", "hash_alg": "SHA256", "hostkey": "ssh-rsa"}, {"hash": "3c:c3:38:f8:55:39:c0:4a:5a:17:89:60:2c:a1:fc:6a", "hash_alg": "MD5", "hostkey": "ssh-rsa"}], "kex": [{"algorithm": "diffie-hellman-group-exchange-sha256", "keysize": 1024}, {"algorithm": "diffie-hellman-group-exchange-sha1", "keysize": 1024}, {"algorithm": "diffie-hellman-group14-sha1"}, {"algorithm": "diffie-hellman-group1-sha1"}], "key": [{"algorithm": "ssh-rsa", "keysize": 1024}, {"algorithm": "ssh-rsa-cert-v01@openssh.com", "casize": 3072, "keysize": 1024}], "mac": ["hmac-md5", "hmac-sha1", "umac-64@openssh.com", "hmac-ripemd160", "hmac-ripemd160@openssh.com", "hmac-sha1-96", "hmac-md5-96"], "target": "localhost"}

View File

@ -5,6 +5,7 @@
(gen) compression: enabled (zlib@openssh.com) (gen) compression: enabled (zlib@openssh.com)
# security # security
(cve) CVE-2018-15473 -- (CVSSv2: 5.3) enumerate usernames due to timing discrepencies
(cve) CVE-2016-3115 -- (CVSSv2: 5.5) bypass command restrictions via crafted X11 forwarding data (cve) CVE-2016-3115 -- (CVSSv2: 5.5) bypass command restrictions via crafted X11 forwarding data
(cve) CVE-2016-1907 -- (CVSSv2: 5.0) cause DoS via crafted network traffic (out of bounds read) (cve) CVE-2016-1907 -- (CVSSv2: 5.0) cause DoS via crafted network traffic (out of bounds read)
(cve) CVE-2015-6564 -- (CVSSv2: 6.9) privilege escalation via leveraging sshd uid (cve) CVE-2015-6564 -- (CVSSv2: 6.9) privilege escalation via leveraging sshd uid
@ -33,11 +34,13 @@
# host-key algorithms # host-key algorithms
(key) ssh-rsa (1024-bit) -- [fail] using weak hashing algorithm (key) ssh-rsa (1024-bit) -- [fail] using weak hashing algorithm
 `- [warn] using small 1024-bit modulus  `- [fail] using small 1024-bit modulus
`- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28 `- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28
`- [info] a future deprecation notice has been issued in OpenSSH 8.2: https://www.openssh.com/txt/release-8.2
(key) ssh-rsa-cert-v01@openssh.com (1024-bit cert/3072-bit CA) -- [fail] using weak hashing algorithm (key) ssh-rsa-cert-v01@openssh.com (1024-bit cert/3072-bit CA) -- [fail] using weak hashing algorithm
 `- [warn] using small 1024-bit modulus  `- [fail] using small 1024-bit modulus
`- [info] available since OpenSSH 5.6 `- [info] available since OpenSSH 5.6
`- [info] a future deprecation notice has been issued in OpenSSH 8.2: https://www.openssh.com/txt/release-8.2
# encryption algorithms (ciphers) # encryption algorithms (ciphers)
(enc) aes128-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 (enc) aes128-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52

View File

@ -1 +1 @@
{"banner": {"comments": null, "protocol": [2, 0], "raw": "SSH-2.0-OpenSSH_5.6", "software": "OpenSSH_5.6"}, "compression": ["none", "zlib@openssh.com"], "enc": ["aes128-ctr", "aes192-ctr", "aes256-ctr", "arcfour256", "arcfour128", "aes128-cbc", "3des-cbc", "blowfish-cbc", "cast128-cbc", "aes192-cbc", "aes256-cbc", "arcfour", "rijndael-cbc@lysator.liu.se"], "fingerprints": [{"fp": "SHA256:nsWtdJ9Z67Vrf7OsUzQov7esXhsWAfVppArGh25u244", "type": "ssh-rsa"}], "kex": [{"algorithm": "diffie-hellman-group-exchange-sha256", "keysize": 1024}, {"algorithm": "diffie-hellman-group-exchange-sha1", "keysize": 1024}, {"algorithm": "diffie-hellman-group14-sha1"}, {"algorithm": "diffie-hellman-group1-sha1"}], "key": [{"algorithm": "ssh-rsa", "keysize": 3072}, {"algorithm": "ssh-rsa-cert-v01@openssh.com", "casize": 1024, "keysize": 3072}], "mac": ["hmac-md5", "hmac-sha1", "umac-64@openssh.com", "hmac-ripemd160", "hmac-ripemd160@openssh.com", "hmac-sha1-96", "hmac-md5-96"]} {"banner": {"comments": null, "protocol": [2, 0], "raw": "SSH-2.0-OpenSSH_5.6", "software": "OpenSSH_5.6"}, "compression": ["none", "zlib@openssh.com"], "enc": ["aes128-ctr", "aes192-ctr", "aes256-ctr", "arcfour256", "arcfour128", "aes128-cbc", "3des-cbc", "blowfish-cbc", "cast128-cbc", "aes192-cbc", "aes256-cbc", "arcfour", "rijndael-cbc@lysator.liu.se"], "fingerprints": [{"hash": "nsWtdJ9Z67Vrf7OsUzQov7esXhsWAfVppArGh25u244", "hash_alg": "SHA256", "hostkey": "ssh-rsa"}, {"hash": "18:e2:51:fe:21:6c:78:d0:b8:cf:32:d4:bd:56:42:e1", "hash_alg": "MD5", "hostkey": "ssh-rsa"}], "kex": [{"algorithm": "diffie-hellman-group-exchange-sha256", "keysize": 1024}, {"algorithm": "diffie-hellman-group-exchange-sha1", "keysize": 1024}, {"algorithm": "diffie-hellman-group14-sha1"}, {"algorithm": "diffie-hellman-group1-sha1"}], "key": [{"algorithm": "ssh-rsa", "keysize": 3072}, {"algorithm": "ssh-rsa-cert-v01@openssh.com", "casize": 1024, "keysize": 3072}], "mac": ["hmac-md5", "hmac-sha1", "umac-64@openssh.com", "hmac-ripemd160", "hmac-ripemd160@openssh.com", "hmac-sha1-96", "hmac-md5-96"], "target": "localhost"}

View File

@ -5,6 +5,7 @@
(gen) compression: enabled (zlib@openssh.com) (gen) compression: enabled (zlib@openssh.com)
# security # security
(cve) CVE-2018-15473 -- (CVSSv2: 5.3) enumerate usernames due to timing discrepencies
(cve) CVE-2016-3115 -- (CVSSv2: 5.5) bypass command restrictions via crafted X11 forwarding data (cve) CVE-2016-3115 -- (CVSSv2: 5.5) bypass command restrictions via crafted X11 forwarding data
(cve) CVE-2016-1907 -- (CVSSv2: 5.0) cause DoS via crafted network traffic (out of bounds read) (cve) CVE-2016-1907 -- (CVSSv2: 5.0) cause DoS via crafted network traffic (out of bounds read)
(cve) CVE-2015-6564 -- (CVSSv2: 6.9) privilege escalation via leveraging sshd uid (cve) CVE-2015-6564 -- (CVSSv2: 6.9) privilege escalation via leveraging sshd uid
@ -34,9 +35,11 @@
# host-key algorithms # host-key algorithms
(key) ssh-rsa (3072-bit) -- [fail] using weak hashing algorithm (key) ssh-rsa (3072-bit) -- [fail] using weak hashing algorithm
`- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28 `- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28
`- [info] a future deprecation notice has been issued in OpenSSH 8.2: https://www.openssh.com/txt/release-8.2
(key) ssh-rsa-cert-v01@openssh.com (3072-bit cert/1024-bit CA) -- [fail] using weak hashing algorithm (key) ssh-rsa-cert-v01@openssh.com (3072-bit cert/1024-bit CA) -- [fail] using weak hashing algorithm
 `- [warn] using small 1024-bit modulus  `- [fail] using small 1024-bit modulus
`- [info] available since OpenSSH 5.6 `- [info] available since OpenSSH 5.6
`- [info] a future deprecation notice has been issued in OpenSSH 8.2: https://www.openssh.com/txt/release-8.2
# encryption algorithms (ciphers) # encryption algorithms (ciphers)
(enc) aes128-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 (enc) aes128-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52

View File

@ -1 +1 @@
{"banner": {"comments": null, "protocol": [2, 0], "raw": "SSH-2.0-OpenSSH_5.6", "software": "OpenSSH_5.6"}, "compression": ["none", "zlib@openssh.com"], "enc": ["aes128-ctr", "aes192-ctr", "aes256-ctr", "arcfour256", "arcfour128", "aes128-cbc", "3des-cbc", "blowfish-cbc", "cast128-cbc", "aes192-cbc", "aes256-cbc", "arcfour", "rijndael-cbc@lysator.liu.se"], "fingerprints": [{"fp": "SHA256:nsWtdJ9Z67Vrf7OsUzQov7esXhsWAfVppArGh25u244", "type": "ssh-rsa"}], "kex": [{"algorithm": "diffie-hellman-group-exchange-sha256", "keysize": 1024}, {"algorithm": "diffie-hellman-group-exchange-sha1", "keysize": 1024}, {"algorithm": "diffie-hellman-group14-sha1"}, {"algorithm": "diffie-hellman-group1-sha1"}], "key": [{"algorithm": "ssh-rsa", "keysize": 3072}, {"algorithm": "ssh-rsa-cert-v01@openssh.com", "casize": 3072, "keysize": 3072}], "mac": ["hmac-md5", "hmac-sha1", "umac-64@openssh.com", "hmac-ripemd160", "hmac-ripemd160@openssh.com", "hmac-sha1-96", "hmac-md5-96"]} {"banner": {"comments": null, "protocol": [2, 0], "raw": "SSH-2.0-OpenSSH_5.6", "software": "OpenSSH_5.6"}, "compression": ["none", "zlib@openssh.com"], "enc": ["aes128-ctr", "aes192-ctr", "aes256-ctr", "arcfour256", "arcfour128", "aes128-cbc", "3des-cbc", "blowfish-cbc", "cast128-cbc", "aes192-cbc", "aes256-cbc", "arcfour", "rijndael-cbc@lysator.liu.se"], "fingerprints": [{"hash": "nsWtdJ9Z67Vrf7OsUzQov7esXhsWAfVppArGh25u244", "hash_alg": "SHA256", "hostkey": "ssh-rsa"}, {"hash": "18:e2:51:fe:21:6c:78:d0:b8:cf:32:d4:bd:56:42:e1", "hash_alg": "MD5", "hostkey": "ssh-rsa"}], "kex": [{"algorithm": "diffie-hellman-group-exchange-sha256", "keysize": 1024}, {"algorithm": "diffie-hellman-group-exchange-sha1", "keysize": 1024}, {"algorithm": "diffie-hellman-group14-sha1"}, {"algorithm": "diffie-hellman-group1-sha1"}], "key": [{"algorithm": "ssh-rsa", "keysize": 3072}, {"algorithm": "ssh-rsa-cert-v01@openssh.com", "casize": 3072, "keysize": 3072}], "mac": ["hmac-md5", "hmac-sha1", "umac-64@openssh.com", "hmac-ripemd160", "hmac-ripemd160@openssh.com", "hmac-sha1-96", "hmac-md5-96"], "target": "localhost"}

View File

@ -5,6 +5,7 @@
(gen) compression: enabled (zlib@openssh.com) (gen) compression: enabled (zlib@openssh.com)
# security # security
(cve) CVE-2018-15473 -- (CVSSv2: 5.3) enumerate usernames due to timing discrepencies
(cve) CVE-2016-3115 -- (CVSSv2: 5.5) bypass command restrictions via crafted X11 forwarding data (cve) CVE-2016-3115 -- (CVSSv2: 5.5) bypass command restrictions via crafted X11 forwarding data
(cve) CVE-2016-1907 -- (CVSSv2: 5.0) cause DoS via crafted network traffic (out of bounds read) (cve) CVE-2016-1907 -- (CVSSv2: 5.0) cause DoS via crafted network traffic (out of bounds read)
(cve) CVE-2015-6564 -- (CVSSv2: 6.9) privilege escalation via leveraging sshd uid (cve) CVE-2015-6564 -- (CVSSv2: 6.9) privilege escalation via leveraging sshd uid
@ -34,8 +35,10 @@
# host-key algorithms # host-key algorithms
(key) ssh-rsa (3072-bit) -- [fail] using weak hashing algorithm (key) ssh-rsa (3072-bit) -- [fail] using weak hashing algorithm
`- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28 `- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28
`- [info] a future deprecation notice has been issued in OpenSSH 8.2: https://www.openssh.com/txt/release-8.2
(key) ssh-rsa-cert-v01@openssh.com (3072-bit cert/3072-bit CA) -- [fail] using weak hashing algorithm (key) ssh-rsa-cert-v01@openssh.com (3072-bit cert/3072-bit CA) -- [fail] using weak hashing algorithm
`- [info] available since OpenSSH 5.6 `- [info] available since OpenSSH 5.6
`- [info] a future deprecation notice has been issued in OpenSSH 8.2: https://www.openssh.com/txt/release-8.2
# encryption algorithms (ciphers) # encryption algorithms (ciphers)
(enc) aes128-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 (enc) aes128-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52

View File

@ -1 +1 @@
{"banner": {"comments": null, "protocol": [2, 0], "raw": "SSH-2.0-OpenSSH_8.0", "software": "OpenSSH_8.0"}, "compression": ["none", "zlib@openssh.com"], "enc": ["chacha20-poly1305@openssh.com", "aes128-ctr", "aes192-ctr", "aes256-ctr", "aes128-gcm@openssh.com", "aes256-gcm@openssh.com"], "fingerprints": [{"fp": "SHA256:UrnXIVH+7dlw8UqYocl48yUEcKrthGDQG2CPCgp7MxU", "type": "ssh-ed25519"}, {"fp": "SHA256:nsWtdJ9Z67Vrf7OsUzQov7esXhsWAfVppArGh25u244", "type": "ssh-rsa"}], "kex": [{"algorithm": "curve25519-sha256"}, {"algorithm": "curve25519-sha256@libssh.org"}, {"algorithm": "ecdh-sha2-nistp256"}, {"algorithm": "ecdh-sha2-nistp384"}, {"algorithm": "ecdh-sha2-nistp521"}, {"algorithm": "diffie-hellman-group-exchange-sha256", "keysize": 2048}, {"algorithm": "diffie-hellman-group16-sha512"}, {"algorithm": "diffie-hellman-group18-sha512"}, {"algorithm": "diffie-hellman-group14-sha256"}, {"algorithm": "diffie-hellman-group14-sha1"}], "key": [{"algorithm": "rsa-sha2-512", "keysize": 3072}, {"algorithm": "rsa-sha2-256", "keysize": 3072}, {"algorithm": "ssh-rsa", "keysize": 3072}, {"algorithm": "ecdsa-sha2-nistp256"}, {"algorithm": "ssh-ed25519"}], "mac": ["umac-64-etm@openssh.com", "umac-128-etm@openssh.com", "hmac-sha2-256-etm@openssh.com", "hmac-sha2-512-etm@openssh.com", "hmac-sha1-etm@openssh.com", "umac-64@openssh.com", "umac-128@openssh.com", "hmac-sha2-256", "hmac-sha2-512", "hmac-sha1"]} {"banner": {"comments": null, "protocol": [2, 0], "raw": "SSH-2.0-OpenSSH_8.0", "software": "OpenSSH_8.0"}, "compression": ["none", "zlib@openssh.com"], "enc": ["chacha20-poly1305@openssh.com", "aes128-ctr", "aes192-ctr", "aes256-ctr", "aes128-gcm@openssh.com", "aes256-gcm@openssh.com"], "fingerprints": [{"hash": "UrnXIVH+7dlw8UqYocl48yUEcKrthGDQG2CPCgp7MxU", "hash_alg": "SHA256", "hostkey": "ssh-ed25519"}, {"hash": "1e:0c:7b:34:73:bf:52:41:b0:f9:d1:a9:ab:98:c7:c9", "hash_alg": "MD5", "hostkey": "ssh-ed25519"}, {"hash": "nsWtdJ9Z67Vrf7OsUzQov7esXhsWAfVppArGh25u244", "hash_alg": "SHA256", "hostkey": "ssh-rsa"}, {"hash": "18:e2:51:fe:21:6c:78:d0:b8:cf:32:d4:bd:56:42:e1", "hash_alg": "MD5", "hostkey": "ssh-rsa"}], "kex": [{"algorithm": "curve25519-sha256"}, {"algorithm": "curve25519-sha256@libssh.org"}, {"algorithm": "ecdh-sha2-nistp256"}, {"algorithm": "ecdh-sha2-nistp384"}, {"algorithm": "ecdh-sha2-nistp521"}, {"algorithm": "diffie-hellman-group-exchange-sha256", "keysize": 2048}, {"algorithm": "diffie-hellman-group16-sha512"}, {"algorithm": "diffie-hellman-group18-sha512"}, {"algorithm": "diffie-hellman-group14-sha256"}, {"algorithm": "diffie-hellman-group14-sha1"}], "key": [{"algorithm": "rsa-sha2-512", "keysize": 3072}, {"algorithm": "rsa-sha2-256", "keysize": 3072}, {"algorithm": "ssh-rsa", "keysize": 3072}, {"algorithm": "ecdsa-sha2-nistp256"}, {"algorithm": "ssh-ed25519"}], "mac": ["umac-64-etm@openssh.com", "umac-128-etm@openssh.com", "hmac-sha2-256-etm@openssh.com", "hmac-sha2-512-etm@openssh.com", "hmac-sha1-etm@openssh.com", "umac-64@openssh.com", "umac-128@openssh.com", "hmac-sha2-256", "hmac-sha2-512", "hmac-sha1"], "target": "localhost"}

View File

@ -25,6 +25,7 @@
(key) rsa-sha2-256 (3072-bit) -- [info] available since OpenSSH 7.2 (key) rsa-sha2-256 (3072-bit) -- [info] available since OpenSSH 7.2
(key) ssh-rsa (3072-bit) -- [fail] using weak hashing algorithm (key) ssh-rsa (3072-bit) -- [fail] using weak hashing algorithm
`- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28 `- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28
`- [info] a future deprecation notice has been issued in OpenSSH 8.2: https://www.openssh.com/txt/release-8.2
(key) ecdsa-sha2-nistp256 -- [fail] using weak elliptic curves (key) ecdsa-sha2-nistp256 -- [fail] using weak elliptic curves
 `- [warn] using weak random number generator could reveal the key  `- [warn] using weak random number generator could reveal the key
`- [info] available since OpenSSH 5.7, Dropbear SSH 2013.62 `- [info] available since OpenSSH 5.7, Dropbear SSH 2013.62

View File

@ -1 +1 @@
{"banner": {"comments": null, "protocol": [2, 0], "raw": "SSH-2.0-OpenSSH_8.0", "software": "OpenSSH_8.0"}, "compression": ["none", "zlib@openssh.com"], "enc": ["chacha20-poly1305@openssh.com", "aes128-ctr", "aes192-ctr", "aes256-ctr", "aes128-gcm@openssh.com", "aes256-gcm@openssh.com"], "fingerprints": [{"fp": "SHA256:UrnXIVH+7dlw8UqYocl48yUEcKrthGDQG2CPCgp7MxU", "type": "ssh-ed25519"}], "kex": [{"algorithm": "curve25519-sha256"}, {"algorithm": "curve25519-sha256@libssh.org"}, {"algorithm": "ecdh-sha2-nistp256"}, {"algorithm": "ecdh-sha2-nistp384"}, {"algorithm": "ecdh-sha2-nistp521"}, {"algorithm": "diffie-hellman-group-exchange-sha256", "keysize": 2048}, {"algorithm": "diffie-hellman-group16-sha512"}, {"algorithm": "diffie-hellman-group18-sha512"}, {"algorithm": "diffie-hellman-group14-sha256"}, {"algorithm": "diffie-hellman-group14-sha1"}], "key": [{"algorithm": "ssh-ed25519"}, {"algorithm": "ssh-ed25519-cert-v01@openssh.com"}], "mac": ["umac-64-etm@openssh.com", "umac-128-etm@openssh.com", "hmac-sha2-256-etm@openssh.com", "hmac-sha2-512-etm@openssh.com", "hmac-sha1-etm@openssh.com", "umac-64@openssh.com", "umac-128@openssh.com", "hmac-sha2-256", "hmac-sha2-512", "hmac-sha1"]} {"banner": {"comments": null, "protocol": [2, 0], "raw": "SSH-2.0-OpenSSH_8.0", "software": "OpenSSH_8.0"}, "compression": ["none", "zlib@openssh.com"], "enc": ["chacha20-poly1305@openssh.com", "aes128-ctr", "aes192-ctr", "aes256-ctr", "aes128-gcm@openssh.com", "aes256-gcm@openssh.com"], "fingerprints": [{"hash": "UrnXIVH+7dlw8UqYocl48yUEcKrthGDQG2CPCgp7MxU", "hash_alg": "SHA256", "hostkey": "ssh-ed25519"}, {"hash": "1e:0c:7b:34:73:bf:52:41:b0:f9:d1:a9:ab:98:c7:c9", "hash_alg": "MD5", "hostkey": "ssh-ed25519"}], "kex": [{"algorithm": "curve25519-sha256"}, {"algorithm": "curve25519-sha256@libssh.org"}, {"algorithm": "ecdh-sha2-nistp256"}, {"algorithm": "ecdh-sha2-nistp384"}, {"algorithm": "ecdh-sha2-nistp521"}, {"algorithm": "diffie-hellman-group-exchange-sha256", "keysize": 2048}, {"algorithm": "diffie-hellman-group16-sha512"}, {"algorithm": "diffie-hellman-group18-sha512"}, {"algorithm": "diffie-hellman-group14-sha256"}, {"algorithm": "diffie-hellman-group14-sha1"}], "key": [{"algorithm": "ssh-ed25519"}, {"algorithm": "ssh-ed25519-cert-v01@openssh.com"}], "mac": ["umac-64-etm@openssh.com", "umac-128-etm@openssh.com", "hmac-sha2-256-etm@openssh.com", "hmac-sha2-512-etm@openssh.com", "hmac-sha1-etm@openssh.com", "umac-64@openssh.com", "umac-128@openssh.com", "hmac-sha2-256", "hmac-sha2-512", "hmac-sha1"], "target": "localhost"}

View File

@ -1 +1 @@
{"banner": {"comments": null, "protocol": [2, 0], "raw": "SSH-2.0-OpenSSH_8.0", "software": "OpenSSH_8.0"}, "compression": ["none", "zlib@openssh.com"], "enc": ["chacha20-poly1305@openssh.com", "aes256-gcm@openssh.com", "aes128-gcm@openssh.com", "aes256-ctr", "aes192-ctr", "aes128-ctr"], "fingerprints": [{"fp": "SHA256:UrnXIVH+7dlw8UqYocl48yUEcKrthGDQG2CPCgp7MxU", "type": "ssh-ed25519"}], "kex": [{"algorithm": "curve25519-sha256"}, {"algorithm": "curve25519-sha256@libssh.org"}, {"algorithm": "diffie-hellman-group-exchange-sha256", "keysize": 2048}], "key": [{"algorithm": "ssh-ed25519"}], "mac": ["hmac-sha2-256-etm@openssh.com", "hmac-sha2-512-etm@openssh.com", "umac-128-etm@openssh.com"]} {"banner": {"comments": null, "protocol": [2, 0], "raw": "SSH-2.0-OpenSSH_8.0", "software": "OpenSSH_8.0"}, "compression": ["none", "zlib@openssh.com"], "enc": ["chacha20-poly1305@openssh.com", "aes256-gcm@openssh.com", "aes128-gcm@openssh.com", "aes256-ctr", "aes192-ctr", "aes128-ctr"], "fingerprints": [{"hash": "UrnXIVH+7dlw8UqYocl48yUEcKrthGDQG2CPCgp7MxU", "hash_alg": "SHA256", "hostkey": "ssh-ed25519"}, {"hash": "1e:0c:7b:34:73:bf:52:41:b0:f9:d1:a9:ab:98:c7:c9", "hash_alg": "MD5", "hostkey": "ssh-ed25519"}], "kex": [{"algorithm": "curve25519-sha256"}, {"algorithm": "curve25519-sha256@libssh.org"}, {"algorithm": "diffie-hellman-group-exchange-sha256", "keysize": 2048}], "key": [{"algorithm": "ssh-ed25519"}], "mac": ["hmac-sha2-256-etm@openssh.com", "hmac-sha2-512-etm@openssh.com", "umac-128-etm@openssh.com"], "target": "localhost"}

View File

@ -1 +1 @@
{"banner": {"comments": "", "protocol": [2, 0], "raw": "", "software": "tinyssh_noversion"}, "compression": ["none"], "enc": ["chacha20-poly1305@openssh.com"], "fingerprints": [{"fp": "SHA256:89ocln1x7KNqnMgWffGoYtD70ksJ4FrH7BMJHa7SrwU", "type": "ssh-ed25519"}], "kex": [{"algorithm": "curve25519-sha256"}, {"algorithm": "curve25519-sha256@libssh.org"}, {"algorithm": "sntrup4591761x25519-sha512@tinyssh.org"}], "key": [{"algorithm": "ssh-ed25519"}], "mac": ["hmac-sha2-256"]} {"banner": {"comments": "", "protocol": [2, 0], "raw": "", "software": "tinyssh_noversion"}, "compression": ["none"], "enc": ["chacha20-poly1305@openssh.com"], "fingerprints": [{"hash": "89ocln1x7KNqnMgWffGoYtD70ksJ4FrH7BMJHa7SrwU", "hash_alg": "SHA256", "hostkey": "ssh-ed25519"}, {"hash": "dd:9c:6d:f9:b0:8c:af:fa:c2:65:81:5d:5d:56:f8:21", "hash_alg": "MD5", "hostkey": "ssh-ed25519"}], "kex": [{"algorithm": "curve25519-sha256"}, {"algorithm": "curve25519-sha256@libssh.org"}, {"algorithm": "sntrup4591761x25519-sha512@tinyssh.org"}], "key": [{"algorithm": "ssh-ed25519"}], "mac": ["hmac-sha2-256"], "target": "localhost"}

View File

@ -1,6 +1,6 @@
# general # general
(gen) software: TinySSH noversion (gen) software: TinySSH noversion
(gen) compatibility: OpenSSH 8.0+, Dropbear SSH 2018.76+ (gen) compatibility: OpenSSH 8.0-8.4, Dropbear SSH 2018.76+
(gen) compression: disabled (gen) compression: disabled
# key exchange algorithms # key exchange algorithms

View File

@ -7,6 +7,7 @@ class TestAuditConf:
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def init(self, ssh_audit): def init(self, ssh_audit):
self.AuditConf = ssh_audit.AuditConf self.AuditConf = ssh_audit.AuditConf
self.OutputBuffer = ssh_audit.OutputBuffer
self.usage = ssh_audit.usage self.usage = ssh_audit.usage
self.process_commandline = process_commandline self.process_commandline = process_commandline
@ -21,9 +22,8 @@ class TestAuditConf:
'colors': True, 'colors': True,
'verbose': False, 'verbose': False,
'level': 'info', 'level': 'info',
'ipv4': True, 'ipv4': False,
'ipv6': True, 'ipv6': False
'ipvo': ()
} }
for k, v in kwargs.items(): for k, v in kwargs.items():
options[k] = v options[k] = v
@ -37,7 +37,6 @@ class TestAuditConf:
assert conf.level == options['level'] assert conf.level == options['level']
assert conf.ipv4 == options['ipv4'] assert conf.ipv4 == options['ipv4']
assert conf.ipv6 == options['ipv6'] assert conf.ipv6 == options['ipv6']
assert conf.ipvo == options['ipvo']
def test_audit_conf_defaults(self): def test_audit_conf_defaults(self):
conf = self.AuditConf() conf = self.AuditConf()
@ -63,57 +62,38 @@ class TestAuditConf:
conf.port = port conf.port = port
excinfo.match(r'.*invalid port.*') excinfo.match(r'.*invalid port.*')
def test_audit_conf_ipvo(self): def test_audit_conf_ip_version_preference(self):
# ipv4-only # ipv4-only
conf = self.AuditConf() conf = self.AuditConf()
conf.ipv4 = True conf.ipv4 = True
assert conf.ipv4 is True assert conf.ipv4 is True
assert conf.ipv6 is False assert conf.ipv6 is False
assert conf.ipvo == (4,) assert conf.ip_version_preference == [4]
# ipv6-only # ipv6-only
conf = self.AuditConf() conf = self.AuditConf()
conf.ipv6 = True conf.ipv6 = True
assert conf.ipv4 is False assert conf.ipv4 is False
assert conf.ipv6 is True assert conf.ipv6 is True
assert conf.ipvo == (6,) assert conf.ip_version_preference == [6]
# ipv4-only (by removing ipv6)
conf = self.AuditConf()
conf.ipv6 = False
assert conf.ipv4 is True
assert conf.ipv6 is False
assert conf.ipvo == (4, )
# ipv6-only (by removing ipv4)
conf = self.AuditConf()
conf.ipv4 = False
assert conf.ipv4 is False
assert conf.ipv6 is True
assert conf.ipvo == (6, )
# ipv4-preferred # ipv4-preferred
conf = self.AuditConf() conf = self.AuditConf()
conf.ipv4 = True conf.ipv4 = True
conf.ipv6 = True conf.ipv6 = True
assert conf.ipv4 is True assert conf.ipv4 is True
assert conf.ipv6 is True assert conf.ipv6 is True
assert conf.ipvo == (4, 6) assert conf.ip_version_preference == [4, 6]
# ipv6-preferred # ipv6-preferred
conf = self.AuditConf() conf = self.AuditConf()
conf.ipv6 = True conf.ipv6 = True
conf.ipv4 = True conf.ipv4 = True
assert conf.ipv4 is True assert conf.ipv4 is True
assert conf.ipv6 is True assert conf.ipv6 is True
assert conf.ipvo == (6, 4) assert conf.ip_version_preference == [6, 4]
# ipvo empty # defaults
conf = self.AuditConf() conf = self.AuditConf()
conf.ipvo = () assert conf.ipv4 is False
assert conf.ipv4 is True assert conf.ipv6 is False
assert conf.ipv6 is True assert conf.ip_version_preference == []
assert conf.ipvo == ()
# ipvo validation
conf = self.AuditConf()
conf.ipvo = (1, 2, 3, 4, 5, 6)
assert conf.ipvo == (4, 6)
conf.ipvo = (4, 4, 4, 6, 6)
assert conf.ipvo == (4, 6)
def test_audit_conf_level(self): def test_audit_conf_level(self):
conf = self.AuditConf() conf = self.AuditConf()
@ -127,7 +107,7 @@ class TestAuditConf:
def test_audit_conf_process_commandline(self): def test_audit_conf_process_commandline(self):
# pylint: disable=too-many-statements # pylint: disable=too-many-statements
c = lambda x: self.process_commandline(x.split(), self.usage) # noqa c = lambda x: self.process_commandline(self.OutputBuffer, x.split(), self.usage) # noqa
with pytest.raises(SystemExit): with pytest.raises(SystemExit):
conf = c('') conf = c('')
with pytest.raises(SystemExit): with pytest.raises(SystemExit):

View File

@ -35,9 +35,9 @@ def test_prevent_runtime_error_regression(ssh_audit, kex):
kex.set_host_key("ssh-rsa7", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00") kex.set_host_key("ssh-rsa7", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00")
kex.set_host_key("ssh-rsa8", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00") kex.set_host_key("ssh-rsa8", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00")
rv = ssh_audit.build_struct(banner=None, kex=kex) rv = ssh_audit.build_struct('localhost', banner=None, kex=kex)
assert len(rv["fingerprints"]) == 9 assert len(rv["fingerprints"]) == (9 * 2) # Each host key generates two hash fingerprints: one using SHA256, and one using MD5.
for key in ['banner', 'compression', 'enc', 'fingerprints', 'kex', 'key', 'mac']: for key in ['banner', 'compression', 'enc', 'fingerprints', 'kex', 'key', 'mac']:
assert key in rv assert key in rv

View File

@ -2,12 +2,15 @@ import socket
import errno import errno
import pytest import pytest
from ssh_audit.outputbuffer import OutputBuffer
# pylint: disable=attribute-defined-outside-init # pylint: disable=attribute-defined-outside-init
class TestErrors: class TestErrors:
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def init(self, ssh_audit): def init(self, ssh_audit):
self.AuditConf = ssh_audit.AuditConf self.AuditConf = ssh_audit.AuditConf
self.OutputBuffer = ssh_audit.OutputBuffer
self.audit = ssh_audit.audit self.audit = ssh_audit.audit
def _conf(self): def _conf(self):
@ -21,14 +24,21 @@ class TestErrors:
conf = self._conf() conf = self._conf()
spy.begin() spy.begin()
out = OutputBuffer()
if exit_expected: if exit_expected:
with pytest.raises(SystemExit): with pytest.raises(SystemExit):
self.audit(conf) self.audit(out, conf)
else: else:
ret = self.audit(conf) ret = self.audit(out, conf)
assert ret != 0 assert ret != 0
out.write()
lines = spy.flush() lines = spy.flush()
# If the last line is empty, delete it.
if len(lines) > 1 and lines[-1] == '':
del lines[-1]
return lines return lines
def test_connection_unresolved(self, output_spy, virtual_socket): def test_connection_unresolved(self, output_spy, virtual_socket):
@ -157,6 +167,6 @@ class TestErrors:
conf = self._conf() conf = self._conf()
conf.ssh1, conf.ssh2 = True, False conf.ssh1, conf.ssh2 = True, False
lines = self._audit(output_spy, conf) lines = self._audit(output_spy, conf)
assert len(lines) == 3 assert len(lines) == 4
assert 'error reading packet' in lines[-1] assert 'error reading packet' in lines[-1]
assert 'major versions differ' in lines[-1] assert 'major versions differ' in lines[-1]

View File

@ -2,102 +2,107 @@ import pytest
# pylint: disable=attribute-defined-outside-init # pylint: disable=attribute-defined-outside-init
class TestOutput: class TestOutputBuffer:
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def init(self, ssh_audit): def init(self, ssh_audit):
self.Output = ssh_audit.Output
self.OutputBuffer = ssh_audit.OutputBuffer self.OutputBuffer = ssh_audit.OutputBuffer
def test_output_buffer_no_lines(self, output_spy): def test_outputbuffer_no_lines(self, output_spy):
output_spy.begin() output_spy.begin()
with self.OutputBuffer() as obuf: obuf = self.OutputBuffer()
pass obuf.write()
assert output_spy.flush() == [] assert output_spy.flush() == ['']
output_spy.begin() output_spy.begin()
with self.OutputBuffer() as obuf:
pass
obuf.flush()
assert output_spy.flush() == []
def test_output_buffer_no_flush(self, output_spy): def test_outputbuffer_defaults(self):
output_spy.begin() obuf = self.OutputBuffer()
with self.OutputBuffer():
print('abc')
assert output_spy.flush() == []
def test_output_buffer_flush(self, output_spy):
output_spy.begin()
with self.OutputBuffer() as obuf:
print('abc')
print()
print('def')
obuf.flush()
assert output_spy.flush() == ['abc', '', 'def']
def test_output_defaults(self):
out = self.Output()
# default: on # default: on
assert out.batch is False assert obuf.batch is False
assert out.use_colors is True assert obuf.use_colors is True
assert out.level == 'info' assert obuf.level == 'info'
def test_output_colors(self, output_spy): def test_outputbuffer_colors(self, output_spy):
out = self.Output() out = self.OutputBuffer()
# test without colors
# Test without colors.
out.use_colors = False out.use_colors = False
output_spy.begin() output_spy.begin()
out.info('info color') out.info('info color')
out.write()
assert output_spy.flush() == ['info color'] assert output_spy.flush() == ['info color']
output_spy.begin() output_spy.begin()
out.head('head color') out.head('head color')
out.write()
assert output_spy.flush() == ['head color'] assert output_spy.flush() == ['head color']
output_spy.begin() output_spy.begin()
out.good('good color') out.good('good color')
out.write()
assert output_spy.flush() == ['good color'] assert output_spy.flush() == ['good color']
output_spy.begin() output_spy.begin()
out.warn('warn color') out.warn('warn color')
out.write()
assert output_spy.flush() == ['warn color'] assert output_spy.flush() == ['warn color']
output_spy.begin() output_spy.begin()
out.fail('fail color') out.fail('fail color')
out.write()
assert output_spy.flush() == ['fail color'] assert output_spy.flush() == ['fail color']
# If colors aren't supported by this system, skip the color tests.
if not out.colors_supported: if not out.colors_supported:
return return
# test with colors
# Test with colors.
out.use_colors = True out.use_colors = True
output_spy.begin() output_spy.begin()
out.info('info color') out.info('info color')
out.write()
assert output_spy.flush() == ['info color'] assert output_spy.flush() == ['info color']
output_spy.begin() output_spy.begin()
out.head('head color') out.head('head color')
assert output_spy.flush() == ['\x1b[0;36mhead color\x1b[0m'] out.write()
assert output_spy.flush() in [['\x1b[0;36mhead color\x1b[0m'], ['\x1b[0;96mhead color\x1b[0m']]
output_spy.begin() output_spy.begin()
out.good('good color') out.good('good color')
assert output_spy.flush() == ['\x1b[0;32mgood color\x1b[0m'] out.write()
assert output_spy.flush() in [['\x1b[0;32mgood color\x1b[0m'], ['\x1b[0;92mgood color\x1b[0m']]
output_spy.begin() output_spy.begin()
out.warn('warn color') out.warn('warn color')
assert output_spy.flush() == ['\x1b[0;33mwarn color\x1b[0m'] out.write()
assert output_spy.flush() in [['\x1b[0;33mwarn color\x1b[0m'], ['\x1b[0;93mwarn color\x1b[0m']]
output_spy.begin() output_spy.begin()
out.fail('fail color') out.fail('fail color')
assert output_spy.flush() == ['\x1b[0;31mfail color\x1b[0m'] out.write()
assert output_spy.flush() in [['\x1b[0;31mfail color\x1b[0m'], ['\x1b[0;91mfail color\x1b[0m']]
def test_output_sep(self, output_spy): def test_outputbuffer_sep(self, output_spy):
out = self.Output() out = self.OutputBuffer()
output_spy.begin() output_spy.begin()
out.sep() out.sep()
out.sep() out.sep()
out.sep() out.sep()
out.write()
assert output_spy.flush() == ['', '', ''] assert output_spy.flush() == ['', '', '']
def test_output_levels(self): def test_outputbuffer_levels(self):
out = self.Output() out = self.OutputBuffer()
assert out.get_level('info') == 0 assert out.get_level('info') == 0
assert out.get_level('good') == 0 assert out.get_level('good') == 0
assert out.get_level('warn') == 1 assert out.get_level('warn') == 1
assert out.get_level('fail') == 2 assert out.get_level('fail') == 2
assert out.get_level('unknown') > 2 assert out.get_level('unknown') > 2
def test_output_level_property(self): def test_outputbuffer_level_property(self):
out = self.Output() out = self.OutputBuffer()
out.level = 'info' out.level = 'info'
assert out.level == 'info' assert out.level == 'info'
out.level = 'good' out.level = 'good'
@ -109,8 +114,8 @@ class TestOutput:
out.level = 'invalid level' out.level = 'invalid level'
assert out.level == 'unknown' assert out.level == 'unknown'
def test_output_level(self, output_spy): def test_outputbuffer_level(self, output_spy):
out = self.Output() out = self.OutputBuffer()
# visible: all # visible: all
out.level = 'info' out.level = 'info'
output_spy.begin() output_spy.begin()
@ -119,6 +124,7 @@ class TestOutput:
out.good('good color') out.good('good color')
out.warn('warn color') out.warn('warn color')
out.fail('fail color') out.fail('fail color')
out.write()
assert len(output_spy.flush()) == 5 assert len(output_spy.flush()) == 5
# visible: head, warn, fail # visible: head, warn, fail
out.level = 'warn' out.level = 'warn'
@ -128,6 +134,7 @@ class TestOutput:
out.good('good color') out.good('good color')
out.warn('warn color') out.warn('warn color')
out.fail('fail color') out.fail('fail color')
out.write()
assert len(output_spy.flush()) == 3 assert len(output_spy.flush()) == 3
# visible: head, fail # visible: head, fail
out.level = 'fail' out.level = 'fail'
@ -137,6 +144,7 @@ class TestOutput:
out.good('good color') out.good('good color')
out.warn('warn color') out.warn('warn color')
out.fail('fail color') out.fail('fail color')
out.write()
assert len(output_spy.flush()) == 2 assert len(output_spy.flush()) == 2
# visible: head # visible: head
out.level = 'invalid level' out.level = 'invalid level'
@ -146,10 +154,11 @@ class TestOutput:
out.good('good color') out.good('good color')
out.warn('warn color') out.warn('warn color')
out.fail('fail color') out.fail('fail color')
out.write()
assert len(output_spy.flush()) == 1 assert len(output_spy.flush()) == 1
def test_output_batch(self, output_spy): def test_outputbuffer_batch(self, output_spy):
out = self.Output() out = self.OutputBuffer()
# visible: all # visible: all
output_spy.begin() output_spy.begin()
out.level = 'info' out.level = 'info'
@ -159,6 +168,7 @@ class TestOutput:
out.good('good color') out.good('good color')
out.warn('warn color') out.warn('warn color')
out.fail('fail color') out.fail('fail color')
out.write()
assert len(output_spy.flush()) == 5 assert len(output_spy.flush()) == 5
# visible: all except head # visible: all except head
output_spy.begin() output_spy.begin()
@ -169,4 +179,5 @@ class TestOutput:
out.good('good color') out.good('good color')
out.warn('warn color') out.warn('warn color')
out.fail('fail color') out.fail('fail color')
out.write()
assert len(output_spy.flush()) == 4 assert len(output_spy.flush()) == 4

View File

@ -8,6 +8,7 @@ class TestResolve:
def init(self, ssh_audit): def init(self, ssh_audit):
self.AuditConf = ssh_audit.AuditConf self.AuditConf = ssh_audit.AuditConf
self.audit = ssh_audit.audit self.audit = ssh_audit.audit
self.OutputBuffer = ssh_audit.OutputBuffer
self.ssh_socket = ssh_audit.SSH_Socket self.ssh_socket = ssh_audit.SSH_Socket
def _conf(self): def _conf(self):
@ -19,11 +20,11 @@ class TestResolve:
def test_resolve_error(self, output_spy, virtual_socket): def test_resolve_error(self, output_spy, virtual_socket):
vsocket = virtual_socket vsocket = virtual_socket
vsocket.gsock.addrinfodata['localhost#22'] = socket.gaierror(8, 'hostname nor servname provided, or not known') vsocket.gsock.addrinfodata['localhost#22'] = socket.gaierror(8, 'hostname nor servname provided, or not known')
s = self.ssh_socket('localhost', 22)
conf = self._conf() conf = self._conf()
s = self.ssh_socket(self.OutputBuffer(), 'localhost', 22, conf.ip_version_preference)
output_spy.begin() output_spy.begin()
with pytest.raises(SystemExit): with pytest.raises(SystemExit):
list(s._resolve(conf.ipvo)) list(s._resolve())
lines = output_spy.flush() lines = output_spy.flush()
assert len(lines) == 1 assert len(lines) == 1
assert 'hostname nor servname provided' in lines[-1] assert 'hostname nor servname provided' in lines[-1]
@ -31,49 +32,50 @@ class TestResolve:
def test_resolve_hostname_without_records(self, output_spy, virtual_socket): def test_resolve_hostname_without_records(self, output_spy, virtual_socket):
vsocket = virtual_socket vsocket = virtual_socket
vsocket.gsock.addrinfodata['localhost#22'] = [] vsocket.gsock.addrinfodata['localhost#22'] = []
s = self.ssh_socket('localhost', 22)
conf = self._conf() conf = self._conf()
s = self.ssh_socket(self.OutputBuffer(), 'localhost', 22, conf.ip_version_preference)
output_spy.begin() output_spy.begin()
r = list(s._resolve(conf.ipvo)) r = list(s._resolve())
assert len(r) == 0 assert len(r) == 0
def test_resolve_ipv4(self, virtual_socket): def test_resolve_ipv4(self, virtual_socket):
conf = self._conf() conf = self._conf()
conf.ipv4 = True conf.ipv4 = True
s = self.ssh_socket('localhost', 22) s = self.ssh_socket(self.OutputBuffer(), 'localhost', 22, conf.ip_version_preference)
r = list(s._resolve(conf.ipvo)) r = list(s._resolve())
assert len(r) == 1 assert len(r) == 1
assert r[0] == (socket.AF_INET, ('127.0.0.1', 22)) assert r[0] == (socket.AF_INET, ('127.0.0.1', 22))
def test_resolve_ipv6(self, virtual_socket): def test_resolve_ipv6(self, virtual_socket):
s = self.ssh_socket('localhost', 22)
conf = self._conf() conf = self._conf()
conf.ipv6 = True conf.ipv6 = True
r = list(s._resolve(conf.ipvo)) s = self.ssh_socket(self.OutputBuffer(), 'localhost', 22, conf.ip_version_preference)
r = list(s._resolve())
assert len(r) == 1 assert len(r) == 1
assert r[0] == (socket.AF_INET6, ('::1', 22)) assert r[0] == (socket.AF_INET6, ('::1', 22))
def test_resolve_ipv46_both(self, virtual_socket): def test_resolve_ipv46_both(self, virtual_socket):
s = self.ssh_socket('localhost', 22)
conf = self._conf() conf = self._conf()
r = list(s._resolve(conf.ipvo)) s = self.ssh_socket(self.OutputBuffer(), 'localhost', 22, conf.ip_version_preference)
r = list(s._resolve())
assert len(r) == 2 assert len(r) == 2
assert r[0] == (socket.AF_INET, ('127.0.0.1', 22)) assert r[0] == (socket.AF_INET, ('127.0.0.1', 22))
assert r[1] == (socket.AF_INET6, ('::1', 22)) assert r[1] == (socket.AF_INET6, ('::1', 22))
def test_resolve_ipv46_order(self, virtual_socket): def test_resolve_ipv46_order(self, virtual_socket):
s = self.ssh_socket('localhost', 22)
conf = self._conf() conf = self._conf()
conf.ipv4 = True conf.ipv4 = True
conf.ipv6 = True conf.ipv6 = True
r = list(s._resolve(conf.ipvo)) s = self.ssh_socket(self.OutputBuffer(), 'localhost', 22, conf.ip_version_preference)
r = list(s._resolve())
assert len(r) == 2 assert len(r) == 2
assert r[0] == (socket.AF_INET, ('127.0.0.1', 22)) assert r[0] == (socket.AF_INET, ('127.0.0.1', 22))
assert r[1] == (socket.AF_INET6, ('::1', 22)) assert r[1] == (socket.AF_INET6, ('::1', 22))
conf = self._conf() conf = self._conf()
conf.ipv6 = True conf.ipv6 = True
conf.ipv4 = True conf.ipv4 = True
r = list(s._resolve(conf.ipvo)) s = self.ssh_socket(self.OutputBuffer(), 'localhost', 22, conf.ip_version_preference)
r = list(s._resolve())
assert len(r) == 2 assert len(r) == 2
assert r[0] == (socket.AF_INET6, ('::1', 22)) assert r[0] == (socket.AF_INET6, ('::1', 22))
assert r[1] == (socket.AF_INET, ('127.0.0.1', 22)) assert r[1] == (socket.AF_INET, ('127.0.0.1', 22))

View File

@ -1,5 +1,6 @@
import pytest import pytest
from ssh_audit.outputbuffer import OutputBuffer
from ssh_audit.ssh_socket import SSH_Socket from ssh_audit.ssh_socket import SSH_Socket
@ -7,24 +8,25 @@ from ssh_audit.ssh_socket import SSH_Socket
class TestSocket: class TestSocket:
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def init(self, ssh_audit): def init(self, ssh_audit):
self.OutputBuffer = OutputBuffer
self.ssh_socket = SSH_Socket self.ssh_socket = SSH_Socket
def test_invalid_host(self, virtual_socket): def test_invalid_host(self, virtual_socket):
with pytest.raises(ValueError): with pytest.raises(ValueError):
self.ssh_socket(None, 22) self.ssh_socket(self.OutputBuffer(), None, 22)
def test_invalid_port(self, virtual_socket): def test_invalid_port(self, virtual_socket):
with pytest.raises(ValueError): with pytest.raises(ValueError):
self.ssh_socket('localhost', 'abc') self.ssh_socket(self.OutputBuffer(), 'localhost', 'abc')
with pytest.raises(ValueError): with pytest.raises(ValueError):
self.ssh_socket('localhost', -1) self.ssh_socket(self.OutputBuffer(), 'localhost', -1)
with pytest.raises(ValueError): with pytest.raises(ValueError):
self.ssh_socket('localhost', 0) self.ssh_socket(self.OutputBuffer(), 'localhost', 0)
with pytest.raises(ValueError): with pytest.raises(ValueError):
self.ssh_socket('localhost', 65536) self.ssh_socket(self.OutputBuffer(), 'localhost', 65536)
def test_not_connected_socket(self, virtual_socket): def test_not_connected_socket(self, virtual_socket):
sock = self.ssh_socket('localhost', 22) sock = self.ssh_socket(self.OutputBuffer(), 'localhost', 22)
banner, header, err = sock.get_banner() banner, header, err = sock.get_banner()
assert banner is None assert banner is None
assert len(header) == 0 assert len(header) == 0

View File

@ -3,6 +3,7 @@ import pytest
from ssh_audit.auditconf import AuditConf from ssh_audit.auditconf import AuditConf
from ssh_audit.fingerprint import Fingerprint from ssh_audit.fingerprint import Fingerprint
from ssh_audit.outputbuffer import OutputBuffer
from ssh_audit.protocol import Protocol from ssh_audit.protocol import Protocol
from ssh_audit.readbuf import ReadBuf from ssh_audit.readbuf import ReadBuf
from ssh_audit.ssh1 import SSH1 from ssh_audit.ssh1 import SSH1
@ -15,6 +16,7 @@ from ssh_audit.writebuf import WriteBuf
class TestSSH1: class TestSSH1:
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def init(self, ssh_audit): def init(self, ssh_audit):
self.OutputBuffer = OutputBuffer
self.protocol = Protocol self.protocol = Protocol
self.ssh1 = SSH1 self.ssh1 = SSH1
self.PublicKeyMessage = SSH1_PublicKeyMessage self.PublicKeyMessage = SSH1_PublicKeyMessage
@ -132,9 +134,11 @@ class TestSSH1:
vsocket.rdata.append(b'SSH-1.5-OpenSSH_7.2 ssh-audit-test\r\n') vsocket.rdata.append(b'SSH-1.5-OpenSSH_7.2 ssh-audit-test\r\n')
vsocket.rdata.append(self._create_ssh1_packet(w.write_flush())) vsocket.rdata.append(self._create_ssh1_packet(w.write_flush()))
output_spy.begin() output_spy.begin()
self.audit(self._conf()) out = self.OutputBuffer()
self.audit(out, self._conf())
out.write()
lines = output_spy.flush() lines = output_spy.flush()
assert len(lines) == 13 assert len(lines) == 17
def test_ssh1_server_invalid_first_packet(self, output_spy, virtual_socket): def test_ssh1_server_invalid_first_packet(self, output_spy, virtual_socket):
vsocket = virtual_socket vsocket = virtual_socket
@ -144,10 +148,12 @@ class TestSSH1:
vsocket.rdata.append(b'SSH-1.5-OpenSSH_7.2 ssh-audit-test\r\n') vsocket.rdata.append(b'SSH-1.5-OpenSSH_7.2 ssh-audit-test\r\n')
vsocket.rdata.append(self._create_ssh1_packet(w.write_flush())) vsocket.rdata.append(self._create_ssh1_packet(w.write_flush()))
output_spy.begin() output_spy.begin()
ret = self.audit(self._conf()) out = self.OutputBuffer()
ret = self.audit(out, self._conf())
out.write()
assert ret != 0 assert ret != 0
lines = output_spy.flush() lines = output_spy.flush()
assert len(lines) == 7 assert len(lines) == 10
assert 'unknown message' in lines[-1] assert 'unknown message' in lines[-1]
def test_ssh1_server_invalid_checksum(self, output_spy, virtual_socket): def test_ssh1_server_invalid_checksum(self, output_spy, virtual_socket):
@ -158,8 +164,10 @@ class TestSSH1:
vsocket.rdata.append(b'SSH-1.5-OpenSSH_7.2 ssh-audit-test\r\n') vsocket.rdata.append(b'SSH-1.5-OpenSSH_7.2 ssh-audit-test\r\n')
vsocket.rdata.append(self._create_ssh1_packet(w.write_flush(), False)) vsocket.rdata.append(self._create_ssh1_packet(w.write_flush(), False))
output_spy.begin() output_spy.begin()
out = self.OutputBuffer()
with pytest.raises(SystemExit): with pytest.raises(SystemExit):
self.audit(self._conf()) self.audit(out, self._conf())
out.write()
lines = output_spy.flush() lines = output_spy.flush()
assert len(lines) == 1 assert len(lines) == 3
assert 'checksum' in lines[-1] assert ('checksum' in lines[0]) or ('checksum' in lines[1]) or ('checksum' in lines[2])

View File

@ -3,6 +3,7 @@ import struct
import pytest import pytest
from ssh_audit.auditconf import AuditConf from ssh_audit.auditconf import AuditConf
from ssh_audit.outputbuffer import OutputBuffer
from ssh_audit.protocol import Protocol from ssh_audit.protocol import Protocol
from ssh_audit.readbuf import ReadBuf from ssh_audit.readbuf import ReadBuf
from ssh_audit.ssh2_kex import SSH2_Kex from ssh_audit.ssh2_kex import SSH2_Kex
@ -15,6 +16,7 @@ from ssh_audit.writebuf import WriteBuf
class TestSSH2: class TestSSH2:
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def init(self, ssh_audit): def init(self, ssh_audit):
self.OutputBuffer = OutputBuffer
self.protocol = Protocol self.protocol = Protocol
self.ssh2_kex = SSH2_Kex self.ssh2_kex = SSH2_Kex
self.ssh2_kexparty = SSH2_KexParty self.ssh2_kexparty = SSH2_KexParty
@ -141,9 +143,11 @@ class TestSSH2:
vsocket.rdata.append(b'SSH-2.0-OpenSSH_7.3 ssh-audit-test\r\n') vsocket.rdata.append(b'SSH-2.0-OpenSSH_7.3 ssh-audit-test\r\n')
vsocket.rdata.append(self._create_ssh2_packet(w.write_flush())) vsocket.rdata.append(self._create_ssh2_packet(w.write_flush()))
output_spy.begin() output_spy.begin()
self.audit(self._conf()) out = self.OutputBuffer()
self.audit(out, self._conf())
out.write()
lines = output_spy.flush() lines = output_spy.flush()
assert len(lines) == 67 assert len(lines) == 70
def test_ssh2_server_invalid_first_packet(self, output_spy, virtual_socket): def test_ssh2_server_invalid_first_packet(self, output_spy, virtual_socket):
vsocket = virtual_socket vsocket = virtual_socket
@ -152,8 +156,10 @@ class TestSSH2:
vsocket.rdata.append(b'SSH-2.0-OpenSSH_7.3 ssh-audit-test\r\n') vsocket.rdata.append(b'SSH-2.0-OpenSSH_7.3 ssh-audit-test\r\n')
vsocket.rdata.append(self._create_ssh2_packet(w.write_flush())) vsocket.rdata.append(self._create_ssh2_packet(w.write_flush()))
output_spy.begin() output_spy.begin()
ret = self.audit(self._conf()) out = self.OutputBuffer()
ret = self.audit(out, self._conf())
out.write()
assert ret != 0 assert ret != 0
lines = output_spy.flush() lines = output_spy.flush()
assert len(lines) == 3 assert len(lines) == 5
assert 'unknown message' in lines[-1] assert 'unknown message' in lines[-1]

30
tox.ini
View File

@ -1,7 +1,7 @@
[tox] [tox]
envlist = envlist =
py{py3}-{test,pylint,flake8,vulture} py{py3}-{test,pylint,flake8,vulture}
py{35,36,37,38,39}-{test,mypy,pylint,flake8,vulture} py{36,37,38,39}-{test,mypy,pylint,flake8,vulture}
cov cov
skip_missing_interpreters = true skip_missing_interpreters = true
@ -9,11 +9,11 @@ skip_missing_interpreters = true
deps = deps =
test: pytest<6.0 test: pytest<6.0
test,cov: {[testenv:cov]deps} test,cov: {[testenv:cov]deps}
test,py{35,36,37,38,39}-{type,mypy}: colorama test,py{36,37,38,39}-{type,mypy}: colorama
py{35,36,37,38,39}-{type,mypy}: {[testenv:mypy]deps} py{36,37,38,39}-{type,mypy}: {[testenv:mypy]deps}
py{py3,35,36,37,38,39}-{lint,pylint},lint: {[testenv:pylint]deps} py{py3,36,37,38,39}-{lint,pylint},lint: {[testenv:pylint]deps}
py{py3,35,36,37,38,39}-{lint,flake8},lint: {[testenv:flake8]deps} py{py3,36,37,38,39}-{lint,flake8},lint: {[testenv:flake8]deps}
py{py3,35,36,37,38,39}-{lint,vulture},lint: {[testenv:vulture]deps} py{py3,36,37,38,39}-{lint,vulture},lint: {[testenv:vulture]deps}
setenv = setenv =
SSHAUDIT = {toxinidir}/src SSHAUDIT = {toxinidir}/src
test: COVERAGE_FILE = {toxinidir}/.coverage.{envname} test: COVERAGE_FILE = {toxinidir}/.coverage.{envname}
@ -25,13 +25,13 @@ 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{35,36,37,38,39}-{type,mypy}: {[testenv:mypy]commands} py{36,37,38,39}-{type,mypy}: {[testenv:mypy]commands}
py{py3,35,36,37,38,39}-{lint,pylint},lint: {[testenv:pylint]commands} py{py3,36,37,38,39}-{lint,pylint},lint: {[testenv:pylint]commands}
py{py3,35,36,37,38,39}-{lint,flake8},lint: {[testenv:flake8]commands} py{py3,36,37,38,39}-{lint,flake8},lint: {[testenv:flake8]commands}
py{py3,35,36,37,38,39}-{lint,vulture},lint: {[testenv:vulture]commands} py{py3,36,37,38,39}-{lint,vulture},lint: {[testenv:vulture]commands}
ignore_outcome = #ignore_outcome =
type: true # type: true
lint: true # lint: true
[testenv:cov] [testenv:cov]
deps = deps =
@ -51,7 +51,7 @@ deps =
lxml lxml
mypy mypy
commands = commands =
-mypy \ mypy \
--strict \ --strict \
--show-error-context \ --show-error-context \
--html-report {env:MYPYHTML}.py3.{envname} \ --html-report {env:MYPYHTML}.py3.{envname} \
@ -62,7 +62,7 @@ deps =
mccabe mccabe
pylint pylint
commands = commands =
-pylint \ pylint \
--rcfile tox.ini \ --rcfile tox.ini \
--load-plugins=pylint.extensions.bad_builtin \ --load-plugins=pylint.extensions.bad_builtin \
--load-plugins=pylint.extensions.check_elif \ --load-plugins=pylint.extensions.check_elif \

130
update_windows_man_page.sh Executable file
View File

@ -0,0 +1,130 @@
#!/bin/bash
#
# The MIT License (MIT)
#
# Copyright (C) 2021 Joe Testa (jtesta@positronsecurity.com)
# Copyright (C) 2021 Adam Russell (<adam[at]thecliguy[dot]co[dot]uk>)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
################################################################################
# update_windows_man_page.sh
#
# PURPOSE
# Since Windows lacks a manual reader it's necessary to provide an alternative
# means of reading the man page.
#
# This script should be run as part of the ssh-audit packaging process for
# Windows. It populates the 'WINDOWS_MAN_PAGE' variable in 'globals.py' with
# the contents of the man page. Windows users can then print the content of
# 'WINDOWS_MAN_PAGE' by invoking ssh-audit with the manual parameters
# (--manual / -m).
#
# Cygwin is required.
#
# USAGE
# update_windows_man_page.sh [-m <path-to-man-page>] [-g <path-to-globals.py>]
#
################################################################################
function usage {
echo >&2 "Usage: $0 [-m <path-to-man-page>] [-g <path-to-globals.py>] [-h]"
echo >&2 " -m Specify an alternate man page path (default: ./ssh-audit.1)"
echo >&2 " -g Specify an alternate globals.py path (default: ./src/ssh_audit/globals.py)"
echo >&2 " -h This help message"
}
PLATFORM="$(uname -s)"
# This script is intended for use on Linux and Cygwin only.
case "$PLATFORM" in
Linux | CYGWIN*) ;;
*) echo "Platform not supported: $PLATFORM"
exit 1
;;
esac
MAN_PAGE=./ssh-audit.1
GLOBALS_PY=./src/ssh_audit/globals.py
while getopts "m: g: h" OPTION
do
case "$OPTION" in
m)
MAN_PAGE="$OPTARG"
;;
g)
GLOBALS_PY="$OPTARG"
;;
h)
usage
exit 0
;;
*)
echo >&2 "Invalid parameter(s) provided"
usage
exit 1
;;
esac
done
# Check that the specified files exist.
[ -f "$MAN_PAGE" ] || { echo >&2 "man page file not found: $MAN_PAGE"; exit 1; }
[ -f "$GLOBALS_PY" ] || { echo >&2 "globals.py file not found: $GLOBALS_PY"; exit 1; }
# Check that the 'ul' (do underlining) binary exists.
if [[ "$PLATFORM" = Linux ]]; then
command -v ul >/dev/null 2>&1 || { echo >&2 "ul not found."; exit 1; }
fi
# Check that the 'sed' (stream editor) binary exists.
command -v sed >/dev/null 2>&1 || { echo >&2 "sed not found."; exit 1; }
# Reset the globals.py file, in case it was modified from a prior run.
git checkout $GLOBALS_PY > /dev/null 2>&1
# Remove the Windows man page placeholder from 'globals.py'.
sed -i '/^WINDOWS_MAN_PAGE/d' "$GLOBALS_PY"
echo "Processing man page at ${MAN_PAGE} and placing output into ${GLOBALS_PY}..."
# Append the man page content to 'globals.py'.
# * man outputs a backspace-overwrite sequence rather than an ANSI escape
# sequence.
# * 'MAN_KEEP_FORMATTING' preserves the backspace-overwrite sequence when
# redirected to a file or a pipe.
# * sed converts unicode hyphens into an ASCI equivalent.
# * The 'ul' command converts the backspace-overwrite sequence to an ANSI
# escape sequence. Not required under Cygwin because man outputs ANSI escape
# codes automatically.
echo WINDOWS_MAN_PAGE = '"""' >> "$GLOBALS_PY"
if [[ "$PLATFORM" = CYGWIN* ]]; then
MANWIDTH=80 MAN_KEEP_FORMATTING=1 man "$MAN_PAGE" | sed $'s/\u2010/-/g' >> "$GLOBALS_PY"
else
MANWIDTH=80 MAN_KEEP_FORMATTING=1 man "$MAN_PAGE" | ul | sed $'s/\u2010/-/g' >> "$GLOBALS_PY"
fi
echo '"""' >> "$GLOBALS_PY"
echo "Done."
exit 0

View File

@ -1,21 +0,0 @@
Below are notes for creating a Windows executable.
An executable can only be made on a Windows host because the PyInstaller tool (https://www.pyinstaller.org/) does not support cross-compilation.
On a Windows machine, do the following:
1.) Install Python v3.7.x from https://www.python.org/. (As of this writing v3.8.0 isn't supported.) To make life easier, check the option to add Python to the PATH environment variable.
2.) Using pip, install pyinstaller and colorama:
pip install pyinstaller colorama
3.) Create the executable with:
cd src\ssh_audit
rename ssh_audit.py ssh-audit.py
pyinstaller -D --icon ..\..\windows_icon.ico --add-data policies;policies ssh-audit.py
4.) Rename the "dist\ssh-audit\" folder to "dist\ssh-audit vX.X.X\"
5.) Zip the "dist\ssh-audit vX.X.X\" folder and name it "windows_ssh-audit_vX.X.X.zip" (hint: zip windows_ssh-audit_vX.X.X.zip -r "ssh-audit vX.X.X").