mirror of
https://github.com/jtesta/ssh-audit.git
synced 2025-07-06 05:57:50 -05:00
Compare commits
60 Commits
Author | SHA1 | Date | |
---|---|---|---|
4f2f995b62 | |||
134236fa7f | |||
a6b658d194 | |||
297a807f88 | |||
20d94df400 | |||
b76060cf49 | |||
1cf1c874db | |||
992d8233c9 | |||
f377b7cea3 | |||
70d9ab2e6b | |||
e7d320f602 | |||
682cb66f85 | |||
076681a671 | |||
98a1fb0315 | |||
45da9f20ae | |||
aa21df29e7 | |||
32ed9242af | |||
07862489c4 | |||
e508a963e7 | |||
2f1a2a60b1 | |||
5eb669e01c | |||
8e9fe20fac | |||
83bd049486 | |||
c483fe1861 | |||
741bd631e2 | |||
f96c0501e9 | |||
446a411424 | |||
b300ad1252 | |||
1bbc3feb57 | |||
8f9771c4e6 | |||
8a8c284d9a | |||
1b7cfbec71 | |||
3c0fc8ead4 | |||
ef831d17e0 | |||
36094611ce | |||
49cf91a902 | |||
11e2e77585 | |||
090b5d760b | |||
7878d66a46 | |||
730d6904c2 | |||
e0f0956edc | |||
d42725652f | |||
6b67a2efb3 | |||
c49a0fb22f | |||
dbe14a075e | |||
13d15baa2a | |||
bbb81e24ab | |||
bbbd75ee69 | |||
60de5e55cb | |||
4e2f9da632 | |||
287c551ff8 | |||
d9a4b49560 | |||
a4c78512d8 | |||
1ba4c7c7ca | |||
338ffc5adb | |||
52d1e8f27b | |||
00dc22b00b | |||
0d9881966c | |||
5c8dc5105b | |||
75be333bd2 |
24
.github/workflows/tox.yaml
vendored
Normal file
24
.github/workflows/tox.yaml
vendored
Normal 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
3
.gitignore
vendored
@ -22,3 +22,6 @@ reports/
|
|||||||
/snap/
|
/snap/
|
||||||
/stage/
|
/stage/
|
||||||
/ssh-audit_*.snap
|
/ssh-audit_*.snap
|
||||||
|
|
||||||
|
# Your local server config
|
||||||
|
servers.txt
|
||||||
|
@ -1,11 +1,16 @@
|
|||||||
language: python
|
language: python
|
||||||
|
|
||||||
|
arch:
|
||||||
|
- arm64
|
||||||
|
- amd64
|
||||||
|
- ppc64le
|
||||||
|
|
||||||
python:
|
python:
|
||||||
- "3.5"
|
|
||||||
- "3.6"
|
- "3.6"
|
||||||
- "3.7"
|
- "3.7"
|
||||||
- "3.8"
|
- "3.8"
|
||||||
- "3.9"
|
- "3.9"
|
||||||
|
- "3.10"
|
||||||
|
|
||||||
cache:
|
cache:
|
||||||
- pip
|
- pip
|
||||||
|
@ -11,26 +11,8 @@ However, if you can submit patches that pass all of our automated tests, then yo
|
|||||||
|
|
||||||
Tox is used to do unit testing, linting with [pylint](http://pylint.pycqa.org/en/latest/) & [flake8](https://flake8.pycqa.org/en/latest/), and static type-checking with [mypy](https://mypy.readthedocs.io/en/stable/).
|
Tox is used to do unit testing, linting with [pylint](http://pylint.pycqa.org/en/latest/) & [flake8](https://flake8.pycqa.org/en/latest/), and static type-checking with [mypy](https://mypy.readthedocs.io/en/stable/).
|
||||||
|
|
||||||
### Running tests on Ubuntu 18.04 and later
|
|
||||||
|
|
||||||
For Ubuntu 18.04 or later, install tox with `apt install tox`, then simply run `tox` in the top-level directory. Look for any error messages in the (verbose) output.
|
For Ubuntu 18.04 or later, install tox with `apt install tox`, then simply run `tox` in the top-level directory. Look for any error messages in the (verbose) output.
|
||||||
|
|
||||||
### Running tests on Ubuntu 16.04
|
|
||||||
|
|
||||||
For Ubuntu 16.04 (which is still supported until April 2021), a newer version of tox is needed. The easiest way is to use virtualenv:
|
|
||||||
```
|
|
||||||
$ sudo apt install python3-virtualenv
|
|
||||||
$ virtualenv -p /usr/bin/python3 ~/venv_ssh-audit
|
|
||||||
$ source ~/venv_ssh-audit/bin/activate
|
|
||||||
$ pip install tox
|
|
||||||
```
|
|
||||||
Then, to run the tox tests:
|
|
||||||
```
|
|
||||||
$ source ~/venv_ssh-audit/bin/activate
|
|
||||||
$ cd path/to/ssh-audit
|
|
||||||
$ tox
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Docker Tests
|
## Docker Tests
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
FROM python:3.8-slim
|
FROM python:3.9-slim
|
||||||
|
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
|
|
||||||
|
52
PACKAGING
52
PACKAGING
@ -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
97
PACKAGING.md
Normal 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
|
||||||
|
```
|
70
README.md
70
README.md
@ -1,14 +1,25 @@
|
|||||||
# ssh-audit
|
# ssh-audit
|
||||||
[](https://travis-ci.org/jtesta/ssh-audit)
|
[](https://github.com/jtesta/ssh-audit/blob/master/LICENSE)
|
||||||
<!--
|
[](https://pypi.org/project/ssh-audit/)
|
||||||
[](https://ci.appveyor.com/project/arthepsy/ssh-audit)
|
[](https://hub.docker.com/r/positronsecurity/ssh-audit)
|
||||||
[](https://codecov.io/gh/arthepsy/ssh-audit)
|
[](https://github.com/jtesta/ssh-audit/actions)
|
||||||
[](https://sq.evolutiongaming.com/dashboard?id=arthepsy-github%3Assh-audit%3Adevelop)
|
[](https://github.com/jtesta/ssh-audit/blob/master/CONTRIBUTING.md)
|
||||||
-->
|
|
||||||
**ssh-audit** is a tool for ssh server & client configuration auditing.
|
**ssh-audit** is a tool for ssh server & client configuration auditing.
|
||||||
|
|
||||||
[jtesta/ssh-audit](https://github.com/jtesta/ssh-audit/) (v2.0+) is the updated and maintained version of ssh-audit forked from [arthepsy/ssh-audit](https://github.com/arthepsy/ssh-audit) (v1.x) due to inactivity.
|
[jtesta/ssh-audit](https://github.com/jtesta/ssh-audit/) (v2.0+) is the updated and maintained version of ssh-audit forked from [arthepsy/ssh-audit](https://github.com/arthepsy/ssh-audit) (v1.x) due to inactivity.
|
||||||
|
|
||||||
|
- [Features](#features)
|
||||||
|
- [Usage](#usage)
|
||||||
|
- [Screenshots](#screenshots)
|
||||||
|
- [Server Standard Audit Example](#server-standard-audit-example)
|
||||||
|
- [Server Policy Audit Example](#server-policy-audit-example)
|
||||||
|
- [Client Standard Audit Example](#client-standard-audit-example)
|
||||||
|
- [Hardening Guides](#hardening-guides)
|
||||||
|
- [Pre-Built Packages](#pre-built-packages)
|
||||||
|
- [Web Front-End](#web-front-end)
|
||||||
|
- [ChangeLog](#changelog)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- SSH1 and SSH2 protocol server support;
|
- SSH1 and SSH2 protocol server support;
|
||||||
- analyze SSH client configuration;
|
- analyze SSH client configuration;
|
||||||
@ -21,6 +32,7 @@
|
|||||||
- historical information from OpenSSH, Dropbear SSH and libssh;
|
- historical information from OpenSSH, Dropbear SSH and libssh;
|
||||||
- policy scans to ensure adherence to a hardened/standard configuration;
|
- policy scans to ensure adherence to a hardened/standard configuration;
|
||||||
- runs on Linux and Windows;
|
- runs on Linux and Windows;
|
||||||
|
- supports Python 3.6 - 3.9;
|
||||||
- no dependencies
|
- no dependencies
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
@ -36,11 +48,13 @@ usage: ssh-audit.py [options] <host>
|
|||||||
-c, --client-audit starts a server on port 2222 to audit client
|
-c, --client-audit starts a server on port 2222 to audit client
|
||||||
software config (use -p to change port;
|
software config (use -p to change port;
|
||||||
use -t to change timeout)
|
use -t to change timeout)
|
||||||
-j, --json JSON output
|
-d, --debug Enable debug output.
|
||||||
|
-j, --json JSON output (use -jj to enable indents)
|
||||||
-l, --level=<level> minimum output level (info|warn|fail)
|
-l, --level=<level> minimum output level (info|warn|fail)
|
||||||
-L, --list-policies list all the official, built-in policies
|
-L, --list-policies list all the official, built-in policies
|
||||||
--lookup=<alg1,alg2,...> looks up an algorithm(s) without
|
--lookup=<alg1,alg2,...> looks up an algorithm(s) without
|
||||||
connecting to a server
|
connecting to a server
|
||||||
|
-m, --manual print the man page (Windows only)
|
||||||
-M, --make-policy=<policy.txt> creates a policy based on the target server
|
-M, --make-policy=<policy.txt> creates a policy based on the target server
|
||||||
(i.e.: the target server has the ideal
|
(i.e.: the target server has the ideal
|
||||||
configuration that other servers should
|
configuration that other servers should
|
||||||
@ -53,6 +67,8 @@ usage: ssh-audit.py [options] <host>
|
|||||||
(default: 5)
|
(default: 5)
|
||||||
-T, --targets=<hosts.txt> a file containing a list of target hosts (one
|
-T, --targets=<hosts.txt> a file containing a list of target hosts (one
|
||||||
per line, format HOST[:PORT])
|
per line, format HOST[:PORT])
|
||||||
|
--threads=<threads> number of threads to use when scanning multiple
|
||||||
|
targets (-T/--targets) (default: 32)
|
||||||
-v, --verbose verbose output
|
-v, --verbose verbose output
|
||||||
```
|
```
|
||||||
* if both IPv4 and IPv6 are used, order of precedence can be set by using either `-46` or `-64`.
|
* if both IPv4 and IPv6 are used, order of precedence can be set by using either `-46` or `-64`.
|
||||||
@ -86,7 +102,7 @@ To audit a client configuration, with a listener on port 4567:
|
|||||||
ssh-audit -c -p 4567
|
ssh-audit -c -p 4567
|
||||||
```
|
```
|
||||||
|
|
||||||
To list all official built-in policies (hint: use resulting file paths with `-P`/`--policy`):
|
To list all official built-in policies (hint: use resulting policy names with `-P`/`--policy`):
|
||||||
```
|
```
|
||||||
ssh-audit -L
|
ssh-audit -L
|
||||||
```
|
```
|
||||||
@ -106,17 +122,19 @@ To run a policy audit against many servers:
|
|||||||
ssh-audit -T servers.txt -P ["policy name" | path/to/server_policy.txt]
|
ssh-audit -T servers.txt -P ["policy name" | path/to/server_policy.txt]
|
||||||
```
|
```
|
||||||
|
|
||||||
To create a policy based on a target server (which can be manually edited; see official built-in policies for syntax examples):
|
To create a policy based on a target server (which can be manually edited):
|
||||||
```
|
```
|
||||||
ssh-audit -M new_policy.txt targetserver
|
ssh-audit -M new_policy.txt targetserver
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
### Server Standard Audit Example
|
### Server Standard Audit Example
|
||||||
Below is a screen shot of the standard server-auditing output when connecting to an unhardened OpenSSH v5.3 service:
|
Below is a screen shot of the standard server-auditing output when connecting to an unhardened OpenSSH v5.3 service:
|
||||||

|

|
||||||
|
|
||||||
### Server Policy Audit Example
|
### Server Policy Audit Example
|
||||||
Below is a screen shot of the policy auditing output when connecting to an un-hardened Ubuntu Server 20.04 machine:
|
Below is a screen shot of the policy auditing output when connecting to an un-hardened Ubuntu Server 20.04 machine (hint: use `-L`/`--list-policies` to see names of built-in policies to use with `-P`/`--policy`):
|
||||||

|

|
||||||
|
|
||||||
After applying the steps in the hardening guide (see below), the output changes to the following:
|
After applying the steps in the hardening guide (see below), the output changes to the following:
|
||||||
@ -126,10 +144,10 @@ After applying the steps in the hardening guide (see below), the output changes
|
|||||||
Below is a screen shot of the client-auditing output when an unhardened OpenSSH v7.2 client connects:
|
Below is a screen shot of the client-auditing output when an unhardened OpenSSH v7.2 client connects:
|
||||||

|

|
||||||
|
|
||||||
### Hardening Guides
|
## Hardening Guides
|
||||||
Guides to harden server & client configuration can be found here: [https://www.ssh-audit.com/hardening_guides.html](https://www.ssh-audit.com/hardening_guides.html)
|
Guides to harden server & client configuration can be found here: [https://www.ssh-audit.com/hardening_guides.html](https://www.ssh-audit.com/hardening_guides.html)
|
||||||
|
|
||||||
### Pre-Built Packages
|
## Pre-Built Packages
|
||||||
Pre-built packages are available for Windows (see the releases page), on PyPI, Snap, and Homebrew.
|
Pre-built packages are available for Windows (see the releases page), on PyPI, Snap, and Homebrew.
|
||||||
|
|
||||||
To install from PyPI:
|
To install from PyPI:
|
||||||
@ -153,10 +171,36 @@ $ docker pull positronsecurity/ssh-audit
|
|||||||
```
|
```
|
||||||
(Then run with: `docker run -it -p 2222:2222 positronsecurity/ssh-audit 10.1.1.1`)
|
(Then run with: `docker run -it -p 2222:2222 positronsecurity/ssh-audit 10.1.1.1`)
|
||||||
|
|
||||||
### Web Front-End
|
## Web Front-End
|
||||||
For convenience, a web front-end on top of the command-line tool is available at [https://www.ssh-audit.com/](https://www.ssh-audit.com/).
|
For convenience, a web front-end on top of the command-line tool is available at [https://www.ssh-audit.com/](https://www.ssh-audit.com/).
|
||||||
|
|
||||||
## ChangeLog
|
## ChangeLog
|
||||||
|
### v2.5.0 (2021-08-26)
|
||||||
|
- Fixed crash when running host key tests.
|
||||||
|
- Handles server connection failures more gracefully.
|
||||||
|
- Now prints JSON with indents when `-jj` is used (useful for debugging).
|
||||||
|
- Added MD5 fingerprints to verbose output.
|
||||||
|
- Added `-d`/`--debug` option for getting debugging output; credit [Adam Russell](https://github.com/thecliguy).
|
||||||
|
- Updated JSON output to include MD5 fingerprints. Note that this results in a breaking change in the 'fingerprints' dictionary format.
|
||||||
|
- Updated OpenSSH 8.1 (and earlier) policies to include `rsa-sha2-512` and `rsa-sha2-256`.
|
||||||
|
- Added OpenSSH v8.6 & v8.7 policies.
|
||||||
|
- Added 3 new key exchanges: `gss-gex-sha1-eipGX3TCiQSrx573bT1o1Q==`, `gss-group1-sha1-eipGX3TCiQSrx573bT1o1Q==`, and `gss-group14-sha1-eipGX3TCiQSrx573bT1o1Q==`.
|
||||||
|
- Added 3 new MACs: `hmac-ripemd160-96`, `AEAD_AES_128_GCM`, and `AEAD_AES_256_GCM`.
|
||||||
|
|
||||||
|
### v2.4.0 (2021-02-23)
|
||||||
|
- Added multi-threaded scanning support.
|
||||||
|
- Added built-in Windows manual page (see `-m`/`--manual`); credit [Adam Russell](https://github.com/thecliguy).
|
||||||
|
- Added version check for OpenSSH user enumeration (CVE-2018-15473).
|
||||||
|
- Added deprecation note to host key types based on SHA-1.
|
||||||
|
- Added extra warnings for SSHv1.
|
||||||
|
- Added built-in hardened OpenSSH v8.5 policy.
|
||||||
|
- Upgraded warnings to failures for host key types based on SHA-1.
|
||||||
|
- Fixed crash when receiving unexpected response during host key test.
|
||||||
|
- Fixed hang against older Cisco devices during host key test & gex test.
|
||||||
|
- Fixed improper termination while scanning multiple targets when one target returns an error.
|
||||||
|
- Dropped support for Python 3.5 (which reached EOL in Sept. 2020).
|
||||||
|
- Added 1 new key exchange: `sntrup761x25519-sha512@openssh.com`.
|
||||||
|
|
||||||
### v2.3.1 (2020-10-28)
|
### v2.3.1 (2020-10-28)
|
||||||
- Now parses public key sizes for `rsa-sha2-256-cert-v01@openssh.com` and `rsa-sha2-512-cert-v01@openssh.com` host key types.
|
- Now parses public key sizes for `rsa-sha2-256-cert-v01@openssh.com` and `rsa-sha2-512-cert-v01@openssh.com` host key types.
|
||||||
- Flag `ssh-rsa-cert-v01@openssh.com` as a failure due to SHA-1 hash.
|
- Flag `ssh-rsa-cert-v01@openssh.com` as a failure due to SHA-1 hash.
|
||||||
|
130
build_windows_executable.sh
Executable file
130
build_windows_executable.sh
Executable 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
|
@ -443,6 +443,7 @@ function run_test {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
cid=`docker run -d -p 2222:22 ${IMAGE_NAME}:${IMAGE_VERSION} ${server_exec}`
|
cid=`docker run -d -p 2222:22 ${IMAGE_NAME}:${IMAGE_VERSION} ${server_exec}`
|
||||||
|
#echo "Running: docker run -d -p 2222:22 ${IMAGE_NAME}:${IMAGE_VERSION} ${server_exec}"
|
||||||
if [[ $? != 0 ]]; then
|
if [[ $? != 0 ]]; then
|
||||||
echo -e "${REDB}Failed to run docker image! (exit code: $?)${CLR}"
|
echo -e "${REDB}Failed to run docker image! (exit code: $?)${CLR}"
|
||||||
exit 1
|
exit 1
|
||||||
@ -699,17 +700,17 @@ run_custom_policy_test 'config2' 'test13' $PROGRAM_RETVAL_GOOD
|
|||||||
run_custom_policy_test 'config2' 'test14' $PROGRAM_RETVAL_FAILURE
|
run_custom_policy_test 'config2' 'test14' $PROGRAM_RETVAL_FAILURE
|
||||||
|
|
||||||
# Passing test for built-in OpenSSH 8.0p1 server policy.
|
# Passing test for built-in OpenSSH 8.0p1 server policy.
|
||||||
run_builtin_policy_test "Hardened OpenSSH Server v8.0 (version 1)" "8.0p1" "test1" "-o HostKeyAlgorithms=ssh-ed25519 -o KexAlgorithms=curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha256 -o Ciphers=chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr -o MACs=hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,umac-128-etm@openssh.com" $PROGRAM_RETVAL_GOOD
|
run_builtin_policy_test "Hardened OpenSSH Server v8.0 (version 1)" "8.0p1" "test1" "-o HostKeyAlgorithms=rsa-sha2-512,rsa-sha2-256,ssh-ed25519 -o KexAlgorithms=curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha256 -o Ciphers=chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr -o MACs=hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,umac-128-etm@openssh.com" $PROGRAM_RETVAL_GOOD
|
||||||
|
|
||||||
# Failing test for built-in OpenSSH 8.0p1 server policy (MACs not hardened).
|
# Failing test for built-in OpenSSH 8.0p1 server policy (MACs not hardened).
|
||||||
run_builtin_policy_test "Hardened OpenSSH Server v8.0 (version 1)" "8.0p1" "test2" "-o HostKeyAlgorithms=ssh-ed25519 -o KexAlgorithms=curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha256 -o Ciphers=chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr" $PROGRAM_RETVAL_FAILURE
|
run_builtin_policy_test "Hardened OpenSSH Server v8.0 (version 1)" "8.0p1" "test2" "-o HostKeyAlgorithms=rsa-sha2-512,rsa-sha2-256,ssh-ed25519 -o KexAlgorithms=curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha256 -o Ciphers=chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr" $PROGRAM_RETVAL_FAILURE
|
||||||
|
|
||||||
|
|
||||||
if [[ $num_failures == 0 ]]; then
|
if [[ $num_failures == 0 ]]; then
|
||||||
echo -e "\n${GREENB}ALL TESTS PASS!${CLR}\n"
|
echo -e "\n${GREENB}ALL TESTS PASS!${CLR}\n"
|
||||||
|
rm -rf $TEST_RESULT_DIR
|
||||||
else
|
else
|
||||||
echo -e "\n${REDB}${num_failures} TESTS FAILED!${CLR}\n"
|
echo -e "\n${REDB}${num_failures} TESTS FAILED!${CLR}\n"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
rm -rf $TEST_RESULT_DIR
|
|
||||||
exit 0
|
exit 0
|
||||||
|
@ -18,7 +18,6 @@ classifiers =
|
|||||||
License :: OSI Approved :: MIT License
|
License :: OSI Approved :: MIT License
|
||||||
Operating System :: OS Independent
|
Operating System :: OS Independent
|
||||||
Programming Language :: Python :: 3
|
Programming Language :: Python :: 3
|
||||||
Programming Language :: Python :: 3.5
|
|
||||||
Programming Language :: Python :: 3.6
|
Programming Language :: Python :: 3.6
|
||||||
Programming Language :: Python :: 3.7
|
Programming Language :: Python :: 3.7
|
||||||
Programming Language :: Python :: 3.8
|
Programming Language :: Python :: 3.8
|
||||||
@ -32,7 +31,7 @@ classifiers =
|
|||||||
packages = find:
|
packages = find:
|
||||||
package_dir =
|
package_dir =
|
||||||
= src
|
= src
|
||||||
python_requires = >=3.5,<4
|
python_requires = >=3.6,<4
|
||||||
|
|
||||||
[options.packages.find]
|
[options.packages.find]
|
||||||
where = src
|
where = src
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
name: ssh-audit
|
name: ssh-audit
|
||||||
version: '2.3.1-1'
|
version: '2.4.0-1'
|
||||||
license: 'MIT'
|
license: 'MIT'
|
||||||
summary: ssh-audit
|
summary: ssh-audit
|
||||||
description: |
|
description: |
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (C) 2017-2020 Joe Testa (jtesta@positronsecurity.com)
|
Copyright (C) 2017-2021 Joe Testa (jtesta@positronsecurity.com)
|
||||||
Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu)
|
Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu)
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
@ -55,7 +55,7 @@ class Algorithms:
|
|||||||
if self.ssh1kex is None:
|
if self.ssh1kex is None:
|
||||||
return None
|
return None
|
||||||
item = Algorithms.Item(1, SSH1_KexDB.ALGORITHMS)
|
item = Algorithms.Item(1, SSH1_KexDB.ALGORITHMS)
|
||||||
item.add('key', [u'ssh-rsa1'])
|
item.add('key', ['ssh-rsa1'])
|
||||||
item.add('enc', self.ssh1kex.supported_ciphers)
|
item.add('enc', self.ssh1kex.supported_ciphers)
|
||||||
item.add('aut', self.ssh1kex.supported_authentications)
|
item.add('aut', self.ssh1kex.supported_authentications)
|
||||||
return item
|
return item
|
||||||
@ -131,7 +131,7 @@ class Algorithms:
|
|||||||
# if version is not None:
|
# if version is not None:
|
||||||
# software = SSH.Software(None, product, version, None, None)
|
# software = SSH.Software(None, product, version, None, None)
|
||||||
# break
|
# break
|
||||||
rec = {} # type: Dict[int, Dict[str, Dict[str, Dict[str, int]]]]
|
rec: Dict[int, Dict[str, Dict[str, Dict[str, int]]]] = {}
|
||||||
if software is None:
|
if software is None:
|
||||||
unknown_software = True
|
unknown_software = True
|
||||||
for alg_pair in self.values:
|
for alg_pair in self.values:
|
||||||
@ -206,7 +206,7 @@ class Algorithms:
|
|||||||
def __init__(self, sshv: int, db: Dict[str, Dict[str, List[List[Optional[str]]]]]) -> None:
|
def __init__(self, sshv: int, db: Dict[str, Dict[str, List[List[Optional[str]]]]]) -> None:
|
||||||
self.__sshv = sshv
|
self.__sshv = sshv
|
||||||
self.__db = db
|
self.__db = db
|
||||||
self.__storage = {} # type: Dict[str, List[str]]
|
self.__storage: Dict[str, List[str]] = {}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sshv(self) -> int:
|
def sshv(self) -> int:
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (C) 2017-2020 Joe Testa (jtesta@positronsecurity.com)
|
Copyright (C) 2017-2021 Joe Testa (jtesta@positronsecurity.com)
|
||||||
Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu)
|
Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu)
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
@ -41,45 +41,35 @@ class AuditConf:
|
|||||||
self.client_audit = False
|
self.client_audit = False
|
||||||
self.colors = True
|
self.colors = True
|
||||||
self.json = False
|
self.json = False
|
||||||
|
self.json_print_indent = False
|
||||||
self.verbose = False
|
self.verbose = False
|
||||||
self.level = 'info'
|
self.level = 'info'
|
||||||
self.ipvo = () # type: Sequence[int]
|
self.ip_version_preference: List[int] = [] # Holds only 5 possible values: [] (no preference), [4] (use IPv4 only), [6] (use IPv6 only), [46] (use both IPv4 and IPv6, but prioritize v4), and [64] (use both IPv4 and IPv6, but prioritize v6).
|
||||||
self.ipv4 = False
|
self.ipv4 = False
|
||||||
self.ipv6 = False
|
self.ipv6 = False
|
||||||
self.make_policy = False # When True, creates a policy file from an audit scan.
|
self.make_policy = False # When True, creates a policy file from an audit scan.
|
||||||
self.policy_file = None # type: Optional[str] # File system path to a policy
|
self.policy_file: Optional[str] = None # File system path to a policy
|
||||||
self.policy = None # type: Optional[Policy] # Policy object
|
self.policy: Optional[Policy] = None # Policy object
|
||||||
self.timeout = 5.0
|
self.timeout = 5.0
|
||||||
self.timeout_set = False # Set to True when the user explicitly sets it.
|
self.timeout_set = False # Set to True when the user explicitly sets it.
|
||||||
self.target_file = None # type: Optional[str]
|
self.target_file: Optional[str] = None
|
||||||
self.target_list = [] # type: List[str]
|
self.target_list: List[str] = []
|
||||||
|
self.threads = 32
|
||||||
self.list_policies = False
|
self.list_policies = False
|
||||||
self.lookup = ''
|
self.lookup = ''
|
||||||
|
self.manual = False
|
||||||
|
self.debug = False
|
||||||
|
|
||||||
def __setattr__(self, name: str, value: Union[str, int, float, bool, Sequence[int]]) -> None:
|
def __setattr__(self, name: str, value: Union[str, int, float, bool, Sequence[int]]) -> None:
|
||||||
valid = False
|
valid = False
|
||||||
if name in ['ssh1', 'ssh2', 'batch', 'client_audit', 'colors', 'verbose', 'timeout_set', 'json', 'make_policy', 'list_policies']:
|
if name in ['batch', 'client_audit', 'colors', 'json', 'json_print_indent', 'list_policies', 'manual', 'make_policy', 'ssh1', 'ssh2', 'timeout_set', 'verbose', 'debug']:
|
||||||
valid, value = True, bool(value)
|
valid, value = True, bool(value)
|
||||||
elif name in ['ipv4', 'ipv6']:
|
elif name in ['ipv4', 'ipv6']:
|
||||||
|
valid, value = True, bool(value)
|
||||||
|
if len(self.ip_version_preference) == 2: # Being called more than twice is not valid.
|
||||||
valid = False
|
valid = False
|
||||||
value = bool(value)
|
elif value:
|
||||||
ipv = 4 if name == 'ipv4' else 6
|
self.ip_version_preference.append(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)
|
|
||||||
elif name == 'port':
|
elif name == 'port':
|
||||||
valid, port = True, Utils.parse_int(value)
|
valid, port = True, Utils.parse_int(value)
|
||||||
if port < 1 or port > 65535:
|
if port < 1 or port > 65535:
|
||||||
@ -96,8 +86,13 @@ class AuditConf:
|
|||||||
if value == -1.0:
|
if value == -1.0:
|
||||||
raise ValueError('invalid timeout: {}'.format(value))
|
raise ValueError('invalid timeout: {}'.format(value))
|
||||||
valid = True
|
valid = True
|
||||||
elif name in ['policy_file', 'policy', 'target_file', 'target_list', 'lookup']:
|
elif name in ['ip_version_preference', 'lookup', 'policy_file', 'policy', 'target_file', 'target_list']:
|
||||||
valid = True
|
valid = True
|
||||||
|
elif name == "threads":
|
||||||
|
valid, num_threads = True, Utils.parse_int(value)
|
||||||
|
if num_threads < 1:
|
||||||
|
raise ValueError('invalid number of threads: {}'.format(value))
|
||||||
|
value = num_threads
|
||||||
|
|
||||||
if valid:
|
if valid:
|
||||||
object.__setattr__(self, name, value)
|
object.__setattr__(self, name, value)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (C) 2017-2020 Joe Testa (jtesta@positronsecurity.com)
|
Copyright (C) 2017-2021 Joe Testa (jtesta@positronsecurity.com)
|
||||||
Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu)
|
Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu)
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
@ -33,11 +33,11 @@ class Fingerprint:
|
|||||||
@property
|
@property
|
||||||
def md5(self) -> str:
|
def md5(self) -> str:
|
||||||
h = hashlib.md5(self.__fpd).hexdigest()
|
h = hashlib.md5(self.__fpd).hexdigest()
|
||||||
r = u':'.join(h[i:i + 2] for i in range(0, len(h), 2))
|
r = ':'.join(h[i:i + 2] for i in range(0, len(h), 2))
|
||||||
return u'MD5:{}'.format(r)
|
return 'MD5:{}'.format(r)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sha256(self) -> str:
|
def sha256(self) -> str:
|
||||||
h = base64.b64encode(hashlib.sha256(self.__fpd).digest())
|
h = base64.b64encode(hashlib.sha256(self.__fpd).digest())
|
||||||
r = h.decode('ascii').rstrip('=')
|
r = h.decode('ascii').rstrip('=')
|
||||||
return u'SHA256:{}'.format(r)
|
return 'SHA256:{}'.format(r)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (C) 2017-2020 Joe Testa (jtesta@positronsecurity.com)
|
Copyright (C) 2017-2021 Joe Testa (jtesta@positronsecurity.com)
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
@ -21,17 +21,18 @@
|
|||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
THE SOFTWARE.
|
THE SOFTWARE.
|
||||||
"""
|
"""
|
||||||
import os
|
|
||||||
|
|
||||||
# pylint: disable=unused-import
|
# pylint: disable=unused-import
|
||||||
from typing import Dict, List, Set, Sequence, Tuple, Iterable # noqa: F401
|
from typing import Dict, List, Set, Sequence, Tuple, Iterable # noqa: F401
|
||||||
from typing import Callable, Optional, Union, Any # noqa: F401
|
from typing import Callable, Optional, Union, Any # noqa: F401
|
||||||
|
|
||||||
|
import traceback
|
||||||
|
|
||||||
from ssh_audit.kexdh import KexGroupExchange_SHA1, KexGroupExchange_SHA256
|
from ssh_audit.kexdh import KexGroupExchange_SHA1, KexGroupExchange_SHA256
|
||||||
from ssh_audit.protocol import Protocol
|
|
||||||
from ssh_audit.ssh2_kexdb import SSH2_KexDB
|
from ssh_audit.ssh2_kexdb import SSH2_KexDB
|
||||||
from ssh_audit.ssh2_kex import SSH2_Kex
|
from ssh_audit.ssh2_kex import SSH2_Kex
|
||||||
from ssh_audit.ssh_socket import SSH_Socket
|
from ssh_audit.ssh_socket import SSH_Socket
|
||||||
|
from ssh_audit.outputbuffer import OutputBuffer
|
||||||
|
|
||||||
|
|
||||||
# Performs DH group exchanges to find what moduli are supported, and checks
|
# Performs DH group exchanges to find what moduli are supported, and checks
|
||||||
@ -40,37 +41,38 @@ class GEXTest:
|
|||||||
|
|
||||||
# Creates a new connection to the server. Returns True on success, or False.
|
# Creates a new connection to the server. Returns True on success, or False.
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def reconnect(s: 'SSH_Socket', gex_alg: str) -> bool:
|
def reconnect(out: 'OutputBuffer', s: 'SSH_Socket', kex: 'SSH2_Kex', gex_alg: str) -> bool:
|
||||||
if s.is_connected():
|
if s.is_connected():
|
||||||
return True
|
return True
|
||||||
|
|
||||||
err = s.connect()
|
err = s.connect()
|
||||||
if err is not None:
|
if err is not None:
|
||||||
|
out.v(err, write_now=True)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
unused = None # pylint: disable=unused-variable
|
_, _, err = s.get_banner()
|
||||||
unused2 = None # pylint: disable=unused-variable
|
|
||||||
unused, unused2, err = s.get_banner()
|
|
||||||
if err is not None:
|
if err is not None:
|
||||||
|
out.v(err, write_now=True)
|
||||||
s.close()
|
s.close()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Parse the server's initial KEX.
|
|
||||||
packet_type = 0 # pylint: disable=unused-variable
|
|
||||||
packet_type, payload = s.read_packet(2)
|
|
||||||
kex = SSH2_Kex.parse(payload)
|
|
||||||
|
|
||||||
# Send our KEX using the specified group-exchange and most of the
|
# Send our KEX using the specified group-exchange and most of the
|
||||||
# server's own values.
|
# server's own values.
|
||||||
client_kex = SSH2_Kex(os.urandom(16), [gex_alg], kex.key_algorithms, kex.client, kex.server, False, 0)
|
s.send_kexinit(key_exchanges=[gex_alg], hostkeys=kex.key_algorithms, ciphers=kex.server.encryption, macs=kex.server.mac, compressions=kex.server.compression, languages=kex.server.languages)
|
||||||
s.write_byte(Protocol.MSG_KEXINIT)
|
|
||||||
client_kex.write(s)
|
try:
|
||||||
s.send_packet()
|
# Parse the server's KEX.
|
||||||
|
_, payload = s.read_packet(2)
|
||||||
|
SSH2_Kex.parse(payload)
|
||||||
|
except Exception:
|
||||||
|
out.v("Failed to parse server's kex. Stack trace:\n%s" % str(traceback.format_exc()), write_now=True)
|
||||||
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Runs the DH moduli test against the specified target.
|
# Runs the DH moduli test against the specified target.
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def run(s: 'SSH_Socket', kex: 'SSH2_Kex') -> None:
|
def run(out: 'OutputBuffer', s: 'SSH_Socket', kex: 'SSH2_Kex') -> None:
|
||||||
GEX_ALGS = {
|
GEX_ALGS = {
|
||||||
'diffie-hellman-group-exchange-sha1': KexGroupExchange_SHA1,
|
'diffie-hellman-group-exchange-sha1': KexGroupExchange_SHA1,
|
||||||
'diffie-hellman-group-exchange-sha256': KexGroupExchange_SHA256,
|
'diffie-hellman-group-exchange-sha256': KexGroupExchange_SHA256,
|
||||||
@ -84,13 +86,14 @@ class GEXTest:
|
|||||||
|
|
||||||
# Check if the server supports any of the group-exchange
|
# Check if the server supports any of the group-exchange
|
||||||
# algorithms. If so, test each one.
|
# algorithms. If so, test each one.
|
||||||
for gex_alg in GEX_ALGS:
|
for gex_alg, kex_group_class in GEX_ALGS.items():
|
||||||
if gex_alg in kex.kex_algorithms:
|
if gex_alg in kex.kex_algorithms:
|
||||||
|
out.d('Preparing to perform DH group exchange using ' + gex_alg + '...', write_now=True)
|
||||||
|
|
||||||
if GEXTest.reconnect(s, gex_alg) is False:
|
if GEXTest.reconnect(out, s, kex, gex_alg) is False:
|
||||||
break
|
break
|
||||||
|
|
||||||
kex_group = GEX_ALGS[gex_alg]()
|
kex_group = kex_group_class()
|
||||||
smallest_modulus = -1
|
smallest_modulus = -1
|
||||||
|
|
||||||
# First try a range of weak sizes.
|
# First try a range of weak sizes.
|
||||||
@ -117,7 +120,9 @@ class GEXTest:
|
|||||||
if bits >= smallest_modulus > 0:
|
if bits >= smallest_modulus > 0:
|
||||||
break
|
break
|
||||||
|
|
||||||
if GEXTest.reconnect(s, gex_alg) is False:
|
out.d('Preparing to perform DH group exchange using ' + gex_alg + ' with modulus size ' + str(bits) + '...', write_now=True)
|
||||||
|
|
||||||
|
if GEXTest.reconnect(out, s, kex, gex_alg) is False:
|
||||||
reconnect_failed = True
|
reconnect_failed = True
|
||||||
break
|
break
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (C) 2017-2020 Joe Testa (jtesta@positronsecurity.com)
|
Copyright (C) 2017-2021 Joe Testa (jtesta@positronsecurity.com)
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
@ -21,6 +21,7 @@
|
|||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
THE SOFTWARE.
|
THE SOFTWARE.
|
||||||
"""
|
"""
|
||||||
VERSION = 'v2.3.1'
|
VERSION = 'v2.5.0'
|
||||||
SSH_HEADER = 'SSH-{0}-OpenSSH_8.2' # SSH software to impersonate
|
SSH_HEADER = 'SSH-{0}-OpenSSH_8.2' # SSH software to impersonate
|
||||||
GITHUB_ISSUES_URL = 'https://github.com/jtesta/ssh-audit/issues' # The URL to the Github issues tracker.
|
GITHUB_ISSUES_URL = 'https://github.com/jtesta/ssh-audit/issues' # The URL to the Github issues tracker.
|
||||||
|
WINDOWS_MAN_PAGE = ''
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (C) 2017-2020 Joe Testa (jtesta@positronsecurity.com)
|
Copyright (C) 2017-2021 Joe Testa (jtesta@positronsecurity.com)
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
@ -21,17 +21,18 @@
|
|||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
THE SOFTWARE.
|
THE SOFTWARE.
|
||||||
"""
|
"""
|
||||||
import os
|
|
||||||
|
|
||||||
# pylint: disable=unused-import
|
# pylint: disable=unused-import
|
||||||
from typing import Dict, List, Set, Sequence, Tuple, Iterable # noqa: F401
|
from typing import Dict, List, Set, Sequence, Tuple, Iterable # noqa: F401
|
||||||
from typing import Callable, Optional, Union, Any # noqa: F401
|
from typing import Callable, Optional, Union, Any # noqa: F401
|
||||||
|
|
||||||
|
import traceback
|
||||||
|
|
||||||
from ssh_audit.kexdh import KexDH, KexGroup1, KexGroup14_SHA1, KexGroup14_SHA256, KexCurve25519_SHA256, KexGroup16_SHA512, KexGroup18_SHA512, KexGroupExchange_SHA1, KexGroupExchange_SHA256, KexNISTP256, KexNISTP384, KexNISTP521
|
from ssh_audit.kexdh import KexDH, KexGroup1, KexGroup14_SHA1, KexGroup14_SHA256, KexCurve25519_SHA256, KexGroup16_SHA512, KexGroup18_SHA512, KexGroupExchange_SHA1, KexGroupExchange_SHA256, KexNISTP256, KexNISTP384, KexNISTP521
|
||||||
from ssh_audit.protocol import Protocol
|
|
||||||
from ssh_audit.ssh2_kex import SSH2_Kex
|
from ssh_audit.ssh2_kex import SSH2_Kex
|
||||||
from ssh_audit.ssh2_kexdb import SSH2_KexDB
|
from ssh_audit.ssh2_kexdb import SSH2_KexDB
|
||||||
from ssh_audit.ssh_socket import SSH_Socket
|
from ssh_audit.ssh_socket import SSH_Socket
|
||||||
|
from ssh_audit.outputbuffer import OutputBuffer
|
||||||
|
|
||||||
|
|
||||||
# Obtains host keys, checks their size, and derives their fingerprints.
|
# Obtains host keys, checks their size, and derives their fingerprints.
|
||||||
@ -54,7 +55,7 @@ class HostKeyTest:
|
|||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def run(s: 'SSH_Socket', server_kex: 'SSH2_Kex') -> None:
|
def run(out: 'OutputBuffer', s: 'SSH_Socket', server_kex: 'SSH2_Kex') -> None:
|
||||||
KEX_TO_DHGROUP = {
|
KEX_TO_DHGROUP = {
|
||||||
'diffie-hellman-group1-sha1': KexGroup1,
|
'diffie-hellman-group1-sha1': KexGroup1,
|
||||||
'diffie-hellman-group14-sha1': KexGroup14_SHA1,
|
'diffie-hellman-group14-sha1': KexGroup14_SHA1,
|
||||||
@ -82,10 +83,10 @@ class HostKeyTest:
|
|||||||
break
|
break
|
||||||
|
|
||||||
if kex_str is not None and kex_group is not None:
|
if kex_str is not None and kex_group is not None:
|
||||||
HostKeyTest.perform_test(s, server_kex, kex_str, kex_group, HostKeyTest.HOST_KEY_TYPES)
|
HostKeyTest.perform_test(out, s, server_kex, kex_str, kex_group, HostKeyTest.HOST_KEY_TYPES)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def perform_test(s: 'SSH_Socket', server_kex: 'SSH2_Kex', kex_str: str, kex_group: 'KexDH', host_key_types: Dict[str, Dict[str, bool]]) -> None:
|
def perform_test(out: 'OutputBuffer', s: 'SSH_Socket', server_kex: 'SSH2_Kex', kex_str: str, kex_group: 'KexDH', host_key_types: Dict[str, Dict[str, bool]]) -> None:
|
||||||
hostkey_modulus_size = 0
|
hostkey_modulus_size = 0
|
||||||
ca_modulus_size = 0
|
ca_modulus_size = 0
|
||||||
|
|
||||||
@ -103,6 +104,8 @@ class HostKeyTest:
|
|||||||
|
|
||||||
# If this host key type is supported by the server, we test it.
|
# If this host key type is supported by the server, we test it.
|
||||||
if host_key_type in server_kex.key_algorithms:
|
if host_key_type in server_kex.key_algorithms:
|
||||||
|
out.d('Preparing to obtain ' + host_key_type + ' host key...', write_now=True)
|
||||||
|
|
||||||
cert = host_key_types[host_key_type]['cert']
|
cert = host_key_types[host_key_type]['cert']
|
||||||
variable_key_len = host_key_types[host_key_type]['variable_key_len']
|
variable_key_len = host_key_types[host_key_type]['variable_key_len']
|
||||||
|
|
||||||
@ -110,34 +113,35 @@ class HostKeyTest:
|
|||||||
if not s.is_connected():
|
if not s.is_connected():
|
||||||
err = s.connect()
|
err = s.connect()
|
||||||
if err is not None:
|
if err is not None:
|
||||||
|
out.v(err, write_now=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
_, _, err = s.get_banner()
|
_, _, err = s.get_banner()
|
||||||
if err is not None:
|
if err is not None:
|
||||||
|
out.v(err, write_now=True)
|
||||||
s.close()
|
s.close()
|
||||||
return
|
return
|
||||||
|
|
||||||
# Parse the server's initial KEX.
|
# Send our KEX using the specified group-exchange and most of the server's own values.
|
||||||
packet_type = 0 # pylint: disable=unused-variable
|
s.send_kexinit(key_exchanges=[kex_str], hostkeys=[host_key_type], ciphers=server_kex.server.encryption, macs=server_kex.server.mac, compressions=server_kex.server.compression, languages=server_kex.server.languages)
|
||||||
packet_type, payload = s.read_packet()
|
|
||||||
|
try:
|
||||||
|
# Parse the server's KEX.
|
||||||
|
_, payload = s.read_packet()
|
||||||
SSH2_Kex.parse(payload)
|
SSH2_Kex.parse(payload)
|
||||||
|
except Exception:
|
||||||
# Send the server our KEXINIT message, using only our
|
out.v("Failed to parse server's kex. Stack trace:\n%s" % str(traceback.format_exc()), write_now=True)
|
||||||
# selected kex and host key type. Send the server's own
|
return
|
||||||
# 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()
|
|
||||||
|
|
||||||
# Do the initial DH exchange. The server responds back
|
# Do the initial DH exchange. The server responds back
|
||||||
# with the host key and its length. Bingo. We also get back the host key fingerprint.
|
# with the host key and its length. Bingo. We also get back the host key fingerprint.
|
||||||
kex_group.send_init(s)
|
kex_group.send_init(s)
|
||||||
|
try:
|
||||||
host_key = kex_group.recv_reply(s, variable_key_len)
|
host_key = kex_group.recv_reply(s, variable_key_len)
|
||||||
if host_key is not None:
|
if host_key is not None:
|
||||||
server_kex.set_host_key(host_key_type, host_key)
|
server_kex.set_host_key(host_key_type, host_key)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
hostkey_modulus_size = kex_group.get_hostkey_size()
|
hostkey_modulus_size = kex_group.get_hostkey_size()
|
||||||
ca_modulus_size = kex_group.get_ca_size()
|
ca_modulus_size = kex_group.get_ca_size()
|
||||||
@ -161,12 +165,20 @@ class HostKeyTest:
|
|||||||
if (cert is False) and (hostkey_modulus_size < 2048):
|
if (cert is False) and (hostkey_modulus_size < 2048):
|
||||||
for rsa_type in HostKeyTest.RSA_FAMILY:
|
for rsa_type in HostKeyTest.RSA_FAMILY:
|
||||||
alg_list = SSH2_KexDB.ALGORITHMS['key'][rsa_type]
|
alg_list = SSH2_KexDB.ALGORITHMS['key'][rsa_type]
|
||||||
alg_list.append(['using small %d-bit modulus' % hostkey_modulus_size])
|
|
||||||
|
# If no failure list exists, add an empty failure list.
|
||||||
|
if len(alg_list) < 2:
|
||||||
|
alg_list.append([])
|
||||||
|
alg_list[1].append('using small %d-bit modulus' % hostkey_modulus_size)
|
||||||
elif (cert is True) and ((hostkey_modulus_size < 2048) or (ca_modulus_size > 0 and ca_modulus_size < 2048)): # pylint: disable=chained-comparison
|
elif (cert is True) and ((hostkey_modulus_size < 2048) or (ca_modulus_size > 0 and ca_modulus_size < 2048)): # pylint: disable=chained-comparison
|
||||||
alg_list = SSH2_KexDB.ALGORITHMS['key'][host_key_type]
|
alg_list = SSH2_KexDB.ALGORITHMS['key'][host_key_type]
|
||||||
min_modulus = min(hostkey_modulus_size, ca_modulus_size)
|
min_modulus = min(hostkey_modulus_size, ca_modulus_size)
|
||||||
min_modulus = min_modulus if min_modulus > 0 else max(hostkey_modulus_size, ca_modulus_size)
|
min_modulus = min_modulus if min_modulus > 0 else max(hostkey_modulus_size, ca_modulus_size)
|
||||||
alg_list.append(['using small %d-bit modulus' % min_modulus])
|
|
||||||
|
# If no failure list exists, add an empty failure list.
|
||||||
|
if len(alg_list) < 2:
|
||||||
|
alg_list.append([])
|
||||||
|
alg_list[1].append('using small %d-bit modulus' % min_modulus)
|
||||||
|
|
||||||
# If this host key type is in the RSA family, then mark them all as parsed (since results in one are valid for them all).
|
# If this host key type is in the RSA family, then mark them all as parsed (since results in one are valid for them all).
|
||||||
if host_key_type in HostKeyTest.RSA_FAMILY:
|
if host_key_type in HostKeyTest.RSA_FAMILY:
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (C) 2017-2020 Joe Testa (jtesta@positronsecurity.com)
|
Copyright (C) 2017-2021 Joe Testa (jtesta@positronsecurity.com)
|
||||||
Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu)
|
Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu)
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
@ -37,8 +37,8 @@ from ssh_audit.ssh_socket import SSH_Socket
|
|||||||
|
|
||||||
class KexDH: # pragma: nocover
|
class KexDH: # pragma: nocover
|
||||||
def __init__(self, kex_name: str, hash_alg: str, g: int, p: int) -> None:
|
def __init__(self, kex_name: str, hash_alg: str, g: int, p: int) -> None:
|
||||||
self.__kex_name = kex_name
|
self.__kex_name = kex_name # pylint: disable=unused-private-member
|
||||||
self.__hash_alg = hash_alg
|
self.__hash_alg = hash_alg # pylint: disable=unused-private-member
|
||||||
self.__g = 0
|
self.__g = 0
|
||||||
self.__p = 0
|
self.__p = 0
|
||||||
self.__q = 0
|
self.__q = 0
|
||||||
@ -46,10 +46,10 @@ class KexDH: # pragma: nocover
|
|||||||
self.__e = 0
|
self.__e = 0
|
||||||
self.set_params(g, p)
|
self.set_params(g, p)
|
||||||
|
|
||||||
self.__ed25519_pubkey = None # type: Optional[bytes]
|
self.__ed25519_pubkey: Optional[bytes] = None # pylint: disable=unused-private-member
|
||||||
self.__hostkey_type = None # type: Optional[bytes]
|
self.__hostkey_type: Optional[bytes] = None
|
||||||
self.__hostkey_e = 0
|
self.__hostkey_e = 0 # pylint: disable=unused-private-member
|
||||||
self.__hostkey_n = 0
|
self.__hostkey_n = 0 # pylint: disable=unused-private-member
|
||||||
self.__hostkey_n_len = 0 # Length of the host key modulus.
|
self.__hostkey_n_len = 0 # Length of the host key modulus.
|
||||||
self.__ca_n_len = 0 # Length of the CA key modulus (if hostkey is a cert).
|
self.__ca_n_len = 0 # Length of the CA key modulus (if hostkey is a cert).
|
||||||
|
|
||||||
@ -121,11 +121,11 @@ class KexDH: # pragma: nocover
|
|||||||
|
|
||||||
# The public key exponent.
|
# The public key exponent.
|
||||||
hostkey_e, hostkey_e_len, ptr = KexDH.__get_bytes(hostkey, ptr)
|
hostkey_e, hostkey_e_len, ptr = KexDH.__get_bytes(hostkey, ptr)
|
||||||
self.__hostkey_e = int(binascii.hexlify(hostkey_e), 16)
|
self.__hostkey_e = int(binascii.hexlify(hostkey_e), 16) # pylint: disable=unused-private-member
|
||||||
|
|
||||||
# Here is the modulus size & actual modulus of the host key public key.
|
# Here is the modulus size & actual modulus of the host key public key.
|
||||||
hostkey_n, self.__hostkey_n_len, ptr = KexDH.__get_bytes(hostkey, ptr)
|
hostkey_n, self.__hostkey_n_len, ptr = KexDH.__get_bytes(hostkey, ptr)
|
||||||
self.__hostkey_n = int(binascii.hexlify(hostkey_n), 16)
|
self.__hostkey_n = int(binascii.hexlify(hostkey_n), 16) # pylint: disable=unused-private-member
|
||||||
|
|
||||||
# If this is an RSA certificate, continue parsing to extract the CA
|
# If this is an RSA certificate, continue parsing to extract the CA
|
||||||
# key.
|
# key.
|
||||||
@ -327,7 +327,7 @@ class KexGroupExchange(KexDH):
|
|||||||
s.send_packet()
|
s.send_packet()
|
||||||
|
|
||||||
packet_type, payload = s.read_packet(2)
|
packet_type, payload = s.read_packet(2)
|
||||||
if (packet_type != Protocol.MSG_KEXDH_GEX_GROUP) and (packet_type != Protocol.MSG_DEBUG): # pylint: disable=consider-using-in
|
if packet_type not in [Protocol.MSG_KEXDH_GEX_GROUP, Protocol.MSG_DEBUG]:
|
||||||
# TODO: replace with a better exception type.
|
# TODO: replace with a better exception type.
|
||||||
raise Exception('Expected MSG_KEXDH_GEX_REPLY (%d), but got %d instead.' % (Protocol.MSG_KEXDH_GEX_REPLY, packet_type))
|
raise Exception('Expected MSG_KEXDH_GEX_REPLY (%d), but got %d instead.' % (Protocol.MSG_KEXDH_GEX_REPLY, packet_type))
|
||||||
|
|
||||||
|
@ -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))
|
|
@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (C) 2017-2020 Joe Testa (jtesta@positronsecurity.com)
|
Copyright (C) 2021 Joe Testa (jtesta@positronsecurity.com)
|
||||||
Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu)
|
Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu)
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
@ -22,29 +22,164 @@
|
|||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
THE SOFTWARE.
|
THE SOFTWARE.
|
||||||
"""
|
"""
|
||||||
import io
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
# pylint: disable=unused-import
|
# pylint: disable=unused-import
|
||||||
from typing import Dict, List, Set, Sequence, Tuple, Iterable # noqa: F401
|
from typing import Dict, List, Set, Sequence, Tuple, Iterable # noqa: F401
|
||||||
from typing import Callable, Optional, Union, Any # noqa: F401
|
from typing import Callable, Optional, Union, Any # noqa: F401
|
||||||
|
|
||||||
|
from ssh_audit.utils import Utils
|
||||||
|
|
||||||
class OutputBuffer(List[str]):
|
|
||||||
|
class OutputBuffer:
|
||||||
|
LEVELS: Sequence[str] = ('info', 'warn', 'fail')
|
||||||
|
COLORS = {'head': 36, 'good': 32, 'warn': 33, 'fail': 31}
|
||||||
|
|
||||||
|
# Use brighter colors on Windows for better readability.
|
||||||
|
if Utils.is_windows():
|
||||||
|
COLORS = {'head': 96, 'good': 92, 'warn': 93, 'fail': 91}
|
||||||
|
|
||||||
|
def __init__(self, buffer_output: bool = True) -> None:
|
||||||
|
self.buffer_output = buffer_output
|
||||||
|
self.buffer: List[str] = []
|
||||||
|
self.in_section = False
|
||||||
|
self.section: List[str] = []
|
||||||
|
self.batch = False
|
||||||
|
self.verbose = False
|
||||||
|
self.debug = False
|
||||||
|
self.use_colors = True
|
||||||
|
self.json = False
|
||||||
|
self.__level = 0
|
||||||
|
self.__is_color_supported = ('colorama' in sys.modules) or (os.name == 'posix')
|
||||||
|
self.line_ended = True
|
||||||
|
|
||||||
|
def _print(self, level: str, s: str = '', line_ended: bool = True) -> None:
|
||||||
|
'''Saves output to buffer (if in buffered mode), or immediately prints to stdout otherwise.'''
|
||||||
|
|
||||||
|
# If we're logging only 'warn' or above, and this is an 'info', ignore message.
|
||||||
|
if self.get_level(level) < self.__level:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.use_colors and self.colors_supported and len(s) > 0 and level != 'info':
|
||||||
|
s = "\033[0;%dm%s\033[0m" % (self.COLORS[level], s)
|
||||||
|
|
||||||
|
if self.buffer_output:
|
||||||
|
# Select which list to add to. If we are in a 'with' statement, then this goes in the section buffer, otherwise the general buffer.
|
||||||
|
buf = self.section if self.in_section else self.buffer
|
||||||
|
|
||||||
|
# Determine if a new line should be added, or if the last line should be appended.
|
||||||
|
if not self.line_ended:
|
||||||
|
last_entry = -1 if len(buf) > 0 else 0
|
||||||
|
buf[last_entry] = buf[last_entry] + s
|
||||||
|
else:
|
||||||
|
buf.append(s)
|
||||||
|
|
||||||
|
# When False, this tells the next call to append to the last line we just added.
|
||||||
|
self.line_ended = line_ended
|
||||||
|
else:
|
||||||
|
print(s)
|
||||||
|
|
||||||
|
def get_buffer(self) -> str:
|
||||||
|
'''Returns all buffered output, then clears the buffer.'''
|
||||||
|
self.flush_section()
|
||||||
|
|
||||||
|
buffer_str = "\n".join(self.buffer)
|
||||||
|
self.buffer = []
|
||||||
|
return buffer_str
|
||||||
|
|
||||||
|
def write(self) -> None:
|
||||||
|
'''Writes the output to stdout.'''
|
||||||
|
self.flush_section()
|
||||||
|
print(self.get_buffer(), flush=True)
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
self.flush_section()
|
||||||
|
self.get_buffer()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def level(self) -> str:
|
||||||
|
'''Returns the minimum level for output.'''
|
||||||
|
if self.__level < len(self.LEVELS):
|
||||||
|
return self.LEVELS[self.__level]
|
||||||
|
return 'unknown'
|
||||||
|
|
||||||
|
@level.setter
|
||||||
|
def level(self, name: str) -> None:
|
||||||
|
'''Sets the minimum level for output (one of: 'info', 'warn', 'fail').'''
|
||||||
|
self.__level = self.get_level(name)
|
||||||
|
|
||||||
|
def get_level(self, name: str) -> int:
|
||||||
|
cname = 'info' if name == 'good' else name
|
||||||
|
if cname not in self.LEVELS:
|
||||||
|
return sys.maxsize
|
||||||
|
return self.LEVELS.index(cname)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def colors_supported(self) -> bool:
|
||||||
|
'''Returns True if the system supports color output.'''
|
||||||
|
return self.__is_color_supported
|
||||||
|
|
||||||
|
# When used in a 'with' block, the output to goes into a section; this can be sorted separately when add_section_to_buffer() is later called.
|
||||||
def __enter__(self) -> 'OutputBuffer':
|
def __enter__(self) -> 'OutputBuffer':
|
||||||
# pylint: disable=attribute-defined-outside-init
|
self.in_section = True
|
||||||
self.__buf = io.StringIO()
|
|
||||||
self.__stdout = sys.stdout
|
|
||||||
sys.stdout = self.__buf
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def flush(self, sort_lines: bool = False) -> None:
|
|
||||||
# Lines must be sorted in some cases to ensure consistent testing.
|
|
||||||
if sort_lines:
|
|
||||||
self.sort() # pylint: disable=no-member
|
|
||||||
for line in self: # pylint: disable=not-an-iterable
|
|
||||||
print(line)
|
|
||||||
|
|
||||||
def __exit__(self, *args: Any) -> None:
|
def __exit__(self, *args: Any) -> None:
|
||||||
self.extend(self.__buf.getvalue().splitlines()) # pylint: disable=no-member
|
self.in_section = False
|
||||||
sys.stdout = self.__stdout
|
|
||||||
|
def flush_section(self, sort_section: bool = False) -> None:
|
||||||
|
'''Appends section output (optionally sorting it first) to the end of the buffer, then clears the section output.'''
|
||||||
|
if sort_section:
|
||||||
|
self.section.sort()
|
||||||
|
|
||||||
|
self.buffer.extend(self.section)
|
||||||
|
self.section = []
|
||||||
|
|
||||||
|
def is_section_empty(self) -> bool:
|
||||||
|
'''Returns True if the section buffer is empty, otherwise False.'''
|
||||||
|
return len(self.section) == 0
|
||||||
|
|
||||||
|
def head(self, s: str, line_ended: bool = True) -> 'OutputBuffer':
|
||||||
|
if not self.batch:
|
||||||
|
self._print('head', s, line_ended)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def fail(self, s: str, line_ended: bool = True) -> 'OutputBuffer':
|
||||||
|
self._print('fail', s, line_ended)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def warn(self, s: str, line_ended: bool = True) -> 'OutputBuffer':
|
||||||
|
self._print('warn', s, line_ended)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def info(self, s: str, line_ended: bool = True) -> 'OutputBuffer':
|
||||||
|
self._print('info', s, line_ended)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def good(self, s: str, line_ended: bool = True) -> 'OutputBuffer':
|
||||||
|
self._print('good', s, line_ended)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def sep(self) -> 'OutputBuffer':
|
||||||
|
if not self.batch:
|
||||||
|
self._print('info')
|
||||||
|
return self
|
||||||
|
|
||||||
|
def v(self, s: str, write_now: bool = False) -> 'OutputBuffer':
|
||||||
|
'''Prints a message if verbose output is enabled.'''
|
||||||
|
if self.verbose or self.debug:
|
||||||
|
self.info(s)
|
||||||
|
if write_now:
|
||||||
|
self.write()
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def d(self, s: str, write_now: bool = False) -> 'OutputBuffer':
|
||||||
|
'''Prints a message if verbose output is enabled.'''
|
||||||
|
if self.debug:
|
||||||
|
self.info(s)
|
||||||
|
if write_now:
|
||||||
|
self.write()
|
||||||
|
|
||||||
|
return self
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (C) 2020 Joe Testa (jtesta@positronsecurity.com)
|
Copyright (C) 2020-2021 Joe Testa (jtesta@positronsecurity.com)
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
@ -36,7 +36,7 @@ from ssh_audit.banner import Banner # pylint: disable=unused-import
|
|||||||
class Policy:
|
class Policy:
|
||||||
|
|
||||||
# Each field maps directly to a private member variable of the Policy class.
|
# Each field maps directly to a private member variable of the Policy class.
|
||||||
BUILTIN_POLICIES = {
|
BUILTIN_POLICIES: Dict[str, Dict[str, Union[Optional[str], Optional[List[str]], bool, Dict[str, int]]]] = {
|
||||||
|
|
||||||
# Ubuntu Server policies
|
# Ubuntu Server policies
|
||||||
|
|
||||||
@ -49,15 +49,15 @@ class Policy:
|
|||||||
|
|
||||||
# Generic OpenSSH Server policies
|
# Generic OpenSSH Server policies
|
||||||
|
|
||||||
'Hardened OpenSSH Server v7.7 (version 1)': {'version': '1', 'banner': None, 'compressions': None, 'host_keys': ['ssh-ed25519'], 'optional_host_keys': ['ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': None, 'cakey_sizes': None, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 2048}, 'server_policy': True},
|
'Hardened OpenSSH Server v7.7 (version 1)': {'version': '1', 'banner': None, 'compressions': None, 'host_keys': ['rsa-sha2-512', 'rsa-sha2-256', 'ssh-ed25519'], 'optional_host_keys': ['ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': None, 'cakey_sizes': None, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 2048}, 'server_policy': True},
|
||||||
|
|
||||||
'Hardened OpenSSH Server v7.8 (version 1)': {'version': '1', 'banner': None, 'compressions': None, 'host_keys': ['ssh-ed25519'], 'optional_host_keys': ['ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': None, 'cakey_sizes': None, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 2048}, 'server_policy': True},
|
'Hardened OpenSSH Server v7.8 (version 1)': {'version': '1', 'banner': None, 'compressions': None, 'host_keys': ['rsa-sha2-512', 'rsa-sha2-256', 'ssh-ed25519'], 'optional_host_keys': ['ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': None, 'cakey_sizes': None, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 2048}, 'server_policy': True},
|
||||||
|
|
||||||
'Hardened OpenSSH Server v7.9 (version 1)': {'version': '1', 'banner': None, 'compressions': None, 'host_keys': ['ssh-ed25519'], 'optional_host_keys': ['ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': None, 'cakey_sizes': None, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 2048}, 'server_policy': True},
|
'Hardened OpenSSH Server v7.9 (version 1)': {'version': '1', 'banner': None, 'compressions': None, 'host_keys': ['rsa-sha2-512', 'rsa-sha2-256', 'ssh-ed25519'], 'optional_host_keys': ['ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': None, 'cakey_sizes': None, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 2048}, 'server_policy': True},
|
||||||
|
|
||||||
'Hardened OpenSSH Server v8.0 (version 1)': {'version': '1', 'banner': None, 'compressions': None, 'host_keys': ['ssh-ed25519'], 'optional_host_keys': ['ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': None, 'cakey_sizes': None, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 2048}, 'server_policy': True},
|
'Hardened OpenSSH Server v8.0 (version 1)': {'version': '1', 'banner': None, 'compressions': None, 'host_keys': ['rsa-sha2-512', 'rsa-sha2-256', 'ssh-ed25519'], 'optional_host_keys': ['ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': None, 'cakey_sizes': None, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 2048}, 'server_policy': True},
|
||||||
|
|
||||||
'Hardened OpenSSH Server v8.1 (version 1)': {'version': '1', 'banner': None, 'compressions': None, 'host_keys': ['ssh-ed25519'], 'optional_host_keys': ['sk-ssh-ed25519@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'sk-ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': None, 'cakey_sizes': None, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 2048}, 'server_policy': True},
|
'Hardened OpenSSH Server v8.1 (version 1)': {'version': '1', 'banner': None, 'compressions': None, 'host_keys': ['rsa-sha2-512', 'rsa-sha2-256', 'ssh-ed25519'], 'optional_host_keys': ['sk-ssh-ed25519@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'sk-ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': None, 'cakey_sizes': None, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 2048}, 'server_policy': True},
|
||||||
|
|
||||||
'Hardened OpenSSH Server v8.2 (version 1)': {'version': '1', 'banner': None, 'compressions': None, 'host_keys': ['rsa-sha2-512', 'rsa-sha2-256', 'ssh-ed25519'], 'optional_host_keys': ['sk-ssh-ed25519@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'sk-ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': {'rsa-sha2-256': 4096, 'rsa-sha2-512': 4096}, 'cakey_sizes': {'rsa-sha2-256-cert-v01@openssh.com': 4096, 'rsa-sha2-512-cert-v01@openssh.com': 4096}, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 2048}, 'server_policy': True},
|
'Hardened OpenSSH Server v8.2 (version 1)': {'version': '1', 'banner': None, 'compressions': None, 'host_keys': ['rsa-sha2-512', 'rsa-sha2-256', 'ssh-ed25519'], 'optional_host_keys': ['sk-ssh-ed25519@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'sk-ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': {'rsa-sha2-256': 4096, 'rsa-sha2-512': 4096}, 'cakey_sizes': {'rsa-sha2-256-cert-v01@openssh.com': 4096, 'rsa-sha2-512-cert-v01@openssh.com': 4096}, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 2048}, 'server_policy': True},
|
||||||
|
|
||||||
@ -65,6 +65,12 @@ class Policy:
|
|||||||
|
|
||||||
'Hardened OpenSSH Server v8.4 (version 1)': {'version': '1', 'banner': None, 'compressions': None, 'host_keys': ['rsa-sha2-512', 'rsa-sha2-256', 'ssh-ed25519'], 'optional_host_keys': ['sk-ssh-ed25519@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'sk-ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': {'rsa-sha2-256': 4096, 'rsa-sha2-512': 4096}, 'cakey_sizes': {'rsa-sha2-256-cert-v01@openssh.com': 4096, 'rsa-sha2-512-cert-v01@openssh.com': 4096}, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 2048}, 'server_policy': True},
|
'Hardened OpenSSH Server v8.4 (version 1)': {'version': '1', 'banner': None, 'compressions': None, 'host_keys': ['rsa-sha2-512', 'rsa-sha2-256', 'ssh-ed25519'], 'optional_host_keys': ['sk-ssh-ed25519@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'sk-ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': {'rsa-sha2-256': 4096, 'rsa-sha2-512': 4096}, 'cakey_sizes': {'rsa-sha2-256-cert-v01@openssh.com': 4096, 'rsa-sha2-512-cert-v01@openssh.com': 4096}, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 2048}, 'server_policy': True},
|
||||||
|
|
||||||
|
'Hardened OpenSSH Server v8.5 (version 1)': {'version': '1', 'banner': None, 'compressions': None, 'host_keys': ['rsa-sha2-512', 'rsa-sha2-256', 'ssh-ed25519'], 'optional_host_keys': ['sk-ssh-ed25519@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'sk-ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': {'rsa-sha2-256': 4096, 'rsa-sha2-512': 4096}, 'cakey_sizes': {'rsa-sha2-256-cert-v01@openssh.com': 4096, 'rsa-sha2-512-cert-v01@openssh.com': 4096}, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 2048}, 'server_policy': True},
|
||||||
|
|
||||||
|
'Hardened OpenSSH Server v8.6 (version 1)': {'version': '1', 'banner': None, 'compressions': None, 'host_keys': ['rsa-sha2-512', 'rsa-sha2-256', 'ssh-ed25519'], 'optional_host_keys': ['sk-ssh-ed25519@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'sk-ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': {'rsa-sha2-256': 4096, 'rsa-sha2-512': 4096}, 'cakey_sizes': {'rsa-sha2-256-cert-v01@openssh.com': 4096, 'rsa-sha2-512-cert-v01@openssh.com': 4096}, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 2048}, 'server_policy': True},
|
||||||
|
|
||||||
|
'Hardened OpenSSH Server v8.7 (version 1)': {'version': '1', 'banner': None, 'compressions': None, 'host_keys': ['rsa-sha2-512', 'rsa-sha2-256', 'ssh-ed25519'], 'optional_host_keys': ['sk-ssh-ed25519@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'sk-ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': {'rsa-sha2-256': 4096, 'rsa-sha2-512': 4096}, 'cakey_sizes': {'rsa-sha2-256-cert-v01@openssh.com': 4096, 'rsa-sha2-512-cert-v01@openssh.com': 4096}, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 2048}, 'server_policy': True},
|
||||||
|
|
||||||
|
|
||||||
# Ubuntu Client policies
|
# Ubuntu Client policies
|
||||||
|
|
||||||
@ -74,25 +80,25 @@ class Policy:
|
|||||||
|
|
||||||
'Hardened Ubuntu Client 20.04 LTS (version 2)': {'version': '2', 'banner': None, 'compressions': None, 'host_keys': ['ssh-ed25519', 'ssh-ed25519-cert-v01@openssh.com', 'sk-ssh-ed25519@openssh.com', 'sk-ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512', 'rsa-sha2-512-cert-v01@openssh.com'], 'optional_host_keys': None, 'kex': ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256', 'ext-info-c'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': None, 'cakey_sizes': None, 'dh_modulus_sizes': None, 'server_policy': False},
|
'Hardened Ubuntu Client 20.04 LTS (version 2)': {'version': '2', 'banner': None, 'compressions': None, 'host_keys': ['ssh-ed25519', 'ssh-ed25519-cert-v01@openssh.com', 'sk-ssh-ed25519@openssh.com', 'sk-ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512', 'rsa-sha2-512-cert-v01@openssh.com'], 'optional_host_keys': None, 'kex': ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256', 'ext-info-c'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': None, 'cakey_sizes': None, 'dh_modulus_sizes': None, 'server_policy': False},
|
||||||
|
|
||||||
} # type: Dict[str, Dict[str, Union[Optional[str], Optional[List[str]], bool, Dict[str, int]]]]
|
}
|
||||||
|
|
||||||
|
|
||||||
def __init__(self, policy_file: Optional[str] = None, policy_data: Optional[str] = None, manual_load: bool = False) -> None:
|
def __init__(self, policy_file: Optional[str] = None, policy_data: Optional[str] = None, manual_load: bool = False) -> None:
|
||||||
self._name = None # type: Optional[str]
|
self._name: Optional[str] = None
|
||||||
self._version = None # type: Optional[str]
|
self._version: Optional[str] = None
|
||||||
self._banner = None # type: Optional[str]
|
self._banner: Optional[str] = None
|
||||||
self._compressions = None # type: Optional[List[str]]
|
self._compressions: Optional[List[str]] = None
|
||||||
self._host_keys = None # type: Optional[List[str]]
|
self._host_keys: Optional[List[str]] = None
|
||||||
self._optional_host_keys = None # type: Optional[List[str]]
|
self._optional_host_keys: Optional[List[str]] = None
|
||||||
self._kex = None # type: Optional[List[str]]
|
self._kex: Optional[List[str]] = None
|
||||||
self._ciphers = None # type: Optional[List[str]]
|
self._ciphers: Optional[List[str]] = None
|
||||||
self._macs = None # type: Optional[List[str]]
|
self._macs: Optional[List[str]] = None
|
||||||
self._hostkey_sizes = None # type: Optional[Dict[str, int]]
|
self._hostkey_sizes: Optional[Dict[str, int]] = None
|
||||||
self._cakey_sizes = None # type: Optional[Dict[str, int]]
|
self._cakey_sizes: Optional[Dict[str, int]] = None
|
||||||
self._dh_modulus_sizes = None # type: Optional[Dict[str, int]]
|
self._dh_modulus_sizes: Optional[Dict[str, int]] = None
|
||||||
self._server_policy = True
|
self._server_policy = True
|
||||||
|
|
||||||
self._name_and_version = '' # type: str
|
self._name_and_version: str = ''
|
||||||
|
|
||||||
# Ensure that only one mode was specified.
|
# Ensure that only one mode was specified.
|
||||||
num_modes = 0
|
num_modes = 0
|
||||||
@ -111,7 +117,7 @@ class Policy:
|
|||||||
|
|
||||||
if policy_file is not None:
|
if policy_file is not None:
|
||||||
try:
|
try:
|
||||||
with open(policy_file, "r") as f:
|
with open(policy_file, "r", encoding='utf-8') as f:
|
||||||
policy_data = f.read()
|
policy_data = f.read()
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
print("Error: policy file not found: %s" % policy_file)
|
print("Error: policy file not found: %s" % policy_file)
|
||||||
@ -305,7 +311,7 @@ macs = %s
|
|||||||
'''Evaluates a server configuration against this policy. Returns a tuple of a boolean (True if server adheres to policy) and an array of strings that holds error messages.'''
|
'''Evaluates a server configuration against this policy. Returns a tuple of a boolean (True if server adheres to policy) and an array of strings that holds error messages.'''
|
||||||
|
|
||||||
ret = True
|
ret = True
|
||||||
errors = [] # type: List[Any]
|
errors: List[Any] = []
|
||||||
|
|
||||||
banner_str = str(banner)
|
banner_str = str(banner)
|
||||||
if (self._banner is not None) and (banner_str != self._banner):
|
if (self._banner is not None) and (banner_str != self._banner):
|
||||||
@ -419,8 +425,8 @@ macs = %s
|
|||||||
server_policy_names = []
|
server_policy_names = []
|
||||||
client_policy_names = []
|
client_policy_names = []
|
||||||
|
|
||||||
for policy_name in Policy.BUILTIN_POLICIES:
|
for policy_name, policy in Policy.BUILTIN_POLICIES.items():
|
||||||
if Policy.BUILTIN_POLICIES[policy_name]['server_policy']:
|
if policy['server_policy']:
|
||||||
server_policy_names.append(policy_name)
|
server_policy_names.append(policy_name)
|
||||||
else:
|
else:
|
||||||
client_policy_names.append(policy_name)
|
client_policy_names.append(policy_name)
|
||||||
|
@ -43,14 +43,14 @@ class ReadBuf:
|
|||||||
return self._buf.read(size)
|
return self._buf.read(size)
|
||||||
|
|
||||||
def read_byte(self) -> int:
|
def read_byte(self) -> int:
|
||||||
v = struct.unpack('B', self.read(1))[0] # type: int
|
v: int = struct.unpack('B', self.read(1))[0]
|
||||||
return v
|
return v
|
||||||
|
|
||||||
def read_bool(self) -> bool:
|
def read_bool(self) -> bool:
|
||||||
return self.read_byte() != 0
|
return self.read_byte() != 0
|
||||||
|
|
||||||
def read_int(self) -> int:
|
def read_int(self) -> int:
|
||||||
v = struct.unpack('>I', self.read(4))[0] # type: int
|
v: int = struct.unpack('>I', self.read(4))[0]
|
||||||
return v
|
return v
|
||||||
|
|
||||||
def read_list(self) -> List[str]:
|
def read_list(self) -> List[str]:
|
||||||
|
@ -180,7 +180,7 @@ class Software:
|
|||||||
# pylint: disable=too-many-return-statements
|
# pylint: disable=too-many-return-statements
|
||||||
software = str(banner.software)
|
software = str(banner.software)
|
||||||
mx = re.match(r'^dropbear_([\d\.]+\d+)(.*)', software)
|
mx = re.match(r'^dropbear_([\d\.]+\d+)(.*)', software)
|
||||||
v = None # type: Optional[str]
|
v: Optional[str] = None
|
||||||
if mx is not None:
|
if mx is not None:
|
||||||
patch = cls._fix_patch(mx.group(2))
|
patch = cls._fix_patch(mx.group(2))
|
||||||
v, p = 'Matt Johnston', Product.DropbearSSH
|
v, p = 'Matt Johnston', Product.DropbearSSH
|
||||||
|
@ -29,7 +29,7 @@ from ssh_audit.ssh1_crc32 import SSH1_CRC32
|
|||||||
|
|
||||||
|
|
||||||
class SSH1:
|
class SSH1:
|
||||||
_crc32 = None # type: Optional[SSH1_CRC32]
|
_crc32: Optional[SSH1_CRC32] = None
|
||||||
CIPHERS = ['none', 'idea', 'des', '3des', 'tss', 'rc4', 'blowfish']
|
CIPHERS = ['none', 'idea', 'des', '3des', 'tss', 'rc4', 'blowfish']
|
||||||
AUTHS = ['none', 'rhosts', 'rsa', 'password', 'rhosts_rsa', 'tis', 'kerberos']
|
AUTHS = ['none', 'rhosts', 'rsa', 'password', 'rhosts_rsa', 'tis', 'kerberos']
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ class SSH1_KexDB: # pylint: disable=too-few-public-methods
|
|||||||
FAIL_NA_UNSAFE = 'not implemented in OpenSSH (server), unsafe algorithm'
|
FAIL_NA_UNSAFE = 'not implemented in OpenSSH (server), unsafe algorithm'
|
||||||
TEXT_CIPHER_IDEA = 'cipher used by commercial SSH'
|
TEXT_CIPHER_IDEA = 'cipher used by commercial SSH'
|
||||||
|
|
||||||
ALGORITHMS = {
|
ALGORITHMS: Dict[str, Dict[str, List[List[Optional[str]]]]] = {
|
||||||
'key': {
|
'key': {
|
||||||
'ssh-rsa1': [['1.2.2']],
|
'ssh-rsa1': [['1.2.2']],
|
||||||
},
|
},
|
||||||
@ -55,4 +55,4 @@ class SSH1_KexDB: # pylint: disable=too-few-public-methods
|
|||||||
'tis': [['1.2.2']],
|
'tis': [['1.2.2']],
|
||||||
'kerberos': [['1.2.2', '3.6'], [FAIL_OPENSSH37_REMOVE]],
|
'kerberos': [['1.2.2', '3.6'], [FAIL_OPENSSH37_REMOVE]],
|
||||||
}
|
}
|
||||||
} # type: Dict[str, Dict[str, List[List[Optional[str]]]]]
|
}
|
||||||
|
@ -90,7 +90,7 @@ class SSH1_PublicKeyMessage:
|
|||||||
@property
|
@property
|
||||||
def supported_ciphers(self) -> List[str]:
|
def supported_ciphers(self) -> List[str]:
|
||||||
ciphers = []
|
ciphers = []
|
||||||
for i in range(len(SSH1.CIPHERS)):
|
for i in range(len(SSH1.CIPHERS)): # pylint: disable=consider-using-enumerate
|
||||||
if self.__supported_ciphers_mask & (1 << i) != 0:
|
if self.__supported_ciphers_mask & (1 << i) != 0:
|
||||||
ciphers.append(Utils.to_text(SSH1.CIPHERS[i]))
|
ciphers.append(Utils.to_text(SSH1.CIPHERS[i]))
|
||||||
return ciphers
|
return ciphers
|
||||||
|
@ -41,9 +41,9 @@ class SSH2_Kex:
|
|||||||
self.__follows = follows
|
self.__follows = follows
|
||||||
self.__unused = unused
|
self.__unused = unused
|
||||||
|
|
||||||
self.__rsa_key_sizes = {} # type: Dict[str, Tuple[int, int]]
|
self.__rsa_key_sizes: Dict[str, Tuple[int, int]] = {}
|
||||||
self.__dh_modulus_sizes = {} # type: Dict[str, Tuple[int, int]]
|
self.__dh_modulus_sizes: Dict[str, Tuple[int, int]] = {}
|
||||||
self.__host_keys = {} # type: Dict[str, bytes]
|
self.__host_keys: Dict[str, bytes] = {}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cookie(self) -> bytes:
|
def cookie(self) -> bytes:
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (C) 2017-2020 Joe Testa (jtesta@positronsecurity.com)
|
Copyright (C) 2017-2021 Joe Testa (jtesta@positronsecurity.com)
|
||||||
Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu)
|
Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu)
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
@ -37,6 +37,7 @@ class SSH2_KexDB: # pylint: disable=too-few-public-methods
|
|||||||
FAIL_OPENSSH67_UNSAFE = 'removed (in server) since OpenSSH 6.7, unsafe algorithm'
|
FAIL_OPENSSH67_UNSAFE = 'removed (in server) since OpenSSH 6.7, unsafe algorithm'
|
||||||
FAIL_OPENSSH61_REMOVE = 'removed since OpenSSH 6.1, removed from specification'
|
FAIL_OPENSSH61_REMOVE = 'removed since OpenSSH 6.1, removed from specification'
|
||||||
FAIL_OPENSSH31_REMOVE = 'removed since OpenSSH 3.1'
|
FAIL_OPENSSH31_REMOVE = 'removed since OpenSSH 3.1'
|
||||||
|
INFO_OPENSSH82_FUTURE_DEPRECATION = 'a future deprecation notice has been issued in OpenSSH 8.2: https://www.openssh.com/txt/release-8.2'
|
||||||
FAIL_DBEAR67_DISABLED = 'disabled since Dropbear SSH 2015.67'
|
FAIL_DBEAR67_DISABLED = 'disabled since Dropbear SSH 2015.67'
|
||||||
FAIL_DBEAR53_DISABLED = 'disabled since Dropbear SSH 0.53'
|
FAIL_DBEAR53_DISABLED = 'disabled since Dropbear SSH 0.53'
|
||||||
FAIL_DEPRECATED_CIPHER = 'deprecated cipher'
|
FAIL_DEPRECATED_CIPHER = 'deprecated cipher'
|
||||||
@ -46,6 +47,7 @@ class SSH2_KexDB: # pylint: disable=too-few-public-methods
|
|||||||
FAIL_DEPRECATED_MAC = 'deprecated MAC'
|
FAIL_DEPRECATED_MAC = 'deprecated MAC'
|
||||||
FAIL_1024BIT_MODULUS = 'using small 1024-bit modulus'
|
FAIL_1024BIT_MODULUS = 'using small 1024-bit modulus'
|
||||||
FAIL_UNPROVEN = 'using unproven algorithm'
|
FAIL_UNPROVEN = 'using unproven algorithm'
|
||||||
|
FAIL_HASH_WEAK = 'using weak hashing algorithm'
|
||||||
WARN_CURVES_WEAK = 'using weak elliptic curves'
|
WARN_CURVES_WEAK = 'using weak elliptic curves'
|
||||||
WARN_RNDSIG_KEY = 'using weak random number generator could reveal the key'
|
WARN_RNDSIG_KEY = 'using weak random number generator could reveal the key'
|
||||||
WARN_HASH_WEAK = 'using weak hashing algorithm'
|
WARN_HASH_WEAK = 'using weak hashing algorithm'
|
||||||
@ -59,15 +61,18 @@ class SSH2_KexDB: # pylint: disable=too-few-public-methods
|
|||||||
WARN_OBSOLETE = 'using obsolete algorithm'
|
WARN_OBSOLETE = 'using obsolete algorithm'
|
||||||
WARN_UNTRUSTED = 'using untrusted algorithm'
|
WARN_UNTRUSTED = 'using untrusted algorithm'
|
||||||
|
|
||||||
ALGORITHMS = {
|
ALGORITHMS: Dict[str, Dict[str, List[List[Optional[str]]]]] = {
|
||||||
# Format: 'algorithm_name': [['version_first_appeared_in'], [reason_for_failure1, reason_for_failure2, ...], [warning1, warning2, ...]]
|
# Format: 'algorithm_name': [['version_first_appeared_in'], [reason_for_failure1, reason_for_failure2, ...], [warning1, warning2, ...]]
|
||||||
'kex': {
|
'kex': {
|
||||||
'diffie-hellman-group1-sha1': [['2.3.0,d0.28,l10.2', '6.6', '6.9'], [FAIL_1024BIT_MODULUS, FAIL_OPENSSH67_UNSAFE, FAIL_OPENSSH70_LOGJAM], [WARN_HASH_WEAK]],
|
'diffie-hellman-group1-sha1': [['2.3.0,d0.28,l10.2', '6.6', '6.9'], [FAIL_1024BIT_MODULUS, FAIL_OPENSSH67_UNSAFE, FAIL_OPENSSH70_LOGJAM], [WARN_HASH_WEAK]],
|
||||||
'gss-group1-sha1-toWM5Slw5Ew8Mqkay+al2g==': [[], [FAIL_1024BIT_MODULUS, FAIL_OPENSSH67_UNSAFE, FAIL_OPENSSH70_LOGJAM], [WARN_HASH_WEAK]],
|
'gss-group1-sha1-toWM5Slw5Ew8Mqkay+al2g==': [[], [FAIL_1024BIT_MODULUS, FAIL_OPENSSH67_UNSAFE, FAIL_OPENSSH70_LOGJAM], [WARN_HASH_WEAK]],
|
||||||
|
'gss-gex-sha1-eipGX3TCiQSrx573bT1o1Q==': [[], [], [WARN_HASH_WEAK]],
|
||||||
'gss-gex-sha1-toWM5Slw5Ew8Mqkay+al2g==': [[], [], [WARN_HASH_WEAK]],
|
'gss-gex-sha1-toWM5Slw5Ew8Mqkay+al2g==': [[], [], [WARN_HASH_WEAK]],
|
||||||
'gss-gex-sha1-': [[], [], [WARN_HASH_WEAK]],
|
'gss-gex-sha1-': [[], [], [WARN_HASH_WEAK]],
|
||||||
|
'gss-group1-sha1-eipGX3TCiQSrx573bT1o1Q==': [[], [FAIL_1024BIT_MODULUS], [WARN_HASH_WEAK]],
|
||||||
'gss-group1-sha1-': [[], [FAIL_1024BIT_MODULUS], [WARN_HASH_WEAK]],
|
'gss-group1-sha1-': [[], [FAIL_1024BIT_MODULUS], [WARN_HASH_WEAK]],
|
||||||
'gss-group14-sha1-': [[], [], [WARN_HASH_WEAK]],
|
'gss-group14-sha1-': [[], [], [WARN_HASH_WEAK]],
|
||||||
|
'gss-group14-sha1-eipGX3TCiQSrx573bT1o1Q==': [[], [], [WARN_HASH_WEAK]],
|
||||||
'gss-group14-sha1-toWM5Slw5Ew8Mqkay+al2g==': [[], [], [WARN_HASH_WEAK]],
|
'gss-group14-sha1-toWM5Slw5Ew8Mqkay+al2g==': [[], [], [WARN_HASH_WEAK]],
|
||||||
'gss-group14-sha256-': [[]],
|
'gss-group14-sha256-': [[]],
|
||||||
'gss-group14-sha256-toWM5Slw5Ew8Mqkay+al2g==': [[]],
|
'gss-group14-sha256-toWM5Slw5Ew8Mqkay+al2g==': [[]],
|
||||||
@ -115,7 +120,8 @@ class SSH2_KexDB: # pylint: disable=too-few-public-methods
|
|||||||
'kexguess2@matt.ucc.asn.au': [['d2013.57']],
|
'kexguess2@matt.ucc.asn.au': [['d2013.57']],
|
||||||
'rsa1024-sha1': [[], [FAIL_1024BIT_MODULUS], [WARN_HASH_WEAK]],
|
'rsa1024-sha1': [[], [FAIL_1024BIT_MODULUS], [WARN_HASH_WEAK]],
|
||||||
'rsa2048-sha256': [[]],
|
'rsa2048-sha256': [[]],
|
||||||
'sntrup4591761x25519-sha512@tinyssh.org': [['8.0'], [], [WARN_EXPERIMENTAL]],
|
'sntrup4591761x25519-sha512@tinyssh.org': [['8.0', '8.4'], [], [WARN_EXPERIMENTAL]],
|
||||||
|
'sntrup761x25519-sha512@openssh.com': [['8.5'], [], [WARN_EXPERIMENTAL]],
|
||||||
'kexAlgoCurve25519SHA256': [[]],
|
'kexAlgoCurve25519SHA256': [[]],
|
||||||
'Curve25519SHA256': [[]],
|
'Curve25519SHA256': [[]],
|
||||||
'ext-info-c': [[]], # Extension negotiation (RFC 8308)
|
'ext-info-c': [[]], # Extension negotiation (RFC 8308)
|
||||||
@ -127,20 +133,20 @@ class SSH2_KexDB: # pylint: disable=too-few-public-methods
|
|||||||
'rsa-sha2-512': [['7.2']],
|
'rsa-sha2-512': [['7.2']],
|
||||||
'ssh-ed25519': [['6.5,l10.7.0']],
|
'ssh-ed25519': [['6.5,l10.7.0']],
|
||||||
'ssh-ed25519-cert-v01@openssh.com': [['6.5']],
|
'ssh-ed25519-cert-v01@openssh.com': [['6.5']],
|
||||||
'ssh-rsa': [['2.5.0,d0.28,l10.2'], [WARN_HASH_WEAK]],
|
'ssh-rsa': [['2.5.0,d0.28,l10.2'], [FAIL_HASH_WEAK], [], [INFO_OPENSSH82_FUTURE_DEPRECATION]],
|
||||||
'ssh-dss': [['2.1.0,d0.28,l10.2', '6.9'], [FAIL_1024BIT_MODULUS, FAIL_OPENSSH70_WEAK], [WARN_RNDSIG_KEY]],
|
'ssh-dss': [['2.1.0,d0.28,l10.2', '6.9'], [FAIL_1024BIT_MODULUS, FAIL_OPENSSH70_WEAK], [WARN_RNDSIG_KEY]],
|
||||||
'ecdsa-sha2-nistp256': [['5.7,d2013.62,l10.6.4'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]],
|
'ecdsa-sha2-nistp256': [['5.7,d2013.62,l10.6.4'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]],
|
||||||
'ecdsa-sha2-nistp384': [['5.7,d2013.62,l10.6.4'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]],
|
'ecdsa-sha2-nistp384': [['5.7,d2013.62,l10.6.4'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]],
|
||||||
'ecdsa-sha2-nistp521': [['5.7,d2013.62,l10.6.4'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]],
|
'ecdsa-sha2-nistp521': [['5.7,d2013.62,l10.6.4'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]],
|
||||||
'ecdsa-sha2-1.3.132.0.10': [[], [], [WARN_RNDSIG_KEY]], # ECDSA over secp256k1 (i.e.: the Bitcoin curve)
|
'ecdsa-sha2-1.3.132.0.10': [[], [], [WARN_RNDSIG_KEY]], # ECDSA over secp256k1 (i.e.: the Bitcoin curve)
|
||||||
'x509v3-sign-dss': [[], [FAIL_1024BIT_MODULUS, FAIL_OPENSSH70_WEAK], [WARN_RNDSIG_KEY]],
|
'x509v3-sign-dss': [[], [FAIL_1024BIT_MODULUS, FAIL_OPENSSH70_WEAK], [WARN_RNDSIG_KEY]],
|
||||||
'x509v3-sign-rsa': [[], [], [WARN_HASH_WEAK]],
|
'x509v3-sign-rsa': [[], [FAIL_HASH_WEAK], [], [INFO_OPENSSH82_FUTURE_DEPRECATION]],
|
||||||
'x509v3-sign-rsa-sha256@ssh.com': [[]],
|
'x509v3-sign-rsa-sha256@ssh.com': [[]],
|
||||||
'x509v3-ssh-dss': [[], [FAIL_1024BIT_MODULUS, FAIL_OPENSSH70_WEAK], [WARN_RNDSIG_KEY]],
|
'x509v3-ssh-dss': [[], [FAIL_1024BIT_MODULUS, FAIL_OPENSSH70_WEAK], [WARN_RNDSIG_KEY]],
|
||||||
'x509v3-ssh-rsa': [[], [], [WARN_HASH_WEAK]],
|
'x509v3-ssh-rsa': [[], [FAIL_HASH_WEAK], [], [INFO_OPENSSH82_FUTURE_DEPRECATION]],
|
||||||
'ssh-rsa-cert-v00@openssh.com': [['5.4', '6.9'], [FAIL_OPENSSH70_LEGACY], []],
|
'ssh-rsa-cert-v00@openssh.com': [['5.4', '6.9'], [FAIL_OPENSSH70_LEGACY, FAIL_HASH_WEAK], [], [INFO_OPENSSH82_FUTURE_DEPRECATION]],
|
||||||
'ssh-dss-cert-v00@openssh.com': [['5.4', '6.9'], [FAIL_1024BIT_MODULUS, FAIL_OPENSSH70_LEGACY], [WARN_RNDSIG_KEY]],
|
'ssh-dss-cert-v00@openssh.com': [['5.4', '6.9'], [FAIL_1024BIT_MODULUS, FAIL_OPENSSH70_LEGACY], [WARN_RNDSIG_KEY]],
|
||||||
'ssh-rsa-cert-v01@openssh.com': [['5.6'], [WARN_HASH_WEAK]],
|
'ssh-rsa-cert-v01@openssh.com': [['5.6'], [FAIL_HASH_WEAK], [], [INFO_OPENSSH82_FUTURE_DEPRECATION]],
|
||||||
'ssh-dss-cert-v01@openssh.com': [['5.6', '6.9'], [FAIL_1024BIT_MODULUS, FAIL_OPENSSH70_WEAK], [WARN_RNDSIG_KEY]],
|
'ssh-dss-cert-v01@openssh.com': [['5.6', '6.9'], [FAIL_1024BIT_MODULUS, FAIL_OPENSSH70_WEAK], [WARN_RNDSIG_KEY]],
|
||||||
'ecdsa-sha2-nistp256-cert-v01@openssh.com': [['5.7'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]],
|
'ecdsa-sha2-nistp256-cert-v01@openssh.com': [['5.7'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]],
|
||||||
'ecdsa-sha2-nistp384-cert-v01@openssh.com': [['5.7'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]],
|
'ecdsa-sha2-nistp384-cert-v01@openssh.com': [['5.7'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]],
|
||||||
@ -247,6 +253,7 @@ class SSH2_KexDB: # pylint: disable=too-few-public-methods
|
|||||||
'hmac-md5-96': [['2.5.0', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC, WARN_HASH_WEAK]],
|
'hmac-md5-96': [['2.5.0', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC, WARN_HASH_WEAK]],
|
||||||
'hmac-ripemd': [[], [FAIL_DEPRECATED_MAC], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC]],
|
'hmac-ripemd': [[], [FAIL_DEPRECATED_MAC], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC]],
|
||||||
'hmac-ripemd160': [['2.5.0', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC]],
|
'hmac-ripemd160': [['2.5.0', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC]],
|
||||||
|
'hmac-ripemd160-96': [[], [FAIL_DEPRECATED_MAC], [WARN_ENCRYPT_AND_MAC, WARN_TAG_SIZE]],
|
||||||
'hmac-ripemd160@openssh.com': [['2.1.0', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC]],
|
'hmac-ripemd160@openssh.com': [['2.1.0', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC]],
|
||||||
'umac-64@openssh.com': [['4.7'], [], [WARN_ENCRYPT_AND_MAC, WARN_TAG_SIZE]],
|
'umac-64@openssh.com': [['4.7'], [], [WARN_ENCRYPT_AND_MAC, WARN_TAG_SIZE]],
|
||||||
'umac-128@openssh.com': [['6.2'], [], [WARN_ENCRYPT_AND_MAC]],
|
'umac-128@openssh.com': [['6.2'], [], [WARN_ENCRYPT_AND_MAC]],
|
||||||
@ -267,5 +274,7 @@ class SSH2_KexDB: # pylint: disable=too-few-public-methods
|
|||||||
'aes256-gcm': [[]],
|
'aes256-gcm': [[]],
|
||||||
'chacha20-poly1305@openssh.com': [[]], # Despite the @openssh.com tag, this was never shipped as a MAC in OpenSSH (only as a cipher); it is only implemented as a MAC in Syncplify.
|
'chacha20-poly1305@openssh.com': [[]], # Despite the @openssh.com tag, this was never shipped as a MAC in OpenSSH (only as a cipher); it is only implemented as a MAC in Syncplify.
|
||||||
'crypticore-mac@ssh.com': [[], [FAIL_UNPROVEN]],
|
'crypticore-mac@ssh.com': [[], [FAIL_UNPROVEN]],
|
||||||
|
'AEAD_AES_128_GCM': [[]],
|
||||||
|
'AEAD_AES_256_GCM': [[]],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} # type: Dict[str, Dict[str, List[List[Optional[str]]]]]
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"""
|
"""
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (C) 2017-2020 Joe Testa (jtesta@positronsecurity.com)
|
Copyright (C) 2017-2021 Joe Testa (jtesta@positronsecurity.com)
|
||||||
Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu)
|
Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu)
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
@ -23,9 +23,12 @@
|
|||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
THE SOFTWARE.
|
THE SOFTWARE.
|
||||||
"""
|
"""
|
||||||
|
import concurrent.futures
|
||||||
|
import copy
|
||||||
import getopt
|
import getopt
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
@ -34,6 +37,7 @@ from typing import Dict, List, Set, Sequence, Tuple, Iterable # noqa: F401
|
|||||||
from typing import Callable, Optional, Union, Any # noqa: F401
|
from typing import Callable, Optional, Union, Any # noqa: F401
|
||||||
|
|
||||||
from ssh_audit.globals import VERSION
|
from ssh_audit.globals import VERSION
|
||||||
|
from ssh_audit.globals import WINDOWS_MAN_PAGE
|
||||||
from ssh_audit.algorithm import Algorithm
|
from ssh_audit.algorithm import Algorithm
|
||||||
from ssh_audit.algorithms import Algorithms
|
from ssh_audit.algorithms import Algorithms
|
||||||
from ssh_audit.auditconf import AuditConf
|
from ssh_audit.auditconf import AuditConf
|
||||||
@ -42,7 +46,6 @@ from ssh_audit import exitcodes
|
|||||||
from ssh_audit.fingerprint import Fingerprint
|
from ssh_audit.fingerprint import Fingerprint
|
||||||
from ssh_audit.gextest import GEXTest
|
from ssh_audit.gextest import GEXTest
|
||||||
from ssh_audit.hostkeytest import HostKeyTest
|
from ssh_audit.hostkeytest import HostKeyTest
|
||||||
from ssh_audit.output import Output
|
|
||||||
from ssh_audit.outputbuffer import OutputBuffer
|
from ssh_audit.outputbuffer import OutputBuffer
|
||||||
from ssh_audit.policy import Policy
|
from ssh_audit.policy import Policy
|
||||||
from ssh_audit.product import Product
|
from ssh_audit.product import Product
|
||||||
@ -56,17 +59,18 @@ from ssh_audit.ssh_socket import SSH_Socket
|
|||||||
from ssh_audit.utils import Utils
|
from ssh_audit.utils import Utils
|
||||||
from ssh_audit.versionvulnerabilitydb import VersionVulnerabilityDB
|
from ssh_audit.versionvulnerabilitydb import VersionVulnerabilityDB
|
||||||
|
|
||||||
|
# Only import colorama under Windows. Other OSes can natively handle terminal colors.
|
||||||
try: # pragma: nocover
|
if sys.platform == 'win32':
|
||||||
|
try:
|
||||||
from colorama import init as colorama_init
|
from colorama import init as colorama_init
|
||||||
colorama_init(strip=False) # pragma: nocover
|
colorama_init()
|
||||||
except ImportError: # pragma: nocover
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def usage(err: Optional[str] = None) -> None:
|
def usage(err: Optional[str] = None) -> None:
|
||||||
retval = exitcodes.GOOD
|
retval = exitcodes.GOOD
|
||||||
uout = Output()
|
uout = OutputBuffer()
|
||||||
p = os.path.basename(sys.argv[0])
|
p = os.path.basename(sys.argv[0])
|
||||||
uout.head('# {} {}, https://github.com/jtesta/ssh-audit\n'.format(p, VERSION))
|
uout.head('# {} {}, https://github.com/jtesta/ssh-audit\n'.format(p, VERSION))
|
||||||
if err is not None and len(err) > 0:
|
if err is not None and len(err) > 0:
|
||||||
@ -80,34 +84,38 @@ def usage(err: Optional[str] = None) -> None:
|
|||||||
uout.info(' -6, --ipv6 enable IPv6 (order of precedence)')
|
uout.info(' -6, --ipv6 enable IPv6 (order of precedence)')
|
||||||
uout.info(' -b, --batch batch output')
|
uout.info(' -b, --batch batch output')
|
||||||
uout.info(' -c, --client-audit starts a server on port 2222 to audit client\n software config (use -p to change port;\n use -t to change timeout)')
|
uout.info(' -c, --client-audit starts a server on port 2222 to audit client\n software config (use -p to change port;\n use -t to change timeout)')
|
||||||
uout.info(' -j, --json JSON output')
|
uout.info(' -d, --debug debug output')
|
||||||
|
uout.info(' -j, --json JSON output (use -jj to enable indents)')
|
||||||
uout.info(' -l, --level=<level> minimum output level (info|warn|fail)')
|
uout.info(' -l, --level=<level> minimum output level (info|warn|fail)')
|
||||||
uout.info(' -L, --list-policies list all the official, built-in policies')
|
uout.info(' -L, --list-policies list all the official, built-in policies')
|
||||||
uout.info(' --lookup=<alg1,alg2,...> looks up an algorithm(s) without\n connecting to a server')
|
uout.info(' --lookup=<alg1,alg2,...> looks up an algorithm(s) without\n connecting to a server')
|
||||||
uout.info(' -M, --make-policy=<policy.txt> creates a policy based on the target server\n (i.e.: the target server has the ideal\n configuration that other servers should\n adhere to)')
|
uout.info(' -M, --make-policy=<policy.txt> creates a policy based on the target server\n (i.e.: the target server has the ideal\n configuration that other servers should\n adhere to)')
|
||||||
|
uout.info(' -m, --manual print the man page (Windows only)')
|
||||||
uout.info(' -n, --no-colors disable colors')
|
uout.info(' -n, --no-colors disable colors')
|
||||||
uout.info(' -p, --port=<port> port to connect')
|
uout.info(' -p, --port=<port> port to connect')
|
||||||
uout.info(' -P, --policy=<policy.txt> run a policy test using the specified policy')
|
uout.info(' -P, --policy=<policy.txt> run a policy test using the specified policy')
|
||||||
uout.info(' -t, --timeout=<secs> timeout (in seconds) for connection and reading\n (default: 5)')
|
uout.info(' -t, --timeout=<secs> timeout (in seconds) for connection and reading\n (default: 5)')
|
||||||
uout.info(' -T, --targets=<hosts.txt> a file containing a list of target hosts (one\n per line, format HOST[:PORT])')
|
uout.info(' -T, --targets=<hosts.txt> a file containing a list of target hosts (one\n per line, format HOST[:PORT]). Use --threads\n to control concurrent scans.')
|
||||||
|
uout.info(' --threads=<threads> number of threads to use when scanning multiple\n targets (-T/--targets) (default: 32)')
|
||||||
uout.info(' -v, --verbose verbose output')
|
uout.info(' -v, --verbose verbose output')
|
||||||
uout.sep()
|
uout.sep()
|
||||||
|
uout.write()
|
||||||
sys.exit(retval)
|
sys.exit(retval)
|
||||||
|
|
||||||
|
|
||||||
def output_algorithms(title: str, alg_db: Dict[str, Dict[str, List[List[Optional[str]]]]], alg_type: str, algorithms: List[str], unknown_algs: List[str], is_json_output: bool, program_retval: int, maxlen: int = 0, alg_sizes: Optional[Dict[str, Tuple[int, int]]] = None) -> int: # pylint: disable=too-many-arguments
|
def output_algorithms(out: OutputBuffer, title: str, alg_db: Dict[str, Dict[str, List[List[Optional[str]]]]], alg_type: str, algorithms: List[str], unknown_algs: List[str], is_json_output: bool, program_retval: int, maxlen: int = 0, alg_sizes: Optional[Dict[str, Tuple[int, int]]] = None) -> int: # pylint: disable=too-many-arguments
|
||||||
with OutputBuffer() as obuf:
|
with out:
|
||||||
for algorithm in algorithms:
|
for algorithm in algorithms:
|
||||||
program_retval = output_algorithm(alg_db, alg_type, algorithm, unknown_algs, program_retval, maxlen, alg_sizes)
|
program_retval = output_algorithm(out, alg_db, alg_type, algorithm, unknown_algs, program_retval, maxlen, alg_sizes)
|
||||||
if len(obuf) > 0 and not is_json_output:
|
if not out.is_section_empty() and not is_json_output:
|
||||||
out.head('# ' + title)
|
out.head('# ' + title)
|
||||||
obuf.flush()
|
out.flush_section()
|
||||||
out.sep()
|
out.sep()
|
||||||
|
|
||||||
return program_retval
|
return program_retval
|
||||||
|
|
||||||
|
|
||||||
def output_algorithm(alg_db: Dict[str, Dict[str, List[List[Optional[str]]]]], alg_type: str, alg_name: str, unknown_algs: List[str], program_retval: int, alg_max_len: int = 0, alg_sizes: Optional[Dict[str, Tuple[int, int]]] = None) -> int:
|
def output_algorithm(out: OutputBuffer, alg_db: Dict[str, Dict[str, List[List[Optional[str]]]]], alg_type: str, alg_name: str, unknown_algs: List[str], program_retval: int, alg_max_len: int = 0, alg_sizes: Optional[Dict[str, Tuple[int, int]]] = None) -> int:
|
||||||
prefix = '(' + alg_type + ') '
|
prefix = '(' + alg_type + ') '
|
||||||
if alg_max_len == 0:
|
if alg_max_len == 0:
|
||||||
alg_max_len = len(alg_name)
|
alg_max_len = len(alg_name)
|
||||||
@ -175,7 +183,7 @@ def output_algorithm(alg_db: Dict[str, Dict[str, List[List[Optional[str]]]]], al
|
|||||||
return program_retval
|
return program_retval
|
||||||
|
|
||||||
|
|
||||||
def output_compatibility(algs: Algorithms, client_audit: bool, for_server: bool = True) -> None:
|
def output_compatibility(out: OutputBuffer, algs: Algorithms, client_audit: bool, for_server: bool = True) -> None:
|
||||||
|
|
||||||
# Don't output any compatibility info if we're doing a client audit.
|
# Don't output any compatibility info if we're doing a client audit.
|
||||||
if client_audit:
|
if client_audit:
|
||||||
@ -205,18 +213,18 @@ def output_compatibility(algs: Algorithms, client_audit: bool, for_server: bool
|
|||||||
out.good('(gen) compatibility: ' + ', '.join(comp_text))
|
out.good('(gen) compatibility: ' + ', '.join(comp_text))
|
||||||
|
|
||||||
|
|
||||||
def output_security_sub(sub: str, software: Optional[Software], client_audit: bool, padlen: int) -> None:
|
def output_security_sub(out: OutputBuffer, sub: str, software: Optional[Software], client_audit: bool, padlen: int) -> None:
|
||||||
secdb = VersionVulnerabilityDB.CVE if sub == 'cve' else VersionVulnerabilityDB.TXT
|
secdb = VersionVulnerabilityDB.CVE if sub == 'cve' else VersionVulnerabilityDB.TXT
|
||||||
if software is None or software.product not in secdb:
|
if software is None or software.product not in secdb:
|
||||||
return
|
return
|
||||||
for line in secdb[software.product]:
|
for line in secdb[software.product]:
|
||||||
vfrom = '' # type: str
|
vfrom: str = ''
|
||||||
vtill = '' # type: str
|
vtill: str = ''
|
||||||
vfrom, vtill = line[0:2]
|
vfrom, vtill = line[0:2]
|
||||||
if not software.between_versions(vfrom, vtill):
|
if not software.between_versions(vfrom, vtill):
|
||||||
continue
|
continue
|
||||||
target = 0 # type: int
|
target: int = 0
|
||||||
name = '' # type: str
|
name: str = ''
|
||||||
target, name = line[2:4]
|
target, name = line[2:4]
|
||||||
is_server = target & 1 == 1
|
is_server = target & 1 == 1
|
||||||
is_client = target & 2 == 2
|
is_client = target & 2 == 2
|
||||||
@ -227,8 +235,8 @@ def output_security_sub(sub: str, software: Optional[Software], client_audit: bo
|
|||||||
continue
|
continue
|
||||||
p = '' if out.batch else ' ' * (padlen - len(name))
|
p = '' if out.batch else ' ' * (padlen - len(name))
|
||||||
if sub == 'cve':
|
if sub == 'cve':
|
||||||
cvss = 0.0 # type: float
|
cvss: float = 0.0
|
||||||
descr = '' # type: str
|
descr: str = ''
|
||||||
cvss, descr = line[4:6]
|
cvss, descr = line[4:6]
|
||||||
|
|
||||||
# Critical CVSS scores (>= 8.0) are printed as a fail, otherwise they are printed as a warning.
|
# Critical CVSS scores (>= 8.0) are printed as a fail, otherwise they are printed as a warning.
|
||||||
@ -241,20 +249,23 @@ def output_security_sub(sub: str, software: Optional[Software], client_audit: bo
|
|||||||
out.fail('(sec) {}{} -- {}'.format(name, p, descr))
|
out.fail('(sec) {}{} -- {}'.format(name, p, descr))
|
||||||
|
|
||||||
|
|
||||||
def output_security(banner: Optional[Banner], client_audit: bool, padlen: int, is_json_output: bool) -> None:
|
def output_security(out: OutputBuffer, banner: Optional[Banner], client_audit: bool, padlen: int, is_json_output: bool) -> None:
|
||||||
with OutputBuffer() as obuf:
|
with out:
|
||||||
if banner is not None:
|
if banner is not None:
|
||||||
software = Software.parse(banner)
|
software = Software.parse(banner)
|
||||||
output_security_sub('cve', software, client_audit, padlen)
|
output_security_sub(out, 'cve', software, client_audit, padlen)
|
||||||
output_security_sub('txt', software, client_audit, padlen)
|
output_security_sub(out, 'txt', software, client_audit, padlen)
|
||||||
if len(obuf) > 0 and not is_json_output:
|
if banner.protocol[0] == 1:
|
||||||
|
p = '' if out.batch else ' ' * (padlen - 14)
|
||||||
|
out.fail('(sec) SSH v1 enabled{} -- SSH v1 can be exploited to recover plaintext passwords'.format(p))
|
||||||
|
if not out.is_section_empty() and not is_json_output:
|
||||||
out.head('# security')
|
out.head('# security')
|
||||||
obuf.flush()
|
out.flush_section()
|
||||||
out.sep()
|
out.sep()
|
||||||
|
|
||||||
|
|
||||||
def output_fingerprints(algs: Algorithms, is_json_output: bool, sha256: bool = True) -> None:
|
def output_fingerprints(out: OutputBuffer, algs: Algorithms, is_json_output: bool) -> None:
|
||||||
with OutputBuffer() as obuf:
|
with out:
|
||||||
fps = []
|
fps = []
|
||||||
if algs.ssh1kex is not None:
|
if algs.ssh1kex is not None:
|
||||||
name = 'ssh-rsa1'
|
name = 'ssh-rsa1'
|
||||||
@ -280,18 +291,20 @@ def output_fingerprints(algs: Algorithms, is_json_output: bool, sha256: bool = T
|
|||||||
fps = sorted(fps)
|
fps = sorted(fps)
|
||||||
for fpp in fps:
|
for fpp in fps:
|
||||||
name, fp = fpp
|
name, fp = fpp
|
||||||
fpo = fp.sha256 if sha256 else fp.md5
|
out.good('(fin) {}: {}'.format(name, fp.sha256))
|
||||||
# p = '' if out.batch else ' ' * (padlen - len(name))
|
|
||||||
# out.good('(fin) {0}{1} -- {2} {3}'.format(name, p, bits, fpo))
|
# Output the MD5 hash too if verbose mode is enabled.
|
||||||
out.good('(fin) {}: {}'.format(name, fpo))
|
if out.verbose:
|
||||||
if len(obuf) > 0 and not is_json_output:
|
out.info('(fin) {}: {} -- [info] do not rely on MD5 fingerprints for server identification; it is insecure for this use case'.format(name, fp.md5))
|
||||||
|
|
||||||
|
if not out.is_section_empty() and not is_json_output:
|
||||||
out.head('# fingerprints')
|
out.head('# fingerprints')
|
||||||
obuf.flush()
|
out.flush_section()
|
||||||
out.sep()
|
out.sep()
|
||||||
|
|
||||||
|
|
||||||
# Returns True if no warnings or failures encountered in configuration.
|
# Returns True if no warnings or failures encountered in configuration.
|
||||||
def output_recommendations(algs: Algorithms, software: Optional[Software], is_json_output: bool, padlen: int = 0) -> bool:
|
def output_recommendations(out: OutputBuffer, algs: Algorithms, software: Optional[Software], is_json_output: bool, padlen: int = 0) -> bool:
|
||||||
|
|
||||||
ret = True
|
ret = True
|
||||||
# PuTTY's algorithms cannot be modified, so there's no point in issuing recommendations.
|
# PuTTY's algorithms cannot be modified, so there's no point in issuing recommendations.
|
||||||
@ -323,7 +336,7 @@ def output_recommendations(algs: Algorithms, software: Optional[Software], is_js
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
for_server = True
|
for_server = True
|
||||||
with OutputBuffer() as obuf:
|
with out:
|
||||||
software, alg_rec = algs.get_recommendations(software, for_server)
|
software, alg_rec = algs.get_recommendations(software, for_server)
|
||||||
for sshv in range(2, 0, -1):
|
for sshv in range(2, 0, -1):
|
||||||
if sshv not in alg_rec:
|
if sshv not in alg_rec:
|
||||||
@ -351,20 +364,20 @@ def output_recommendations(algs: Algorithms, software: Optional[Software], is_js
|
|||||||
b = '(SSH{})'.format(sshv) if sshv == 1 else ''
|
b = '(SSH{})'.format(sshv) if sshv == 1 else ''
|
||||||
fm = '(rec) {0}{1}{2}-- {3} algorithm to {4}{5} {6}'
|
fm = '(rec) {0}{1}{2}-- {3} algorithm to {4}{5} {6}'
|
||||||
fn(fm.format(sg, name, p, alg_type, an, chg_additional_info, b))
|
fn(fm.format(sg, name, p, alg_type, an, chg_additional_info, b))
|
||||||
if len(obuf) > 0 and not is_json_output:
|
if not out.is_section_empty() and not is_json_output:
|
||||||
if software is not None:
|
if software is not None:
|
||||||
title = '(for {})'.format(software.display(False))
|
title = '(for {})'.format(software.display(False))
|
||||||
else:
|
else:
|
||||||
title = ''
|
title = ''
|
||||||
out.head('# algorithm recommendations {}'.format(title))
|
out.head('# algorithm recommendations {}'.format(title))
|
||||||
obuf.flush(True) # Sort the output so that it is always stable (needed for repeatable testing).
|
out.flush_section(sort_section=True) # Sort the output so that it is always stable (needed for repeatable testing).
|
||||||
out.sep()
|
out.sep()
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
# Output additional information & notes.
|
# Output additional information & notes.
|
||||||
def output_info(software: Optional['Software'], client_audit: bool, any_problems: bool, is_json_output: bool) -> None:
|
def output_info(out: OutputBuffer, software: Optional['Software'], client_audit: bool, any_problems: bool, is_json_output: bool) -> None:
|
||||||
with OutputBuffer() as obuf:
|
with out:
|
||||||
# Tell user that PuTTY cannot be hardened at the protocol-level.
|
# Tell user that PuTTY cannot be hardened at the protocol-level.
|
||||||
if client_audit and (software is not None) and (software.product == Product.PuTTY):
|
if client_audit and (software is not None) and (software.product == Product.PuTTY):
|
||||||
out.warn('(nfo) PuTTY does not have the option of restricting any algorithms during the SSH handshake.')
|
out.warn('(nfo) PuTTY does not have the option of restricting any algorithms during the SSH handshake.')
|
||||||
@ -373,20 +386,20 @@ def output_info(software: Optional['Software'], client_audit: bool, any_problems
|
|||||||
if any_problems:
|
if any_problems:
|
||||||
out.warn('(nfo) For hardening guides on common OSes, please see: <https://www.ssh-audit.com/hardening_guides.html>')
|
out.warn('(nfo) For hardening guides on common OSes, please see: <https://www.ssh-audit.com/hardening_guides.html>')
|
||||||
|
|
||||||
if len(obuf) > 0 and not is_json_output:
|
if not out.is_section_empty() and not is_json_output:
|
||||||
out.head('# additional info')
|
out.head('# additional info')
|
||||||
obuf.flush()
|
out.flush_section()
|
||||||
out.sep()
|
out.sep()
|
||||||
|
|
||||||
|
|
||||||
# Returns a exitcodes.* flag to denote if any failures or warnings were encountered.
|
# Returns a exitcodes.* flag to denote if any failures or warnings were encountered.
|
||||||
def output(aconf: AuditConf, banner: Optional[Banner], header: List[str], client_host: Optional[str] = None, kex: Optional[SSH2_Kex] = None, pkm: Optional[SSH1_PublicKeyMessage] = None, print_target: bool = False) -> int:
|
def output(out: OutputBuffer, aconf: AuditConf, banner: Optional[Banner], header: List[str], client_host: Optional[str] = None, kex: Optional[SSH2_Kex] = None, pkm: Optional[SSH1_PublicKeyMessage] = None, print_target: bool = False) -> int:
|
||||||
|
|
||||||
program_retval = exitcodes.GOOD
|
program_retval = exitcodes.GOOD
|
||||||
client_audit = client_host is not None # If set, this is a client audit.
|
client_audit = client_host is not None # If set, this is a client audit.
|
||||||
sshv = 1 if pkm is not None else 2
|
sshv = 1 if pkm is not None else 2
|
||||||
algs = Algorithms(pkm, kex)
|
algs = Algorithms(pkm, kex)
|
||||||
with OutputBuffer() as obuf:
|
with out:
|
||||||
if print_target:
|
if print_target:
|
||||||
host = aconf.host
|
host = aconf.host
|
||||||
|
|
||||||
@ -405,18 +418,23 @@ def output(aconf: AuditConf, banner: Optional[Banner], header: List[str], client
|
|||||||
if len(header) > 0:
|
if len(header) > 0:
|
||||||
out.info('(gen) header: ' + '\n'.join(header))
|
out.info('(gen) header: ' + '\n'.join(header))
|
||||||
if banner is not None:
|
if banner is not None:
|
||||||
out.good('(gen) banner: {}'.format(banner))
|
banner_line = '(gen) banner: {}'.format(banner)
|
||||||
|
if sshv == 1 or banner.protocol[0] == 1:
|
||||||
|
out.fail(banner_line)
|
||||||
|
out.fail('(gen) protocol SSH1 enabled')
|
||||||
|
else:
|
||||||
|
out.good(banner_line)
|
||||||
|
|
||||||
if not banner.valid_ascii:
|
if not banner.valid_ascii:
|
||||||
# NOTE: RFC 4253, Section 4.2
|
# NOTE: RFC 4253, Section 4.2
|
||||||
out.warn('(gen) banner contains non-printable ASCII')
|
out.warn('(gen) banner contains non-printable ASCII')
|
||||||
if sshv == 1 or banner.protocol[0] == 1:
|
|
||||||
out.fail('(gen) protocol SSH1 enabled')
|
|
||||||
software = Software.parse(banner)
|
software = Software.parse(banner)
|
||||||
if software is not None:
|
if software is not None:
|
||||||
out.good('(gen) software: {}'.format(software))
|
out.good('(gen) software: {}'.format(software))
|
||||||
else:
|
else:
|
||||||
software = None
|
software = None
|
||||||
output_compatibility(algs, client_audit)
|
output_compatibility(out, algs, client_audit)
|
||||||
if kex is not None:
|
if kex is not None:
|
||||||
compressions = [x for x in kex.server.compression if x != 'none']
|
compressions = [x for x in kex.server.compression if x != 'none']
|
||||||
if len(compressions) > 0:
|
if len(compressions) > 0:
|
||||||
@ -424,47 +442,49 @@ def output(aconf: AuditConf, banner: Optional[Banner], header: List[str], client
|
|||||||
else:
|
else:
|
||||||
cmptxt = 'disabled'
|
cmptxt = 'disabled'
|
||||||
out.good('(gen) compression: {}'.format(cmptxt))
|
out.good('(gen) compression: {}'.format(cmptxt))
|
||||||
if len(obuf) > 0 and not aconf.json: # Print output when it exists and JSON output isn't requested.
|
if not out.is_section_empty() and not aconf.json: # Print output when it exists and JSON output isn't requested.
|
||||||
out.head('# general')
|
out.head('# general')
|
||||||
obuf.flush()
|
out.flush_section()
|
||||||
out.sep()
|
out.sep()
|
||||||
maxlen = algs.maxlen + 1
|
maxlen = algs.maxlen + 1
|
||||||
output_security(banner, client_audit, maxlen, aconf.json)
|
output_security(out, banner, client_audit, maxlen, aconf.json)
|
||||||
# Filled in by output_algorithms() with unidentified algs.
|
# Filled in by output_algorithms() with unidentified algs.
|
||||||
unknown_algorithms = [] # type: List[str]
|
unknown_algorithms: List[str] = []
|
||||||
if pkm is not None:
|
if pkm is not None:
|
||||||
adb = SSH1_KexDB.ALGORITHMS
|
adb = SSH1_KexDB.ALGORITHMS
|
||||||
ciphers = pkm.supported_ciphers
|
ciphers = pkm.supported_ciphers
|
||||||
auths = pkm.supported_authentications
|
auths = pkm.supported_authentications
|
||||||
title, atype = 'SSH1 host-key algorithms', 'key'
|
title, atype = 'SSH1 host-key algorithms', 'key'
|
||||||
program_retval = output_algorithms(title, adb, atype, ['ssh-rsa1'], unknown_algorithms, aconf.json, program_retval, maxlen)
|
program_retval = output_algorithms(out, title, adb, atype, ['ssh-rsa1'], unknown_algorithms, aconf.json, program_retval, maxlen)
|
||||||
title, atype = 'SSH1 encryption algorithms (ciphers)', 'enc'
|
title, atype = 'SSH1 encryption algorithms (ciphers)', 'enc'
|
||||||
program_retval = output_algorithms(title, adb, atype, ciphers, unknown_algorithms, aconf.json, program_retval, maxlen)
|
program_retval = output_algorithms(out, title, adb, atype, ciphers, unknown_algorithms, aconf.json, program_retval, maxlen)
|
||||||
title, atype = 'SSH1 authentication types', 'aut'
|
title, atype = 'SSH1 authentication types', 'aut'
|
||||||
program_retval = output_algorithms(title, adb, atype, auths, unknown_algorithms, aconf.json, program_retval, maxlen)
|
program_retval = output_algorithms(out, title, adb, atype, auths, unknown_algorithms, aconf.json, program_retval, maxlen)
|
||||||
if kex is not None:
|
if kex is not None:
|
||||||
adb = SSH2_KexDB.ALGORITHMS
|
adb = SSH2_KexDB.ALGORITHMS
|
||||||
title, atype = 'key exchange algorithms', 'kex'
|
title, atype = 'key exchange algorithms', 'kex'
|
||||||
program_retval = output_algorithms(title, adb, atype, kex.kex_algorithms, unknown_algorithms, aconf.json, program_retval, maxlen, kex.dh_modulus_sizes())
|
program_retval = output_algorithms(out, title, adb, atype, kex.kex_algorithms, unknown_algorithms, aconf.json, program_retval, maxlen, kex.dh_modulus_sizes())
|
||||||
title, atype = 'host-key algorithms', 'key'
|
title, atype = 'host-key algorithms', 'key'
|
||||||
program_retval = output_algorithms(title, adb, atype, kex.key_algorithms, unknown_algorithms, aconf.json, program_retval, maxlen, kex.rsa_key_sizes())
|
program_retval = output_algorithms(out, title, adb, atype, kex.key_algorithms, unknown_algorithms, aconf.json, program_retval, maxlen, kex.rsa_key_sizes())
|
||||||
title, atype = 'encryption algorithms (ciphers)', 'enc'
|
title, atype = 'encryption algorithms (ciphers)', 'enc'
|
||||||
program_retval = output_algorithms(title, adb, atype, kex.server.encryption, unknown_algorithms, aconf.json, program_retval, maxlen)
|
program_retval = output_algorithms(out, title, adb, atype, kex.server.encryption, unknown_algorithms, aconf.json, program_retval, maxlen)
|
||||||
title, atype = 'message authentication code algorithms', 'mac'
|
title, atype = 'message authentication code algorithms', 'mac'
|
||||||
program_retval = output_algorithms(title, adb, atype, kex.server.mac, unknown_algorithms, aconf.json, program_retval, maxlen)
|
program_retval = output_algorithms(out, title, adb, atype, kex.server.mac, unknown_algorithms, aconf.json, program_retval, maxlen)
|
||||||
output_fingerprints(algs, aconf.json, True)
|
output_fingerprints(out, algs, aconf.json)
|
||||||
perfect_config = output_recommendations(algs, software, aconf.json, maxlen)
|
perfect_config = output_recommendations(out, algs, software, aconf.json, maxlen)
|
||||||
output_info(software, client_audit, not perfect_config, aconf.json)
|
output_info(out, software, client_audit, not perfect_config, aconf.json)
|
||||||
|
|
||||||
if aconf.json:
|
if aconf.json:
|
||||||
print(json.dumps(build_struct(banner, kex=kex, client_host=client_host), sort_keys=True), end='' if len(aconf.target_list) > 0 else "\n") # Print the JSON of the audit info. Skip the newline at the end if multiple targets were given (since each audit dump will go into its own list entry).
|
out.reset()
|
||||||
|
# Build & write the JSON struct.
|
||||||
|
out.info(json.dumps(build_struct(aconf.host, banner, kex=kex, client_host=client_host), indent=4 if aconf.json_print_indent else None, sort_keys=True))
|
||||||
elif len(unknown_algorithms) > 0: # If we encountered any unknown algorithms, ask the user to report them.
|
elif len(unknown_algorithms) > 0: # If we encountered any unknown algorithms, ask the user to report them.
|
||||||
out.warn("\n\n!!! WARNING: unknown algorithm(s) found!: %s. Please email the full output above to the maintainer (jtesta@positronsecurity.com), or create a Github issue at <https://github.com/jtesta/ssh-audit/issues>.\n" % ','.join(unknown_algorithms))
|
out.warn("\n\n!!! WARNING: unknown algorithm(s) found!: %s. Please email the full output above to the maintainer (jtesta@positronsecurity.com), or create a Github issue at <https://github.com/jtesta/ssh-audit/issues>.\n" % ','.join(unknown_algorithms))
|
||||||
|
|
||||||
return program_retval
|
return program_retval
|
||||||
|
|
||||||
|
|
||||||
def evaluate_policy(aconf: AuditConf, banner: Optional['Banner'], client_host: Optional[str], kex: Optional['SSH2_Kex'] = None) -> bool:
|
def evaluate_policy(out: OutputBuffer, aconf: AuditConf, banner: Optional['Banner'], client_host: Optional[str], kex: Optional['SSH2_Kex'] = None) -> bool:
|
||||||
|
|
||||||
if aconf.policy is None:
|
if aconf.policy is None:
|
||||||
raise RuntimeError('Internal error: cannot evaluate against null Policy!')
|
raise RuntimeError('Internal error: cannot evaluate against null Policy!')
|
||||||
@ -472,11 +492,11 @@ def evaluate_policy(aconf: AuditConf, banner: Optional['Banner'], client_host: O
|
|||||||
passed, error_struct, error_str = aconf.policy.evaluate(banner, kex)
|
passed, error_struct, error_str = aconf.policy.evaluate(banner, kex)
|
||||||
if aconf.json:
|
if aconf.json:
|
||||||
json_struct = {'host': aconf.host, 'policy': aconf.policy.get_name_and_version(), 'passed': passed, 'errors': error_struct}
|
json_struct = {'host': aconf.host, 'policy': aconf.policy.get_name_and_version(), 'passed': passed, 'errors': error_struct}
|
||||||
print(json.dumps(json_struct, sort_keys=True))
|
out.info(json.dumps(json_struct, indent=4 if aconf.json_print_indent else None, sort_keys=True))
|
||||||
else:
|
else:
|
||||||
spacing = ''
|
spacing = ''
|
||||||
if aconf.client_audit:
|
if aconf.client_audit:
|
||||||
print("Client IP: %s" % client_host)
|
out.info("Client IP: %s" % client_host)
|
||||||
spacing = " " # So the fields below line up with 'Client IP: '.
|
spacing = " " # So the fields below line up with 'Client IP: '.
|
||||||
else:
|
else:
|
||||||
host = aconf.host
|
host = aconf.host
|
||||||
@ -487,9 +507,9 @@ def evaluate_policy(aconf: AuditConf, banner: Optional['Banner'], client_host: O
|
|||||||
else:
|
else:
|
||||||
host = '%s:%d' % (aconf.host, aconf.port)
|
host = '%s:%d' % (aconf.host, aconf.port)
|
||||||
|
|
||||||
print("Host: %s" % host)
|
out.info("Host: %s" % host)
|
||||||
print("Policy: %s%s" % (spacing, aconf.policy.get_name_and_version()))
|
out.info("Policy: %s%s" % (spacing, aconf.policy.get_name_and_version()))
|
||||||
print("Result: %s" % spacing, end='')
|
out.info("Result: %s" % spacing, line_ended=False)
|
||||||
|
|
||||||
# Use these nice unicode characters in the result message, unless we're on Windows (the cmd.exe terminal doesn't display them properly).
|
# Use these nice unicode characters in the result message, unless we're on Windows (the cmd.exe terminal doesn't display them properly).
|
||||||
icon_good = "✔ "
|
icon_good = "✔ "
|
||||||
@ -507,29 +527,31 @@ def evaluate_policy(aconf: AuditConf, banner: Optional['Banner'], client_host: O
|
|||||||
return passed
|
return passed
|
||||||
|
|
||||||
|
|
||||||
def list_policies() -> None:
|
def list_policies(out: OutputBuffer) -> None:
|
||||||
'''Prints a list of server & client policies.'''
|
'''Prints a list of server & client policies.'''
|
||||||
|
|
||||||
server_policy_names, client_policy_names = Policy.list_builtin_policies()
|
server_policy_names, client_policy_names = Policy.list_builtin_policies()
|
||||||
|
|
||||||
if len(server_policy_names) > 0:
|
if len(server_policy_names) > 0:
|
||||||
out.head('\nServer policies:\n')
|
out.head('\nServer policies:\n')
|
||||||
print(" * \"%s\"" % "\"\n * \"".join(server_policy_names))
|
out.info(" * \"%s\"" % "\"\n * \"".join(server_policy_names))
|
||||||
|
|
||||||
if len(client_policy_names) > 0:
|
if len(client_policy_names) > 0:
|
||||||
out.head('\nClient policies:\n')
|
out.head('\nClient policies:\n')
|
||||||
print(" * \"%s\"" % "\"\n * \"".join(client_policy_names))
|
out.info(" * \"%s\"" % "\"\n * \"".join(client_policy_names))
|
||||||
|
|
||||||
|
out.sep()
|
||||||
if len(server_policy_names) == 0 and len(client_policy_names) == 0:
|
if len(server_policy_names) == 0 and len(client_policy_names) == 0:
|
||||||
print("Error: no built-in policies found!")
|
out.fail("Error: no built-in policies found!")
|
||||||
else:
|
else:
|
||||||
print("\nHint: Use -P and provide the full name of a policy to run a policy scan with.\n")
|
out.info("\nHint: Use -P and provide the full name of a policy to run a policy scan with.\n")
|
||||||
|
out.write()
|
||||||
|
|
||||||
|
|
||||||
def make_policy(aconf: AuditConf, banner: Optional['Banner'], kex: Optional['SSH2_Kex'], client_host: Optional[str]) -> None:
|
def make_policy(aconf: AuditConf, banner: Optional['Banner'], kex: Optional['SSH2_Kex'], client_host: Optional[str]) -> None:
|
||||||
|
|
||||||
# Set the source of this policy to the server host if this is a server audit, otherwise set it to the client address.
|
# Set the source of this policy to the server host if this is a server audit, otherwise set it to the client address.
|
||||||
source = aconf.host # type: Optional[str]
|
source: Optional[str] = aconf.host
|
||||||
if aconf.client_audit:
|
if aconf.client_audit:
|
||||||
source = client_host
|
source = client_host
|
||||||
|
|
||||||
@ -541,7 +563,7 @@ def make_policy(aconf: AuditConf, banner: Optional['Banner'], kex: Optional['SSH
|
|||||||
# Open with mode 'x' (creates the file, or fails if it already exist).
|
# Open with mode 'x' (creates the file, or fails if it already exist).
|
||||||
succeeded = True
|
succeeded = True
|
||||||
try:
|
try:
|
||||||
with open(aconf.policy_file, 'x') as f:
|
with open(aconf.policy_file, 'x', encoding='utf-8') as f:
|
||||||
f.write(policy_data)
|
f.write(policy_data)
|
||||||
except FileExistsError:
|
except FileExistsError:
|
||||||
succeeded = False
|
succeeded = False
|
||||||
@ -552,19 +574,19 @@ def make_policy(aconf: AuditConf, banner: Optional['Banner'], kex: Optional['SSH
|
|||||||
print("Error: file already exists: %s" % aconf.policy_file)
|
print("Error: file already exists: %s" % aconf.policy_file)
|
||||||
|
|
||||||
|
|
||||||
def process_commandline(args: List[str], usage_cb: Callable[..., None]) -> 'AuditConf': # pylint: disable=too-many-statements
|
def process_commandline(out: OutputBuffer, args: List[str], usage_cb: Callable[..., None]) -> 'AuditConf': # pylint: disable=too-many-statements
|
||||||
# pylint: disable=too-many-branches
|
# pylint: disable=too-many-branches
|
||||||
aconf = AuditConf()
|
aconf = AuditConf()
|
||||||
try:
|
try:
|
||||||
sopts = 'h1246M:p:P:jbcnvl:t:T:L'
|
sopts = 'h1246M:p:P:jbcnvl:t:T:Lmd'
|
||||||
lopts = ['help', 'ssh1', 'ssh2', 'ipv4', 'ipv6', 'make-policy=', 'port=', 'policy=', 'json', 'batch', 'client-audit', 'no-colors', 'verbose', 'level=', 'timeout=', 'targets=', 'list-policies', 'lookup=']
|
lopts = ['help', 'ssh1', 'ssh2', 'ipv4', 'ipv6', 'make-policy=', 'port=', 'policy=', 'json', 'batch', 'client-audit', 'no-colors', 'verbose', 'level=', 'timeout=', 'targets=', 'list-policies', 'lookup=', 'threads=', 'manual', 'debug']
|
||||||
opts, args = getopt.gnu_getopt(args, sopts, lopts)
|
opts, args = getopt.gnu_getopt(args, sopts, lopts)
|
||||||
except getopt.GetoptError as err:
|
except getopt.GetoptError as err:
|
||||||
usage_cb(str(err))
|
usage_cb(str(err))
|
||||||
aconf.ssh1, aconf.ssh2 = False, False
|
aconf.ssh1, aconf.ssh2 = False, False
|
||||||
host = '' # type: str
|
host: str = ''
|
||||||
oport = None # type: Optional[str]
|
oport: Optional[str] = None
|
||||||
port = 0 # type: int
|
port: int = 0
|
||||||
for o, a in opts:
|
for o, a in opts:
|
||||||
if o in ('-h', '--help'):
|
if o in ('-h', '--help'):
|
||||||
usage_cb()
|
usage_cb()
|
||||||
@ -585,10 +607,15 @@ def process_commandline(args: List[str], usage_cb: Callable[..., None]) -> 'Audi
|
|||||||
aconf.client_audit = True
|
aconf.client_audit = True
|
||||||
elif o in ('-n', '--no-colors'):
|
elif o in ('-n', '--no-colors'):
|
||||||
aconf.colors = False
|
aconf.colors = False
|
||||||
|
out.use_colors = False
|
||||||
elif o in ('-j', '--json'):
|
elif o in ('-j', '--json'):
|
||||||
|
if aconf.json: # If specified twice, enable indent printing.
|
||||||
|
aconf.json_print_indent = True
|
||||||
|
else:
|
||||||
aconf.json = True
|
aconf.json = True
|
||||||
elif o in ('-v', '--verbose'):
|
elif o in ('-v', '--verbose'):
|
||||||
aconf.verbose = True
|
aconf.verbose = True
|
||||||
|
out.verbose = True
|
||||||
elif o in ('-l', '--level'):
|
elif o in ('-l', '--level'):
|
||||||
if a not in ('info', 'warn', 'fail'):
|
if a not in ('info', 'warn', 'fail'):
|
||||||
usage_cb('level {} is not valid'.format(a))
|
usage_cb('level {} is not valid'.format(a))
|
||||||
@ -603,19 +630,29 @@ def process_commandline(args: List[str], usage_cb: Callable[..., None]) -> 'Audi
|
|||||||
aconf.policy_file = a
|
aconf.policy_file = a
|
||||||
elif o in ('-T', '--targets'):
|
elif o in ('-T', '--targets'):
|
||||||
aconf.target_file = a
|
aconf.target_file = a
|
||||||
|
elif o == '--threads':
|
||||||
|
aconf.threads = int(a)
|
||||||
elif o in ('-L', '--list-policies'):
|
elif o in ('-L', '--list-policies'):
|
||||||
aconf.list_policies = True
|
aconf.list_policies = True
|
||||||
elif o == '--lookup':
|
elif o == '--lookup':
|
||||||
aconf.lookup = a
|
aconf.lookup = a
|
||||||
|
elif o in ('-m', '--manual'):
|
||||||
|
aconf.manual = True
|
||||||
|
elif o in ('-d', '--debug'):
|
||||||
|
aconf.debug = True
|
||||||
|
out.debug = True
|
||||||
|
|
||||||
if len(args) == 0 and aconf.client_audit is False and aconf.target_file is None and aconf.list_policies is False and aconf.lookup == '':
|
if len(args) == 0 and aconf.client_audit is False and aconf.target_file is None and aconf.list_policies is False and aconf.lookup == '' and aconf.manual is False:
|
||||||
usage_cb()
|
usage_cb()
|
||||||
|
|
||||||
|
if aconf.manual:
|
||||||
|
return aconf
|
||||||
|
|
||||||
if aconf.lookup != '':
|
if aconf.lookup != '':
|
||||||
return aconf
|
return aconf
|
||||||
|
|
||||||
if aconf.list_policies:
|
if aconf.list_policies:
|
||||||
list_policies()
|
list_policies(out)
|
||||||
sys.exit(exitcodes.GOOD)
|
sys.exit(exitcodes.GOOD)
|
||||||
|
|
||||||
if aconf.client_audit is False and aconf.target_file is None:
|
if aconf.client_audit is False and aconf.target_file is None:
|
||||||
@ -644,7 +681,7 @@ def process_commandline(args: List[str], usage_cb: Callable[..., None]) -> 'Audi
|
|||||||
|
|
||||||
# If a file containing a list of targets was given, read it.
|
# If a file containing a list of targets was given, read it.
|
||||||
if aconf.target_file is not None:
|
if aconf.target_file is not None:
|
||||||
with open(aconf.target_file, 'r') as f:
|
with open(aconf.target_file, 'r', encoding='utf-8') as f:
|
||||||
aconf.target_list = f.readlines()
|
aconf.target_list = f.readlines()
|
||||||
|
|
||||||
# Strip out whitespace from each line in target file, and skip empty lines.
|
# Strip out whitespace from each line in target file, and skip empty lines.
|
||||||
@ -659,23 +696,26 @@ def process_commandline(args: List[str], usage_cb: Callable[..., None]) -> 'Audi
|
|||||||
try:
|
try:
|
||||||
aconf.policy = Policy(policy_file=aconf.policy_file)
|
aconf.policy = Policy(policy_file=aconf.policy_file)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("Error while loading policy file: %s: %s" % (str(e), traceback.format_exc()))
|
out.fail("Error while loading policy file: %s: %s" % (str(e), traceback.format_exc()))
|
||||||
|
out.write()
|
||||||
sys.exit(exitcodes.UNKNOWN_ERROR)
|
sys.exit(exitcodes.UNKNOWN_ERROR)
|
||||||
|
|
||||||
# If the user wants to do a client audit, but provided a server policy, terminate.
|
# If the user wants to do a client audit, but provided a server policy, terminate.
|
||||||
if aconf.client_audit and aconf.policy.is_server_policy():
|
if aconf.client_audit and aconf.policy.is_server_policy():
|
||||||
print("Error: client audit selected, but server policy provided.")
|
out.fail("Error: client audit selected, but server policy provided.")
|
||||||
|
out.write()
|
||||||
sys.exit(exitcodes.UNKNOWN_ERROR)
|
sys.exit(exitcodes.UNKNOWN_ERROR)
|
||||||
|
|
||||||
# If the user wants to do a server audit, but provided a client policy, terminate.
|
# If the user wants to do a server audit, but provided a client policy, terminate.
|
||||||
if aconf.client_audit is False and aconf.policy.is_server_policy() is False:
|
if aconf.client_audit is False and aconf.policy.is_server_policy() is False:
|
||||||
print("Error: server audit selected, but client policy provided.")
|
out.fail("Error: server audit selected, but client policy provided.")
|
||||||
|
out.write()
|
||||||
sys.exit(exitcodes.UNKNOWN_ERROR)
|
sys.exit(exitcodes.UNKNOWN_ERROR)
|
||||||
|
|
||||||
return aconf
|
return aconf
|
||||||
|
|
||||||
|
|
||||||
def build_struct(banner: Optional['Banner'], kex: Optional['SSH2_Kex'] = None, pkm: Optional['SSH1_PublicKeyMessage'] = None, client_host: Optional[str] = None) -> Any:
|
def build_struct(target_host: str, banner: Optional['Banner'], kex: Optional['SSH2_Kex'] = None, pkm: Optional['SSH1_PublicKeyMessage'] = None, client_host: Optional[str] = None) -> Any:
|
||||||
|
|
||||||
banner_str = ''
|
banner_str = ''
|
||||||
banner_protocol = None
|
banner_protocol = None
|
||||||
@ -687,25 +727,30 @@ def build_struct(banner: Optional['Banner'], kex: Optional['SSH2_Kex'] = None, p
|
|||||||
banner_software = banner.software
|
banner_software = banner.software
|
||||||
banner_comments = banner.comments
|
banner_comments = banner.comments
|
||||||
|
|
||||||
res = {
|
res: Any = {
|
||||||
"banner": {
|
"banner": {
|
||||||
"raw": banner_str,
|
"raw": banner_str,
|
||||||
"protocol": banner_protocol,
|
"protocol": banner_protocol,
|
||||||
"software": banner_software,
|
"software": banner_software,
|
||||||
"comments": banner_comments,
|
"comments": banner_comments,
|
||||||
},
|
},
|
||||||
} # type: Any
|
}
|
||||||
|
|
||||||
|
# If we're scanning a client host, put the client's IP into the results. Otherwise, include the target host.
|
||||||
if client_host is not None:
|
if client_host is not None:
|
||||||
res['client_ip'] = client_host
|
res['client_ip'] = client_host
|
||||||
|
else:
|
||||||
|
res['target'] = target_host
|
||||||
|
|
||||||
if kex is not None:
|
if kex is not None:
|
||||||
res['compression'] = kex.server.compression
|
res['compression'] = kex.server.compression
|
||||||
|
|
||||||
res['kex'] = []
|
res['kex'] = []
|
||||||
alg_sizes = kex.dh_modulus_sizes()
|
alg_sizes = kex.dh_modulus_sizes()
|
||||||
for algorithm in kex.kex_algorithms:
|
for algorithm in kex.kex_algorithms:
|
||||||
entry = {
|
entry: Any = {
|
||||||
'algorithm': algorithm,
|
'algorithm': algorithm,
|
||||||
} # type: Any
|
}
|
||||||
if algorithm in alg_sizes:
|
if algorithm in alg_sizes:
|
||||||
hostkey_size, ca_size = alg_sizes[algorithm]
|
hostkey_size, ca_size = alg_sizes[algorithm]
|
||||||
entry['keysize'] = hostkey_size
|
entry['keysize'] = hostkey_size
|
||||||
@ -747,11 +792,18 @@ def build_struct(banner: Optional['Banner'], kex: Optional['SSH2_Kex'] = None, p
|
|||||||
# Skip over certificate host types (or we would return invalid fingerprints).
|
# Skip over certificate host types (or we would return invalid fingerprints).
|
||||||
if '-cert-' in host_key_type:
|
if '-cert-' in host_key_type:
|
||||||
continue
|
continue
|
||||||
entry = {
|
|
||||||
'type': host_key_type,
|
# Add the SHA256 and MD5 fingerprints.
|
||||||
'fp': fp.sha256,
|
res['fingerprints'].append({
|
||||||
}
|
'hostkey': host_key_type,
|
||||||
res['fingerprints'].append(entry)
|
'hash_alg': 'SHA256',
|
||||||
|
'hash': fp.sha256[7:]
|
||||||
|
})
|
||||||
|
res['fingerprints'].append({
|
||||||
|
'hostkey': host_key_type,
|
||||||
|
'hash_alg': 'MD5',
|
||||||
|
'hash': fp.md5[4:]
|
||||||
|
})
|
||||||
else:
|
else:
|
||||||
pkm_supported_ciphers = None
|
pkm_supported_ciphers = None
|
||||||
pkm_supported_authentications = None
|
pkm_supported_authentications = None
|
||||||
@ -773,19 +825,30 @@ def build_struct(banner: Optional['Banner'], kex: Optional['SSH2_Kex'] = None, p
|
|||||||
|
|
||||||
|
|
||||||
# Returns one of the exitcodes.* flags.
|
# Returns one of the exitcodes.* flags.
|
||||||
def audit(aconf: AuditConf, sshv: Optional[int] = None, print_target: bool = False) -> int:
|
def audit(out: OutputBuffer, aconf: AuditConf, sshv: Optional[int] = None, print_target: bool = False) -> int:
|
||||||
program_retval = exitcodes.GOOD
|
program_retval = exitcodes.GOOD
|
||||||
out.batch = aconf.batch
|
out.batch = aconf.batch
|
||||||
out.verbose = aconf.verbose
|
out.verbose = aconf.verbose
|
||||||
|
out.debug = aconf.debug
|
||||||
out.level = aconf.level
|
out.level = aconf.level
|
||||||
out.use_colors = aconf.colors
|
out.use_colors = aconf.colors
|
||||||
s = SSH_Socket(aconf.host, aconf.port, aconf.ipvo, aconf.timeout, aconf.timeout_set)
|
s = SSH_Socket(out, aconf.host, aconf.port, aconf.ip_version_preference, aconf.timeout, aconf.timeout_set)
|
||||||
|
|
||||||
if aconf.client_audit:
|
if aconf.client_audit:
|
||||||
|
out.v("Listening for client connection on port %d..." % aconf.port, write_now=True)
|
||||||
s.listen_and_accept()
|
s.listen_and_accept()
|
||||||
else:
|
else:
|
||||||
|
out.v("Starting audit of %s:%d..." % ('[%s]' % aconf.host if Utils.is_ipv6_address(aconf.host) else aconf.host, aconf.port), write_now=True)
|
||||||
err = s.connect()
|
err = s.connect()
|
||||||
|
|
||||||
if err is not None:
|
if err is not None:
|
||||||
out.fail(err)
|
out.fail(err)
|
||||||
|
|
||||||
|
# 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)
|
sys.exit(exitcodes.CONNECTION_ERROR)
|
||||||
|
|
||||||
if sshv is None:
|
if sshv is None:
|
||||||
@ -798,7 +861,7 @@ def audit(aconf: AuditConf, sshv: Optional[int] = None, print_target: bool = Fal
|
|||||||
else:
|
else:
|
||||||
err = '[exception] did not receive banner: {}'.format(err)
|
err = '[exception] did not receive banner: {}'.format(err)
|
||||||
if err is None:
|
if err is None:
|
||||||
s.send_algorithms() # Send the algorithms we support (except we don't since this isn't a real SSH connection).
|
s.send_kexinit() # Send the algorithms we support (except we don't since this isn't a real SSH connection).
|
||||||
|
|
||||||
packet_type, payload = s.read_packet(sshv)
|
packet_type, payload = s.read_packet(sshv)
|
||||||
if packet_type < 0:
|
if packet_type < 0:
|
||||||
@ -806,12 +869,14 @@ def audit(aconf: AuditConf, sshv: Optional[int] = None, print_target: bool = Fal
|
|||||||
if len(payload) > 0:
|
if len(payload) > 0:
|
||||||
payload_txt = payload.decode('utf-8')
|
payload_txt = payload.decode('utf-8')
|
||||||
else:
|
else:
|
||||||
payload_txt = u'empty'
|
payload_txt = 'empty'
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
payload_txt = u'"{}"'.format(repr(payload).lstrip('b')[1:-1])
|
payload_txt = '"{}"'.format(repr(payload).lstrip('b')[1:-1])
|
||||||
if payload_txt == u'Protocol major versions differ.':
|
if payload_txt == 'Protocol major versions differ.':
|
||||||
if sshv == 2 and aconf.ssh1:
|
if sshv == 2 and aconf.ssh1:
|
||||||
return audit(aconf, 1)
|
ret = audit(out, aconf, 1)
|
||||||
|
out.write()
|
||||||
|
return ret
|
||||||
err = '[exception] error reading packet ({})'.format(payload_txt)
|
err = '[exception] error reading packet ({})'.format(payload_txt)
|
||||||
else:
|
else:
|
||||||
err_pair = None
|
err_pair = None
|
||||||
@ -824,24 +889,29 @@ def audit(aconf: AuditConf, sshv: Optional[int] = None, print_target: bool = Fal
|
|||||||
'instead received unknown message ({2})'
|
'instead received unknown message ({2})'
|
||||||
err = fmt.format(err_pair[0], err_pair[1], packet_type)
|
err = fmt.format(err_pair[0], err_pair[1], packet_type)
|
||||||
if err is not None:
|
if err is not None:
|
||||||
output(aconf, banner, header)
|
output(out, aconf, banner, header)
|
||||||
out.fail(err)
|
out.fail(err)
|
||||||
return exitcodes.CONNECTION_ERROR
|
return exitcodes.CONNECTION_ERROR
|
||||||
if sshv == 1:
|
if sshv == 1:
|
||||||
program_retval = output(aconf, banner, header, pkm=SSH1_PublicKeyMessage.parse(payload))
|
program_retval = output(out, aconf, banner, header, pkm=SSH1_PublicKeyMessage.parse(payload))
|
||||||
elif sshv == 2:
|
elif sshv == 2:
|
||||||
|
try:
|
||||||
kex = SSH2_Kex.parse(payload)
|
kex = SSH2_Kex.parse(payload)
|
||||||
|
except Exception:
|
||||||
|
out.fail("Failed to parse server's kex. Stack trace:\n%s" % str(traceback.format_exc()))
|
||||||
|
return exitcodes.CONNECTION_ERROR
|
||||||
|
|
||||||
if aconf.client_audit is False:
|
if aconf.client_audit is False:
|
||||||
HostKeyTest.run(s, kex)
|
HostKeyTest.run(out, s, kex)
|
||||||
GEXTest.run(s, kex)
|
GEXTest.run(out, s, kex)
|
||||||
|
|
||||||
# This is a standard audit scan.
|
# This is a standard audit scan.
|
||||||
if (aconf.policy is None) and (aconf.make_policy is False):
|
if (aconf.policy is None) and (aconf.make_policy is False):
|
||||||
program_retval = output(aconf, banner, header, client_host=s.client_host, kex=kex, print_target=print_target)
|
program_retval = output(out, aconf, banner, header, client_host=s.client_host, kex=kex, print_target=print_target)
|
||||||
|
|
||||||
# This is a policy test.
|
# This is a policy test.
|
||||||
elif (aconf.policy is not None) and (aconf.make_policy is False):
|
elif (aconf.policy is not None) and (aconf.make_policy is False):
|
||||||
program_retval = exitcodes.GOOD if evaluate_policy(aconf, banner, s.client_host, kex=kex) else exitcodes.FAILURE
|
program_retval = exitcodes.GOOD if evaluate_policy(out, aconf, banner, s.client_host, kex=kex) else exitcodes.FAILURE
|
||||||
|
|
||||||
# A new policy should be made from this scan.
|
# A new policy should be made from this scan.
|
||||||
elif (aconf.policy is None) and (aconf.make_policy is True):
|
elif (aconf.policy is None) and (aconf.make_policy is True):
|
||||||
@ -853,7 +923,7 @@ def audit(aconf: AuditConf, sshv: Optional[int] = None, print_target: bool = Fal
|
|||||||
return program_retval
|
return program_retval
|
||||||
|
|
||||||
|
|
||||||
def algorithm_lookup(alg_names: str) -> int:
|
def algorithm_lookup(out: OutputBuffer, alg_names: str) -> int:
|
||||||
'''Looks up a comma-separated list of algorithms and outputs their security properties. Returns an exitcodes.* flag.'''
|
'''Looks up a comma-separated list of algorithms and outputs their security properties. Returns an exitcodes.* flag.'''
|
||||||
retval = exitcodes.GOOD
|
retval = exitcodes.GOOD
|
||||||
alg_types = {
|
alg_types = {
|
||||||
@ -879,13 +949,13 @@ def algorithm_lookup(alg_names: str) -> int:
|
|||||||
for (outer_k, outer_v) in adb.items()
|
for (outer_k, outer_v) in adb.items()
|
||||||
}
|
}
|
||||||
|
|
||||||
unknown_algorithms = [] # type: List[str]
|
unknown_algorithms: List[str] = []
|
||||||
padding = len(max(algorithm_names, key=len))
|
padding = len(max(algorithm_names, key=len))
|
||||||
|
|
||||||
for alg_type in alg_types:
|
for alg_type in alg_types:
|
||||||
if len(algorithms_dict[alg_type]) > 0:
|
if len(algorithms_dict[alg_type]) > 0:
|
||||||
title = str(alg_types.get(alg_type))
|
title = str(alg_types.get(alg_type))
|
||||||
retval = output_algorithms(title, adb, alg_type, list(algorithms_dict[alg_type]), unknown_algorithms, False, retval, padding)
|
retval = output_algorithms(out, title, adb, alg_type, list(algorithms_dict[alg_type]), unknown_algorithms, False, retval, padding)
|
||||||
|
|
||||||
algorithms_dict_flattened = [
|
algorithms_dict_flattened = [
|
||||||
alg_name
|
alg_name
|
||||||
@ -902,8 +972,8 @@ def algorithm_lookup(alg_names: str) -> int:
|
|||||||
similar_algorithms = [
|
similar_algorithms = [
|
||||||
alg_unknown + " --> (" + alg_type + ") " + alg_name
|
alg_unknown + " --> (" + alg_type + ") " + alg_name
|
||||||
for alg_unknown in algorithms_not_found
|
for alg_unknown in algorithms_not_found
|
||||||
for alg_type in adb.keys()
|
for alg_type, alg_names in adb.items()
|
||||||
for alg_name in adb[alg_type]
|
for alg_name in alg_names
|
||||||
# Perform a case-insensitive comparison using 'casefold'
|
# Perform a case-insensitive comparison using 'casefold'
|
||||||
# and match substrings using the 'in' operator.
|
# and match substrings using the 'in' operator.
|
||||||
if alg_unknown.casefold() in alg_name.casefold()
|
if alg_unknown.casefold() in alg_name.casefold()
|
||||||
@ -915,7 +985,7 @@ def algorithm_lookup(alg_names: str) -> int:
|
|||||||
for algorithm_not_found in algorithms_not_found:
|
for algorithm_not_found in algorithms_not_found:
|
||||||
out.fail(algorithm_not_found)
|
out.fail(algorithm_not_found)
|
||||||
|
|
||||||
print()
|
out.sep()
|
||||||
|
|
||||||
if len(similar_algorithms) > 0:
|
if len(similar_algorithms) > 0:
|
||||||
retval = exitcodes.FAILURE
|
retval = exitcodes.FAILURE
|
||||||
@ -926,14 +996,77 @@ def algorithm_lookup(alg_names: str) -> int:
|
|||||||
return retval
|
return retval
|
||||||
|
|
||||||
|
|
||||||
out = Output()
|
# Worker thread for scanning multiple targets concurrently.
|
||||||
|
def target_worker_thread(host: str, port: int, shared_aconf: AuditConf) -> Tuple[int, str]:
|
||||||
|
ret = -1
|
||||||
|
string_output = ''
|
||||||
|
|
||||||
|
out = OutputBuffer()
|
||||||
|
out.verbose = shared_aconf.verbose
|
||||||
|
my_aconf = copy.deepcopy(shared_aconf)
|
||||||
|
my_aconf.host = host
|
||||||
|
my_aconf.port = port
|
||||||
|
|
||||||
|
# If we're outputting JSON, turn off colors and ensure 'info' level messages go through.
|
||||||
|
if my_aconf.json:
|
||||||
|
out.json = True
|
||||||
|
out.use_colors = False
|
||||||
|
|
||||||
|
out.v("Running against: %s:%d..." % (my_aconf.host, my_aconf.port), write_now=True)
|
||||||
|
try:
|
||||||
|
ret = audit(out, my_aconf, print_target=True)
|
||||||
|
string_output = out.get_buffer()
|
||||||
|
except Exception:
|
||||||
|
ret = -1
|
||||||
|
string_output = "An exception occurred while scanning %s:%d:\n%s" % (host, port, str(traceback.format_exc()))
|
||||||
|
|
||||||
|
return ret, string_output
|
||||||
|
|
||||||
|
|
||||||
|
def windows_manual(out: OutputBuffer) -> int:
|
||||||
|
'''Prints the man page on Windows. Returns an exitcodes.* flag.'''
|
||||||
|
|
||||||
|
retval = exitcodes.GOOD
|
||||||
|
|
||||||
|
if sys.platform != 'win32':
|
||||||
|
out.fail("The '-m' and '--manual' parameters are reserved for use on Windows only.\nUsers of other operating systems should read the man page.")
|
||||||
|
retval = exitcodes.FAILURE
|
||||||
|
return retval
|
||||||
|
|
||||||
|
# If colors are disabled, strip the ANSI color codes from the man page.
|
||||||
|
windows_man_page = WINDOWS_MAN_PAGE
|
||||||
|
if not out.use_colors:
|
||||||
|
windows_man_page = re.sub(r'\x1b\[\d+?m', '', windows_man_page)
|
||||||
|
|
||||||
|
out.info(windows_man_page)
|
||||||
|
return retval
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
aconf = process_commandline(sys.argv[1:], usage)
|
out = OutputBuffer()
|
||||||
|
aconf = process_commandline(out, sys.argv[1:], usage)
|
||||||
|
|
||||||
|
# If we're on Windows, but the colorama module could not be imported, print a warning if we're in verbose mode.
|
||||||
|
if (sys.platform == 'win32') and ('colorama' not in sys.modules):
|
||||||
|
out.v("WARNING: colorama module not found. Colorized output will be disabled.", write_now=True)
|
||||||
|
|
||||||
|
# If we're outputting JSON, turn off colors and ensure 'info' level messages go through.
|
||||||
|
if aconf.json:
|
||||||
|
out.json = True
|
||||||
|
out.use_colors = False
|
||||||
|
|
||||||
|
if aconf.manual:
|
||||||
|
# If the colorama module was not be imported, turn off colors in order
|
||||||
|
# to output a plain text version of the man page.
|
||||||
|
if (sys.platform == 'win32') and ('colorama' not in sys.modules):
|
||||||
|
out.use_colors = False
|
||||||
|
retval = windows_manual(out)
|
||||||
|
out.write()
|
||||||
|
sys.exit(retval)
|
||||||
|
|
||||||
if aconf.lookup != '':
|
if aconf.lookup != '':
|
||||||
retval = algorithm_lookup(aconf.lookup)
|
retval = algorithm_lookup(out, aconf.lookup)
|
||||||
|
out.write()
|
||||||
sys.exit(retval)
|
sys.exit(retval)
|
||||||
|
|
||||||
# If multiple targets were specified...
|
# If multiple targets were specified...
|
||||||
@ -945,20 +1078,33 @@ def main() -> int:
|
|||||||
print('[', end='')
|
print('[', end='')
|
||||||
|
|
||||||
# Loop through each target in the list.
|
# Loop through each target in the list.
|
||||||
for i, target in enumerate(aconf.target_list):
|
target_servers = []
|
||||||
aconf.host, port = Utils.parse_host_and_port(target)
|
for _, target in enumerate(aconf.target_list):
|
||||||
if port == 0:
|
host, port = Utils.parse_host_and_port(target, default_port=22)
|
||||||
port = 22
|
target_servers.append((host, port))
|
||||||
aconf.port = port
|
|
||||||
|
|
||||||
new_ret = audit(aconf, print_target=True)
|
# A ranked list of return codes. Those with higher indices will take precendence over lower ones. For example, if three servers are scanned, yielding WARNING, GOOD, and UNKNOWN_ERROR, the overall result will be UNKNOWN_ERROR, since its index is the highest. Errors have highest priority, followed by failures, then warnings.
|
||||||
|
ranked_return_codes = [exitcodes.GOOD, exitcodes.WARNING, exitcodes.FAILURE, exitcodes.CONNECTION_ERROR, exitcodes.UNKNOWN_ERROR]
|
||||||
|
|
||||||
# Set the return value only if an unknown error occurred, a failure occurred, or if a warning occurred and the previous value was good.
|
# Queue all worker threads.
|
||||||
if (new_ret == exitcodes.UNKNOWN_ERROR) or (new_ret == exitcodes.FAILURE) or ((new_ret == exitcodes.WARNING) and (ret == exitcodes.GOOD)):
|
num_target_servers = len(target_servers)
|
||||||
ret = new_ret
|
num_processed = 0
|
||||||
|
out.v("Scanning %u targets with %s%u threads..." % (num_target_servers, '(at most) ' if aconf.threads > num_target_servers else '', aconf.threads), write_now=True)
|
||||||
|
with concurrent.futures.ThreadPoolExecutor(max_workers=aconf.threads) as executor:
|
||||||
|
future_to_server = {executor.submit(target_worker_thread, target_server[0], target_server[1], aconf): target_server for target_server in target_servers}
|
||||||
|
for future in concurrent.futures.as_completed(future_to_server):
|
||||||
|
worker_ret, worker_output = future.result()
|
||||||
|
|
||||||
|
# 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.
|
# Don't print a delimiter after the last target was handled.
|
||||||
if i + 1 != len(aconf.target_list):
|
num_processed += 1
|
||||||
|
if num_processed < num_target_servers:
|
||||||
if aconf.json:
|
if aconf.json:
|
||||||
print(", ", end='')
|
print(", ", end='')
|
||||||
else:
|
else:
|
||||||
@ -967,9 +1113,11 @@ def main() -> int:
|
|||||||
if aconf.json:
|
if aconf.json:
|
||||||
print(']')
|
print(']')
|
||||||
|
|
||||||
|
else: # Just a scan against a single target.
|
||||||
|
ret = audit(out, aconf)
|
||||||
|
out.write()
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
else:
|
|
||||||
return audit(aconf)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__': # pragma: nocover
|
if __name__ == '__main__': # pragma: nocover
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (C) 2017-2020 Joe Testa (jtesta@positronsecurity.com)
|
Copyright (C) 2017-2021 Joe Testa (jtesta@positronsecurity.com)
|
||||||
Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu)
|
Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu)
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
@ -36,7 +36,7 @@ from typing import Callable, Optional, Union, Any # noqa: F401
|
|||||||
from ssh_audit import exitcodes
|
from ssh_audit import exitcodes
|
||||||
from ssh_audit.banner import Banner
|
from ssh_audit.banner import Banner
|
||||||
from ssh_audit.globals import SSH_HEADER
|
from ssh_audit.globals import SSH_HEADER
|
||||||
from ssh_audit.output import Output
|
from ssh_audit.outputbuffer import OutputBuffer
|
||||||
from ssh_audit.protocol import Protocol
|
from ssh_audit.protocol import Protocol
|
||||||
from ssh_audit.readbuf import ReadBuf
|
from ssh_audit.readbuf import ReadBuf
|
||||||
from ssh_audit.ssh1 import SSH1
|
from ssh_audit.ssh1 import SSH1
|
||||||
@ -52,14 +52,15 @@ class SSH_Socket(ReadBuf, WriteBuf):
|
|||||||
|
|
||||||
SM_BANNER_SENT = 1
|
SM_BANNER_SENT = 1
|
||||||
|
|
||||||
def __init__(self, host: Optional[str], port: int, ipvo: Optional[Sequence[int]] = None, timeout: Union[int, float] = 5, timeout_set: bool = False) -> None:
|
def __init__(self, outputbuffer: 'OutputBuffer', host: Optional[str], port: int, ip_version_preference: List[int] = [], timeout: Union[int, float] = 5, timeout_set: bool = False) -> None: # pylint: disable=dangerous-default-value
|
||||||
super(SSH_Socket, self).__init__()
|
super(SSH_Socket, self).__init__()
|
||||||
self.__sock = None # type: Optional[socket.socket]
|
self.__outputbuffer = outputbuffer
|
||||||
self.__sock_map = {} # type: Dict[int, socket.socket]
|
self.__sock: Optional[socket.socket] = None
|
||||||
|
self.__sock_map: Dict[int, socket.socket] = {}
|
||||||
self.__block_size = 8
|
self.__block_size = 8
|
||||||
self.__state = 0
|
self.__state = 0
|
||||||
self.__header = [] # type: List[str]
|
self.__header: List[str] = []
|
||||||
self.__banner = None # type: Optional[Banner]
|
self.__banner: Optional[Banner] = None
|
||||||
if host is None:
|
if host is None:
|
||||||
raise ValueError('undefined host')
|
raise ValueError('undefined host')
|
||||||
nport = Utils.parse_int(port)
|
nport = Utils.parse_int(port)
|
||||||
@ -67,35 +68,30 @@ class SSH_Socket(ReadBuf, WriteBuf):
|
|||||||
raise ValueError('invalid port: {}'.format(port))
|
raise ValueError('invalid port: {}'.format(port))
|
||||||
self.__host = host
|
self.__host = host
|
||||||
self.__port = nport
|
self.__port = nport
|
||||||
if ipvo is not None:
|
self.__ip_version_preference = ip_version_preference # Holds only 5 possible values: [] (no preference), [4] (use IPv4 only), [6] (use IPv6 only), [46] (use both IPv4 and IPv6, but prioritize v4), and [64] (use both IPv4 and IPv6, but prioritize v6).
|
||||||
self.__ipvo = ipvo
|
|
||||||
else:
|
|
||||||
self.__ipvo = ()
|
|
||||||
self.__timeout = timeout
|
self.__timeout = timeout
|
||||||
self.__timeout_set = timeout_set
|
self.__timeout_set = timeout_set
|
||||||
self.client_host = None # type: Optional[str]
|
self.client_host: Optional[str] = None
|
||||||
self.client_port = None
|
self.client_port = None
|
||||||
|
|
||||||
def _resolve(self, ipvo: Sequence[int]) -> Iterable[Tuple[int, Tuple[Any, ...]]]:
|
def _resolve(self) -> Iterable[Tuple[int, Tuple[Any, ...]]]:
|
||||||
ipvo = tuple([x for x in Utils.unique_seq(ipvo) if x in (4, 6)])
|
# If __ip_version_preference has only one entry, then it means that ONLY that IP version should be used.
|
||||||
ipvo_len = len(ipvo)
|
if len(self.__ip_version_preference) == 1:
|
||||||
prefer_ipvo = ipvo_len > 0
|
family = socket.AF_INET if self.__ip_version_preference[0] == 4 else socket.AF_INET6
|
||||||
prefer_ipv4 = prefer_ipvo and ipvo[0] == 4
|
|
||||||
if ipvo_len == 1:
|
|
||||||
family = socket.AF_INET if ipvo[0] == 4 else socket.AF_INET6
|
|
||||||
else:
|
else:
|
||||||
family = socket.AF_UNSPEC
|
family = socket.AF_UNSPEC
|
||||||
try:
|
try:
|
||||||
stype = socket.SOCK_STREAM
|
stype = socket.SOCK_STREAM
|
||||||
r = socket.getaddrinfo(self.__host, self.__port, family, stype)
|
r = socket.getaddrinfo(self.__host, self.__port, family, stype)
|
||||||
if prefer_ipvo:
|
|
||||||
r = sorted(r, key=lambda x: x[0], reverse=not prefer_ipv4)
|
# If the user has a preference for using IPv4 over IPv6 (or vice-versa), then sort the list returned by getaddrinfo() so that the preferred address type comes first.
|
||||||
check = any(stype == rline[2] for rline in r)
|
if len(self.__ip_version_preference) == 2:
|
||||||
|
r = sorted(r, key=lambda x: x[0], reverse=(self.__ip_version_preference[0] == 6))
|
||||||
for af, socktype, _proto, _canonname, addr in r:
|
for af, socktype, _proto, _canonname, addr in r:
|
||||||
if not check or socktype == socket.SOCK_STREAM:
|
if socktype == socket.SOCK_STREAM:
|
||||||
yield af, addr
|
yield af, addr
|
||||||
except socket.error as e:
|
except socket.error as e:
|
||||||
Output().fail('[exception] {}'.format(e))
|
self.__outputbuffer.fail('[exception] {}'.format(e)).write()
|
||||||
sys.exit(exitcodes.CONNECTION_ERROR)
|
sys.exit(exitcodes.CONNECTION_ERROR)
|
||||||
|
|
||||||
# Listens on a server socket and accepts one connection (used for
|
# Listens on a server socket and accepts one connection (used for
|
||||||
@ -156,11 +152,12 @@ class SSH_Socket(ReadBuf, WriteBuf):
|
|||||||
def connect(self) -> Optional[str]:
|
def connect(self) -> Optional[str]:
|
||||||
'''Returns None on success, or an error string.'''
|
'''Returns None on success, or an error string.'''
|
||||||
err = None
|
err = None
|
||||||
for af, addr in self._resolve(self.__ipvo):
|
for af, addr in self._resolve():
|
||||||
s = None
|
s = None
|
||||||
try:
|
try:
|
||||||
s = socket.socket(af, socket.SOCK_STREAM)
|
s = socket.socket(af, socket.SOCK_STREAM)
|
||||||
s.settimeout(self.__timeout)
|
s.settimeout(self.__timeout)
|
||||||
|
self.__outputbuffer.d(("Connecting to %s:%d..." % ('[%s]' % addr[0] if Utils.is_ipv6_address(addr[0]) else addr[0], addr[1])), write_now=True)
|
||||||
s.connect(addr)
|
s.connect(addr)
|
||||||
self.__sock = s
|
self.__sock = s
|
||||||
return None
|
return None
|
||||||
@ -175,6 +172,8 @@ class SSH_Socket(ReadBuf, WriteBuf):
|
|||||||
return '[exception] {}'.format(errm)
|
return '[exception] {}'.format(errm)
|
||||||
|
|
||||||
def get_banner(self, sshv: int = 2) -> Tuple[Optional['Banner'], List[str], Optional[str]]:
|
def get_banner(self, sshv: int = 2) -> Tuple[Optional['Banner'], List[str], Optional[str]]:
|
||||||
|
self.__outputbuffer.d('Getting banner...', write_now=True)
|
||||||
|
|
||||||
if self.__sock is None:
|
if self.__sock is None:
|
||||||
return self.__banner, self.__header, 'not connected'
|
return self.__banner, self.__header, 'not connected'
|
||||||
if self.__banner is not None:
|
if self.__banner is not None:
|
||||||
@ -230,15 +229,11 @@ class SSH_Socket(ReadBuf, WriteBuf):
|
|||||||
except socket.error as e:
|
except socket.error as e:
|
||||||
return -1, str(e.args[-1])
|
return -1, str(e.args[-1])
|
||||||
|
|
||||||
def send_algorithms(self) -> None:
|
# Send a KEXINIT with the lists of key exchanges, hostkeys, ciphers, MACs, compressions, and languages that we "support".
|
||||||
|
def send_kexinit(self, key_exchanges: List[str] = ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'ecdh-sha2-nistp256', 'ecdh-sha2-nistp384', 'ecdh-sha2-nistp521', 'diffie-hellman-group-exchange-sha256', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group14-sha256'], hostkeys: List[str] = ['rsa-sha2-512', 'rsa-sha2-256', 'ssh-rsa', 'ecdsa-sha2-nistp256', 'ssh-ed25519'], ciphers: List[str] = ['chacha20-poly1305@openssh.com', 'aes128-ctr', 'aes192-ctr', 'aes256-ctr', 'aes128-gcm@openssh.com', 'aes256-gcm@openssh.com'], macs: List[str] = ['umac-64-etm@openssh.com', 'umac-128-etm@openssh.com', 'hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'hmac-sha1-etm@openssh.com', 'umac-64@openssh.com', 'umac-128@openssh.com', 'hmac-sha2-256', 'hmac-sha2-512', 'hmac-sha1'], compressions: List[str] = ['none', 'zlib@openssh.com'], languages: List[str] = ['']) -> None: # pylint: disable=dangerous-default-value
|
||||||
'''Sends the list of supported host keys, key exchanges, ciphers, and MACs. Emulates OpenSSH v8.2.'''
|
'''Sends the list of supported host keys, key exchanges, ciphers, and MACs. Emulates OpenSSH v8.2.'''
|
||||||
|
|
||||||
key_exchanges = ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'ecdh-sha2-nistp256', 'ecdh-sha2-nistp384', 'ecdh-sha2-nistp521', 'diffie-hellman-group-exchange-sha256', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group14-sha256']
|
self.__outputbuffer.d('KEX initialisation...', write_now=True)
|
||||||
hostkeys = ['rsa-sha2-512', 'rsa-sha2-256', 'ssh-rsa', 'ecdsa-sha2-nistp256', 'ssh-ed25519']
|
|
||||||
ciphers = ['chacha20-poly1305@openssh.com', 'aes128-ctr', 'aes192-ctr', 'aes256-ctr', 'aes128-gcm@openssh.com', 'aes256-gcm@openssh.com']
|
|
||||||
macs = ['umac-64-etm@openssh.com', 'umac-128-etm@openssh.com', 'hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'hmac-sha1-etm@openssh.com', 'umac-64@openssh.com', 'umac-128@openssh.com', 'hmac-sha2-256', 'hmac-sha2-512', 'hmac-sha1']
|
|
||||||
compressions = ['none', 'zlib@openssh.com']
|
|
||||||
languages = ['']
|
|
||||||
|
|
||||||
kexparty = SSH2_KexParty(ciphers, macs, compressions, languages)
|
kexparty = SSH2_KexParty(ciphers, macs, compressions, languages)
|
||||||
kex = SSH2_Kex(os.urandom(16), key_exchanges, hostkeys, kexparty, kexparty, False, 0)
|
kex = SSH2_Kex(os.urandom(16), key_exchanges, hostkeys, kexparty, kexparty, False, 0)
|
||||||
@ -279,7 +274,7 @@ class SSH_Socket(ReadBuf, WriteBuf):
|
|||||||
payload_length = packet_length - padding_length - 1
|
payload_length = packet_length - padding_length - 1
|
||||||
check_size = 4 + 1 + payload_length + padding_length
|
check_size = 4 + 1 + payload_length + padding_length
|
||||||
if check_size % self.__block_size != 0:
|
if check_size % self.__block_size != 0:
|
||||||
Output().fail('[exception] invalid ssh packet (block size)')
|
self.__outputbuffer.fail('[exception] invalid ssh packet (block size)').write()
|
||||||
sys.exit(exitcodes.CONNECTION_ERROR)
|
sys.exit(exitcodes.CONNECTION_ERROR)
|
||||||
self.ensure_read(payload_length)
|
self.ensure_read(payload_length)
|
||||||
if sshv == 1:
|
if sshv == 1:
|
||||||
@ -294,7 +289,7 @@ class SSH_Socket(ReadBuf, WriteBuf):
|
|||||||
if sshv == 1:
|
if sshv == 1:
|
||||||
rcrc = SSH1.crc32(padding + payload)
|
rcrc = SSH1.crc32(padding + payload)
|
||||||
if crc != rcrc:
|
if crc != rcrc:
|
||||||
Output().fail('[exception] packet checksum CRC32 mismatch.')
|
self.__outputbuffer.fail('[exception] packet checksum CRC32 mismatch.').write()
|
||||||
sys.exit(exitcodes.CONNECTION_ERROR)
|
sys.exit(exitcodes.CONNECTION_ERROR)
|
||||||
else:
|
else:
|
||||||
self.ensure_read(padding_length)
|
self.ensure_read(padding_length)
|
||||||
@ -343,6 +338,6 @@ class SSH_Socket(ReadBuf, WriteBuf):
|
|||||||
|
|
||||||
def __cleanup(self) -> None:
|
def __cleanup(self) -> None:
|
||||||
self._close_socket(self.__sock)
|
self._close_socket(self.__sock)
|
||||||
for fd in self.__sock_map:
|
for sock in self.__sock_map.values():
|
||||||
self._close_socket(self.__sock_map[fd])
|
self._close_socket(sock)
|
||||||
self.__sock = None
|
self.__sock = None
|
||||||
|
@ -30,12 +30,12 @@ from ssh_audit.algorithm import Algorithm
|
|||||||
|
|
||||||
class Timeframe:
|
class Timeframe:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.__storage = {} # type: Dict[str, List[Optional[str]]]
|
self.__storage: Dict[str, List[Optional[str]]] = {}
|
||||||
|
|
||||||
def __contains__(self, product: str) -> bool:
|
def __contains__(self, product: str) -> bool:
|
||||||
return product in self.__storage
|
return product in self.__storage
|
||||||
|
|
||||||
def __getitem__(self, product): # type: (str) -> Sequence[Optional[str]]
|
def __getitem__(self, product: str) -> Sequence[Optional[str]]:
|
||||||
return tuple(self.__storage.get(product, [None] * 4))
|
return tuple(self.__storage.get(product, [None] * 4))
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
@ -51,7 +51,7 @@ class Timeframe:
|
|||||||
return self[product][1 if bool(for_server) else 3]
|
return self[product][1 if bool(for_server) else 3]
|
||||||
|
|
||||||
def _update(self, versions: Optional[str], pos: int) -> None:
|
def _update(self, versions: Optional[str], pos: int) -> None:
|
||||||
ssh_versions = {} # type: Dict[str, str]
|
ssh_versions: Dict[str, str] = {}
|
||||||
for_srv, for_cli = pos < 2, pos > 1
|
for_srv, for_cli = pos < 2, pos > 1
|
||||||
for v in (versions or '').split(','):
|
for v in (versions or '').split(','):
|
||||||
ssh_prod, ssh_ver, is_cli = Algorithm.get_ssh_version(v)
|
ssh_prod, ssh_ver, is_cli = Algorithm.get_ssh_version(v)
|
||||||
|
@ -96,7 +96,7 @@ class Utils:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def unique_seq(cls, seq: Sequence[Any]) -> Sequence[Any]:
|
def unique_seq(cls, seq: Sequence[Any]) -> Sequence[Any]:
|
||||||
seen = set() # type: Set[Any]
|
seen: Set[Any] = set()
|
||||||
|
|
||||||
def _seen_add(x: Any) -> bool:
|
def _seen_add(x: Any) -> bool:
|
||||||
seen.add(x)
|
seen.add(x)
|
||||||
@ -129,10 +129,10 @@ class Utils:
|
|||||||
return -1.0
|
return -1.0
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_host_and_port(host_and_port: str) -> Tuple[str, int]:
|
def parse_host_and_port(host_and_port: str, default_port: int = 0) -> Tuple[str, int]:
|
||||||
'''Parses a string into a tuple of its host and port. The port is 0 if not specified.'''
|
'''Parses a string into a tuple of its host and port. The port is 0 if not specified.'''
|
||||||
host = host_and_port
|
host = host_and_port
|
||||||
port = 0
|
port = default_port
|
||||||
|
|
||||||
mx = re.match(r'^\[([^\]]+)\](?::(\d+))?$', host_and_port)
|
mx = re.match(r'^\[([^\]]+)\](?::(\d+))?$', host_and_port)
|
||||||
if mx is not None:
|
if mx is not None:
|
||||||
|
@ -33,7 +33,7 @@ class VersionVulnerabilityDB: # pylint: disable=too-few-public-methods
|
|||||||
# Example: if it affects servers, both remote & local, then affected
|
# Example: if it affects servers, both remote & local, then affected
|
||||||
# = 1. If it affects servers, but is a local issue only,
|
# = 1. If it affects servers, but is a local issue only,
|
||||||
# then affected = 1 + 4 = 5.
|
# then affected = 1 + 4 = 5.
|
||||||
CVE = {
|
CVE: Dict[str, List[List[Any]]] = {
|
||||||
'Dropbear SSH': [
|
'Dropbear SSH': [
|
||||||
['0.0', '2018.76', 1, 'CVE-2018-15599', 5.0, 'remote users may enumerate users on the system'],
|
['0.0', '2018.76', 1, 'CVE-2018-15599', 5.0, 'remote users may enumerate users on the system'],
|
||||||
['0.0', '2017.74', 5, 'CVE-2017-9079', 4.7, 'local users can read certain files as root'],
|
['0.0', '2017.74', 5, 'CVE-2017-9079', 4.7, 'local users can read certain files as root'],
|
||||||
@ -66,6 +66,7 @@ class VersionVulnerabilityDB: # pylint: disable=too-few-public-methods
|
|||||||
['0.4.7', '0.5.2', 1, 'CVE-2012-4560', 7.5, 'cause DoS or execute arbitrary code (buffer overflow)'],
|
['0.4.7', '0.5.2', 1, 'CVE-2012-4560', 7.5, 'cause DoS or execute arbitrary code (buffer overflow)'],
|
||||||
['0.4.7', '0.5.2', 1, 'CVE-2012-4559', 6.8, 'cause DoS or execute arbitrary code (double free)']],
|
['0.4.7', '0.5.2', 1, 'CVE-2012-4559', 6.8, 'cause DoS or execute arbitrary code (double free)']],
|
||||||
'OpenSSH': [
|
'OpenSSH': [
|
||||||
|
['1.0', '7.7', 1, 'CVE-2018-15473', 5.3, 'enumerate usernames due to timing discrepencies'],
|
||||||
['7.2', '7.2p2', 1, 'CVE-2016-6515', 7.8, 'cause DoS via long password string (crypt CPU consumption)'],
|
['7.2', '7.2p2', 1, 'CVE-2016-6515', 7.8, 'cause DoS via long password string (crypt CPU consumption)'],
|
||||||
['1.2.2', '7.2', 1, 'CVE-2016-3115', 5.5, 'bypass command restrictions via crafted X11 forwarding data'],
|
['1.2.2', '7.2', 1, 'CVE-2016-3115', 5.5, 'bypass command restrictions via crafted X11 forwarding data'],
|
||||||
['5.4', '7.1', 1, 'CVE-2016-1907', 5.0, 'cause DoS via crafted network traffic (out of bounds read)'],
|
['5.4', '7.1', 1, 'CVE-2016-1907', 5.0, 'cause DoS via crafted network traffic (out of bounds read)'],
|
||||||
@ -139,12 +140,12 @@ class VersionVulnerabilityDB: # pylint: disable=too-few-public-methods
|
|||||||
['0.0', '0.66', 2, 'CVE-2016-2563', 7.5, 'buffer overflow in SCP command-line utility'],
|
['0.0', '0.66', 2, 'CVE-2016-2563', 7.5, 'buffer overflow in SCP command-line utility'],
|
||||||
['0.0', '0.65', 2, 'CVE-2015-5309', 4.3, 'integer overflow in terminal-handling code'],
|
['0.0', '0.65', 2, 'CVE-2015-5309', 4.3, 'integer overflow in terminal-handling code'],
|
||||||
]
|
]
|
||||||
} # type: Dict[str, List[List[Any]]]
|
}
|
||||||
TXT = {
|
TXT: Dict[str, List[List[Any]]] = {
|
||||||
'Dropbear SSH': [
|
'Dropbear SSH': [
|
||||||
['0.28', '0.34', 1, 'remote root exploit', 'remote format string buffer overflow exploit (exploit-db#387)']],
|
['0.28', '0.34', 1, 'remote root exploit', 'remote format string buffer overflow exploit (exploit-db#387)']],
|
||||||
'libssh': [
|
'libssh': [
|
||||||
['0.3.3', '0.3.3', 1, 'null pointer check', 'missing null pointer check in "crypt_set_algorithms_server"'],
|
['0.3.3', '0.3.3', 1, 'null pointer check', 'missing null pointer check in "crypt_set_algorithms_server"'],
|
||||||
['0.3.3', '0.3.3', 1, 'integer overflow', 'integer overflow in "buffer_get_data"'],
|
['0.3.3', '0.3.3', 1, 'integer overflow', 'integer overflow in "buffer_get_data"'],
|
||||||
['0.3.3', '0.3.3', 3, 'heap overflow', 'heap overflow in "packet_decrypt"']]
|
['0.3.3', '0.3.3', 3, 'heap overflow', 'heap overflow in "packet_decrypt"']]
|
||||||
} # type: Dict[str, List[List[Any]]]
|
}
|
||||||
|
@ -54,7 +54,7 @@ class WriteBuf:
|
|||||||
return self.write(v)
|
return self.write(v)
|
||||||
|
|
||||||
def write_list(self, v: List[str]) -> 'WriteBuf':
|
def write_list(self, v: List[str]) -> 'WriteBuf':
|
||||||
return self.write_string(u','.join(v))
|
return self.write_string(','.join(v))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _bitlength(cls, n: int) -> int:
|
def _bitlength(cls, n: int) -> int:
|
||||||
|
21
ssh-audit.1
21
ssh-audit.1
@ -1,4 +1,4 @@
|
|||||||
.TH SSH-AUDIT 1 "October 19, 2020"
|
.TH SSH-AUDIT 1 "March 2, 2021"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
\fBssh-audit\fP \- SSH server & client configuration auditor
|
\fBssh-audit\fP \- SSH server & client configuration auditor
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@ -46,10 +46,15 @@ Enables grepable output.
|
|||||||
.br
|
.br
|
||||||
Starts a server on port 2222 to audit client software configuration. Use -p/--port=<port> to change port and -t/--timeout=<secs> to change listen timeout.
|
Starts a server on port 2222 to audit client software configuration. Use -p/--port=<port> to change port and -t/--timeout=<secs> to change listen timeout.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B -d, \-\-debug
|
||||||
|
.br
|
||||||
|
Enable debug output.
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B -j, \-\-json
|
.B -j, \-\-json
|
||||||
.br
|
.br
|
||||||
Output results in JSON format.
|
Output results in JSON format. Specify twice (-jj) to enable indent printing (useful for debugging).
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B -l, \-\-level=<info|warn|fail>
|
.B -l, \-\-level=<info|warn|fail>
|
||||||
@ -66,6 +71,11 @@ List all official, built-in policies for common systems. Their full names can t
|
|||||||
.br
|
.br
|
||||||
Look up the security information of an algorithm(s) in the internal database. Does not connect to a server.
|
Look up the security information of an algorithm(s) in the internal database. Does not connect to a server.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B -m, \-\-manual
|
||||||
|
.br
|
||||||
|
Print the man page (Windows only).
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B -M, \-\-make-policy=<custom_policy.txt>
|
.B -M, \-\-make-policy=<custom_policy.txt>
|
||||||
.br
|
.br
|
||||||
@ -94,7 +104,12 @@ The timeout, in seconds, for creating connections and reading data from the sock
|
|||||||
.TP
|
.TP
|
||||||
.B -T, \-\-targets=<hosts.txt>
|
.B -T, \-\-targets=<hosts.txt>
|
||||||
.br
|
.br
|
||||||
A file containing a list of target hosts. Each line must have one host, in the format of HOST[:PORT].
|
A file containing a list of target hosts. Each line must have one host, in the format of HOST[:PORT]. Use --threads to control concurrent scans.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-\-threads=<threads>
|
||||||
|
.br
|
||||||
|
The number of threads to use when scanning multiple targets (with -T/--targets). Default is 32.
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B -v, \-\-verbose
|
.B -v, \-\-verbose
|
||||||
|
@ -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"}
|
||||||
|
@ -23,8 +23,9 @@
|
|||||||
[0;33m `- [warn] using weak random number generator could reveal the key[0m
|
[0;33m `- [warn] using weak random number generator could reveal the key[0m
|
||||||
`- [info] available since OpenSSH 5.7, Dropbear SSH 2013.62
|
`- [info] available since OpenSSH 5.7, Dropbear SSH 2013.62
|
||||||
[0;31m(key) ssh-rsa (1024-bit) -- [fail] using weak hashing algorithm[0m
|
[0;31m(key) ssh-rsa (1024-bit) -- [fail] using weak hashing algorithm[0m
|
||||||
[0;33m `- [warn] using small 1024-bit modulus[0m
|
[0;31m `- [fail] using small 1024-bit modulus[0m
|
||||||
`- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28
|
`- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28
|
||||||
|
`- [info] a future deprecation notice has been issued in OpenSSH 8.2: https://www.openssh.com/txt/release-8.2
|
||||||
[0;31m(key) ssh-dss -- [fail] using small 1024-bit modulus[0m
|
[0;31m(key) ssh-dss -- [fail] using small 1024-bit modulus[0m
|
||||||
[0;31m `- [fail] removed (in server) and disabled (in client) since OpenSSH 7.0, weak algorithm[0m
|
[0;31m `- [fail] removed (in server) and disabled (in client) since OpenSSH 7.0, weak algorithm[0m
|
||||||
[0;33m `- [warn] using weak random number generator could reveal the key[0m
|
[0;33m `- [warn] using weak random number generator could reveal the key[0m
|
||||||
|
@ -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"}
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
[0;36m# general[0m
|
[0;36m# general[0m
|
||||||
[0;32m(gen) banner: SSH-1.99-OpenSSH_4.0[0m
|
[0;31m(gen) banner: SSH-1.99-OpenSSH_4.0[0m
|
||||||
[0;31m(gen) protocol SSH1 enabled[0m
|
[0;31m(gen) protocol SSH1 enabled[0m
|
||||||
[0;32m(gen) software: OpenSSH 4.0[0m
|
[0;32m(gen) software: OpenSSH 4.0[0m
|
||||||
[0;32m(gen) compatibility: OpenSSH 3.9-6.6, Dropbear SSH 0.53+ (some functionality from 0.52)[0m
|
[0;32m(gen) compatibility: OpenSSH 3.9-6.6, Dropbear SSH 0.53+ (some functionality from 0.52)[0m
|
||||||
[0;32m(gen) compression: enabled (zlib)[0m
|
[0;32m(gen) compression: enabled (zlib)[0m
|
||||||
|
|
||||||
[0;36m# security[0m
|
[0;36m# security[0m
|
||||||
|
[0;33m(cve) CVE-2018-15473 -- (CVSSv2: 5.3) enumerate usernames due to timing discrepencies[0m
|
||||||
[0;33m(cve) CVE-2016-3115 -- (CVSSv2: 5.5) bypass command restrictions via crafted X11 forwarding data[0m
|
[0;33m(cve) CVE-2016-3115 -- (CVSSv2: 5.5) bypass command restrictions via crafted X11 forwarding data[0m
|
||||||
[0;33m(cve) CVE-2014-1692 -- (CVSSv2: 7.5) cause DoS via triggering error condition (memory corruption)[0m
|
[0;33m(cve) CVE-2014-1692 -- (CVSSv2: 7.5) cause DoS via triggering error condition (memory corruption)[0m
|
||||||
[0;33m(cve) CVE-2012-0814 -- (CVSSv2: 3.5) leak data via debug messages[0m
|
[0;33m(cve) CVE-2012-0814 -- (CVSSv2: 3.5) leak data via debug messages[0m
|
||||||
@ -24,6 +25,7 @@
|
|||||||
[0;33m(cve) CVE-2006-4924 -- (CVSSv2: 7.8) cause DoS via crafted packet (CPU consumption)[0m
|
[0;33m(cve) CVE-2006-4924 -- (CVSSv2: 7.8) cause DoS via crafted packet (CPU consumption)[0m
|
||||||
[0;33m(cve) CVE-2006-0225 -- (CVSSv2: 4.6) execute arbitrary code[0m
|
[0;33m(cve) CVE-2006-0225 -- (CVSSv2: 4.6) execute arbitrary code[0m
|
||||||
[0;33m(cve) CVE-2005-2798 -- (CVSSv2: 5.0) leak data about authentication credentials[0m
|
[0;33m(cve) CVE-2005-2798 -- (CVSSv2: 5.0) leak data about authentication credentials[0m
|
||||||
|
[0;31m(sec) SSH v1 enabled -- SSH v1 can be exploited to recover plaintext passwords[0m
|
||||||
|
|
||||||
[0;36m# key exchange algorithms[0m
|
[0;36m# key exchange algorithms[0m
|
||||||
[0;31m(kex) diffie-hellman-group-exchange-sha1 (1024-bit) -- [fail] using small 1024-bit modulus[0m
|
[0;31m(kex) diffie-hellman-group-exchange-sha1 (1024-bit) -- [fail] using small 1024-bit modulus[0m
|
||||||
@ -39,8 +41,9 @@
|
|||||||
|
|
||||||
[0;36m# host-key algorithms[0m
|
[0;36m# host-key algorithms[0m
|
||||||
[0;31m(key) ssh-rsa (1024-bit) -- [fail] using weak hashing algorithm[0m
|
[0;31m(key) ssh-rsa (1024-bit) -- [fail] using weak hashing algorithm[0m
|
||||||
[0;33m `- [warn] using small 1024-bit modulus[0m
|
[0;31m `- [fail] using small 1024-bit modulus[0m
|
||||||
`- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28
|
`- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28
|
||||||
|
`- [info] a future deprecation notice has been issued in OpenSSH 8.2: https://www.openssh.com/txt/release-8.2
|
||||||
[0;31m(key) ssh-dss -- [fail] using small 1024-bit modulus[0m
|
[0;31m(key) ssh-dss -- [fail] using small 1024-bit modulus[0m
|
||||||
[0;31m `- [fail] removed (in server) and disabled (in client) since OpenSSH 7.0, weak algorithm[0m
|
[0;31m `- [fail] removed (in server) and disabled (in client) since OpenSSH 7.0, weak algorithm[0m
|
||||||
[0;33m `- [warn] using weak random number generator could reveal the key[0m
|
[0;33m `- [warn] using weak random number generator could reveal the key[0m
|
||||||
|
@ -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"}
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
[0;32m(gen) compression: enabled (zlib@openssh.com)[0m
|
[0;32m(gen) compression: enabled (zlib@openssh.com)[0m
|
||||||
|
|
||||||
[0;36m# security[0m
|
[0;36m# security[0m
|
||||||
|
[0;33m(cve) CVE-2018-15473 -- (CVSSv2: 5.3) enumerate usernames due to timing discrepencies[0m
|
||||||
[0;33m(cve) CVE-2016-3115 -- (CVSSv2: 5.5) bypass command restrictions via crafted X11 forwarding data[0m
|
[0;33m(cve) CVE-2016-3115 -- (CVSSv2: 5.5) bypass command restrictions via crafted X11 forwarding data[0m
|
||||||
[0;33m(cve) CVE-2016-1907 -- (CVSSv2: 5.0) cause DoS via crafted network traffic (out of bounds read)[0m
|
[0;33m(cve) CVE-2016-1907 -- (CVSSv2: 5.0) cause DoS via crafted network traffic (out of bounds read)[0m
|
||||||
[0;33m(cve) CVE-2015-6564 -- (CVSSv2: 6.9) privilege escalation via leveraging sshd uid[0m
|
[0;33m(cve) CVE-2015-6564 -- (CVSSv2: 6.9) privilege escalation via leveraging sshd uid[0m
|
||||||
@ -33,8 +34,9 @@
|
|||||||
|
|
||||||
[0;36m# host-key algorithms[0m
|
[0;36m# host-key algorithms[0m
|
||||||
[0;31m(key) ssh-rsa (1024-bit) -- [fail] using weak hashing algorithm[0m
|
[0;31m(key) ssh-rsa (1024-bit) -- [fail] using weak hashing algorithm[0m
|
||||||
[0;33m `- [warn] using small 1024-bit modulus[0m
|
[0;31m `- [fail] using small 1024-bit modulus[0m
|
||||||
`- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28
|
`- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28
|
||||||
|
`- [info] a future deprecation notice has been issued in OpenSSH 8.2: https://www.openssh.com/txt/release-8.2
|
||||||
[0;31m(key) ssh-dss -- [fail] using small 1024-bit modulus[0m
|
[0;31m(key) ssh-dss -- [fail] using small 1024-bit modulus[0m
|
||||||
[0;31m `- [fail] removed (in server) and disabled (in client) since OpenSSH 7.0, weak algorithm[0m
|
[0;31m `- [fail] removed (in server) and disabled (in client) since OpenSSH 7.0, weak algorithm[0m
|
||||||
[0;33m `- [warn] using weak random number generator could reveal the key[0m
|
[0;33m `- [warn] using weak random number generator could reveal the key[0m
|
||||||
|
@ -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"}
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
[0;32m(gen) compression: enabled (zlib@openssh.com)[0m
|
[0;32m(gen) compression: enabled (zlib@openssh.com)[0m
|
||||||
|
|
||||||
[0;36m# security[0m
|
[0;36m# security[0m
|
||||||
|
[0;33m(cve) CVE-2018-15473 -- (CVSSv2: 5.3) enumerate usernames due to timing discrepencies[0m
|
||||||
[0;33m(cve) CVE-2016-3115 -- (CVSSv2: 5.5) bypass command restrictions via crafted X11 forwarding data[0m
|
[0;33m(cve) CVE-2016-3115 -- (CVSSv2: 5.5) bypass command restrictions via crafted X11 forwarding data[0m
|
||||||
[0;33m(cve) CVE-2016-1907 -- (CVSSv2: 5.0) cause DoS via crafted network traffic (out of bounds read)[0m
|
[0;33m(cve) CVE-2016-1907 -- (CVSSv2: 5.0) cause DoS via crafted network traffic (out of bounds read)[0m
|
||||||
[0;33m(cve) CVE-2015-6564 -- (CVSSv2: 6.9) privilege escalation via leveraging sshd uid[0m
|
[0;33m(cve) CVE-2015-6564 -- (CVSSv2: 6.9) privilege escalation via leveraging sshd uid[0m
|
||||||
@ -33,11 +34,13 @@
|
|||||||
|
|
||||||
[0;36m# host-key algorithms[0m
|
[0;36m# host-key algorithms[0m
|
||||||
[0;31m(key) ssh-rsa (1024-bit) -- [fail] using weak hashing algorithm[0m
|
[0;31m(key) ssh-rsa (1024-bit) -- [fail] using weak hashing algorithm[0m
|
||||||
[0;33m `- [warn] using small 1024-bit modulus[0m
|
[0;31m `- [fail] using small 1024-bit modulus[0m
|
||||||
`- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28
|
`- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28
|
||||||
|
`- [info] a future deprecation notice has been issued in OpenSSH 8.2: https://www.openssh.com/txt/release-8.2
|
||||||
[0;31m(key) ssh-rsa-cert-v01@openssh.com (1024-bit cert/1024-bit CA) -- [fail] using weak hashing algorithm[0m
|
[0;31m(key) ssh-rsa-cert-v01@openssh.com (1024-bit cert/1024-bit CA) -- [fail] using weak hashing algorithm[0m
|
||||||
[0;33m `- [warn] using small 1024-bit modulus[0m
|
[0;31m `- [fail] using small 1024-bit modulus[0m
|
||||||
`- [info] available since OpenSSH 5.6
|
`- [info] available since OpenSSH 5.6
|
||||||
|
`- [info] a future deprecation notice has been issued in OpenSSH 8.2: https://www.openssh.com/txt/release-8.2
|
||||||
|
|
||||||
[0;36m# encryption algorithms (ciphers)[0m
|
[0;36m# encryption algorithms (ciphers)[0m
|
||||||
[0;32m(enc) aes128-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52[0m
|
[0;32m(enc) aes128-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52[0m
|
||||||
|
@ -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"}
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
[0;32m(gen) compression: enabled (zlib@openssh.com)[0m
|
[0;32m(gen) compression: enabled (zlib@openssh.com)[0m
|
||||||
|
|
||||||
[0;36m# security[0m
|
[0;36m# security[0m
|
||||||
|
[0;33m(cve) CVE-2018-15473 -- (CVSSv2: 5.3) enumerate usernames due to timing discrepencies[0m
|
||||||
[0;33m(cve) CVE-2016-3115 -- (CVSSv2: 5.5) bypass command restrictions via crafted X11 forwarding data[0m
|
[0;33m(cve) CVE-2016-3115 -- (CVSSv2: 5.5) bypass command restrictions via crafted X11 forwarding data[0m
|
||||||
[0;33m(cve) CVE-2016-1907 -- (CVSSv2: 5.0) cause DoS via crafted network traffic (out of bounds read)[0m
|
[0;33m(cve) CVE-2016-1907 -- (CVSSv2: 5.0) cause DoS via crafted network traffic (out of bounds read)[0m
|
||||||
[0;33m(cve) CVE-2015-6564 -- (CVSSv2: 6.9) privilege escalation via leveraging sshd uid[0m
|
[0;33m(cve) CVE-2015-6564 -- (CVSSv2: 6.9) privilege escalation via leveraging sshd uid[0m
|
||||||
@ -33,11 +34,13 @@
|
|||||||
|
|
||||||
[0;36m# host-key algorithms[0m
|
[0;36m# host-key algorithms[0m
|
||||||
[0;31m(key) ssh-rsa (1024-bit) -- [fail] using weak hashing algorithm[0m
|
[0;31m(key) ssh-rsa (1024-bit) -- [fail] using weak hashing algorithm[0m
|
||||||
[0;33m `- [warn] using small 1024-bit modulus[0m
|
[0;31m `- [fail] using small 1024-bit modulus[0m
|
||||||
`- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28
|
`- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28
|
||||||
|
`- [info] a future deprecation notice has been issued in OpenSSH 8.2: https://www.openssh.com/txt/release-8.2
|
||||||
[0;31m(key) ssh-rsa-cert-v01@openssh.com (1024-bit cert/3072-bit CA) -- [fail] using weak hashing algorithm[0m
|
[0;31m(key) ssh-rsa-cert-v01@openssh.com (1024-bit cert/3072-bit CA) -- [fail] using weak hashing algorithm[0m
|
||||||
[0;33m `- [warn] using small 1024-bit modulus[0m
|
[0;31m `- [fail] using small 1024-bit modulus[0m
|
||||||
`- [info] available since OpenSSH 5.6
|
`- [info] available since OpenSSH 5.6
|
||||||
|
`- [info] a future deprecation notice has been issued in OpenSSH 8.2: https://www.openssh.com/txt/release-8.2
|
||||||
|
|
||||||
[0;36m# encryption algorithms (ciphers)[0m
|
[0;36m# encryption algorithms (ciphers)[0m
|
||||||
[0;32m(enc) aes128-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52[0m
|
[0;32m(enc) aes128-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52[0m
|
||||||
|
@ -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"}
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
[0;32m(gen) compression: enabled (zlib@openssh.com)[0m
|
[0;32m(gen) compression: enabled (zlib@openssh.com)[0m
|
||||||
|
|
||||||
[0;36m# security[0m
|
[0;36m# security[0m
|
||||||
|
[0;33m(cve) CVE-2018-15473 -- (CVSSv2: 5.3) enumerate usernames due to timing discrepencies[0m
|
||||||
[0;33m(cve) CVE-2016-3115 -- (CVSSv2: 5.5) bypass command restrictions via crafted X11 forwarding data[0m
|
[0;33m(cve) CVE-2016-3115 -- (CVSSv2: 5.5) bypass command restrictions via crafted X11 forwarding data[0m
|
||||||
[0;33m(cve) CVE-2016-1907 -- (CVSSv2: 5.0) cause DoS via crafted network traffic (out of bounds read)[0m
|
[0;33m(cve) CVE-2016-1907 -- (CVSSv2: 5.0) cause DoS via crafted network traffic (out of bounds read)[0m
|
||||||
[0;33m(cve) CVE-2015-6564 -- (CVSSv2: 6.9) privilege escalation via leveraging sshd uid[0m
|
[0;33m(cve) CVE-2015-6564 -- (CVSSv2: 6.9) privilege escalation via leveraging sshd uid[0m
|
||||||
@ -34,9 +35,11 @@
|
|||||||
[0;36m# host-key algorithms[0m
|
[0;36m# host-key algorithms[0m
|
||||||
[0;31m(key) ssh-rsa (3072-bit) -- [fail] using weak hashing algorithm[0m
|
[0;31m(key) ssh-rsa (3072-bit) -- [fail] using weak hashing algorithm[0m
|
||||||
`- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28
|
`- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28
|
||||||
|
`- [info] a future deprecation notice has been issued in OpenSSH 8.2: https://www.openssh.com/txt/release-8.2
|
||||||
[0;31m(key) ssh-rsa-cert-v01@openssh.com (3072-bit cert/1024-bit CA) -- [fail] using weak hashing algorithm[0m
|
[0;31m(key) ssh-rsa-cert-v01@openssh.com (3072-bit cert/1024-bit CA) -- [fail] using weak hashing algorithm[0m
|
||||||
[0;33m `- [warn] using small 1024-bit modulus[0m
|
[0;31m `- [fail] using small 1024-bit modulus[0m
|
||||||
`- [info] available since OpenSSH 5.6
|
`- [info] available since OpenSSH 5.6
|
||||||
|
`- [info] a future deprecation notice has been issued in OpenSSH 8.2: https://www.openssh.com/txt/release-8.2
|
||||||
|
|
||||||
[0;36m# encryption algorithms (ciphers)[0m
|
[0;36m# encryption algorithms (ciphers)[0m
|
||||||
[0;32m(enc) aes128-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52[0m
|
[0;32m(enc) aes128-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52[0m
|
||||||
|
@ -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"}
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
[0;32m(gen) compression: enabled (zlib@openssh.com)[0m
|
[0;32m(gen) compression: enabled (zlib@openssh.com)[0m
|
||||||
|
|
||||||
[0;36m# security[0m
|
[0;36m# security[0m
|
||||||
|
[0;33m(cve) CVE-2018-15473 -- (CVSSv2: 5.3) enumerate usernames due to timing discrepencies[0m
|
||||||
[0;33m(cve) CVE-2016-3115 -- (CVSSv2: 5.5) bypass command restrictions via crafted X11 forwarding data[0m
|
[0;33m(cve) CVE-2016-3115 -- (CVSSv2: 5.5) bypass command restrictions via crafted X11 forwarding data[0m
|
||||||
[0;33m(cve) CVE-2016-1907 -- (CVSSv2: 5.0) cause DoS via crafted network traffic (out of bounds read)[0m
|
[0;33m(cve) CVE-2016-1907 -- (CVSSv2: 5.0) cause DoS via crafted network traffic (out of bounds read)[0m
|
||||||
[0;33m(cve) CVE-2015-6564 -- (CVSSv2: 6.9) privilege escalation via leveraging sshd uid[0m
|
[0;33m(cve) CVE-2015-6564 -- (CVSSv2: 6.9) privilege escalation via leveraging sshd uid[0m
|
||||||
@ -34,8 +35,10 @@
|
|||||||
[0;36m# host-key algorithms[0m
|
[0;36m# host-key algorithms[0m
|
||||||
[0;31m(key) ssh-rsa (3072-bit) -- [fail] using weak hashing algorithm[0m
|
[0;31m(key) ssh-rsa (3072-bit) -- [fail] using weak hashing algorithm[0m
|
||||||
`- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28
|
`- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28
|
||||||
|
`- [info] a future deprecation notice has been issued in OpenSSH 8.2: https://www.openssh.com/txt/release-8.2
|
||||||
[0;31m(key) ssh-rsa-cert-v01@openssh.com (3072-bit cert/3072-bit CA) -- [fail] using weak hashing algorithm[0m
|
[0;31m(key) ssh-rsa-cert-v01@openssh.com (3072-bit cert/3072-bit CA) -- [fail] using weak hashing algorithm[0m
|
||||||
`- [info] available since OpenSSH 5.6
|
`- [info] available since OpenSSH 5.6
|
||||||
|
`- [info] a future deprecation notice has been issued in OpenSSH 8.2: https://www.openssh.com/txt/release-8.2
|
||||||
|
|
||||||
[0;36m# encryption algorithms (ciphers)[0m
|
[0;36m# encryption algorithms (ciphers)[0m
|
||||||
[0;32m(enc) aes128-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52[0m
|
[0;32m(enc) aes128-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52[0m
|
||||||
|
@ -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"}
|
||||||
|
@ -25,6 +25,7 @@
|
|||||||
[0;32m(key) rsa-sha2-256 (3072-bit) -- [info] available since OpenSSH 7.2[0m
|
[0;32m(key) rsa-sha2-256 (3072-bit) -- [info] available since OpenSSH 7.2[0m
|
||||||
[0;31m(key) ssh-rsa (3072-bit) -- [fail] using weak hashing algorithm[0m
|
[0;31m(key) ssh-rsa (3072-bit) -- [fail] using weak hashing algorithm[0m
|
||||||
`- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28
|
`- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28
|
||||||
|
`- [info] a future deprecation notice has been issued in OpenSSH 8.2: https://www.openssh.com/txt/release-8.2
|
||||||
[0;31m(key) ecdsa-sha2-nistp256 -- [fail] using weak elliptic curves[0m
|
[0;31m(key) ecdsa-sha2-nistp256 -- [fail] using weak elliptic curves[0m
|
||||||
[0;33m `- [warn] using weak random number generator could reveal the key[0m
|
[0;33m `- [warn] using weak random number generator could reveal the key[0m
|
||||||
`- [info] available since OpenSSH 5.7, Dropbear SSH 2013.62
|
`- [info] available since OpenSSH 5.7, Dropbear SSH 2013.62
|
||||||
|
@ -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"}
|
||||||
|
@ -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"}
|
||||||
|
@ -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"}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[0;36m# general[0m
|
[0;36m# general[0m
|
||||||
[0;32m(gen) software: TinySSH noversion[0m
|
[0;32m(gen) software: TinySSH noversion[0m
|
||||||
[0;32m(gen) compatibility: OpenSSH 8.0+, Dropbear SSH 2018.76+[0m
|
[0;32m(gen) compatibility: OpenSSH 8.0-8.4, Dropbear SSH 2018.76+[0m
|
||||||
[0;32m(gen) compression: disabled[0m
|
[0;32m(gen) compression: disabled[0m
|
||||||
|
|
||||||
[0;36m# key exchange algorithms[0m
|
[0;36m# key exchange algorithms[0m
|
||||||
|
@ -7,6 +7,7 @@ class TestAuditConf:
|
|||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def init(self, ssh_audit):
|
def init(self, ssh_audit):
|
||||||
self.AuditConf = ssh_audit.AuditConf
|
self.AuditConf = ssh_audit.AuditConf
|
||||||
|
self.OutputBuffer = ssh_audit.OutputBuffer
|
||||||
self.usage = ssh_audit.usage
|
self.usage = ssh_audit.usage
|
||||||
self.process_commandline = process_commandline
|
self.process_commandline = process_commandline
|
||||||
|
|
||||||
@ -21,9 +22,8 @@ class TestAuditConf:
|
|||||||
'colors': True,
|
'colors': True,
|
||||||
'verbose': False,
|
'verbose': False,
|
||||||
'level': 'info',
|
'level': 'info',
|
||||||
'ipv4': True,
|
'ipv4': False,
|
||||||
'ipv6': True,
|
'ipv6': False
|
||||||
'ipvo': ()
|
|
||||||
}
|
}
|
||||||
for k, v in kwargs.items():
|
for k, v in kwargs.items():
|
||||||
options[k] = v
|
options[k] = v
|
||||||
@ -37,7 +37,6 @@ class TestAuditConf:
|
|||||||
assert conf.level == options['level']
|
assert conf.level == options['level']
|
||||||
assert conf.ipv4 == options['ipv4']
|
assert conf.ipv4 == options['ipv4']
|
||||||
assert conf.ipv6 == options['ipv6']
|
assert conf.ipv6 == options['ipv6']
|
||||||
assert conf.ipvo == options['ipvo']
|
|
||||||
|
|
||||||
def test_audit_conf_defaults(self):
|
def test_audit_conf_defaults(self):
|
||||||
conf = self.AuditConf()
|
conf = self.AuditConf()
|
||||||
@ -63,57 +62,38 @@ class TestAuditConf:
|
|||||||
conf.port = port
|
conf.port = port
|
||||||
excinfo.match(r'.*invalid port.*')
|
excinfo.match(r'.*invalid port.*')
|
||||||
|
|
||||||
def test_audit_conf_ipvo(self):
|
def test_audit_conf_ip_version_preference(self):
|
||||||
# ipv4-only
|
# ipv4-only
|
||||||
conf = self.AuditConf()
|
conf = self.AuditConf()
|
||||||
conf.ipv4 = True
|
conf.ipv4 = True
|
||||||
assert conf.ipv4 is True
|
assert conf.ipv4 is True
|
||||||
assert conf.ipv6 is False
|
assert conf.ipv6 is False
|
||||||
assert conf.ipvo == (4,)
|
assert conf.ip_version_preference == [4]
|
||||||
# ipv6-only
|
# ipv6-only
|
||||||
conf = self.AuditConf()
|
conf = self.AuditConf()
|
||||||
conf.ipv6 = True
|
conf.ipv6 = True
|
||||||
assert conf.ipv4 is False
|
assert conf.ipv4 is False
|
||||||
assert conf.ipv6 is True
|
assert conf.ipv6 is True
|
||||||
assert conf.ipvo == (6,)
|
assert conf.ip_version_preference == [6]
|
||||||
# ipv4-only (by removing ipv6)
|
|
||||||
conf = self.AuditConf()
|
|
||||||
conf.ipv6 = False
|
|
||||||
assert conf.ipv4 is True
|
|
||||||
assert conf.ipv6 is False
|
|
||||||
assert conf.ipvo == (4, )
|
|
||||||
# ipv6-only (by removing ipv4)
|
|
||||||
conf = self.AuditConf()
|
|
||||||
conf.ipv4 = False
|
|
||||||
assert conf.ipv4 is False
|
|
||||||
assert conf.ipv6 is True
|
|
||||||
assert conf.ipvo == (6, )
|
|
||||||
# ipv4-preferred
|
# ipv4-preferred
|
||||||
conf = self.AuditConf()
|
conf = self.AuditConf()
|
||||||
conf.ipv4 = True
|
conf.ipv4 = True
|
||||||
conf.ipv6 = True
|
conf.ipv6 = True
|
||||||
assert conf.ipv4 is True
|
assert conf.ipv4 is True
|
||||||
assert conf.ipv6 is True
|
assert conf.ipv6 is True
|
||||||
assert conf.ipvo == (4, 6)
|
assert conf.ip_version_preference == [4, 6]
|
||||||
# ipv6-preferred
|
# ipv6-preferred
|
||||||
conf = self.AuditConf()
|
conf = self.AuditConf()
|
||||||
conf.ipv6 = True
|
conf.ipv6 = True
|
||||||
conf.ipv4 = True
|
conf.ipv4 = True
|
||||||
assert conf.ipv4 is True
|
assert conf.ipv4 is True
|
||||||
assert conf.ipv6 is True
|
assert conf.ipv6 is True
|
||||||
assert conf.ipvo == (6, 4)
|
assert conf.ip_version_preference == [6, 4]
|
||||||
# ipvo empty
|
# defaults
|
||||||
conf = self.AuditConf()
|
conf = self.AuditConf()
|
||||||
conf.ipvo = ()
|
assert conf.ipv4 is False
|
||||||
assert conf.ipv4 is True
|
assert conf.ipv6 is False
|
||||||
assert conf.ipv6 is True
|
assert conf.ip_version_preference == []
|
||||||
assert conf.ipvo == ()
|
|
||||||
# ipvo validation
|
|
||||||
conf = self.AuditConf()
|
|
||||||
conf.ipvo = (1, 2, 3, 4, 5, 6)
|
|
||||||
assert conf.ipvo == (4, 6)
|
|
||||||
conf.ipvo = (4, 4, 4, 6, 6)
|
|
||||||
assert conf.ipvo == (4, 6)
|
|
||||||
|
|
||||||
def test_audit_conf_level(self):
|
def test_audit_conf_level(self):
|
||||||
conf = self.AuditConf()
|
conf = self.AuditConf()
|
||||||
@ -127,7 +107,7 @@ class TestAuditConf:
|
|||||||
|
|
||||||
def test_audit_conf_process_commandline(self):
|
def test_audit_conf_process_commandline(self):
|
||||||
# pylint: disable=too-many-statements
|
# pylint: disable=too-many-statements
|
||||||
c = lambda x: self.process_commandline(x.split(), self.usage) # noqa
|
c = lambda x: self.process_commandline(self.OutputBuffer, x.split(), self.usage) # noqa
|
||||||
with pytest.raises(SystemExit):
|
with pytest.raises(SystemExit):
|
||||||
conf = c('')
|
conf = c('')
|
||||||
with pytest.raises(SystemExit):
|
with pytest.raises(SystemExit):
|
||||||
|
@ -35,9 +35,9 @@ def test_prevent_runtime_error_regression(ssh_audit, kex):
|
|||||||
kex.set_host_key("ssh-rsa7", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00")
|
kex.set_host_key("ssh-rsa7", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00")
|
||||||
kex.set_host_key("ssh-rsa8", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00")
|
kex.set_host_key("ssh-rsa8", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00")
|
||||||
|
|
||||||
rv = ssh_audit.build_struct(banner=None, kex=kex)
|
rv = ssh_audit.build_struct('localhost', banner=None, kex=kex)
|
||||||
|
|
||||||
assert len(rv["fingerprints"]) == 9
|
assert len(rv["fingerprints"]) == (9 * 2) # Each host key generates two hash fingerprints: one using SHA256, and one using MD5.
|
||||||
|
|
||||||
for key in ['banner', 'compression', 'enc', 'fingerprints', 'kex', 'key', 'mac']:
|
for key in ['banner', 'compression', 'enc', 'fingerprints', 'kex', 'key', 'mac']:
|
||||||
assert key in rv
|
assert key in rv
|
||||||
|
@ -2,12 +2,15 @@ import socket
|
|||||||
import errno
|
import errno
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from ssh_audit.outputbuffer import OutputBuffer
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=attribute-defined-outside-init
|
# pylint: disable=attribute-defined-outside-init
|
||||||
class TestErrors:
|
class TestErrors:
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def init(self, ssh_audit):
|
def init(self, ssh_audit):
|
||||||
self.AuditConf = ssh_audit.AuditConf
|
self.AuditConf = ssh_audit.AuditConf
|
||||||
|
self.OutputBuffer = ssh_audit.OutputBuffer
|
||||||
self.audit = ssh_audit.audit
|
self.audit = ssh_audit.audit
|
||||||
|
|
||||||
def _conf(self):
|
def _conf(self):
|
||||||
@ -21,14 +24,21 @@ class TestErrors:
|
|||||||
conf = self._conf()
|
conf = self._conf()
|
||||||
spy.begin()
|
spy.begin()
|
||||||
|
|
||||||
|
out = OutputBuffer()
|
||||||
if exit_expected:
|
if exit_expected:
|
||||||
with pytest.raises(SystemExit):
|
with pytest.raises(SystemExit):
|
||||||
self.audit(conf)
|
self.audit(out, conf)
|
||||||
else:
|
else:
|
||||||
ret = self.audit(conf)
|
ret = self.audit(out, conf)
|
||||||
assert ret != 0
|
assert ret != 0
|
||||||
|
|
||||||
|
out.write()
|
||||||
lines = spy.flush()
|
lines = spy.flush()
|
||||||
|
|
||||||
|
# If the last line is empty, delete it.
|
||||||
|
if len(lines) > 1 and lines[-1] == '':
|
||||||
|
del lines[-1]
|
||||||
|
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
def test_connection_unresolved(self, output_spy, virtual_socket):
|
def test_connection_unresolved(self, output_spy, virtual_socket):
|
||||||
@ -157,6 +167,6 @@ class TestErrors:
|
|||||||
conf = self._conf()
|
conf = self._conf()
|
||||||
conf.ssh1, conf.ssh2 = True, False
|
conf.ssh1, conf.ssh2 = True, False
|
||||||
lines = self._audit(output_spy, conf)
|
lines = self._audit(output_spy, conf)
|
||||||
assert len(lines) == 3
|
assert len(lines) == 4
|
||||||
assert 'error reading packet' in lines[-1]
|
assert 'error reading packet' in lines[-1]
|
||||||
assert 'major versions differ' in lines[-1]
|
assert 'major versions differ' in lines[-1]
|
||||||
|
@ -2,102 +2,107 @@ import pytest
|
|||||||
|
|
||||||
|
|
||||||
# pylint: disable=attribute-defined-outside-init
|
# pylint: disable=attribute-defined-outside-init
|
||||||
class TestOutput:
|
class TestOutputBuffer:
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def init(self, ssh_audit):
|
def init(self, ssh_audit):
|
||||||
self.Output = ssh_audit.Output
|
|
||||||
self.OutputBuffer = ssh_audit.OutputBuffer
|
self.OutputBuffer = ssh_audit.OutputBuffer
|
||||||
|
|
||||||
def test_output_buffer_no_lines(self, output_spy):
|
def test_outputbuffer_no_lines(self, output_spy):
|
||||||
output_spy.begin()
|
output_spy.begin()
|
||||||
with self.OutputBuffer() as obuf:
|
obuf = self.OutputBuffer()
|
||||||
pass
|
obuf.write()
|
||||||
assert output_spy.flush() == []
|
assert output_spy.flush() == ['']
|
||||||
output_spy.begin()
|
output_spy.begin()
|
||||||
with self.OutputBuffer() as obuf:
|
|
||||||
pass
|
|
||||||
obuf.flush()
|
|
||||||
assert output_spy.flush() == []
|
|
||||||
|
|
||||||
def test_output_buffer_no_flush(self, output_spy):
|
def test_outputbuffer_defaults(self):
|
||||||
output_spy.begin()
|
obuf = self.OutputBuffer()
|
||||||
with self.OutputBuffer():
|
|
||||||
print('abc')
|
|
||||||
assert output_spy.flush() == []
|
|
||||||
|
|
||||||
def test_output_buffer_flush(self, output_spy):
|
|
||||||
output_spy.begin()
|
|
||||||
with self.OutputBuffer() as obuf:
|
|
||||||
print('abc')
|
|
||||||
print()
|
|
||||||
print('def')
|
|
||||||
obuf.flush()
|
|
||||||
assert output_spy.flush() == ['abc', '', 'def']
|
|
||||||
|
|
||||||
def test_output_defaults(self):
|
|
||||||
out = self.Output()
|
|
||||||
# default: on
|
# default: on
|
||||||
assert out.batch is False
|
assert obuf.batch is False
|
||||||
assert out.use_colors is True
|
assert obuf.use_colors is True
|
||||||
assert out.level == 'info'
|
assert obuf.level == 'info'
|
||||||
|
|
||||||
def test_output_colors(self, output_spy):
|
def test_outputbuffer_colors(self, output_spy):
|
||||||
out = self.Output()
|
out = self.OutputBuffer()
|
||||||
# test without colors
|
|
||||||
|
# Test without colors.
|
||||||
out.use_colors = False
|
out.use_colors = False
|
||||||
|
|
||||||
output_spy.begin()
|
output_spy.begin()
|
||||||
out.info('info color')
|
out.info('info color')
|
||||||
|
out.write()
|
||||||
assert output_spy.flush() == ['info color']
|
assert output_spy.flush() == ['info color']
|
||||||
|
|
||||||
output_spy.begin()
|
output_spy.begin()
|
||||||
out.head('head color')
|
out.head('head color')
|
||||||
|
out.write()
|
||||||
assert output_spy.flush() == ['head color']
|
assert output_spy.flush() == ['head color']
|
||||||
|
|
||||||
output_spy.begin()
|
output_spy.begin()
|
||||||
out.good('good color')
|
out.good('good color')
|
||||||
|
out.write()
|
||||||
assert output_spy.flush() == ['good color']
|
assert output_spy.flush() == ['good color']
|
||||||
|
|
||||||
output_spy.begin()
|
output_spy.begin()
|
||||||
out.warn('warn color')
|
out.warn('warn color')
|
||||||
|
out.write()
|
||||||
assert output_spy.flush() == ['warn color']
|
assert output_spy.flush() == ['warn color']
|
||||||
|
|
||||||
output_spy.begin()
|
output_spy.begin()
|
||||||
out.fail('fail color')
|
out.fail('fail color')
|
||||||
|
out.write()
|
||||||
assert output_spy.flush() == ['fail color']
|
assert output_spy.flush() == ['fail color']
|
||||||
|
|
||||||
|
# If colors aren't supported by this system, skip the color tests.
|
||||||
if not out.colors_supported:
|
if not out.colors_supported:
|
||||||
return
|
return
|
||||||
# test with colors
|
|
||||||
|
# Test with colors.
|
||||||
out.use_colors = True
|
out.use_colors = True
|
||||||
|
|
||||||
output_spy.begin()
|
output_spy.begin()
|
||||||
out.info('info color')
|
out.info('info color')
|
||||||
|
out.write()
|
||||||
assert output_spy.flush() == ['info color']
|
assert output_spy.flush() == ['info color']
|
||||||
|
|
||||||
output_spy.begin()
|
output_spy.begin()
|
||||||
out.head('head color')
|
out.head('head color')
|
||||||
assert output_spy.flush() == ['\x1b[0;36mhead color\x1b[0m']
|
out.write()
|
||||||
|
assert output_spy.flush() in [['\x1b[0;36mhead color\x1b[0m'], ['\x1b[0;96mhead color\x1b[0m']]
|
||||||
|
|
||||||
output_spy.begin()
|
output_spy.begin()
|
||||||
out.good('good color')
|
out.good('good color')
|
||||||
assert output_spy.flush() == ['\x1b[0;32mgood color\x1b[0m']
|
out.write()
|
||||||
|
assert output_spy.flush() in [['\x1b[0;32mgood color\x1b[0m'], ['\x1b[0;92mgood color\x1b[0m']]
|
||||||
|
|
||||||
output_spy.begin()
|
output_spy.begin()
|
||||||
out.warn('warn color')
|
out.warn('warn color')
|
||||||
assert output_spy.flush() == ['\x1b[0;33mwarn color\x1b[0m']
|
out.write()
|
||||||
|
assert output_spy.flush() in [['\x1b[0;33mwarn color\x1b[0m'], ['\x1b[0;93mwarn color\x1b[0m']]
|
||||||
|
|
||||||
output_spy.begin()
|
output_spy.begin()
|
||||||
out.fail('fail color')
|
out.fail('fail color')
|
||||||
assert output_spy.flush() == ['\x1b[0;31mfail color\x1b[0m']
|
out.write()
|
||||||
|
assert output_spy.flush() in [['\x1b[0;31mfail color\x1b[0m'], ['\x1b[0;91mfail color\x1b[0m']]
|
||||||
|
|
||||||
def test_output_sep(self, output_spy):
|
def test_outputbuffer_sep(self, output_spy):
|
||||||
out = self.Output()
|
out = self.OutputBuffer()
|
||||||
output_spy.begin()
|
output_spy.begin()
|
||||||
out.sep()
|
out.sep()
|
||||||
out.sep()
|
out.sep()
|
||||||
out.sep()
|
out.sep()
|
||||||
|
out.write()
|
||||||
assert output_spy.flush() == ['', '', '']
|
assert output_spy.flush() == ['', '', '']
|
||||||
|
|
||||||
def test_output_levels(self):
|
def test_outputbuffer_levels(self):
|
||||||
out = self.Output()
|
out = self.OutputBuffer()
|
||||||
assert out.get_level('info') == 0
|
assert out.get_level('info') == 0
|
||||||
assert out.get_level('good') == 0
|
assert out.get_level('good') == 0
|
||||||
assert out.get_level('warn') == 1
|
assert out.get_level('warn') == 1
|
||||||
assert out.get_level('fail') == 2
|
assert out.get_level('fail') == 2
|
||||||
assert out.get_level('unknown') > 2
|
assert out.get_level('unknown') > 2
|
||||||
|
|
||||||
def test_output_level_property(self):
|
def test_outputbuffer_level_property(self):
|
||||||
out = self.Output()
|
out = self.OutputBuffer()
|
||||||
out.level = 'info'
|
out.level = 'info'
|
||||||
assert out.level == 'info'
|
assert out.level == 'info'
|
||||||
out.level = 'good'
|
out.level = 'good'
|
||||||
@ -109,8 +114,8 @@ class TestOutput:
|
|||||||
out.level = 'invalid level'
|
out.level = 'invalid level'
|
||||||
assert out.level == 'unknown'
|
assert out.level == 'unknown'
|
||||||
|
|
||||||
def test_output_level(self, output_spy):
|
def test_outputbuffer_level(self, output_spy):
|
||||||
out = self.Output()
|
out = self.OutputBuffer()
|
||||||
# visible: all
|
# visible: all
|
||||||
out.level = 'info'
|
out.level = 'info'
|
||||||
output_spy.begin()
|
output_spy.begin()
|
||||||
@ -119,6 +124,7 @@ class TestOutput:
|
|||||||
out.good('good color')
|
out.good('good color')
|
||||||
out.warn('warn color')
|
out.warn('warn color')
|
||||||
out.fail('fail color')
|
out.fail('fail color')
|
||||||
|
out.write()
|
||||||
assert len(output_spy.flush()) == 5
|
assert len(output_spy.flush()) == 5
|
||||||
# visible: head, warn, fail
|
# visible: head, warn, fail
|
||||||
out.level = 'warn'
|
out.level = 'warn'
|
||||||
@ -128,6 +134,7 @@ class TestOutput:
|
|||||||
out.good('good color')
|
out.good('good color')
|
||||||
out.warn('warn color')
|
out.warn('warn color')
|
||||||
out.fail('fail color')
|
out.fail('fail color')
|
||||||
|
out.write()
|
||||||
assert len(output_spy.flush()) == 3
|
assert len(output_spy.flush()) == 3
|
||||||
# visible: head, fail
|
# visible: head, fail
|
||||||
out.level = 'fail'
|
out.level = 'fail'
|
||||||
@ -137,6 +144,7 @@ class TestOutput:
|
|||||||
out.good('good color')
|
out.good('good color')
|
||||||
out.warn('warn color')
|
out.warn('warn color')
|
||||||
out.fail('fail color')
|
out.fail('fail color')
|
||||||
|
out.write()
|
||||||
assert len(output_spy.flush()) == 2
|
assert len(output_spy.flush()) == 2
|
||||||
# visible: head
|
# visible: head
|
||||||
out.level = 'invalid level'
|
out.level = 'invalid level'
|
||||||
@ -146,10 +154,11 @@ class TestOutput:
|
|||||||
out.good('good color')
|
out.good('good color')
|
||||||
out.warn('warn color')
|
out.warn('warn color')
|
||||||
out.fail('fail color')
|
out.fail('fail color')
|
||||||
|
out.write()
|
||||||
assert len(output_spy.flush()) == 1
|
assert len(output_spy.flush()) == 1
|
||||||
|
|
||||||
def test_output_batch(self, output_spy):
|
def test_outputbuffer_batch(self, output_spy):
|
||||||
out = self.Output()
|
out = self.OutputBuffer()
|
||||||
# visible: all
|
# visible: all
|
||||||
output_spy.begin()
|
output_spy.begin()
|
||||||
out.level = 'info'
|
out.level = 'info'
|
||||||
@ -159,6 +168,7 @@ class TestOutput:
|
|||||||
out.good('good color')
|
out.good('good color')
|
||||||
out.warn('warn color')
|
out.warn('warn color')
|
||||||
out.fail('fail color')
|
out.fail('fail color')
|
||||||
|
out.write()
|
||||||
assert len(output_spy.flush()) == 5
|
assert len(output_spy.flush()) == 5
|
||||||
# visible: all except head
|
# visible: all except head
|
||||||
output_spy.begin()
|
output_spy.begin()
|
||||||
@ -169,4 +179,5 @@ class TestOutput:
|
|||||||
out.good('good color')
|
out.good('good color')
|
||||||
out.warn('warn color')
|
out.warn('warn color')
|
||||||
out.fail('fail color')
|
out.fail('fail color')
|
||||||
|
out.write()
|
||||||
assert len(output_spy.flush()) == 4
|
assert len(output_spy.flush()) == 4
|
@ -8,6 +8,7 @@ class TestResolve:
|
|||||||
def init(self, ssh_audit):
|
def init(self, ssh_audit):
|
||||||
self.AuditConf = ssh_audit.AuditConf
|
self.AuditConf = ssh_audit.AuditConf
|
||||||
self.audit = ssh_audit.audit
|
self.audit = ssh_audit.audit
|
||||||
|
self.OutputBuffer = ssh_audit.OutputBuffer
|
||||||
self.ssh_socket = ssh_audit.SSH_Socket
|
self.ssh_socket = ssh_audit.SSH_Socket
|
||||||
|
|
||||||
def _conf(self):
|
def _conf(self):
|
||||||
@ -19,11 +20,11 @@ class TestResolve:
|
|||||||
def test_resolve_error(self, output_spy, virtual_socket):
|
def test_resolve_error(self, output_spy, virtual_socket):
|
||||||
vsocket = virtual_socket
|
vsocket = virtual_socket
|
||||||
vsocket.gsock.addrinfodata['localhost#22'] = socket.gaierror(8, 'hostname nor servname provided, or not known')
|
vsocket.gsock.addrinfodata['localhost#22'] = socket.gaierror(8, 'hostname nor servname provided, or not known')
|
||||||
s = self.ssh_socket('localhost', 22)
|
|
||||||
conf = self._conf()
|
conf = self._conf()
|
||||||
|
s = self.ssh_socket(self.OutputBuffer(), 'localhost', 22, conf.ip_version_preference)
|
||||||
output_spy.begin()
|
output_spy.begin()
|
||||||
with pytest.raises(SystemExit):
|
with pytest.raises(SystemExit):
|
||||||
list(s._resolve(conf.ipvo))
|
list(s._resolve())
|
||||||
lines = output_spy.flush()
|
lines = output_spy.flush()
|
||||||
assert len(lines) == 1
|
assert len(lines) == 1
|
||||||
assert 'hostname nor servname provided' in lines[-1]
|
assert 'hostname nor servname provided' in lines[-1]
|
||||||
@ -31,49 +32,50 @@ class TestResolve:
|
|||||||
def test_resolve_hostname_without_records(self, output_spy, virtual_socket):
|
def test_resolve_hostname_without_records(self, output_spy, virtual_socket):
|
||||||
vsocket = virtual_socket
|
vsocket = virtual_socket
|
||||||
vsocket.gsock.addrinfodata['localhost#22'] = []
|
vsocket.gsock.addrinfodata['localhost#22'] = []
|
||||||
s = self.ssh_socket('localhost', 22)
|
|
||||||
conf = self._conf()
|
conf = self._conf()
|
||||||
|
s = self.ssh_socket(self.OutputBuffer(), 'localhost', 22, conf.ip_version_preference)
|
||||||
output_spy.begin()
|
output_spy.begin()
|
||||||
r = list(s._resolve(conf.ipvo))
|
r = list(s._resolve())
|
||||||
assert len(r) == 0
|
assert len(r) == 0
|
||||||
|
|
||||||
def test_resolve_ipv4(self, virtual_socket):
|
def test_resolve_ipv4(self, virtual_socket):
|
||||||
conf = self._conf()
|
conf = self._conf()
|
||||||
conf.ipv4 = True
|
conf.ipv4 = True
|
||||||
s = self.ssh_socket('localhost', 22)
|
s = self.ssh_socket(self.OutputBuffer(), 'localhost', 22, conf.ip_version_preference)
|
||||||
r = list(s._resolve(conf.ipvo))
|
r = list(s._resolve())
|
||||||
assert len(r) == 1
|
assert len(r) == 1
|
||||||
assert r[0] == (socket.AF_INET, ('127.0.0.1', 22))
|
assert r[0] == (socket.AF_INET, ('127.0.0.1', 22))
|
||||||
|
|
||||||
def test_resolve_ipv6(self, virtual_socket):
|
def test_resolve_ipv6(self, virtual_socket):
|
||||||
s = self.ssh_socket('localhost', 22)
|
|
||||||
conf = self._conf()
|
conf = self._conf()
|
||||||
conf.ipv6 = True
|
conf.ipv6 = True
|
||||||
r = list(s._resolve(conf.ipvo))
|
s = self.ssh_socket(self.OutputBuffer(), 'localhost', 22, conf.ip_version_preference)
|
||||||
|
r = list(s._resolve())
|
||||||
assert len(r) == 1
|
assert len(r) == 1
|
||||||
assert r[0] == (socket.AF_INET6, ('::1', 22))
|
assert r[0] == (socket.AF_INET6, ('::1', 22))
|
||||||
|
|
||||||
def test_resolve_ipv46_both(self, virtual_socket):
|
def test_resolve_ipv46_both(self, virtual_socket):
|
||||||
s = self.ssh_socket('localhost', 22)
|
|
||||||
conf = self._conf()
|
conf = self._conf()
|
||||||
r = list(s._resolve(conf.ipvo))
|
s = self.ssh_socket(self.OutputBuffer(), 'localhost', 22, conf.ip_version_preference)
|
||||||
|
r = list(s._resolve())
|
||||||
assert len(r) == 2
|
assert len(r) == 2
|
||||||
assert r[0] == (socket.AF_INET, ('127.0.0.1', 22))
|
assert r[0] == (socket.AF_INET, ('127.0.0.1', 22))
|
||||||
assert r[1] == (socket.AF_INET6, ('::1', 22))
|
assert r[1] == (socket.AF_INET6, ('::1', 22))
|
||||||
|
|
||||||
def test_resolve_ipv46_order(self, virtual_socket):
|
def test_resolve_ipv46_order(self, virtual_socket):
|
||||||
s = self.ssh_socket('localhost', 22)
|
|
||||||
conf = self._conf()
|
conf = self._conf()
|
||||||
conf.ipv4 = True
|
conf.ipv4 = True
|
||||||
conf.ipv6 = True
|
conf.ipv6 = True
|
||||||
r = list(s._resolve(conf.ipvo))
|
s = self.ssh_socket(self.OutputBuffer(), 'localhost', 22, conf.ip_version_preference)
|
||||||
|
r = list(s._resolve())
|
||||||
assert len(r) == 2
|
assert len(r) == 2
|
||||||
assert r[0] == (socket.AF_INET, ('127.0.0.1', 22))
|
assert r[0] == (socket.AF_INET, ('127.0.0.1', 22))
|
||||||
assert r[1] == (socket.AF_INET6, ('::1', 22))
|
assert r[1] == (socket.AF_INET6, ('::1', 22))
|
||||||
conf = self._conf()
|
conf = self._conf()
|
||||||
conf.ipv6 = True
|
conf.ipv6 = True
|
||||||
conf.ipv4 = True
|
conf.ipv4 = True
|
||||||
r = list(s._resolve(conf.ipvo))
|
s = self.ssh_socket(self.OutputBuffer(), 'localhost', 22, conf.ip_version_preference)
|
||||||
|
r = list(s._resolve())
|
||||||
assert len(r) == 2
|
assert len(r) == 2
|
||||||
assert r[0] == (socket.AF_INET6, ('::1', 22))
|
assert r[0] == (socket.AF_INET6, ('::1', 22))
|
||||||
assert r[1] == (socket.AF_INET, ('127.0.0.1', 22))
|
assert r[1] == (socket.AF_INET, ('127.0.0.1', 22))
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from ssh_audit.outputbuffer import OutputBuffer
|
||||||
from ssh_audit.ssh_socket import SSH_Socket
|
from ssh_audit.ssh_socket import SSH_Socket
|
||||||
|
|
||||||
|
|
||||||
@ -7,24 +8,25 @@ from ssh_audit.ssh_socket import SSH_Socket
|
|||||||
class TestSocket:
|
class TestSocket:
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def init(self, ssh_audit):
|
def init(self, ssh_audit):
|
||||||
|
self.OutputBuffer = OutputBuffer
|
||||||
self.ssh_socket = SSH_Socket
|
self.ssh_socket = SSH_Socket
|
||||||
|
|
||||||
def test_invalid_host(self, virtual_socket):
|
def test_invalid_host(self, virtual_socket):
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
self.ssh_socket(None, 22)
|
self.ssh_socket(self.OutputBuffer(), None, 22)
|
||||||
|
|
||||||
def test_invalid_port(self, virtual_socket):
|
def test_invalid_port(self, virtual_socket):
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
self.ssh_socket('localhost', 'abc')
|
self.ssh_socket(self.OutputBuffer(), 'localhost', 'abc')
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
self.ssh_socket('localhost', -1)
|
self.ssh_socket(self.OutputBuffer(), 'localhost', -1)
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
self.ssh_socket('localhost', 0)
|
self.ssh_socket(self.OutputBuffer(), 'localhost', 0)
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
self.ssh_socket('localhost', 65536)
|
self.ssh_socket(self.OutputBuffer(), 'localhost', 65536)
|
||||||
|
|
||||||
def test_not_connected_socket(self, virtual_socket):
|
def test_not_connected_socket(self, virtual_socket):
|
||||||
sock = self.ssh_socket('localhost', 22)
|
sock = self.ssh_socket(self.OutputBuffer(), 'localhost', 22)
|
||||||
banner, header, err = sock.get_banner()
|
banner, header, err = sock.get_banner()
|
||||||
assert banner is None
|
assert banner is None
|
||||||
assert len(header) == 0
|
assert len(header) == 0
|
||||||
|
@ -3,6 +3,7 @@ import pytest
|
|||||||
|
|
||||||
from ssh_audit.auditconf import AuditConf
|
from ssh_audit.auditconf import AuditConf
|
||||||
from ssh_audit.fingerprint import Fingerprint
|
from ssh_audit.fingerprint import Fingerprint
|
||||||
|
from ssh_audit.outputbuffer import OutputBuffer
|
||||||
from ssh_audit.protocol import Protocol
|
from ssh_audit.protocol import Protocol
|
||||||
from ssh_audit.readbuf import ReadBuf
|
from ssh_audit.readbuf import ReadBuf
|
||||||
from ssh_audit.ssh1 import SSH1
|
from ssh_audit.ssh1 import SSH1
|
||||||
@ -15,6 +16,7 @@ from ssh_audit.writebuf import WriteBuf
|
|||||||
class TestSSH1:
|
class TestSSH1:
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def init(self, ssh_audit):
|
def init(self, ssh_audit):
|
||||||
|
self.OutputBuffer = OutputBuffer
|
||||||
self.protocol = Protocol
|
self.protocol = Protocol
|
||||||
self.ssh1 = SSH1
|
self.ssh1 = SSH1
|
||||||
self.PublicKeyMessage = SSH1_PublicKeyMessage
|
self.PublicKeyMessage = SSH1_PublicKeyMessage
|
||||||
@ -132,9 +134,11 @@ class TestSSH1:
|
|||||||
vsocket.rdata.append(b'SSH-1.5-OpenSSH_7.2 ssh-audit-test\r\n')
|
vsocket.rdata.append(b'SSH-1.5-OpenSSH_7.2 ssh-audit-test\r\n')
|
||||||
vsocket.rdata.append(self._create_ssh1_packet(w.write_flush()))
|
vsocket.rdata.append(self._create_ssh1_packet(w.write_flush()))
|
||||||
output_spy.begin()
|
output_spy.begin()
|
||||||
self.audit(self._conf())
|
out = self.OutputBuffer()
|
||||||
|
self.audit(out, self._conf())
|
||||||
|
out.write()
|
||||||
lines = output_spy.flush()
|
lines = output_spy.flush()
|
||||||
assert len(lines) == 13
|
assert len(lines) == 17
|
||||||
|
|
||||||
def test_ssh1_server_invalid_first_packet(self, output_spy, virtual_socket):
|
def test_ssh1_server_invalid_first_packet(self, output_spy, virtual_socket):
|
||||||
vsocket = virtual_socket
|
vsocket = virtual_socket
|
||||||
@ -144,10 +148,12 @@ class TestSSH1:
|
|||||||
vsocket.rdata.append(b'SSH-1.5-OpenSSH_7.2 ssh-audit-test\r\n')
|
vsocket.rdata.append(b'SSH-1.5-OpenSSH_7.2 ssh-audit-test\r\n')
|
||||||
vsocket.rdata.append(self._create_ssh1_packet(w.write_flush()))
|
vsocket.rdata.append(self._create_ssh1_packet(w.write_flush()))
|
||||||
output_spy.begin()
|
output_spy.begin()
|
||||||
ret = self.audit(self._conf())
|
out = self.OutputBuffer()
|
||||||
|
ret = self.audit(out, self._conf())
|
||||||
|
out.write()
|
||||||
assert ret != 0
|
assert ret != 0
|
||||||
lines = output_spy.flush()
|
lines = output_spy.flush()
|
||||||
assert len(lines) == 7
|
assert len(lines) == 10
|
||||||
assert 'unknown message' in lines[-1]
|
assert 'unknown message' in lines[-1]
|
||||||
|
|
||||||
def test_ssh1_server_invalid_checksum(self, output_spy, virtual_socket):
|
def test_ssh1_server_invalid_checksum(self, output_spy, virtual_socket):
|
||||||
@ -158,8 +164,10 @@ class TestSSH1:
|
|||||||
vsocket.rdata.append(b'SSH-1.5-OpenSSH_7.2 ssh-audit-test\r\n')
|
vsocket.rdata.append(b'SSH-1.5-OpenSSH_7.2 ssh-audit-test\r\n')
|
||||||
vsocket.rdata.append(self._create_ssh1_packet(w.write_flush(), False))
|
vsocket.rdata.append(self._create_ssh1_packet(w.write_flush(), False))
|
||||||
output_spy.begin()
|
output_spy.begin()
|
||||||
|
out = self.OutputBuffer()
|
||||||
with pytest.raises(SystemExit):
|
with pytest.raises(SystemExit):
|
||||||
self.audit(self._conf())
|
self.audit(out, self._conf())
|
||||||
|
out.write()
|
||||||
lines = output_spy.flush()
|
lines = output_spy.flush()
|
||||||
assert len(lines) == 1
|
assert len(lines) == 3
|
||||||
assert 'checksum' in lines[-1]
|
assert ('checksum' in lines[0]) or ('checksum' in lines[1]) or ('checksum' in lines[2])
|
||||||
|
@ -3,6 +3,7 @@ import struct
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from ssh_audit.auditconf import AuditConf
|
from ssh_audit.auditconf import AuditConf
|
||||||
|
from ssh_audit.outputbuffer import OutputBuffer
|
||||||
from ssh_audit.protocol import Protocol
|
from ssh_audit.protocol import Protocol
|
||||||
from ssh_audit.readbuf import ReadBuf
|
from ssh_audit.readbuf import ReadBuf
|
||||||
from ssh_audit.ssh2_kex import SSH2_Kex
|
from ssh_audit.ssh2_kex import SSH2_Kex
|
||||||
@ -15,6 +16,7 @@ from ssh_audit.writebuf import WriteBuf
|
|||||||
class TestSSH2:
|
class TestSSH2:
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def init(self, ssh_audit):
|
def init(self, ssh_audit):
|
||||||
|
self.OutputBuffer = OutputBuffer
|
||||||
self.protocol = Protocol
|
self.protocol = Protocol
|
||||||
self.ssh2_kex = SSH2_Kex
|
self.ssh2_kex = SSH2_Kex
|
||||||
self.ssh2_kexparty = SSH2_KexParty
|
self.ssh2_kexparty = SSH2_KexParty
|
||||||
@ -141,9 +143,11 @@ class TestSSH2:
|
|||||||
vsocket.rdata.append(b'SSH-2.0-OpenSSH_7.3 ssh-audit-test\r\n')
|
vsocket.rdata.append(b'SSH-2.0-OpenSSH_7.3 ssh-audit-test\r\n')
|
||||||
vsocket.rdata.append(self._create_ssh2_packet(w.write_flush()))
|
vsocket.rdata.append(self._create_ssh2_packet(w.write_flush()))
|
||||||
output_spy.begin()
|
output_spy.begin()
|
||||||
self.audit(self._conf())
|
out = self.OutputBuffer()
|
||||||
|
self.audit(out, self._conf())
|
||||||
|
out.write()
|
||||||
lines = output_spy.flush()
|
lines = output_spy.flush()
|
||||||
assert len(lines) == 67
|
assert len(lines) == 70
|
||||||
|
|
||||||
def test_ssh2_server_invalid_first_packet(self, output_spy, virtual_socket):
|
def test_ssh2_server_invalid_first_packet(self, output_spy, virtual_socket):
|
||||||
vsocket = virtual_socket
|
vsocket = virtual_socket
|
||||||
@ -152,8 +156,10 @@ class TestSSH2:
|
|||||||
vsocket.rdata.append(b'SSH-2.0-OpenSSH_7.3 ssh-audit-test\r\n')
|
vsocket.rdata.append(b'SSH-2.0-OpenSSH_7.3 ssh-audit-test\r\n')
|
||||||
vsocket.rdata.append(self._create_ssh2_packet(w.write_flush()))
|
vsocket.rdata.append(self._create_ssh2_packet(w.write_flush()))
|
||||||
output_spy.begin()
|
output_spy.begin()
|
||||||
ret = self.audit(self._conf())
|
out = self.OutputBuffer()
|
||||||
|
ret = self.audit(out, self._conf())
|
||||||
|
out.write()
|
||||||
assert ret != 0
|
assert ret != 0
|
||||||
lines = output_spy.flush()
|
lines = output_spy.flush()
|
||||||
assert len(lines) == 3
|
assert len(lines) == 5
|
||||||
assert 'unknown message' in lines[-1]
|
assert 'unknown message' in lines[-1]
|
||||||
|
30
tox.ini
30
tox.ini
@ -1,7 +1,7 @@
|
|||||||
[tox]
|
[tox]
|
||||||
envlist =
|
envlist =
|
||||||
py{py3}-{test,pylint,flake8,vulture}
|
py{py3}-{test,pylint,flake8,vulture}
|
||||||
py{35,36,37,38,39}-{test,mypy,pylint,flake8,vulture}
|
py{36,37,38,39}-{test,mypy,pylint,flake8,vulture}
|
||||||
cov
|
cov
|
||||||
skip_missing_interpreters = true
|
skip_missing_interpreters = true
|
||||||
|
|
||||||
@ -9,11 +9,11 @@ skip_missing_interpreters = true
|
|||||||
deps =
|
deps =
|
||||||
test: pytest<6.0
|
test: pytest<6.0
|
||||||
test,cov: {[testenv:cov]deps}
|
test,cov: {[testenv:cov]deps}
|
||||||
test,py{35,36,37,38,39}-{type,mypy}: colorama
|
test,py{36,37,38,39}-{type,mypy}: colorama
|
||||||
py{35,36,37,38,39}-{type,mypy}: {[testenv:mypy]deps}
|
py{36,37,38,39}-{type,mypy}: {[testenv:mypy]deps}
|
||||||
py{py3,35,36,37,38,39}-{lint,pylint},lint: {[testenv:pylint]deps}
|
py{py3,36,37,38,39}-{lint,pylint},lint: {[testenv:pylint]deps}
|
||||||
py{py3,35,36,37,38,39}-{lint,flake8},lint: {[testenv:flake8]deps}
|
py{py3,36,37,38,39}-{lint,flake8},lint: {[testenv:flake8]deps}
|
||||||
py{py3,35,36,37,38,39}-{lint,vulture},lint: {[testenv:vulture]deps}
|
py{py3,36,37,38,39}-{lint,vulture},lint: {[testenv:vulture]deps}
|
||||||
setenv =
|
setenv =
|
||||||
SSHAUDIT = {toxinidir}/src
|
SSHAUDIT = {toxinidir}/src
|
||||||
test: COVERAGE_FILE = {toxinidir}/.coverage.{envname}
|
test: COVERAGE_FILE = {toxinidir}/.coverage.{envname}
|
||||||
@ -25,13 +25,13 @@ commands =
|
|||||||
test: coverage combine
|
test: coverage combine
|
||||||
test: coverage report --show-missing
|
test: coverage report --show-missing
|
||||||
test: coverage html -d {toxinidir}/reports/html/coverage.{envname}
|
test: coverage html -d {toxinidir}/reports/html/coverage.{envname}
|
||||||
py{35,36,37,38,39}-{type,mypy}: {[testenv:mypy]commands}
|
py{36,37,38,39}-{type,mypy}: {[testenv:mypy]commands}
|
||||||
py{py3,35,36,37,38,39}-{lint,pylint},lint: {[testenv:pylint]commands}
|
py{py3,36,37,38,39}-{lint,pylint},lint: {[testenv:pylint]commands}
|
||||||
py{py3,35,36,37,38,39}-{lint,flake8},lint: {[testenv:flake8]commands}
|
py{py3,36,37,38,39}-{lint,flake8},lint: {[testenv:flake8]commands}
|
||||||
py{py3,35,36,37,38,39}-{lint,vulture},lint: {[testenv:vulture]commands}
|
py{py3,36,37,38,39}-{lint,vulture},lint: {[testenv:vulture]commands}
|
||||||
ignore_outcome =
|
#ignore_outcome =
|
||||||
type: true
|
# type: true
|
||||||
lint: true
|
# lint: true
|
||||||
|
|
||||||
[testenv:cov]
|
[testenv:cov]
|
||||||
deps =
|
deps =
|
||||||
@ -51,7 +51,7 @@ deps =
|
|||||||
lxml
|
lxml
|
||||||
mypy
|
mypy
|
||||||
commands =
|
commands =
|
||||||
-mypy \
|
mypy \
|
||||||
--strict \
|
--strict \
|
||||||
--show-error-context \
|
--show-error-context \
|
||||||
--html-report {env:MYPYHTML}.py3.{envname} \
|
--html-report {env:MYPYHTML}.py3.{envname} \
|
||||||
@ -62,7 +62,7 @@ deps =
|
|||||||
mccabe
|
mccabe
|
||||||
pylint
|
pylint
|
||||||
commands =
|
commands =
|
||||||
-pylint \
|
pylint \
|
||||||
--rcfile tox.ini \
|
--rcfile tox.ini \
|
||||||
--load-plugins=pylint.extensions.bad_builtin \
|
--load-plugins=pylint.extensions.bad_builtin \
|
||||||
--load-plugins=pylint.extensions.check_elif \
|
--load-plugins=pylint.extensions.check_elif \
|
||||||
|
130
update_windows_man_page.sh
Executable file
130
update_windows_man_page.sh
Executable 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
|
@ -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").
|
|
Reference in New Issue
Block a user