1
0
mirror of https://github.com/jtesta/ssh-audit.git synced 2025-07-05 21:42: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/
/stage/
/ssh-audit_*.snap
# Your local server config
servers.txt

View File

@ -1,11 +1,16 @@
language: python
arch:
- arm64
- amd64
- ppc64le
python:
- "3.5"
- "3.6"
- "3.7"
- "3.8"
- "3.9"
- "3.10"
cache:
- 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/).
### 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.
### 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

View File

@ -1,4 +1,4 @@
FROM python:3.8-slim
FROM python:3.9-slim
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
[![Build Status](https://travis-ci.org/jtesta/ssh-audit.svg?branch=master)](https://travis-ci.org/jtesta/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)
[![codecov](https://codecov.io/gh/arthepsy/ssh-audit/branch/develop/graph/badge.svg)](https://codecov.io/gh/arthepsy/ssh-audit)
[![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)
-->
[![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/)
[![Docker Pulls](https://img.shields.io/docker/pulls/positronsecurity/ssh-audit)](https://hub.docker.com/r/positronsecurity/ssh-audit)
[![Build Status](https://github.com/jtesta/ssh-audit/actions/workflows/tox.yaml/badge.svg)](https://github.com/jtesta/ssh-audit/actions)
[![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.
[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
- SSH1 and SSH2 protocol server support;
- analyze SSH client configuration;
@ -21,6 +32,7 @@
- historical information from OpenSSH, Dropbear SSH and libssh;
- policy scans to ensure adherence to a hardened/standard configuration;
- runs on Linux and Windows;
- supports Python 3.6 - 3.9;
- no dependencies
## Usage
@ -36,11 +48,13 @@ usage: ssh-audit.py [options] <host>
-c, --client-audit starts a server on port 2222 to audit client
software config (use -p to change port;
use -t to change timeout)
-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, --list-policies list all the official, built-in policies
--lookup=<alg1,alg2,...> looks up an algorithm(s) without
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
(i.e.: the target server has the ideal
configuration that other servers should
@ -53,6 +67,8 @@ usage: ssh-audit.py [options] <host>
(default: 5)
-T, --targets=<hosts.txt> a file containing a list of target hosts (one
per line, format HOST[:PORT])
--threads=<threads> number of threads to use when scanning multiple
targets (-T/--targets) (default: 32)
-v, --verbose verbose output
```
* 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
```
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
```
@ -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]
```
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
```
## Screenshots
### Server Standard Audit Example
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)
### 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)
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:
![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)
### Pre-Built Packages
## Pre-Built Packages
Pre-built packages are available for Windows (see the releases page), on PyPI, Snap, and Homebrew.
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`)
### 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/).
## 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)
- 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.

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
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
echo -e "${REDB}Failed to run docker image! (exit code: $?)${CLR}"
exit 1
@ -699,17 +700,17 @@ run_custom_policy_test 'config2' 'test13' $PROGRAM_RETVAL_GOOD
run_custom_policy_test 'config2' 'test14' $PROGRAM_RETVAL_FAILURE
# 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).
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
echo -e "\n${GREENB}ALL TESTS PASS!${CLR}\n"
rm -rf $TEST_RESULT_DIR
else
echo -e "\n${REDB}${num_failures} TESTS FAILED!${CLR}\n"
fi
rm -rf $TEST_RESULT_DIR
exit 0

View File

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

View File

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

View File

@ -1,7 +1,7 @@
"""
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)
Permission is hereby granted, free of charge, to any person obtaining a copy
@ -55,7 +55,7 @@ class Algorithms:
if self.ssh1kex is None:
return None
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('aut', self.ssh1kex.supported_authentications)
return item
@ -131,7 +131,7 @@ class Algorithms:
# if version is not None:
# software = SSH.Software(None, product, version, None, None)
# 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:
unknown_software = True
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:
self.__sshv = sshv
self.__db = db
self.__storage = {} # type: Dict[str, List[str]]
self.__storage: Dict[str, List[str]] = {}
@property
def sshv(self) -> int:

View File

@ -1,7 +1,7 @@
"""
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)
Permission is hereby granted, free of charge, to any person obtaining a copy
@ -41,45 +41,35 @@ class AuditConf:
self.client_audit = False
self.colors = True
self.json = False
self.json_print_indent = False
self.verbose = False
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.ipv6 = False
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 = None # type: Optional[Policy] # Policy object
self.policy_file: Optional[str] = None # File system path to a policy
self.policy: Optional[Policy] = None # Policy object
self.timeout = 5.0
self.timeout_set = False # Set to True when the user explicitly sets it.
self.target_file = None # type: Optional[str]
self.target_list = [] # type: List[str]
self.target_file: Optional[str] = None
self.target_list: List[str] = []
self.threads = 32
self.list_policies = False
self.lookup = ''
self.manual = False
self.debug = False
def __setattr__(self, name: str, value: Union[str, int, float, bool, Sequence[int]]) -> None:
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)
elif name in ['ipv4', 'ipv6']:
valid = False
value = bool(value)
ipv = 4 if name == 'ipv4' else 6
if value:
value = tuple(list(self.ipvo) + [ipv])
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)
valid, value = True, bool(value)
if len(self.ip_version_preference) == 2: # Being called more than twice is not valid.
valid = False
elif value:
self.ip_version_preference.append(4 if name == 'ipv4' else 6)
elif name == 'port':
valid, port = True, Utils.parse_int(value)
if port < 1 or port > 65535:
@ -96,8 +86,13 @@ class AuditConf:
if value == -1.0:
raise ValueError('invalid timeout: {}'.format(value))
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
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:
object.__setattr__(self, name, value)

View File

@ -1,7 +1,7 @@
"""
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)
Permission is hereby granted, free of charge, to any person obtaining a copy
@ -33,11 +33,11 @@ class Fingerprint:
@property
def md5(self) -> str:
h = hashlib.md5(self.__fpd).hexdigest()
r = u':'.join(h[i:i + 2] for i in range(0, len(h), 2))
return u'MD5:{}'.format(r)
r = ':'.join(h[i:i + 2] for i in range(0, len(h), 2))
return 'MD5:{}'.format(r)
@property
def sha256(self) -> str:
h = base64.b64encode(hashlib.sha256(self.__fpd).digest())
r = h.decode('ascii').rstrip('=')
return u'SHA256:{}'.format(r)
return 'SHA256:{}'.format(r)

View File

@ -1,7 +1,7 @@
"""
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
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
THE SOFTWARE.
"""
import os
# pylint: disable=unused-import
from typing import Dict, List, Set, Sequence, Tuple, Iterable # 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.protocol import Protocol
from ssh_audit.ssh2_kexdb import SSH2_KexDB
from ssh_audit.ssh2_kex import SSH2_Kex
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
@ -40,37 +41,38 @@ class GEXTest:
# Creates a new connection to the server. Returns True on success, or False.
@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():
return True
err = s.connect()
if err is not None:
out.v(err, write_now=True)
return False
unused = None # pylint: disable=unused-variable
unused2 = None # pylint: disable=unused-variable
unused, unused2, err = s.get_banner()
_, _, err = s.get_banner()
if err is not None:
out.v(err, write_now=True)
s.close()
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
# server's own values.
client_kex = SSH2_Kex(os.urandom(16), [gex_alg], kex.key_algorithms, kex.client, kex.server, False, 0)
s.write_byte(Protocol.MSG_KEXINIT)
client_kex.write(s)
s.send_packet()
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)
try:
# 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
# Runs the DH moduli test against the specified target.
@staticmethod
def run(s: 'SSH_Socket', kex: 'SSH2_Kex') -> None:
def run(out: 'OutputBuffer', s: 'SSH_Socket', kex: 'SSH2_Kex') -> None:
GEX_ALGS = {
'diffie-hellman-group-exchange-sha1': KexGroupExchange_SHA1,
'diffie-hellman-group-exchange-sha256': KexGroupExchange_SHA256,
@ -84,13 +86,14 @@ class GEXTest:
# Check if the server supports any of the group-exchange
# 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:
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
kex_group = GEX_ALGS[gex_alg]()
kex_group = kex_group_class()
smallest_modulus = -1
# First try a range of weak sizes.
@ -117,7 +120,9 @@ class GEXTest:
if bits >= smallest_modulus > 0:
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
break

View File

@ -1,7 +1,7 @@
"""
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
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
THE SOFTWARE.
"""
VERSION = 'v2.3.1'
VERSION = 'v2.5.0'
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.
WINDOWS_MAN_PAGE = ''

View File

@ -1,7 +1,7 @@
"""
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
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
THE SOFTWARE.
"""
import os
# pylint: disable=unused-import
from typing import Dict, List, Set, Sequence, Tuple, Iterable # 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.protocol import Protocol
from ssh_audit.ssh2_kex import SSH2_Kex
from ssh_audit.ssh2_kexdb import SSH2_KexDB
from ssh_audit.ssh_socket import SSH_Socket
from ssh_audit.outputbuffer import OutputBuffer
# Obtains host keys, checks their size, and derives their fingerprints.
@ -54,7 +55,7 @@ class HostKeyTest:
}
@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 = {
'diffie-hellman-group1-sha1': KexGroup1,
'diffie-hellman-group14-sha1': KexGroup14_SHA1,
@ -82,10 +83,10 @@ class HostKeyTest:
break
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
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
ca_modulus_size = 0
@ -103,6 +104,8 @@ class HostKeyTest:
# If this host key type is supported by the server, we test it.
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']
variable_key_len = host_key_types[host_key_type]['variable_key_len']
@ -110,34 +113,35 @@ class HostKeyTest:
if not s.is_connected():
err = s.connect()
if err is not None:
out.v(err, write_now=True)
return
_, _, err = s.get_banner()
if err is not None:
out.v(err, write_now=True)
s.close()
return
# Parse the server's initial KEX.
packet_type = 0 # pylint: disable=unused-variable
packet_type, payload = s.read_packet()
SSH2_Kex.parse(payload)
# Send our KEX using the specified group-exchange and most of the server's own values.
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)
# Send the server our KEXINIT message, using only our
# selected kex and host key type. Send the server's own
# list of ciphers and MACs back to it (this doesn't
# matter, really).
client_kex = SSH2_Kex(os.urandom(16), [kex_str], [host_key_type], server_kex.client, server_kex.server, False, 0)
s.write_byte(Protocol.MSG_KEXINIT)
client_kex.write(s)
s.send_packet()
try:
# Parse the server's KEX.
_, payload = s.read_packet()
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
# 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.
kex_group.send_init(s)
host_key = kex_group.recv_reply(s, variable_key_len)
if host_key is not None:
server_kex.set_host_key(host_key_type, host_key)
try:
host_key = kex_group.recv_reply(s, variable_key_len)
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()
ca_modulus_size = kex_group.get_ca_size()
@ -161,12 +165,20 @@ class HostKeyTest:
if (cert is False) and (hostkey_modulus_size < 2048):
for rsa_type in HostKeyTest.RSA_FAMILY:
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
alg_list = SSH2_KexDB.ALGORITHMS['key'][host_key_type]
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)
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 host_key_type in HostKeyTest.RSA_FAMILY:

View File

@ -1,7 +1,7 @@
"""
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)
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
def __init__(self, kex_name: str, hash_alg: str, g: int, p: int) -> None:
self.__kex_name = kex_name
self.__hash_alg = hash_alg
self.__kex_name = kex_name # pylint: disable=unused-private-member
self.__hash_alg = hash_alg # pylint: disable=unused-private-member
self.__g = 0
self.__p = 0
self.__q = 0
@ -46,10 +46,10 @@ class KexDH: # pragma: nocover
self.__e = 0
self.set_params(g, p)
self.__ed25519_pubkey = None # type: Optional[bytes]
self.__hostkey_type = None # type: Optional[bytes]
self.__hostkey_e = 0
self.__hostkey_n = 0
self.__ed25519_pubkey: Optional[bytes] = None # pylint: disable=unused-private-member
self.__hostkey_type: Optional[bytes] = None
self.__hostkey_e = 0 # pylint: disable=unused-private-member
self.__hostkey_n = 0 # pylint: disable=unused-private-member
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).
@ -121,11 +121,11 @@ class KexDH: # pragma: nocover
# The public key exponent.
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.
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
# key.
@ -327,7 +327,7 @@ class KexGroupExchange(KexDH):
s.send_packet()
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.
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)
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)
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
THE SOFTWARE.
"""
import io
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 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':
# pylint: disable=attribute-defined-outside-init
self.__buf = io.StringIO()
self.__stdout = sys.stdout
sys.stdout = self.__buf
self.in_section = True
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:
self.extend(self.__buf.getvalue().splitlines()) # pylint: disable=no-member
sys.stdout = self.__stdout
self.in_section = False
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)
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
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:
# 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
@ -49,15 +49,15 @@ class Policy:
# 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},
@ -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.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
@ -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},
} # 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:
self._name = None # type: Optional[str]
self._version = None # type: Optional[str]
self._banner = None # type: Optional[str]
self._compressions = None # type: Optional[List[str]]
self._host_keys = None # type: Optional[List[str]]
self._optional_host_keys = None # type: Optional[List[str]]
self._kex = None # type: Optional[List[str]]
self._ciphers = None # type: Optional[List[str]]
self._macs = None # type: Optional[List[str]]
self._hostkey_sizes = None # type: Optional[Dict[str, int]]
self._cakey_sizes = None # type: Optional[Dict[str, int]]
self._dh_modulus_sizes = None # type: Optional[Dict[str, int]]
self._name: Optional[str] = None
self._version: Optional[str] = None
self._banner: Optional[str] = None
self._compressions: Optional[List[str]] = None
self._host_keys: Optional[List[str]] = None
self._optional_host_keys: Optional[List[str]] = None
self._kex: Optional[List[str]] = None
self._ciphers: Optional[List[str]] = None
self._macs: Optional[List[str]] = None
self._hostkey_sizes: Optional[Dict[str, int]] = None
self._cakey_sizes: Optional[Dict[str, int]] = None
self._dh_modulus_sizes: Optional[Dict[str, int]] = None
self._server_policy = True
self._name_and_version = '' # type: str
self._name_and_version: str = ''
# Ensure that only one mode was specified.
num_modes = 0
@ -111,7 +117,7 @@ class Policy:
if policy_file is not None:
try:
with open(policy_file, "r") as f:
with open(policy_file, "r", encoding='utf-8') as f:
policy_data = f.read()
except FileNotFoundError:
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.'''
ret = True
errors = [] # type: List[Any]
errors: List[Any] = []
banner_str = str(banner)
if (self._banner is not None) and (banner_str != self._banner):
@ -419,8 +425,8 @@ macs = %s
server_policy_names = []
client_policy_names = []
for policy_name in Policy.BUILTIN_POLICIES:
if Policy.BUILTIN_POLICIES[policy_name]['server_policy']:
for policy_name, policy in Policy.BUILTIN_POLICIES.items():
if policy['server_policy']:
server_policy_names.append(policy_name)
else:
client_policy_names.append(policy_name)

View File

@ -43,14 +43,14 @@ class ReadBuf:
return self._buf.read(size)
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
def read_bool(self) -> bool:
return self.read_byte() != 0
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
def read_list(self) -> List[str]:

View File

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

View File

@ -29,7 +29,7 @@ from ssh_audit.ssh1_crc32 import SSH1_CRC32
class SSH1:
_crc32 = None # type: Optional[SSH1_CRC32]
_crc32: Optional[SSH1_CRC32] = None
CIPHERS = ['none', 'idea', 'des', '3des', 'tss', 'rc4', 'blowfish']
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'
TEXT_CIPHER_IDEA = 'cipher used by commercial SSH'
ALGORITHMS = {
ALGORITHMS: Dict[str, Dict[str, List[List[Optional[str]]]]] = {
'key': {
'ssh-rsa1': [['1.2.2']],
},
@ -55,4 +55,4 @@ class SSH1_KexDB: # pylint: disable=too-few-public-methods
'tis': [['1.2.2']],
'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
def supported_ciphers(self) -> List[str]:
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:
ciphers.append(Utils.to_text(SSH1.CIPHERS[i]))
return ciphers

View File

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

View File

@ -1,7 +1,7 @@
"""
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)
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_OPENSSH61_REMOVE = 'removed since OpenSSH 6.1, removed from specification'
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_DBEAR53_DISABLED = 'disabled since Dropbear SSH 0.53'
FAIL_DEPRECATED_CIPHER = 'deprecated cipher'
@ -46,6 +47,7 @@ class SSH2_KexDB: # pylint: disable=too-few-public-methods
FAIL_DEPRECATED_MAC = 'deprecated MAC'
FAIL_1024BIT_MODULUS = 'using small 1024-bit modulus'
FAIL_UNPROVEN = 'using unproven algorithm'
FAIL_HASH_WEAK = 'using weak hashing algorithm'
WARN_CURVES_WEAK = 'using weak elliptic curves'
WARN_RNDSIG_KEY = 'using weak random number generator could reveal the key'
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_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, ...]]
'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]],
'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-': [[], [], [WARN_HASH_WEAK]],
'gss-group1-sha1-eipGX3TCiQSrx573bT1o1Q==': [[], [FAIL_1024BIT_MODULUS], [WARN_HASH_WEAK]],
'gss-group1-sha1-': [[], [FAIL_1024BIT_MODULUS], [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-sha256-': [[]],
'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']],
'rsa1024-sha1': [[], [FAIL_1024BIT_MODULUS], [WARN_HASH_WEAK]],
'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': [[]],
'Curve25519SHA256': [[]],
'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']],
'ssh-ed25519': [['6.5,l10.7.0']],
'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]],
'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-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)
'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-ssh-dss': [[], [FAIL_1024BIT_MODULUS, FAIL_OPENSSH70_WEAK], [WARN_RNDSIG_KEY]],
'x509v3-ssh-rsa': [[], [], [WARN_HASH_WEAK]],
'ssh-rsa-cert-v00@openssh.com': [['5.4', '6.9'], [FAIL_OPENSSH70_LEGACY], []],
'x509v3-ssh-rsa': [[], [FAIL_HASH_WEAK], [], [INFO_OPENSSH82_FUTURE_DEPRECATION]],
'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-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]],
'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]],
@ -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-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-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]],
'umac-64@openssh.com': [['4.7'], [], [WARN_ENCRYPT_AND_MAC, WARN_TAG_SIZE]],
'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': [[]],
'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]],
'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)
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)
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
THE SOFTWARE.
"""
import concurrent.futures
import copy
import getopt
import json
import os
import re
import sys
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 ssh_audit.globals import VERSION
from ssh_audit.globals import WINDOWS_MAN_PAGE
from ssh_audit.algorithm import Algorithm
from ssh_audit.algorithms import Algorithms
from ssh_audit.auditconf import AuditConf
@ -42,7 +46,6 @@ from ssh_audit import exitcodes
from ssh_audit.fingerprint import Fingerprint
from ssh_audit.gextest import GEXTest
from ssh_audit.hostkeytest import HostKeyTest
from ssh_audit.output import Output
from ssh_audit.outputbuffer import OutputBuffer
from ssh_audit.policy import Policy
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.versionvulnerabilitydb import VersionVulnerabilityDB
try: # pragma: nocover
from colorama import init as colorama_init
colorama_init(strip=False) # pragma: nocover
except ImportError: # pragma: nocover
pass
# Only import colorama under Windows. Other OSes can natively handle terminal colors.
if sys.platform == 'win32':
try:
from colorama import init as colorama_init
colorama_init()
except ImportError:
pass
def usage(err: Optional[str] = None) -> None:
retval = exitcodes.GOOD
uout = Output()
uout = OutputBuffer()
p = os.path.basename(sys.argv[0])
uout.head('# {} {}, https://github.com/jtesta/ssh-audit\n'.format(p, VERSION))
if err is not None and len(err) > 0:
@ -80,34 +84,38 @@ def usage(err: Optional[str] = None) -> None:
uout.info(' -6, --ipv6 enable IPv6 (order of precedence)')
uout.info(' -b, --batch batch output')
uout.info(' -c, --client-audit starts a server on port 2222 to audit client\n software config (use -p to change port;\n use -t to change timeout)')
uout.info(' -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, --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(' -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(' -p, --port=<port> port to connect')
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, --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.sep()
uout.write()
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
with OutputBuffer() as obuf:
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 out:
for algorithm in algorithms:
program_retval = output_algorithm(alg_db, alg_type, algorithm, unknown_algs, program_retval, maxlen, alg_sizes)
if len(obuf) > 0 and not is_json_output:
program_retval = output_algorithm(out, alg_db, alg_type, algorithm, unknown_algs, program_retval, maxlen, alg_sizes)
if not out.is_section_empty() and not is_json_output:
out.head('# ' + title)
obuf.flush()
out.flush_section()
out.sep()
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 + ') '
if alg_max_len == 0:
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
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.
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))
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
if software is None or software.product not in secdb:
return
for line in secdb[software.product]:
vfrom = '' # type: str
vtill = '' # type: str
vfrom: str = ''
vtill: str = ''
vfrom, vtill = line[0:2]
if not software.between_versions(vfrom, vtill):
continue
target = 0 # type: int
name = '' # type: str
target: int = 0
name: str = ''
target, name = line[2:4]
is_server = target & 1 == 1
is_client = target & 2 == 2
@ -227,8 +235,8 @@ def output_security_sub(sub: str, software: Optional[Software], client_audit: bo
continue
p = '' if out.batch else ' ' * (padlen - len(name))
if sub == 'cve':
cvss = 0.0 # type: float
descr = '' # type: str
cvss: float = 0.0
descr: str = ''
cvss, descr = line[4:6]
# 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))
def output_security(banner: Optional[Banner], client_audit: bool, padlen: int, is_json_output: bool) -> None:
with OutputBuffer() as obuf:
def output_security(out: OutputBuffer, banner: Optional[Banner], client_audit: bool, padlen: int, is_json_output: bool) -> None:
with out:
if banner is not None:
software = Software.parse(banner)
output_security_sub('cve', software, client_audit, padlen)
output_security_sub('txt', software, client_audit, padlen)
if len(obuf) > 0 and not is_json_output:
output_security_sub(out, 'cve', software, client_audit, padlen)
output_security_sub(out, 'txt', software, client_audit, padlen)
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')
obuf.flush()
out.flush_section()
out.sep()
def output_fingerprints(algs: Algorithms, is_json_output: bool, sha256: bool = True) -> None:
with OutputBuffer() as obuf:
def output_fingerprints(out: OutputBuffer, algs: Algorithms, is_json_output: bool) -> None:
with out:
fps = []
if algs.ssh1kex is not None:
name = 'ssh-rsa1'
@ -280,18 +291,20 @@ def output_fingerprints(algs: Algorithms, is_json_output: bool, sha256: bool = T
fps = sorted(fps)
for fpp in fps:
name, fp = fpp
fpo = fp.sha256 if sha256 else fp.md5
# p = '' if out.batch else ' ' * (padlen - len(name))
# out.good('(fin) {0}{1} -- {2} {3}'.format(name, p, bits, fpo))
out.good('(fin) {}: {}'.format(name, fpo))
if len(obuf) > 0 and not is_json_output:
out.good('(fin) {}: {}'.format(name, fp.sha256))
# Output the MD5 hash too if verbose mode is enabled.
if out.verbose:
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')
obuf.flush()
out.flush_section()
out.sep()
# 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
# 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
for_server = True
with OutputBuffer() as obuf:
with out:
software, alg_rec = algs.get_recommendations(software, for_server)
for sshv in range(2, 0, -1):
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 ''
fm = '(rec) {0}{1}{2}-- {3} algorithm to {4}{5} {6}'
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:
title = '(for {})'.format(software.display(False))
else:
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()
return ret
# Output additional information & notes.
def output_info(software: Optional['Software'], client_audit: bool, any_problems: bool, is_json_output: bool) -> None:
with OutputBuffer() as obuf:
def output_info(out: OutputBuffer, software: Optional['Software'], client_audit: bool, any_problems: bool, is_json_output: bool) -> None:
with out:
# Tell user that PuTTY cannot be hardened at the protocol-level.
if client_audit and (software is not None) and (software.product == Product.PuTTY):
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:
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')
obuf.flush()
out.flush_section()
out.sep()
# 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
client_audit = client_host is not None # If set, this is a client audit.
sshv = 1 if pkm is not None else 2
algs = Algorithms(pkm, kex)
with OutputBuffer() as obuf:
with out:
if print_target:
host = aconf.host
@ -405,18 +418,23 @@ def output(aconf: AuditConf, banner: Optional[Banner], header: List[str], client
if len(header) > 0:
out.info('(gen) header: ' + '\n'.join(header))
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:
# NOTE: RFC 4253, Section 4.2
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)
if software is not None:
out.good('(gen) software: {}'.format(software))
else:
software = None
output_compatibility(algs, client_audit)
output_compatibility(out, algs, client_audit)
if kex is not None:
compressions = [x for x in kex.server.compression if x != 'none']
if len(compressions) > 0:
@ -424,47 +442,49 @@ def output(aconf: AuditConf, banner: Optional[Banner], header: List[str], client
else:
cmptxt = 'disabled'
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')
obuf.flush()
out.flush_section()
out.sep()
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.
unknown_algorithms = [] # type: List[str]
unknown_algorithms: List[str] = []
if pkm is not None:
adb = SSH1_KexDB.ALGORITHMS
ciphers = pkm.supported_ciphers
auths = pkm.supported_authentications
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'
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'
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:
adb = SSH2_KexDB.ALGORITHMS
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'
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'
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'
program_retval = output_algorithms(title, adb, atype, kex.server.mac, unknown_algorithms, aconf.json, program_retval, maxlen)
output_fingerprints(algs, aconf.json, True)
perfect_config = output_recommendations(algs, software, aconf.json, maxlen)
output_info(software, client_audit, not perfect_config, aconf.json)
program_retval = output_algorithms(out, title, adb, atype, kex.server.mac, unknown_algorithms, aconf.json, program_retval, maxlen)
output_fingerprints(out, algs, aconf.json)
perfect_config = output_recommendations(out, algs, software, aconf.json, maxlen)
output_info(out, software, client_audit, not perfect_config, 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.
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
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:
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)
if aconf.json:
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:
spacing = ''
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: '.
else:
host = aconf.host
@ -487,9 +507,9 @@ def evaluate_policy(aconf: AuditConf, banner: Optional['Banner'], client_host: O
else:
host = '%s:%d' % (aconf.host, aconf.port)
print("Host: %s" % host)
print("Policy: %s%s" % (spacing, aconf.policy.get_name_and_version()))
print("Result: %s" % spacing, end='')
out.info("Host: %s" % host)
out.info("Policy: %s%s" % (spacing, aconf.policy.get_name_and_version()))
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).
icon_good = ""
@ -507,29 +527,31 @@ def evaluate_policy(aconf: AuditConf, banner: Optional['Banner'], client_host: O
return passed
def list_policies() -> None:
def list_policies(out: OutputBuffer) -> None:
'''Prints a list of server & client policies.'''
server_policy_names, client_policy_names = Policy.list_builtin_policies()
if len(server_policy_names) > 0:
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:
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:
print("Error: no built-in policies found!")
out.fail("Error: no built-in policies found!")
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:
# 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:
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).
succeeded = True
try:
with open(aconf.policy_file, 'x') as f:
with open(aconf.policy_file, 'x', encoding='utf-8') as f:
f.write(policy_data)
except FileExistsError:
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)
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
aconf = AuditConf()
try:
sopts = 'h1246M:p:P:jbcnvl:t:T:L'
lopts = ['help', 'ssh1', 'ssh2', 'ipv4', 'ipv6', 'make-policy=', 'port=', 'policy=', 'json', 'batch', 'client-audit', 'no-colors', 'verbose', 'level=', 'timeout=', 'targets=', 'list-policies', 'lookup=']
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=', 'threads=', 'manual', 'debug']
opts, args = getopt.gnu_getopt(args, sopts, lopts)
except getopt.GetoptError as err:
usage_cb(str(err))
aconf.ssh1, aconf.ssh2 = False, False
host = '' # type: str
oport = None # type: Optional[str]
port = 0 # type: int
host: str = ''
oport: Optional[str] = None
port: int = 0
for o, a in opts:
if o in ('-h', '--help'):
usage_cb()
@ -585,10 +607,15 @@ def process_commandline(args: List[str], usage_cb: Callable[..., None]) -> 'Audi
aconf.client_audit = True
elif o in ('-n', '--no-colors'):
aconf.colors = False
out.use_colors = False
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'):
aconf.verbose = True
out.verbose = True
elif o in ('-l', '--level'):
if a not in ('info', 'warn', 'fail'):
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
elif o in ('-T', '--targets'):
aconf.target_file = a
elif o == '--threads':
aconf.threads = int(a)
elif o in ('-L', '--list-policies'):
aconf.list_policies = True
elif o == '--lookup':
aconf.lookup = a
elif o in ('-m', '--manual'):
aconf.manual = True
elif o in ('-d', '--debug'):
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()
if aconf.manual:
return aconf
if aconf.lookup != '':
return aconf
if aconf.list_policies:
list_policies()
list_policies(out)
sys.exit(exitcodes.GOOD)
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 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()
# 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:
aconf.policy = Policy(policy_file=aconf.policy_file)
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)
# 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():
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)
# 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:
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)
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_protocol = None
@ -687,25 +727,30 @@ def build_struct(banner: Optional['Banner'], kex: Optional['SSH2_Kex'] = None, p
banner_software = banner.software
banner_comments = banner.comments
res = {
res: Any = {
"banner": {
"raw": banner_str,
"protocol": banner_protocol,
"software": banner_software,
"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:
res['client_ip'] = client_host
else:
res['target'] = target_host
if kex is not None:
res['compression'] = kex.server.compression
res['kex'] = []
alg_sizes = kex.dh_modulus_sizes()
for algorithm in kex.kex_algorithms:
entry = {
entry: Any = {
'algorithm': algorithm,
} # type: Any
}
if algorithm in alg_sizes:
hostkey_size, ca_size = alg_sizes[algorithm]
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).
if '-cert-' in host_key_type:
continue
entry = {
'type': host_key_type,
'fp': fp.sha256,
}
res['fingerprints'].append(entry)
# Add the SHA256 and MD5 fingerprints.
res['fingerprints'].append({
'hostkey': host_key_type,
'hash_alg': 'SHA256',
'hash': fp.sha256[7:]
})
res['fingerprints'].append({
'hostkey': host_key_type,
'hash_alg': 'MD5',
'hash': fp.md5[4:]
})
else:
pkm_supported_ciphers = 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.
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
out.batch = aconf.batch
out.verbose = aconf.verbose
out.debug = aconf.debug
out.level = aconf.level
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:
out.v("Listening for client connection on port %d..." % aconf.port, write_now=True)
s.listen_and_accept()
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()
if err is not None:
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:
sshv = 2 if aconf.ssh2 else 1
@ -798,7 +861,7 @@ def audit(aconf: AuditConf, sshv: Optional[int] = None, print_target: bool = Fal
else:
err = '[exception] did not receive banner: {}'.format(err)
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)
if packet_type < 0:
@ -806,12 +869,14 @@ def audit(aconf: AuditConf, sshv: Optional[int] = None, print_target: bool = Fal
if len(payload) > 0:
payload_txt = payload.decode('utf-8')
else:
payload_txt = u'empty'
payload_txt = 'empty'
except UnicodeDecodeError:
payload_txt = u'"{}"'.format(repr(payload).lstrip('b')[1:-1])
if payload_txt == u'Protocol major versions differ.':
payload_txt = '"{}"'.format(repr(payload).lstrip('b')[1:-1])
if payload_txt == 'Protocol major versions differ.':
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)
else:
err_pair = None
@ -824,24 +889,29 @@ def audit(aconf: AuditConf, sshv: Optional[int] = None, print_target: bool = Fal
'instead received unknown message ({2})'
err = fmt.format(err_pair[0], err_pair[1], packet_type)
if err is not None:
output(aconf, banner, header)
output(out, aconf, banner, header)
out.fail(err)
return exitcodes.CONNECTION_ERROR
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:
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:
HostKeyTest.run(s, kex)
GEXTest.run(s, kex)
HostKeyTest.run(out, s, kex)
GEXTest.run(out, s, kex)
# This is a standard audit scan.
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.
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.
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
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.'''
retval = exitcodes.GOOD
alg_types = {
@ -879,13 +949,13 @@ def algorithm_lookup(alg_names: str) -> int:
for (outer_k, outer_v) in adb.items()
}
unknown_algorithms = [] # type: List[str]
unknown_algorithms: List[str] = []
padding = len(max(algorithm_names, key=len))
for alg_type in alg_types:
if len(algorithms_dict[alg_type]) > 0:
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 = [
alg_name
@ -902,8 +972,8 @@ def algorithm_lookup(alg_names: str) -> int:
similar_algorithms = [
alg_unknown + " --> (" + alg_type + ") " + alg_name
for alg_unknown in algorithms_not_found
for alg_type in adb.keys()
for alg_name in adb[alg_type]
for alg_type, alg_names in adb.items()
for alg_name in alg_names
# Perform a case-insensitive comparison using 'casefold'
# and match substrings using the 'in' operator.
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:
out.fail(algorithm_not_found)
print()
out.sep()
if len(similar_algorithms) > 0:
retval = exitcodes.FAILURE
@ -926,14 +996,77 @@ def algorithm_lookup(alg_names: str) -> int:
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:
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 != '':
retval = algorithm_lookup(aconf.lookup)
retval = algorithm_lookup(out, aconf.lookup)
out.write()
sys.exit(retval)
# If multiple targets were specified...
@ -945,31 +1078,46 @@ def main() -> int:
print('[', end='')
# Loop through each target in the list.
for i, target in enumerate(aconf.target_list):
aconf.host, port = Utils.parse_host_and_port(target)
if port == 0:
port = 22
aconf.port = port
target_servers = []
for _, target in enumerate(aconf.target_list):
host, port = Utils.parse_host_and_port(target, default_port=22)
target_servers.append((host, 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.
if (new_ret == exitcodes.UNKNOWN_ERROR) or (new_ret == exitcodes.FAILURE) or ((new_ret == exitcodes.WARNING) and (ret == exitcodes.GOOD)):
ret = new_ret
# Queue all worker threads.
num_target_servers = len(target_servers)
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 i + 1 != len(aconf.target_list):
if aconf.json:
print(", ", end='')
else:
print(("-" * 80) + "\n")
# If this worker's return code is ranked higher that what we've cached so far, update our cache.
if ranked_return_codes.index(worker_ret) > ranked_return_codes.index(ret):
ret = worker_ret
# print("Worker for %s:%d returned %d: [%s]" % (target_server[0], target_server[1], worker_ret, worker_output))
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:
print(']')
return ret
else:
return audit(aconf)
else: # Just a scan against a single target.
ret = audit(out, aconf)
out.write()
return ret
if __name__ == '__main__': # pragma: nocover

View File

@ -1,7 +1,7 @@
"""
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)
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.banner import Banner
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.readbuf import ReadBuf
from ssh_audit.ssh1 import SSH1
@ -52,14 +52,15 @@ class SSH_Socket(ReadBuf, WriteBuf):
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__()
self.__sock = None # type: Optional[socket.socket]
self.__sock_map = {} # type: Dict[int, socket.socket]
self.__outputbuffer = outputbuffer
self.__sock: Optional[socket.socket] = None
self.__sock_map: Dict[int, socket.socket] = {}
self.__block_size = 8
self.__state = 0
self.__header = [] # type: List[str]
self.__banner = None # type: Optional[Banner]
self.__header: List[str] = []
self.__banner: Optional[Banner] = None
if host is None:
raise ValueError('undefined host')
nport = Utils.parse_int(port)
@ -67,35 +68,30 @@ class SSH_Socket(ReadBuf, WriteBuf):
raise ValueError('invalid port: {}'.format(port))
self.__host = host
self.__port = nport
if ipvo is not None:
self.__ipvo = ipvo
else:
self.__ipvo = ()
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.__timeout = timeout
self.__timeout_set = timeout_set
self.client_host = None # type: Optional[str]
self.client_host: Optional[str] = None
self.client_port = None
def _resolve(self, ipvo: Sequence[int]) -> Iterable[Tuple[int, Tuple[Any, ...]]]:
ipvo = tuple([x for x in Utils.unique_seq(ipvo) if x in (4, 6)])
ipvo_len = len(ipvo)
prefer_ipvo = ipvo_len > 0
prefer_ipv4 = prefer_ipvo and ipvo[0] == 4
if ipvo_len == 1:
family = socket.AF_INET if ipvo[0] == 4 else socket.AF_INET6
def _resolve(self) -> Iterable[Tuple[int, Tuple[Any, ...]]]:
# If __ip_version_preference has only one entry, then it means that ONLY that IP version should be used.
if len(self.__ip_version_preference) == 1:
family = socket.AF_INET if self.__ip_version_preference[0] == 4 else socket.AF_INET6
else:
family = socket.AF_UNSPEC
try:
stype = socket.SOCK_STREAM
r = socket.getaddrinfo(self.__host, self.__port, family, stype)
if prefer_ipvo:
r = sorted(r, key=lambda x: x[0], reverse=not prefer_ipv4)
check = any(stype == rline[2] for rline in r)
# 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.
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:
if not check or socktype == socket.SOCK_STREAM:
if socktype == socket.SOCK_STREAM:
yield af, addr
except socket.error as e:
Output().fail('[exception] {}'.format(e))
self.__outputbuffer.fail('[exception] {}'.format(e)).write()
sys.exit(exitcodes.CONNECTION_ERROR)
# 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]:
'''Returns None on success, or an error string.'''
err = None
for af, addr in self._resolve(self.__ipvo):
for af, addr in self._resolve():
s = None
try:
s = socket.socket(af, socket.SOCK_STREAM)
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)
self.__sock = s
return None
@ -175,6 +172,8 @@ class SSH_Socket(ReadBuf, WriteBuf):
return '[exception] {}'.format(errm)
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:
return self.__banner, self.__header, 'not connected'
if self.__banner is not None:
@ -230,15 +229,11 @@ class SSH_Socket(ReadBuf, WriteBuf):
except socket.error as e:
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.'''
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']
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 = ['']
self.__outputbuffer.d('KEX initialisation...', write_now=True)
kexparty = SSH2_KexParty(ciphers, macs, compressions, languages)
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
check_size = 4 + 1 + payload_length + padding_length
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)
self.ensure_read(payload_length)
if sshv == 1:
@ -294,7 +289,7 @@ class SSH_Socket(ReadBuf, WriteBuf):
if sshv == 1:
rcrc = SSH1.crc32(padding + payload)
if crc != rcrc:
Output().fail('[exception] packet checksum CRC32 mismatch.')
self.__outputbuffer.fail('[exception] packet checksum CRC32 mismatch.').write()
sys.exit(exitcodes.CONNECTION_ERROR)
else:
self.ensure_read(padding_length)
@ -343,6 +338,6 @@ class SSH_Socket(ReadBuf, WriteBuf):
def __cleanup(self) -> None:
self._close_socket(self.__sock)
for fd in self.__sock_map:
self._close_socket(self.__sock_map[fd])
for sock in self.__sock_map.values():
self._close_socket(sock)
self.__sock = None

View File

@ -30,12 +30,12 @@ from ssh_audit.algorithm import Algorithm
class Timeframe:
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:
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))
def __str__(self) -> str:
@ -51,7 +51,7 @@ class Timeframe:
return self[product][1 if bool(for_server) else 3]
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 v in (versions or '').split(','):
ssh_prod, ssh_ver, is_cli = Algorithm.get_ssh_version(v)

View File

@ -96,7 +96,7 @@ class Utils:
@classmethod
def unique_seq(cls, seq: Sequence[Any]) -> Sequence[Any]:
seen = set() # type: Set[Any]
seen: Set[Any] = set()
def _seen_add(x: Any) -> bool:
seen.add(x)
@ -129,10 +129,10 @@ class Utils:
return -1.0
@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.'''
host = host_and_port
port = 0
port = default_port
mx = re.match(r'^\[([^\]]+)\](?::(\d+))?$', host_and_port)
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
# = 1. If it affects servers, but is a local issue only,
# then affected = 1 + 4 = 5.
CVE = {
CVE: Dict[str, List[List[Any]]] = {
'Dropbear SSH': [
['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'],
@ -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-4559', 6.8, 'cause DoS or execute arbitrary code (double free)']],
'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)'],
['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)'],
@ -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.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': [
['0.28', '0.34', 1, 'remote root exploit', 'remote format string buffer overflow exploit (exploit-db#387)']],
'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, 'integer overflow', 'integer overflow in "buffer_get_data"'],
['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)
def write_list(self, v: List[str]) -> 'WriteBuf':
return self.write_string(u','.join(v))
return self.write_string(','.join(v))
@classmethod
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
\fBssh-audit\fP \- SSH server & client configuration auditor
.SH SYNOPSIS
@ -46,10 +46,15 @@ Enables grepable output.
.br
Starts a server on port 2222 to audit client software configuration. Use -p/--port=<port> to change port and -t/--timeout=<secs> to change listen timeout.
.TP
.B -d, \-\-debug
.br
Enable debug output.
.TP
.B -j, \-\-json
.br
Output results in JSON format.
Output results in JSON format. Specify twice (-jj) to enable indent printing (useful for debugging).
.TP
.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
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
.B -M, \-\-make-policy=<custom_policy.txt>
.br
@ -94,7 +104,12 @@ The timeout, in seconds, for creating connections and reading data from the sock
.TP
.B -T, \-\-targets=<hosts.txt>
.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
.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
`- [info] available since OpenSSH 5.7, Dropbear SSH 2013.62
(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] 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
 `- [fail] removed (in server) and disabled (in client) since OpenSSH 7.0, weak algorithm
 `- [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
(gen) banner: SSH-1.99-OpenSSH_4.0
(gen) banner: SSH-1.99-OpenSSH_4.0
(gen) protocol SSH1 enabled
(gen) software: OpenSSH 4.0
(gen) compatibility: OpenSSH 3.9-6.6, Dropbear SSH 0.53+ (some functionality from 0.52)
(gen) compression: enabled (zlib)
# 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-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
@ -24,6 +25,7 @@
(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-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
(kex) diffie-hellman-group-exchange-sha1 (1024-bit) -- [fail] using small 1024-bit modulus
@ -39,8 +41,9 @@
# host-key algorithms
(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] 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
 `- [fail] removed (in server) and disabled (in client) since OpenSSH 7.0, weak algorithm
 `- [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)
# 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-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
@ -33,8 +34,9 @@
# host-key algorithms
(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] 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
 `- [fail] removed (in server) and disabled (in client) since OpenSSH 7.0, weak algorithm
 `- [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)
# 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-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
@ -33,11 +34,13 @@
# host-key algorithms
(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] 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
 `- [warn] using small 1024-bit modulus
 `- [fail] using small 1024-bit modulus
`- [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)
(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)
# 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-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
@ -33,11 +34,13 @@
# host-key algorithms
(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] 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
 `- [warn] using small 1024-bit modulus
 `- [fail] using small 1024-bit modulus
`- [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)
(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)
# 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-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
@ -34,9 +35,11 @@
# host-key algorithms
(key) ssh-rsa (3072-bit) -- [fail] using weak hashing algorithm
`- [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
 `- [warn] using small 1024-bit modulus
 `- [fail] using small 1024-bit modulus
`- [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)
(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)
# 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-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
@ -34,8 +35,10 @@
# host-key algorithms
(key) ssh-rsa (3072-bit) -- [fail] using weak hashing algorithm
`- [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
`- [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)
(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) ssh-rsa (3072-bit) -- [fail] using weak hashing algorithm
`- [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
 `- [warn] using weak random number generator could reveal the key
`- [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
(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
# key exchange algorithms

View File

@ -7,6 +7,7 @@ class TestAuditConf:
@pytest.fixture(autouse=True)
def init(self, ssh_audit):
self.AuditConf = ssh_audit.AuditConf
self.OutputBuffer = ssh_audit.OutputBuffer
self.usage = ssh_audit.usage
self.process_commandline = process_commandline
@ -21,9 +22,8 @@ class TestAuditConf:
'colors': True,
'verbose': False,
'level': 'info',
'ipv4': True,
'ipv6': True,
'ipvo': ()
'ipv4': False,
'ipv6': False
}
for k, v in kwargs.items():
options[k] = v
@ -37,7 +37,6 @@ class TestAuditConf:
assert conf.level == options['level']
assert conf.ipv4 == options['ipv4']
assert conf.ipv6 == options['ipv6']
assert conf.ipvo == options['ipvo']
def test_audit_conf_defaults(self):
conf = self.AuditConf()
@ -63,57 +62,38 @@ class TestAuditConf:
conf.port = port
excinfo.match(r'.*invalid port.*')
def test_audit_conf_ipvo(self):
def test_audit_conf_ip_version_preference(self):
# ipv4-only
conf = self.AuditConf()
conf.ipv4 = True
assert conf.ipv4 is True
assert conf.ipv6 is False
assert conf.ipvo == (4,)
assert conf.ip_version_preference == [4]
# ipv6-only
conf = self.AuditConf()
conf.ipv6 = True
assert conf.ipv4 is False
assert conf.ipv6 is True
assert conf.ipvo == (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, )
assert conf.ip_version_preference == [6]
# ipv4-preferred
conf = self.AuditConf()
conf.ipv4 = True
conf.ipv6 = True
assert conf.ipv4 is True
assert conf.ipv6 is True
assert conf.ipvo == (4, 6)
assert conf.ip_version_preference == [4, 6]
# ipv6-preferred
conf = self.AuditConf()
conf.ipv6 = True
conf.ipv4 = True
assert conf.ipv4 is True
assert conf.ipv6 is True
assert conf.ipvo == (6, 4)
# ipvo empty
assert conf.ip_version_preference == [6, 4]
# defaults
conf = self.AuditConf()
conf.ipvo = ()
assert conf.ipv4 is True
assert conf.ipv6 is True
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)
assert conf.ipv4 is False
assert conf.ipv6 is False
assert conf.ip_version_preference == []
def test_audit_conf_level(self):
conf = self.AuditConf()
@ -127,7 +107,7 @@ class TestAuditConf:
def test_audit_conf_process_commandline(self):
# 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):
conf = c('')
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-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']:
assert key in rv

View File

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

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@ import pytest
from ssh_audit.auditconf import AuditConf
from ssh_audit.fingerprint import Fingerprint
from ssh_audit.outputbuffer import OutputBuffer
from ssh_audit.protocol import Protocol
from ssh_audit.readbuf import ReadBuf
from ssh_audit.ssh1 import SSH1
@ -15,6 +16,7 @@ from ssh_audit.writebuf import WriteBuf
class TestSSH1:
@pytest.fixture(autouse=True)
def init(self, ssh_audit):
self.OutputBuffer = OutputBuffer
self.protocol = Protocol
self.ssh1 = SSH1
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(self._create_ssh1_packet(w.write_flush()))
output_spy.begin()
self.audit(self._conf())
out = self.OutputBuffer()
self.audit(out, self._conf())
out.write()
lines = output_spy.flush()
assert len(lines) == 13
assert len(lines) == 17
def test_ssh1_server_invalid_first_packet(self, output_spy, 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(self._create_ssh1_packet(w.write_flush()))
output_spy.begin()
ret = self.audit(self._conf())
out = self.OutputBuffer()
ret = self.audit(out, self._conf())
out.write()
assert ret != 0
lines = output_spy.flush()
assert len(lines) == 7
assert len(lines) == 10
assert 'unknown message' in lines[-1]
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(self._create_ssh1_packet(w.write_flush(), False))
output_spy.begin()
out = self.OutputBuffer()
with pytest.raises(SystemExit):
self.audit(self._conf())
self.audit(out, self._conf())
out.write()
lines = output_spy.flush()
assert len(lines) == 1
assert 'checksum' in lines[-1]
assert len(lines) == 3
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
from ssh_audit.auditconf import AuditConf
from ssh_audit.outputbuffer import OutputBuffer
from ssh_audit.protocol import Protocol
from ssh_audit.readbuf import ReadBuf
from ssh_audit.ssh2_kex import SSH2_Kex
@ -15,6 +16,7 @@ from ssh_audit.writebuf import WriteBuf
class TestSSH2:
@pytest.fixture(autouse=True)
def init(self, ssh_audit):
self.OutputBuffer = OutputBuffer
self.protocol = Protocol
self.ssh2_kex = SSH2_Kex
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(self._create_ssh2_packet(w.write_flush()))
output_spy.begin()
self.audit(self._conf())
out = self.OutputBuffer()
self.audit(out, self._conf())
out.write()
lines = output_spy.flush()
assert len(lines) == 67
assert len(lines) == 70
def test_ssh2_server_invalid_first_packet(self, output_spy, 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(self._create_ssh2_packet(w.write_flush()))
output_spy.begin()
ret = self.audit(self._conf())
out = self.OutputBuffer()
ret = self.audit(out, self._conf())
out.write()
assert ret != 0
lines = output_spy.flush()
assert len(lines) == 3
assert len(lines) == 5
assert 'unknown message' in lines[-1]

30
tox.ini
View File

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