├── .detect-secrets.json ├── .dockleignore ├── .github └── dependabot.yml ├── .gitignore ├── .gitlab-ci.yml ├── .travis.yml ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── analyze_hosts.py ├── analyze_hosts.sh ├── display_results.py ├── fours.sh ├── requirements.txt ├── results_to_html.py ├── sonar-project.properties ├── templates └── results.html ├── test_ssl_handshake.sh └── tox.ini /.detect-secrets.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.2.0", 3 | "plugins_used": [ 4 | { 5 | "name": "ArtifactoryDetector" 6 | }, 7 | { 8 | "name": "AWSKeyDetector" 9 | }, 10 | { 11 | "name": "AzureStorageKeyDetector" 12 | }, 13 | { 14 | "name": "Base64HighEntropyString", 15 | "limit": 4.5 16 | }, 17 | { 18 | "name": "BasicAuthDetector" 19 | }, 20 | { 21 | "name": "CloudantDetector" 22 | }, 23 | { 24 | "name": "GitHubTokenDetector" 25 | }, 26 | { 27 | "name": "HexHighEntropyString", 28 | "limit": 3.0 29 | }, 30 | { 31 | "name": "IbmCloudIamDetector" 32 | }, 33 | { 34 | "name": "IbmCosHmacDetector" 35 | }, 36 | { 37 | "name": "JwtTokenDetector" 38 | }, 39 | { 40 | "name": "KeywordDetector", 41 | "keyword_exclude": "" 42 | }, 43 | { 44 | "name": "MailchimpDetector" 45 | }, 46 | { 47 | "name": "NpmDetector" 48 | }, 49 | { 50 | "name": "PrivateKeyDetector" 51 | }, 52 | { 53 | "name": "SendGridDetector" 54 | }, 55 | { 56 | "name": "SlackDetector" 57 | }, 58 | { 59 | "name": "SoftlayerDetector" 60 | }, 61 | { 62 | "name": "SquareOAuthDetector" 63 | }, 64 | { 65 | "name": "StripeDetector" 66 | }, 67 | { 68 | "name": "TwilioKeyDetector" 69 | } 70 | ], 71 | "filters_used": [ 72 | { 73 | "path": "detect_secrets.filters.allowlist.is_line_allowlisted" 74 | }, 75 | { 76 | "path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies", 77 | "min_level": 2 78 | }, 79 | { 80 | "path": "detect_secrets.filters.heuristic.is_indirect_reference" 81 | }, 82 | { 83 | "path": "detect_secrets.filters.heuristic.is_likely_id_string" 84 | }, 85 | { 86 | "path": "detect_secrets.filters.heuristic.is_lock_file" 87 | }, 88 | { 89 | "path": "detect_secrets.filters.heuristic.is_not_alphanumeric_string" 90 | }, 91 | { 92 | "path": "detect_secrets.filters.heuristic.is_potential_uuid" 93 | }, 94 | { 95 | "path": "detect_secrets.filters.heuristic.is_prefixed_with_dollar_sign" 96 | }, 97 | { 98 | "path": "detect_secrets.filters.heuristic.is_sequential_string" 99 | }, 100 | { 101 | "path": "detect_secrets.filters.heuristic.is_swagger_file" 102 | }, 103 | { 104 | "path": "detect_secrets.filters.heuristic.is_templated_secret" 105 | } 106 | ], 107 | "results": {}, 108 | "generated_at": "2022-10-18T16:35:47Z" 109 | } 110 | -------------------------------------------------------------------------------- /.dockleignore: -------------------------------------------------------------------------------- 1 | # allow root user, to properly use nmap 2 | CIS-DI-0001 -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | reviewers: 9 | - PeterMosmans 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | \#*# 2 | *.py[cod] 3 | \.* 4 | analyze_hosts.output 5 | doc8.ini 6 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Pipeline template 3 | # Includes several jobs 4 | include: 5 | - project: tooling/ci-templates 6 | ref: main 7 | file: 8 | - detect-secrets.gitlab-ci.yml 9 | - hadolint.gitlab-ci.yml 10 | - docker_build.gitlab-ci.yml 11 | - docker_push.gitlab-ci.yml 12 | - dive.gitlab-ci.yml 13 | - dockle.gitlab-ci.yml 14 | - dependency-check.gitlab-ci.yml 15 | - test.gitlab-ci.yml 16 | 17 | variables: 18 | IMAGE_NAMESPACE: gofwd 19 | IMAGE_REPOSITORY: ${IMAGE_NAMESPACE}/analyze_hosts 20 | IMAGE_TAG: ${CI_COMMIT_SHORT_SHA} 21 | IMAGE_TAR: ${IMAGE_TAG}.tgz 22 | # Don't mind the container being run as root 23 | DOCKLE_PARMS: "--ignore CIS-DI-0001" 24 | 25 | stages: 26 | # security checks that don't need build 27 | - sast 28 | # build the image 29 | - build 30 | # security checks after build 31 | - security_testing 32 | # test build 33 | - test 34 | # push build 35 | - push 36 | 37 | .sast: 38 | stage: sast 39 | 40 | .security_testing: 41 | stage: security_testing 42 | needs: 43 | - build 44 | variables: 45 | GIT_STRATEGY: none 46 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.8" 4 | install: 5 | - pip install -r requirements.txt 6 | # script: tox 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.15.0](https://git.go-forward.net/tooling/security-scripts/-/compare/1.14.0...1.15.0) (2023-01-12) 4 | 5 | ### Features 6 | 7 | - add fours (Snap Shot Security Scanner) 8 | ([5603e4d](https://git.go-forward.net/tooling/security-scripts/-/commit/5603e4de2d9f2265fb7eed2f7731bb02591a9229)) 9 | - add more alert strings for Nikto and nmap scripts 10 | ([ae0008c](https://git.go-forward.net/tooling/security-scripts/-/commit/ae0008cdd985b747feb4cc8971908647fdc347e6)) 11 | 12 | ### Bug Fixes 13 | 14 | - don't log anything for 'empty' port numbers 15 | ([d077f07](https://git.go-forward.net/tooling/security-scripts/-/commit/d077f07ecf3b9a36501fed91c872872895086912)) 16 | 17 | ## [1.14.0](https://git.go-forward.net/tooling/security-scripts/-/compare/1.13.0...1.14.0) (2022-10-27) 18 | 19 | ### Features 20 | 21 | - bump version number for consistency 22 | ([a356a21](https://git.go-forward.net/tooling/security-scripts/-/commit/a356a21034f2974316a0817751ad670ecb71c7bd)) 23 | - change default working directory to /workdir 24 | ([f430cc0](https://git.go-forward.net/tooling/security-scripts/-/commit/f430cc022d3fa8bb3f1bb0b06c14d1e733c5aaca)) 25 | - hardcode name of project/Docker image 26 | ([a999117](https://git.go-forward.net/tooling/security-scripts/-/commit/a9991170af7e5db3225efcd275b7326e0cf6b881)) 27 | - simplify testing stage 28 | ([7b2662d](https://git.go-forward.net/tooling/security-scripts/-/commit/7b2662d6ccd356d2a5282c97e50075d836c8ba60)) 29 | 30 | ### Bug Fixes 31 | 32 | - suppress dockle false positive 33 | ([2f0957c](https://git.go-forward.net/tooling/security-scripts/-/commit/2f0957c15344c992871e188cd148bd8662a81cfe)) 34 | 35 | ## [1.13.0](https://git.go-forward.net/tooling/security-scripts/-/compare/1.12.0...1.13.0) (2022-07-21) 36 | 37 | ### Features 38 | 39 | - add basic pipeline 40 | ([fff2bc0](https://git.go-forward.net/tooling/security-scripts/-/commit/fff2bc0f7023926fb8b97574dd2786ae33bc4045)) 41 | - add several automated security tests 42 | ([0469f4f](https://git.go-forward.net/tooling/security-scripts/-/commit/0469f4fd0b71124911e74e12ebcb772b4ce70ee3)) 43 | - add workaround for wappalyzer error message 44 | ([b815d88](https://git.go-forward.net/tooling/security-scripts/-/commit/b815d88b4e32dcdd371c16fa3f28bde1b941537c)) 45 | 46 | ### Bug Fixes 47 | 48 | - clean up Dockerfile linting issues 49 | ([05c6a53](https://git.go-forward.net/tooling/security-scripts/-/commit/05c6a53f5f4f3ffb1f3c517a2d807e4a24ad0adf)) 50 | - handle connection errors gracefully 51 | ([8fb96fe](https://git.go-forward.net/tooling/security-scripts/-/commit/8fb96fe7fdbd76c47d453547f2f9afaff5c60359)) 52 | 53 | ## [1.12.0](https://github.com/PeterMosmans/security-scripts/compare/1.11.0...1.12.0) (2022-01-11) 54 | 55 | ### Bug Fixes 56 | 57 | - remove unused inline pylint directives 58 | ([0e785bf](https://github.com/PeterMosmans/security-scripts/commit/0e785bff05e14cccce56e05e9e2fcda5b828e72e)) 59 | 60 | ## [1.11.0](https://github.com/PeterMosmans/security-scripts/compare/1.10.0...1.11.0) (2021-11-30) 61 | 62 | ### Features 63 | 64 | - enforce timeout for nikto 65 | ([d751eaa](https://github.com/PeterMosmans/security-scripts/commit/d751eaa1a215c0487c2ccdf5726f246a5dd39438)) 66 | - ensure that Python output is unbuffered 67 | ([83d7fdd](https://github.com/PeterMosmans/security-scripts/commit/83d7fdd75a8228a9f17a1e32a116af36fd431b8b)) 68 | - rewrite deprecated function names 69 | ([e4bfbec](https://github.com/PeterMosmans/security-scripts/commit/e4bfbec7b973684c9d054b0c26dc36559739e8d8)) 70 | 71 | ## [1.10.0](https://github.com/PeterMosmans/security-scripts/compare/1.9.0...1.10.0) (2021-05-27) 72 | 73 | ### Features 74 | 75 | - support file-based argugments 76 | ([e33a6eb](https://github.com/PeterMosmans/security-scripts/commit/e33a6eb9bd37f2384ae2891b85355e4c966fbd5f)) 77 | 78 | ## [1.9.0](https://github.com/PeterMosmans/security-scripts/compare/v1.8.0...v1.9.0) (2021-04-15) 79 | 80 | ### Features 81 | 82 | - add multiple options to separate HTTP checks 83 | ([c038d28](https://github.com/PeterMosmans/security-scripts/commit/c038d28ab1d8413957e2b543d3da4bf8b2d696e8)) 84 | - add support for testssl parameters 85 | ([df202c0](https://github.com/PeterMosmans/security-scripts/commit/df202c00670bd7429763ad90ddfc590c7b0ed80c)) 86 | - switch protocol when trying to analyze framework 87 | ([ed25f28](https://github.com/PeterMosmans/security-scripts/commit/ed25f286ee87d8a3cd1737d2379c958332064449)) 88 | 89 | ### Bug Fixes 90 | 91 | - change settings YAML syntax for tuning parameters 92 | ([beaea06](https://github.com/PeterMosmans/security-scripts/commit/beaea06fa1eed64fdecc933eead5811354fe8a31)) 93 | - ensure that testssl untrusted parameter is properly used 94 | ([02dc66d](https://github.com/PeterMosmans/security-scripts/commit/02dc66d47366d7740909f1a04008b7a703789751)) 95 | 96 | ## [1.8.0](https://github.com/PeterMosmans/security-scripts/compare/v1.7.0...v1.8.0) (2020-09-18) 97 | 98 | ### Features 99 | 100 | - add option to enforce SSL/TLS check 101 | ([b641f7a](https://github.com/PeterMosmans/security-scripts/commit/b641f7a78ab2d4579d016d8821ab3c6c3c6836c7)) 102 | 103 | ### Bug Fixes 104 | 105 | - ensure that a host contains a port key 106 | ([6083104](https://github.com/PeterMosmans/security-scripts/commit/6083104f28598f7ea71ccb557557c0e7bbebe0c9)) 107 | 108 | ## [1.7.0](https://github.com/PeterMosmans/security-scripts/compare/v1.6.0...v1.7.0) (2020-08-24) 109 | 110 | ### Features 111 | 112 | - generate alert when unexpected open port is found 113 | ([9cbc4a4](https://github.com/PeterMosmans/security-scripts/commit/9cbc4a458ece2a213d05cfd1ddd446c56ed1fb1c)) 114 | - **display_hosts:** add several new options 115 | ([6c7077e](https://github.com/PeterMosmans/security-scripts/commit/6c7077e9be6c0b7e6e9e35ef0c781b277d65bfb3)) 116 | - add display_results helper script 117 | ([6016cab](https://github.com/PeterMosmans/security-scripts/commit/6016cab79f6a6020929b6479dca10e6ad3e83206)) 118 | - improve displaying results, show unexpected open ports 119 | ([5f8a8af](https://github.com/PeterMosmans/security-scripts/commit/5f8a8aff01a86d66fb340f630f603e07a53d30ee)) 120 | 121 | ### Bug Fixes 122 | 123 | - ensure results are shown without specifying host 124 | ([0efe1c5](https://github.com/PeterMosmans/security-scripts/commit/0efe1c573c9a569b6ece19be9bdf2113c407ddee)) 125 | 126 | ## [1.6.0](https://github.com/PeterMosmans/security-scripts/compare/v1.5.0...v1.6.0) (2020-08-06) 127 | 128 | ### Features 129 | 130 | - add option to use exit code != 0 when alerts are detected 131 | ([ded66fa](https://github.com/PeterMosmans/security-scripts/commit/ded66fa26d8fc7141c709560383b505ce6d54ae8)) 132 | - support more Nikto parameters 133 | ([d5592cc](https://github.com/PeterMosmans/security-scripts/commit/d5592ccda17fcc2bcf4559245066e01d59755d88)) 134 | 135 | ### Bug Fixes 136 | 137 | - ensure that Control-C is properly handled 138 | ([8b7c2de](https://github.com/PeterMosmans/security-scripts/commit/8b7c2deeba1a2e5d30b6f4630319a0a2355134a4)) 139 | - ensure that settings file is not obligatory 140 | ([8698e9c](https://github.com/PeterMosmans/security-scripts/commit/8698e9c8ed50436455b28f64e5d7c95b503289ad)) 141 | - in debug mode, show options after all options have been parsed 142 | ([890054f](https://github.com/PeterMosmans/security-scripts/commit/890054f9ceaa89dca12bc98e307412db84ad424b)) 143 | 144 | ## [1.5.0](https://github.com/PeterMosmans/security-scripts/compare/v1.4.0...v1.5.0) (2020-08-06) 145 | 146 | ### Features 147 | 148 | - add support for YAML settings file 149 | ([b5d422a](https://github.com/PeterMosmans/security-scripts/commit/b5d422a623eb1cd594ab2676618294e79872821c)) 150 | - add YAML library requirement 151 | ([d6e3068](https://github.com/PeterMosmans/security-scripts/commit/d6e30688ee4bef8466f5a0d24931bcb5dcb4e8d3)) 152 | 153 | ## [1.4.0](https://github.com/PeterMosmans/security-scripts/compare/v1.3.0...v1.4.0) (2020-08-03) 154 | 155 | ### Features 156 | 157 | - add Dockerfile 158 | ([1deb82d](https://github.com/PeterMosmans/security-scripts/commit/1deb82db8021e2ee797e7c4b27d2a312019bfe0e)) 159 | 160 | ### Bug Fixes 161 | 162 | - change testssl.sh parameter 163 | ([1fc2f4d](https://github.com/PeterMosmans/security-scripts/commit/1fc2f4db9f01d50c8f43a6e2b38ec2aa6b6a5f8c)) 164 | 165 | ## [1.3.0](https://github.com/PeterMosmans/security-scripts/compare/v1.2.0...v1.3.0) (2020-06-24) 166 | 167 | ### Features 168 | 169 | - add support to keep or discard line endings from logfiles 170 | ([c87c07f](https://github.com/PeterMosmans/security-scripts/commit/c87c07f35603f2304e499d791bd5dbbaec93ed10)) 171 | - optimize WPscan by enforcing update and not showing banner 172 | ([b855bf4](https://github.com/PeterMosmans/security-scripts/commit/b855bf4a46f2936db2a638d4732c1a536d3eae2e)) 173 | 174 | ### Bug Fixes 175 | 176 | - ensure correct type is passed when parsing logs 177 | ([7c876f8](https://github.com/PeterMosmans/security-scripts/commit/7c876f8cba71a6e850bae4e40082e4a6cb8b39ea)) 178 | - ensure proper logging when not compacting strings 179 | ([ed2c84c](https://github.com/PeterMosmans/security-scripts/commit/ed2c84c62e734f3bd77cc417b74a4f40bd7845bc)) 180 | - ensure that nmap logfile gets properly concatenated into log 181 | ([8b5a633](https://github.com/PeterMosmans/security-scripts/commit/8b5a6331e1588f6d1bbd0798708687b622b3f44a)) 182 | - ensure universal similar line endings 183 | ([feb6ab0](https://github.com/PeterMosmans/security-scripts/commit/feb6ab0282f6962e271c41463ec7b090dc0b3b89)) 184 | 185 | ## [1.2.0](https://github.com/PeterMosmans/security-scripts/compare/v1.1.0...v1.2.0) (2020-05-29) 186 | 187 | ### Features 188 | 189 | - add more nmap alerts and info 190 | ([6371495](https://github.com/PeterMosmans/security-scripts/commit/637149586d13dc30e793a47100f67d38533e6fb2)) 191 | - add more nmap info strings 192 | ([7aa7045](https://github.com/PeterMosmans/security-scripts/commit/7aa704562585fba37b909f678e84630e161c9c47)) 193 | - remove more prepended characters before alerts / info 194 | ([cdd5cc9](https://github.com/PeterMosmans/security-scripts/commit/cdd5cc940a1fac056ec7b93f0e3b1528dc357cab)) 195 | - store obtained information in new info value 196 | ([81a6fbd](https://github.com/PeterMosmans/security-scripts/commit/81a6fbdd810549930dff235b342412693cd647bf)) 197 | 198 | ### Bug Fixes 199 | 200 | - move nmap alert strings to info 201 | ([3c2c281](https://github.com/PeterMosmans/security-scripts/commit/3c2c28135837d206802522b4c4e2d889ec7b007d)) 202 | 203 | ## [1.1.0](https://github.com/PeterMosmans/security-scripts/compare/v1.0.0...v1.1.0) (2020-05-28) 204 | 205 | ### Features 206 | 207 | - add initial version of showing obtained nmap plugin info 208 | ([91b039b](https://github.com/PeterMosmans/security-scripts/commit/91b039b831642947241d9332819581f2e0523f25)) 209 | - add more nmap alerts 210 | ([f45224e](https://github.com/PeterMosmans/security-scripts/commit/f45224eac7ac124867cf431460f175136ee99148)) 211 | - add testssl.sh alert 212 | ([e5536e6](https://github.com/PeterMosmans/security-scripts/commit/e5536e6725073813ffa77a1927cd8adf16e9152f)) 213 | - add testssl.sh alerts 214 | ([f3bf2e6](https://github.com/PeterMosmans/security-scripts/commit/f3bf2e69062ba34b0aa712d3705a56d5a2bb97d9)) 215 | - enforce nikto to run non-interactive 216 | ([0adf0b9](https://github.com/PeterMosmans/security-scripts/commit/0adf0b9d71db631f81c63e263539b959fa84566d)) 217 | - remove Python2 compatibility (simplify code) 218 | ([57e62cb](https://github.com/PeterMosmans/security-scripts/commit/57e62cb46317d06d7ac07c1092aa520669fdd893)) 219 | - store version string of tool being used 220 | ([7e8af96](https://github.com/PeterMosmans/security-scripts/commit/7e8af96363f7ac7169d467e8ea7de4505500485a)) 221 | - use version first, to enable better sorting 222 | ([3692207](https://github.com/PeterMosmans/security-scripts/commit/36922076b6b2738e0550ead84d7dc306c96604b4)) 223 | 224 | ### Bug Fixes 225 | 226 | - ensure no raw line endings are logged 227 | ([8c3d981](https://github.com/PeterMosmans/security-scripts/commit/8c3d9813b9761b3dea6454bbc91101e0e1bae005)) 228 | - ensure that line endings are kept when adding logs 229 | ([f79dbe6](https://github.com/PeterMosmans/security-scripts/commit/f79dbe63739fc6a18b369ce973f7cdf3d2d0a4f5)) 230 | - ensure that logfile strings are properly read 231 | ([10e5e4f](https://github.com/PeterMosmans/security-scripts/commit/10e5e4ff796ecf8739292d21f879e43c82c459b0)) 232 | - ensure that nmap command line isn't flagged as alert 233 | ([a093cad](https://github.com/PeterMosmans/security-scripts/commit/a093cad1afaf93f183b0cbdeb490f4bc341e3d6b)) 234 | - ensure that wpscan ignores any server-supplied redirects 235 | ([cc19dc3](https://github.com/PeterMosmans/security-scripts/commit/cc19dc397e75818f323a63e0d294065e8f2f4f40)) 236 | - properly read and append existing logfiles 237 | ([aae922a](https://github.com/PeterMosmans/security-scripts/commit/aae922a6104505a9ec1d7b1ca2f7354e0d1f9d6e)) 238 | - remove obsolete inheritance from object 239 | ([e231d27](https://github.com/PeterMosmans/security-scripts/commit/e231d27a946495f775b76b8cab08b7142ee515b5)) 240 | 241 | ## [1.0.0](https://github.com/PeterMosmans/security-scripts/compare/v0.44.2...v1.0.0) (2020-05-28) 242 | 243 | ### Features 244 | 245 | - enable JSON output 246 | ([018d1a9](https://github.com/PeterMosmans/security-scripts/commit/018d1a998f34584a56d884a6f12ff5b15025c80b)) 247 | - filter out unnecessary characters in alert string 248 | ([49b3fea](https://github.com/PeterMosmans/security-scripts/commit/49b3feafc970fbef6c1a81b0854343046329eb84)) 249 | - log alert per port instead of generic lines 250 | ([3ea2241](https://github.com/PeterMosmans/security-scripts/commit/3ea22411e8eacac886c6f8d1fd4eb8f625e233b7)) 251 | - log port number with alert line 252 | ([6c180b6](https://github.com/PeterMosmans/security-scripts/commit/6c180b6d0bfd1455fca73b368be8719563e8f60a)) 253 | - parse port number to add with nmap alert 254 | ([9a1daef](https://github.com/PeterMosmans/security-scripts/commit/9a1daefd62aff6cb4d4beb731e58854620320b01)) 255 | - record all ciphers when performing testssl.sh 256 | ([19d05f4](https://github.com/PeterMosmans/security-scripts/commit/19d05f4feda1c52c55d4f0e24a02abd7f951b910)) 257 | - reduce default maximum scan time from 20 to to 10 minutes 258 | ([fb2c73e](https://github.com/PeterMosmans/security-scripts/commit/fb2c73e6ff5432fb1c00c971a21f03fbb5ceb25b)) 259 | - remove whitespace and superfluous line endings in alerts 260 | ([264ccb4](https://github.com/PeterMosmans/security-scripts/commit/264ccb45cc43cdafc256045639a4680787693a1c)) 261 | 262 | ### Bug Fixes 263 | 264 | - properly split lines in logfiles and tool outputs 265 | ([02c490e](https://github.com/PeterMosmans/security-scripts/commit/02c490e5ac86597f4cb0fc7ee956f296135ee8e1)) 266 | - use format strings and properly show port number 267 | ([e832c6e](https://github.com/PeterMosmans/security-scripts/commit/e832c6e8db2552d07b3459132687ba223900e73d)) 268 | 269 | ### [0.44.2](https://github.com/PeterMosmans/security-scripts/compare/v0.44.1...v0.44.2) (2020-05-25) 270 | 271 | ### Bug Fixes 272 | 273 | - make process handler Python3 proof 274 | ([638bc6e](https://github.com/PeterMosmans/security-scripts/commit/638bc6e3ebd5edc5fdd597708e34493c4f76f6ad)) 275 | 276 | ### 0.44.1 (2020-03-04) 277 | 278 | ### Bug Fixes 279 | 280 | - allow program to continue with --no-portscan and without nmap 281 | ([9e1eaed](https://github.com/PeterMosmans/security-scripts/commit/9e1eaedac73c4814292156642ceda40c2f9bf7f8)) 282 | - respect --dry-run when performing --check-redirect 283 | ([294d364](https://github.com/PeterMosmans/security-scripts/commit/294d364604031b9feba63909d24101115afc29a3)) 284 | - setting umask only when necessary 285 | ([7fbbddd](https://github.com/PeterMosmans/security-scripts/commit/7fbbdddbd0a4232bfcb0e1981a56a75ffbcdc5ef)) 286 | - still use nmap as tool if --no-portscan is specified 287 | ([85ce908](https://github.com/PeterMosmans/security-scripts/commit/85ce908e9c3459b2d828cd13efb62f62d32752a8)) 288 | - typo 289 | ([9fa9b91](https://github.com/PeterMosmans/security-scripts/commit/9fa9b916575b6b6651c02ddd2c31285acf59511f)) 290 | - use specified port numbers even when nmap is not present 291 | ([bf83792](https://github.com/PeterMosmans/security-scripts/commit/bf83792c8db1fabba10491ee32a3e990a4896554)) 292 | - use specified ports when not performing portscan 293 | ([cda920c](https://github.com/PeterMosmans/security-scripts/commit/cda920c59e4144dcba91ad49594217d124fe3dec)) 294 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | # Use a base image to build (and download) the tools on 3 | FROM python:slim-buster as build 4 | 5 | LABEL maintainer="support@go-forward.net" \ 6 | vendor="Go Forward" \ 7 | org.label-schema.name="analyze_hosts" \ 8 | org.label-schema.description="A tool around several security tools to simplify scanning of hosts for network vulnerabilities" \ 9 | org.label-schema.url="https://github.com/PeterMosmans/security-scripts" \ 10 | org.label-schema.vcs-url="https://github.com/PeterMosmans/security-scripts" \ 11 | org.label-schema.maintainer="support@go-forward.net" 12 | 13 | WORKDIR / 14 | 15 | # Create virtual environment 16 | RUN python3 -m venv /opt/venv 17 | ENV PATH="/opt/venv/bin:$PATH" 18 | 19 | # Install necessary binaries including dependencies 20 | # hadolint ignore=DL3008 21 | RUN apt-get update && apt-get install -y --no-install-recommends \ 22 | curl \ 23 | gcc \ 24 | git \ 25 | libc6-dev \ 26 | unzip \ 27 | && apt-get clean \ 28 | && rm -rf /var/lib/apt/lists/* 29 | 30 | # Install packages as specified in the requirements.txt file 31 | COPY requirements.txt . 32 | RUN pip3 install -r requirements.txt --no-cache-dir 33 | 34 | # Clone nikto.pl 35 | RUN git clone --depth=1 https://github.com/sullo/nikto /tmp/nikto && \ 36 | rm -rf /tmp/nikto/program/.git && \ 37 | mv /tmp/nikto/program /usr/lib/nikto 38 | 39 | # Clone testssl.sh 40 | RUN git clone --depth=1 https://github.com/drwetter/testssl.sh /tmp/testssl && \ 41 | mkdir /usr/lib/testssl && \ 42 | mv /tmp/testssl/bin/openssl.Linux.x86_64 /usr/lib/testssl/openssl && \ 43 | chmod ugo+x /usr/lib/testssl/openssl && \ 44 | mv /tmp/testssl/etc/ /usr/lib/testssl/etc/ && \ 45 | mv /tmp/testssl/testssl.sh /usr/lib/testssl/testssl.sh && \ 46 | chmod ugo+x /usr/lib/testssl/testssl.sh 47 | 48 | FROM python:slim-buster as release 49 | COPY --from=build /opt/venv /opt/venv 50 | COPY --from=build /usr/lib/nikto/ /usr/lib/nikto/ 51 | COPY --from=build /usr/lib/testssl/ /usr/lib/testssl/ 52 | COPY analyze_hosts.py /usr/local/bin/analyze_hosts.py 53 | COPY results_to_html.py /usr/local/bin/results_to_html.py 54 | COPY templates/results.html /usr/share/templates/results.html 55 | COPY fours.sh /usr/local/bin/fours.sh 56 | RUN ln -s /usr/lib/nikto/nikto.pl /usr/local/bin/nikto.pl && \ 57 | ln -s /usr/lib/nikto/nikto.pl /usr/local/bin/nikto && \ 58 | ln -s /usr/local/bin/analyze_hosts.py /usr/local/bin/analyze_hosts && \ 59 | ln -s /usr/local/bin/results_to_html.py /usr/local/bin/results_to_html && \ 60 | ln -s /usr/lib/testssl/testssl.sh /usr/local/bin/testssl.sh 61 | 62 | # Install necessary binaries 63 | # hadolint ignore=DL3008 64 | RUN apt-get update && \ 65 | apt-get install -y --no-install-recommends \ 66 | bsdmainutils \ 67 | curl \ 68 | dnsutils \ 69 | git \ 70 | libnet-ssleay-perl \ 71 | make \ 72 | nmap \ 73 | procps \ 74 | && apt-get clean \ 75 | && rm -rf /var/lib/apt/lists/* 76 | 77 | # Esnsure that Python output is not buffered 78 | ENV PATH="/opt/venv/bin:$PATH" \ 79 | LC_ALL=C.UTF-8 \ 80 | PYTHONUNBUFFERED=1 81 | 82 | # hadolint ignore=DL3002 83 | USER root 84 | WORKDIR /workdir 85 | ENTRYPOINT ["/usr/local/bin/analyze_hosts"] 86 | CMD ["--help"] 87 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | {one line to give the program's name and a brief idea of what it does.} 635 | Copyright (C) {year} {name of author} 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | {project} Copyright (C) {year} {fullname} 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | 676 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Generic Makefile for Docker images 2 | 3 | # Copyright (C) 2018-2022 Peter Mosmans [Go Forward] 4 | # SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | # TAG != git tag|tail -1 7 | TAG = "latest" 8 | #NAME != basename $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) 9 | NAME = analyze_hosts 10 | DOCKER_IMG := gofwd/$(NAME):$(TAG) 11 | 12 | 13 | # Recipes that aren't filenames: This ensures that they always will be executed 14 | .PHONY: image test 15 | 16 | # Build image 17 | image: 18 | @echo "Building $(DOCKER_IMG)..." && \ 19 | DOCKER_BUILDKIT=1 docker build . -t $(DOCKER_IMG) 20 | 21 | test: 22 | analyze_hosts.py --version && results_to_html --help 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # security-scripts 2 | 3 | A collection of security related Python and Bash shell scripts, mainly revolving 4 | around testing hosts for security vulnerabilities. For the shell scripts no 5 | fancy programming framework is required, all that is needed is a Bash shell. 6 | 7 | Note that it is highly recommended to use `analyze_hosts.py` as it is the most 8 | recent version. No new features will be added to the Bash version 9 | `analyze_hosts.sh`. 10 | 11 | `analyze_hosts` is also available as Docker image, including the open source 12 | scanners `droopescan`, `nmap`, `Nikto` and `testssl.sh`. Build it yourself using 13 | the supplied Dockerfile, or grab the image from Docker Hub 14 | 15 | ``` 16 | docker run --rm gofwd/analyze_hosts 17 | ``` 18 | 19 | You can also run the included tools that way; just override the entrypoint. As 20 | an example, run `testssl.sh`: 21 | 22 | ``` 23 | docker run --rm --entrypoint 'testssl.sh' gofwd/analyze_hosts 24 | ``` 25 | 26 | ## analyze_hosts.py 27 | 28 | A simple wrapper script around several open source security tools to simplify 29 | scanning of hosts for network vulnerabilities. The script lets you analyze one 30 | or several hosts for common misconfiguration vulnerabilities and weaknesses. 31 | 32 | The main objectives for the script is to make it as easy as possible to perform 33 | generic security tests, without any heavy prerequisites, make the output as 34 | informative as possible, and use open source tools. It can easily be used as 35 | scheduled task, or be implemented in Continuous Integration environments. 36 | 37 | The only requirements are `nmap` and `Python3`. 38 | 39 | As the scan output can be written to a JSON file it can be used to generate 40 | deltas (differences) between scans, or to use the output for further inspection. 41 | 42 | ### Installation 43 | 44 | Note that you can also run `analyze_hosts` straight from a Docker image: 45 | 46 | ``` 47 | docker run --rm gofwd/analyze_hosts 48 | ``` 49 | 50 | One-time installation steps without virtualenv (all required Python libraries 51 | are specified in the `requirements.txt` file): 52 | 53 | ``` 54 | git clone https://github.com/PeterMosmans/security-scripts && \ 55 | cd security-script && \ 56 | pip3 install -r requirements.txt 57 | ``` 58 | 59 | ### Usage 60 | 61 | ``` 62 | usage: analyze_hosts.py [-h] [--version] [--dry-run] [-i INPUTFILE] [-o OUTPUT_FILE] [--compact] [--queuefile QUEUEFILE] [--resume] [--settings SETTINGS] 63 | [--exit-code] [--force] [--debug] [-v] [-q] [--allports] [-n] [-p PORT] [--up] [--udp] [--framework] [--http] [--compression] 64 | [--headers] [--trace] [--redirect] [--force-ssl] [--json JSON] [--ssl] [--nikto] [--sslcert] [-w] [--proxy PROXY] 65 | [--timeout TIMEOUT] [--threads THREADS] [--user-agent USER_AGENT] [--password PASSWORD] [--username USERNAME] [--maxtime MAXTIME] 66 | [target] 67 | 68 | analyze_hosts - scans one or more hosts for security misconfigurations 69 | 70 | Copyright (C) 2015-2022 Peter Mosmans [Go Forward] 71 | This program is free software: you can redistribute it and/or modify 72 | it under the terms of the GNU General Public License as published by 73 | the Free Software Foundation, either version 3 of the License, or 74 | (at your option) any later version. 75 | 76 | positional arguments: 77 | target [TARGET] can be a single (IP) address, an IP range, or multiple comma-separated addressess 78 | 79 | optional arguments: 80 | -h, --help show this help message and exit 81 | --version Show version and exit 82 | --dry-run Only show commands, don't actually do anything 83 | -i INPUTFILE, --inputfile INPUTFILE 84 | A file containing targets, one per line 85 | -o OUTPUT_FILE, --output-file OUTPUT_FILE 86 | output file containing all scanresults (default analyze_hosts.output) 87 | --compact Only log raw logfiles and alerts to file 88 | --queuefile QUEUEFILE 89 | the queuefile 90 | --resume Resume working on the queue 91 | --settings SETTINGS Name of settings file to use (default analyze_hosts.yml) 92 | --exit-code When supplied, return exit code 1 when alerts are discovered 93 | --force Ignore / overwrite the queuefile 94 | --debug Show debug information 95 | -v, --verbose Be more verbose 96 | -q, --quiet Do not show scan outputs on the console 97 | --allports Run a full-blown nmap scan on all ports 98 | -n, --no-portscan Do NOT run a nmap portscan 99 | -p PORT, --port PORT Specific port(s) to scan 100 | --up Assume host is up (do not rely on ping probe) 101 | --udp Check for open UDP ports as well 102 | --framework Analyze the website and run webscans 103 | --http Check for various HTTP vulnerabilities (compression, headers, trace) 104 | --compression Check for webserver compression 105 | --headers Check for various HTTP headers 106 | --trace Check webserver for HTTP TRACE method 107 | --redirect Check for insecure redirect 108 | --force-ssl Enforce SSL/TLS check on all open ports 109 | --json JSON Save output in JSON file 110 | --ssl Check for various SSL/TLS vulnerabilities 111 | --nikto Run a nikto scan 112 | --sslcert Download SSL certificate 113 | -w, --whois Perform a whois lookup 114 | --proxy PROXY Use proxy server (host:port) 115 | --timeout TIMEOUT Timeout for requests in seconds (default 10) 116 | --threads THREADS Maximum number of threads (default 5) 117 | --user-agent USER_AGENT 118 | Custom User-Agent to use (default analyze_hosts) 119 | --password PASSWORD Password for HTTP basic host authentication 120 | --username USERNAME Username for HTTP basic host authentication 121 | --maxtime MAXTIME Timeout for scans in seconds (default 600) 122 | ``` 123 | 124 | The script `analyze_hosts` automatically execute other scans (based on their 125 | fingerprint or open ports): 126 | 127 | ``` 128 | droopescan 129 | nikto 130 | testssl.sh 131 | WPscan 132 | ``` 133 | 134 | You can use the following environment variables (all uppercase) to specify the 135 | tools if they cannot be found in the standard paths: 136 | 137 | CURL, DROOPESCAN, NIKTO, OPENSSL, TESTSSL, WPSCAN 138 | 139 | ### Suppressing false positives 140 | 141 | A settings file can be used (`--settings`) to configure or tweak scan parameters 142 | per host / port combination. This allows you to suppress false positives in scan 143 | results. Currently the Nikto `Plugins`, `Tuning` and `output` parameters are 144 | supported, as well as a list of allowed / expected open ports, and testssl 145 | parameters: 146 | 147 | Example settings file: 148 | 149 | ``` 150 | targets: 151 | 127.0.0.1: 152 | allowed_ports: [22, 80, 443] 153 | ports: 154 | - port: 80 155 | nikto_plugins: "@@ALL" 156 | nikto_tuning: "x1" 157 | nikto_output: "report.html" 158 | - port: 443 159 | testssl_untrusted: true 160 | testssl: 161 | - "--ccs-injection" 162 | - "--ticketbleed" 163 | - "--robot" 164 | 165 | ``` 166 | 167 | This will supply the `-Plugins '@@ALL' -Tuning 'x1' -output 'report.html' 168 | parameters to Nikto, when port 80 is scanned. 169 | 170 | Furthermore, it will not generate an alert when an open port other than port 22, 171 | 80 or 443 is found. By default, an alert will be generated if an open port other 172 | than 80 or 443 is found. 173 | 174 | There will no alert be generated if the SSL/TLS endpoint on port 443 contains an 175 | untrusted (self-signed) certificate. And instead of all default tests, only 176 | three SSL/TLS tests will be performed. 177 | 178 | ### JSON format 179 | 180 | ``` 181 | { 182 | "arguments": { 183 | "target": "1.2.3.1/30", 184 | "version": false, 185 | "dry_run": false, 186 | "inputfile": "0frnfb4e", 187 | "output_file": "output.txt, 188 | "compact": true, 189 | "queuefile": "analyze_hosts.queue", 190 | "resume": false, 191 | "force": false, 192 | "debug": false, 193 | "verbose": false, 194 | "quiet": false, 195 | "allports": false, 196 | "no_portscan": false, 197 | "port": null, 198 | "up": false, 199 | "udp": false, 200 | "framework": false, 201 | "http": true, 202 | "json": "results.json", 203 | "ssl": true, 204 | "nikto": true, 205 | "sslcert": false, 206 | "trace": false, 207 | "whois": false, 208 | "proxy": null, 209 | "timeout": true, 210 | "threads": 5, 211 | "user_agent": "analyze_hosts", 212 | "password": null, 213 | "username": null, 214 | "maxtime": 1200, 215 | "testssl.sh": true, 216 | "curl": false, 217 | "wpscan": true, 218 | "droopescan": true, 219 | "nmap": true, 220 | "nmap_arguments": "-sV --open -sS --script=banner,dns-nsid,dns-recursion,http-cisco-anyconnect,http-php-version,http-title,http-trace,ntp-info,ntp-monlist,nbstat,rdp-enum-encryption,rpcinfo,sip-methods,smb-os-discovery,smb-security-mode,smtp-open-relay,ssh2-enum-algos,vnc-info,xmlrpc-methods,xmpp-info" 221 | }, 222 | "date_start": "2020-05-26 31:33:06" 223 | "results": { 224 | "1.2.3.1": { 225 | "ports": [ 226 | 53 227 | ] 228 | }, 229 | "1.2.3.2": { 230 | "ports": [] 231 | }, 232 | "1.2.3.3": { 233 | "ports": [ 234 | 80, 235 | 443 236 | ], 237 | "alerts": [ 238 | ":443 LUCKY13 (CVE-2013-0169), experimental potentially VULNERABLE, uses cipher block chaining (CBC) ciphers with TLS. Check patches" 239 | ] 240 | }, 241 | "1.2.3.4": { 242 | "ports": [ 243 | 80, 244 | 443 245 | ], 246 | "alerts": [ 247 | ":443 + OSVDB-3092: /download/: This might be interesting...", 248 | ":443 + OSVDB-3092: /status/: This might be interesting...", 249 | ":443 + OSVDB-4231: /DHrPp.xml: Coccoon from Apache-XML project reveals file system path in error messages.", 250 | ":443 + OSVDB-3092: /upgrade.php: upgrade.php was found." 251 | ] 252 | } 253 | }, 254 | "date_finish": "2020-05-26 31:33:07" 255 | } 256 | ``` 257 | 258 | ## display_results.py 259 | 260 | A little helper script that formats the scan results nicely, so that scan 261 | results can easily be reviewed. 262 | 263 | ``` 264 | usage: display_results.py [-h] [--info] [--version] [inputfile] 265 | 266 | display_results version 0.0.1 - displays scan results nicely 267 | 268 | positional arguments: 269 | inputfile A JSON file containing scan results 270 | 271 | optional arguments: 272 | -h, --help show this help message and exit 273 | --info Show also informational items 274 | --version Show version and exit 275 | 276 | ``` 277 | 278 | ## analyze-hosts.sh 279 | 280 | A simple wrapper script around several open source security tools to simplify 281 | scanning of hosts for network vulnerabilities. The script lets you analyze one 282 | or several hosts for common misconfiguration vulnerabilities and weaknesses. The 283 | main objectives for the script is to make it as easy as possible to perform 284 | generic security tests, without any heavy prerequisites, make the output as 285 | informative as possible, and use open source tools.... 286 | 287 | - [cipherscan](https://github.com/jvehent/cipherscan) 288 | - curl 289 | - nmap 290 | - [openssl-1.0.2-chacha](https://github.com/PeterMosmans/openssl/tree/1.0.2-chacha/) 291 | - [whatweb](https://github.com/urbanadventurer/WhatWeb) 292 | 293 | * whois 294 | 295 | ### Examples 296 | 297 | #### SSL certificates 298 | 299 | ``` 300 | ./analyze_hosts.sh --sslcert www.google.com 301 | ``` 302 | 303 | Shows details of a certificate, like the issuer and subject. It warns when 304 | certificate is expired or when the certificate is a certificate authority. 305 | 306 | Example output: 307 | 308 | ``` 309 | trying to retrieve SSL x.509 certificate on www.google.com:443... received 310 | issuer= 311 | countryName = US 312 | organizationName = Google Inc 313 | commonName = Google Internet Authority G2 314 | subject= 315 | countryName = US 316 | stateOrProvinceName = California 317 | localityName = Mountain View 318 | organizationName = Google Inc 319 | commonName = www.google.com 320 | OK: certificate is valid between 16-07-2014 and 14-10-2014 321 | ``` 322 | 323 | #### SSL/TLS ciphers 324 | 325 | ``` 326 | ./analyze_hosts.sh --ssl --sslports 443 -v www.microsoft.com 327 | ``` 328 | 329 | Checks which ciphers are allowed. It warns when insecure ciphers are being used. 330 | By default the ports 443, 465, 993, 995 and 3389 and are checked. You can 331 | specify the ports by using --sslports The -v flag outputs all results, regardles 332 | of the message type. 333 | 334 | Example output: 335 | 336 | ``` 337 | prio ciphersuite protocols pfs_keysize 338 | 1 RC4-MD5 SSLv3,TLSv1 339 | 2 RC4-SHA SSLv3,TLSv1 340 | 3 DES-CBC3-SHA SSLv3,TLSv1 341 | 4 AES256-SHA TLSv1 342 | 5 AES128-SHA TLSv1 343 | 344 | Certificate: UNTRUSTED, 2048 bit, sha1WithRSAEncryption signature 345 | trying to retrieve SSL x.509 certificate on www.microsoft.com:443... received 346 | issuer= 347 | domainComponent = com 348 | domainComponent = microsoft 349 | domainComponent = corp 350 | domainComponent = redmond 351 | commonName = MSIT Machine Auth CA 2 352 | subject= 353 | countryName = US 354 | stateOrProvinceName = WA 355 | localityName = Redmond 356 | organizationName = Microsoft Corporation 357 | organizationalUnitName = MSCOM 358 | commonName = www.microsoft.com 359 | OK: certificate is valid between 12-01-2013 and 12-01-2015 360 | 361 | performing nmap sslscan on www.microsoft.com ports 443... 362 | Nmap scan report for www.microsoft.com (134.170.184.133) 363 | Host is up (0.15s latency). 364 | PORT STATE SERVICE 365 | 443/tcp open https 366 | | ssl-enum-ciphers: 367 | | SSLv3: 368 | | ciphers: 369 | | TLS_RSA_WITH_3DES_EDE_CBC_SHA - strong 370 | | TLS_RSA_WITH_RC4_128_MD5 - strong 371 | | TLS_RSA_WITH_RC4_128_SHA - strong 372 | | compressors: 373 | | NULL 374 | | TLSv1.0: 375 | | ciphers: 376 | | TLS_RSA_WITH_3DES_EDE_CBC_SHA - strong 377 | | TLS_RSA_WITH_AES_128_CBC_SHA - strong 378 | | TLS_RSA_WITH_AES_256_CBC_SHA - strong 379 | | TLS_RSA_WITH_RC4_128_MD5 - strong 380 | | TLS_RSA_WITH_RC4_128_SHA - strong 381 | | compressors: 382 | | NULL 383 | |_ least strength: strong 384 | ``` 385 | 386 | ### usage 387 | 388 | ``` 389 | ./analyze_hosts.sh [OPTION]... [HOST] 390 | 391 | Scanning options: 392 | -a, --all perform all basic scans 393 | --max perform all advanced scans (more thorough) 394 | -b, --basic perform basic scans (fingerprint, ssl, trace) 395 | results of HOST matches regexp FILTER 396 | --dns test for recursive query and version string 397 | -f perform web fingerprinting (all webports) 398 | --fingerprint perform all web fingerprinting methods 399 | -h, --header show webserver headers (all webports) 400 | -n, --nikto nikto webscan (all webports) 401 | -p nmap portscan (top 1000 TCP ports) 402 | --ports nmap portscan (all ports, TCP and UDP) 403 | --redirect test for open secure redirect 404 | -s check SSL configuration 405 | --ssl perform all SSL configuration checks 406 | --sslcert show details of SSL certificate 407 | --timeout=SECONDS change timeout for tools (default 60) 408 | --ssh perform SSH configuration checks 409 | -t check webserver for HTTP TRACE method 410 | --trace perform all HTTP TRACE method checks 411 | -w, --whois perform WHOIS lookup for (hostname and) IP address 412 | -W confirm WHOIS results before continuing scan 413 | --filter=FILTER only proceed with scan of HOST if WHOIS 414 | --wordlist=filename scan webserver for existence of files in filename 415 | 416 | Port selection (comma separated list): 417 | --webports=PORTS use PORTS for web scans (default 80,443,8080) 418 | --sslports=PORTS use PORTS for ssl scans (default 443,465,993,995,3389) 419 | 420 | Logging and input file: 421 | -d, --directory=DIR location of temporary files (default /tmp) 422 | -i, --inputfile=FILE use a file containing hostnames 423 | -l, --log log each scan in a separate logfile 424 | --nocolor don't use fancy colors in screen output 425 | -o, --output=FILE concatenate all OK and WARNING messages into FILE 426 | -q, --quiet quiet 427 | -v, --verbose show server responses 428 | 429 | Default programs: 430 | --cipherscan=FILE location of cipherscan (default cipherscan) 431 | --openssl=FILE location of openssl (default openssl) 432 | 433 | -u update this script (if it's a cloned repository) 434 | --update force update (overwrite all local modifications) 435 | --version print version information and exit 436 | 437 | BLUE: INFO, status messages 438 | GREEN: OK, secure settings 439 | RED: WARNING, possible vulnerabilities 440 | 441 | [HOST] can be a single (IP) address, an IP range, eg. 127.0.0.1-255 442 | or multiple comma-separated addressess 443 | ``` 444 | 445 | ### history 446 | 447 | - since 0.88: preliminary support for starttls xmpp 448 | 449 | ## test_ssl_handhake.sh 450 | 451 | A script to test TLS/SSL handshakes with. Several bugtests are included: 452 | 453 | - 128 cipherlimit when using tls1_2 protocol 454 | - aRSA cipher order 455 | - version intolerant server 456 | 457 | \$ ./test_ssl_handshake.sh 458 | 459 | ``` 460 | (c) 2014 Peter Mosmans [Go Forward] 461 | Licensed under the GPL 3.0 462 | 463 | tests SSL/TLS handshakes (for known bugs) 464 | 465 | usage: ./test_ssl_handshake.sh target[:port] [start] 466 | 467 | [start] number of ciphers to start with (default 125) 468 | --ciphers=FILE a file containing a list which ciphers to use 469 | --cipherstring=CIPHERSTRING 470 | cipherstring (default ) 471 | -f | --force continue even though the error has been detected 472 | --iterate iterate through all the ciphers instead of adding 473 | --openssl=FILE location of openssl (default ) 474 | -v | --verbose be more verbose, please 475 | 476 | tests: 477 | --128 test for 128 cipherlimit 478 | --intolerant test for version intolerant server 479 | --rsa test for RSA order sensitivity 480 | 481 | by default, all tests will be performed 482 | ``` 483 | -------------------------------------------------------------------------------- /analyze_hosts.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """analyze_hosts - scans one or more hosts for security misconfigurations 5 | 6 | Copyright (C) 2015-2023 Peter Mosmans [Go Forward] 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | """ 12 | 13 | import argparse 14 | import io 15 | import datetime 16 | import json 17 | import logging 18 | import os 19 | import queue 20 | import re 21 | import signal 22 | import ssl 23 | import subprocess 24 | import sys 25 | import tempfile 26 | import threading 27 | import time 28 | import warnings 29 | 30 | 31 | try: 32 | import nmap 33 | except ImportError: 34 | print( 35 | "[-] Please install python-nmap, e.g. pip3 install python-nmap", file=sys.stderr 36 | ) 37 | sys.exit(-1) 38 | try: 39 | import requests 40 | import yaml 41 | import Wappalyzer 42 | except ImportError as exception: 43 | print( 44 | f"[-] Please install required modules, e.g. pip3 install -r requirements.txt: {exception}", 45 | file=sys.stderr, 46 | ) 47 | sys.stderr.flush() 48 | 49 | # Workaround for https://github.com/chorsley/python-Wappalyzer/issues/40 50 | warnings.simplefilter("ignore") 51 | 52 | NAME = "analyze_hosts" 53 | __version__ = "1.15.0" 54 | ALLPORTS = [ 55 | (22, "ssh"), 56 | (25, "smtp"), 57 | (80, "http"), 58 | (443, "https"), 59 | (465, "smtps"), 60 | (993, "imaps"), 61 | (995, "pop3s"), 62 | (8080, "http-proxy"), 63 | ] 64 | SSL_PORTS = [25, 443, 465, 993, 995] 65 | # Default list of allowed open ports, different ports will generate an alert 66 | ALLOWED_OPEN_PORTS = [ 67 | 80, 68 | 443, 69 | ] 70 | NIKTO_ALERTS = [ 71 | "+ OSVDB-", 72 | "Entry '/index.php/user/register/' in robots.txt returned a non-forbidden or redirect HTTP code", 73 | "Retrieved access-control-allow-origin header: *", 74 | "Retrieved x-powered-by header", 75 | "The anti-clickjacking X-Frame-Options header is not present", 76 | "This might be interesting", 77 | "Uncommon header", 78 | "created without the httponly flag", 79 | "entries which should be manually viewed", 80 | ] 81 | NMAP_ALERTS = [ 82 | "3des-cbc", 83 | "arcfour", 84 | "blowfish-cbc", 85 | "cast128-cbc", 86 | "diffie-hellman-group-exchange-sha1", 87 | "diffie-hellman-group1-sha1", 88 | "diffie-hellman-group14-sha1", 89 | "ecdh-sha2-nistp", 90 | "ecdsa", 91 | "hmac-md5", 92 | "hmac-sha1", 93 | "http-php-version", 94 | "message_signing: disabled", 95 | "mountd ", 96 | "msrpc", 97 | "netbios-ssn ", 98 | "ssh-dss", 99 | "umac-64", 100 | ] 101 | # All these keywords will be suffixed with ': ' 102 | NMAP_INFO = [ 103 | "Computer name", 104 | "Domain name", 105 | "NetBIOS computer name", 106 | "authentication_level", 107 | "banner", 108 | "challenge_response", 109 | "http-server-header", 110 | "http-title", 111 | "message_signing", 112 | "nbstat" "smb-security-mode", 113 | "smtp-open-relay", 114 | ] 115 | NMAP_ARGUMENTS = ["-sV", "--open"] # A list of default arguments to pass to nmap 116 | NMAP_SCRIPTS = [ 117 | "banner", 118 | "dns-nsid", 119 | "dns-recursion", 120 | "http-cisco-anyconnect", 121 | "http-php-version", 122 | "http-title", 123 | "http-trace", 124 | "ntp-info", 125 | "ntp-monlist", 126 | "nbstat", 127 | "rdp-enum-encryption", 128 | "rpcinfo", 129 | "sip-methods", 130 | "smb-os-discovery", 131 | "smb-security-mode", 132 | "smtp-open-relay", 133 | "ssh2-enum-algos", 134 | "vnc-info", 135 | "xmlrpc-methods", 136 | "xmpp-info", 137 | ] 138 | TESTSSL_ALERTS = [ 139 | "(deprecated)", 140 | "DES-CBC3", 141 | "NOT ok", 142 | "TLS1: ", 143 | "VULNERABLE", 144 | ] 145 | TESTSSL_UNTRUSTED = [ 146 | "NOT ok (self signed CA in chain)", 147 | "NOT ok -- neither CRL nor OCSP URI provided", 148 | ] 149 | 150 | # A regular expression of prepend characters to remove in a line 151 | REMOVE_PREPEND_LINE = r"^[| _+]*" 152 | UNKNOWN = -1 153 | # The program has the following loglevels: 154 | # logging.DEBUG = 10 debug messages (module constant) 155 | # logging.INFO = 20 verbose status messages (module constant) 156 | COMMAND = 23 # tool command line 157 | STATUS = 25 # generic status messages 158 | # ERROR = 40 recoverable error messages (module constant) 159 | # CRITICAL = 50 abort program (module constant) 160 | 161 | # The following levels are used for the actual scanning output: 162 | LOGS = 30 # scan output / logfiles 163 | ALERT = 35 # vulnerabilities found 164 | 165 | 166 | class LogFormatter(logging.Formatter): 167 | """Class to format log messages based on their type.""" 168 | 169 | # pylint: disable=protected-access 170 | FORMATS = { 171 | logging.DEBUG: logging._STYLES["{"][0]("[d] {message}"), 172 | logging.INFO: logging._STYLES["{"][0]("[*] {message}"), 173 | "STATUS": logging._STYLES["{"][0]("[+] {message}"), 174 | "ALERT": logging._STYLES["{"][0]("[!] {message}"), 175 | logging.ERROR: logging._STYLES["{"][0]("[-] {message}"), 176 | logging.CRITICAL: logging._STYLES["{"][0]("[-] FATAL: {message}"), 177 | "DEFAULT": logging._STYLES["{"][0]("{message}"), 178 | } 179 | 180 | def format(self, record): 181 | self._style = self.FORMATS.get(record.levelno, self.FORMATS["DEFAULT"]) 182 | return logging.Formatter.format(self, record) 183 | 184 | 185 | class LogFilter: # pylint: disable=too-few-public-methods 186 | """Class to remove certain log levels.""" 187 | 188 | def __init__(self, filterlist): 189 | self.__filterlist = filterlist 190 | 191 | def filter(self, logRecord): # pylint: disable=invalid-name 192 | """Remove logRecord if it is part of filterlist.""" 193 | return logRecord.levelno not in self.__filterlist 194 | 195 | 196 | def abort_program(text, error_code=-1): 197 | """Log critical error @text and exit program with @error_code.""" 198 | logging.critical(text) 199 | sys.exit(error_code) 200 | 201 | 202 | def analyze_url(url, port, options, logfile, host_results): 203 | """Analyze a URL using wappalyzer and execute corresponding scans.""" 204 | wappalyzer = Wappalyzer.Wappalyzer.latest() 205 | page = requests_get(url, options) 206 | if not page: 207 | logging.debug("%s Got no reply - cannot analyze that", url) 208 | return 209 | if page.status_code in [400] and "http://" in url: 210 | # Retry with a different protocol, as the site might also be securely accessible 211 | url = url.replace("http://", "https://") 212 | page = requests_get(url, options) 213 | if page.status_code == 200: 214 | webpage = Wappalyzer.WebPage(url, page.text, page.headers) 215 | analysis = wappalyzer.analyze(webpage) 216 | # Format logmessage as info message, so that it ends up in logfile 217 | logging.log(LOGS, "[*] %s Analysis: %s", url, analysis) 218 | if "Drupal" in analysis: 219 | do_droopescan(url, port, "drupal", options, logfile, host_results) 220 | if "Joomla" in analysis: 221 | do_droopescan(url, port, "joomla", options, logfile, host_results) 222 | if "WordPress" in analysis: 223 | do_wpscan(url, port, options, logfile) 224 | else: 225 | logging.debug("%s Got result %s - cannot analyze that", url, page.status_code) 226 | 227 | 228 | def requests_get(url, options, headers=None, allow_redirects=True): 229 | """Generic wrapper around requests object.""" 230 | # Don't try this at home, kids! Disabling SSL verification 231 | verify = False 232 | if not headers: 233 | headers = {"User-Agent": options["user_agent"]} 234 | if not verify: 235 | # pylint: disable=E1101 236 | requests.packages.urllib3.disable_warnings( 237 | requests.packages.urllib3.exceptions.InsecureRequestWarning 238 | ) 239 | proxies = None 240 | if options["proxy"]: 241 | proxies = { 242 | "http": f"http://{options['proxy']}", 243 | "https": f"https://{options['proxy']}", 244 | } 245 | try: 246 | request = requests.get( 247 | url, 248 | headers=headers, 249 | proxies=proxies, 250 | verify=verify, 251 | allow_redirects=allow_redirects, 252 | ) 253 | except ( 254 | requests.exceptions.ConnectionError, 255 | requests.exceptions.RequestException, 256 | ) as exception: 257 | logging.log(STATUS, "%s Could not connect: %s", url, exception) 258 | request = None 259 | return request 260 | 261 | 262 | def http_checks(host, port, protocol, options, logfile, host_results): 263 | """Perform various HTTP checks.""" 264 | ssl = False 265 | if "ssl" in protocol or "https" in protocol: 266 | ssl = True 267 | url = f"https://{host}:{port}" 268 | else: 269 | url = f"http://{host}:{port}" 270 | if options["nikto"]: 271 | do_nikto(host, port, options, logfile, host_results) 272 | if options["framework"]: 273 | analyze_url(url, port, options, logfile, host_results) 274 | if options["http"] or options["compression"]: 275 | check_compression(url, port, options, host_results, use_ssl=ssl) 276 | if options["http"] or options["headers"]: 277 | check_headers(url, port, options, host_results, use_ssl=ssl) 278 | if options["http"] or options["redirect"]: 279 | check_redirect(url, port, options, host_results) 280 | if options["http"] or options["trace"]: 281 | check_trace(host, port, options, logfile, host_results) 282 | 283 | 284 | def tls_checks(host, port, protocol, options, logfile, host_results): 285 | """Perform various SSL/TLS checks.""" 286 | if options["ssl"]: 287 | do_testssl(host, port, protocol, options, logfile, host_results) 288 | if options["sslcert"]: 289 | download_cert(host, port, options, logfile) 290 | 291 | 292 | def check_redirect(url, port, options, host_results): 293 | """Check for insecure open redirect.""" 294 | request = requests_get( 295 | url, 296 | options, 297 | headers={"Host": "EVIL-INSERTED-HOST", "User-Agent": options["user_agent"]}, 298 | allow_redirects=False, 299 | ) 300 | if ( 301 | request 302 | and request.status_code == 302 303 | and "Location" in request.headers 304 | and "EVIL-INSERTED-HOST" in request.headers["Location"] 305 | ): 306 | add_item( 307 | host_results, 308 | url, 309 | port, 310 | options, 311 | f"{url} vulnerable to open insecure redirect: {request.headers['Location']}", 312 | ALERT, 313 | ) 314 | 315 | 316 | def check_headers(url, port, options, host_results, use_ssl=False): 317 | """Check HTTP headers for omissions / insecure settings.""" 318 | request = requests_get( 319 | url, 320 | options, 321 | headers={"User-Agent": options["user_agent"]}, 322 | allow_redirects=False, 323 | ) 324 | if not request: 325 | return 326 | logging.debug( 327 | "%s Received status %s and the following headers: %s", 328 | url, 329 | request.status_code, 330 | request.headers, 331 | ) 332 | security_headers = ["X-Content-Type-Options", "X-XSS-Protection"] 333 | if use_ssl: 334 | security_headers.append("Strict-Transport-Security") 335 | if request.status_code == 200: 336 | if "X-Frame-Options" not in request.headers: 337 | add_item( 338 | host_results, 339 | url, 340 | port, 341 | options, 342 | f"{url} lacks an X-Frame-Options header", 343 | ALERT, 344 | ) 345 | elif "*" in request.headers["X-Frame-Options"]: 346 | add_item( 347 | host_results, 348 | url, 349 | port, 350 | options, 351 | f"{url} has an insecure X-Frame-Options header: {request.headers['X-Frame-Options']}", 352 | ALERT, 353 | ) 354 | for header in security_headers: 355 | if header not in request.headers: 356 | add_item( 357 | host_results, 358 | url, 359 | port, 360 | options, 361 | f"{url} lacks a {header} header", 362 | ALERT, 363 | ) 364 | 365 | 366 | def check_compression(url, port, options, host_results, use_ssl=False): 367 | """Check which compression methods are supported.""" 368 | request = requests_get(url, options, allow_redirects=True) 369 | if not request: 370 | return 371 | if request.history: 372 | # check if protocol was changed: if so, abort checks 373 | if (not use_ssl and "https" in request.url) or ( 374 | use_ssl and "https" not in request.url 375 | ): 376 | logging.debug( 377 | "%s protocol has changed while testing to %s - aborting compression test", 378 | url, 379 | request.url, 380 | ) 381 | return 382 | url = request.url 383 | for compression in [ 384 | "br", 385 | "bzip2", 386 | "compress", 387 | "deflate", 388 | "exi", 389 | "gzip", 390 | "identity", 391 | "lzma", 392 | "pack200-gzip", 393 | "peerdist", 394 | "sdch", 395 | "xpress", 396 | "xz", 397 | ]: 398 | request = requests_get( 399 | url, 400 | options, 401 | headers={ 402 | "User-Agent": options["user_agent"], 403 | "Accept-Encoding": compression, 404 | }, 405 | allow_redirects=False, 406 | ) 407 | if request and request.status_code == 200: 408 | if "Content-Encoding" in request.headers: 409 | if compression in request.headers["Content-Encoding"]: 410 | add_item( 411 | host_results, 412 | url, 413 | port, 414 | options, 415 | f"{url} supports {compression} compression", 416 | ALERT, 417 | ) 418 | 419 | 420 | def is_admin(): 421 | """Check whether script is executed using root privileges.""" 422 | if os.name == "nt": 423 | try: 424 | import ctypes 425 | 426 | return ctypes.windll.shell32.IsUserAnAdmin() 427 | except ImportError: 428 | return False 429 | else: 430 | return os.geteuid() == 0 # pylint: disable=no-member 431 | 432 | 433 | def preflight_checks(options): 434 | """Check if all tools are there, and disable tools automatically.""" 435 | try: 436 | if options["resume"]: 437 | if ( 438 | not os.path.isfile(options["queuefile"]) 439 | or not os.stat(options["queuefile"]).st_size 440 | ): 441 | abort_program(f"Queuefile {options['queuefile']} is empty") 442 | else: 443 | if ( 444 | os.path.isfile(options["queuefile"]) 445 | and os.stat(options["queuefile"]).st_size 446 | ): 447 | if options["force"]: 448 | os.remove(options["queuefile"]) 449 | else: 450 | abort_program( 451 | f"Queuefile {options['queuefile']} already exists.\n" 452 | + " Use --resume to resume with previous targets, " 453 | + "or use --force to overwrite the queuefile" 454 | ) 455 | except (IOError, OSError) as exception: 456 | logging.error("FAILED: Could not read %s (%s)", options["queuefile"], exception) 457 | for basic in ["nmap"]: 458 | options[basic] = True 459 | if options["udp"] and not is_admin() and not options["dry_run"]: 460 | logging.error("UDP portscan needs root permissions") 461 | if options["framework"]: 462 | try: 463 | import requests 464 | import Wappalyzer 465 | 466 | options["droopescan"] = True 467 | options["wpscan"] = True 468 | except ImportError: 469 | logging.error("Disabling --framework due to missing Python libraries") 470 | options["framework"] = False 471 | if options["wpscan"] and not is_admin(): 472 | logging.error("Disabling --wpscan as this option needs root permissions") 473 | options["wpscan"] = False 474 | options["timeout"] = options["testssl.sh"] 475 | for tool in [ 476 | "nmap", 477 | "curl", 478 | "droopescan", 479 | "nikto", 480 | "testssl.sh", 481 | "timeout", 482 | "wpscan", 483 | ]: 484 | if options[tool]: 485 | logging.debug("Checking whether %s is present... ", tool) 486 | version = "--version" 487 | if tool == "nikto": 488 | version = "-Version" 489 | elif tool == "droopescan": 490 | version = "stats" 491 | result, stdout, stderr = execute_command( 492 | [get_binary(tool), version], options, keep_endings=False 493 | ) 494 | options[f"version_{tool}"] = stdout 495 | if not result: 496 | if tool == "nmap": 497 | if not options["dry_run"] and not options["no_portscan"]: 498 | abort_program("Could not execute nmap, which is necessary") 499 | logging.error( 500 | "Could not execute %s, disabling checks (%s)", tool, stderr 501 | ) 502 | options[tool] = False 503 | else: 504 | logging.debug(stdout) 505 | 506 | 507 | def prepare_nmap_arguments(options): 508 | """Prepare nmap command line arguments.""" 509 | arguments = NMAP_ARGUMENTS 510 | scripts = NMAP_SCRIPTS 511 | if is_admin(): 512 | arguments.append("-sS") 513 | if options["udp"]: 514 | arguments.append("-sU") 515 | elif options["no_portscan"]: 516 | arguments.append("-sn") 517 | else: 518 | arguments.append("-sT") 519 | if options["allports"]: 520 | arguments.append("-p1-65535") 521 | elif options["port"]: 522 | arguments.append("-p" + options["port"]) 523 | if options["no_portscan"] or options["up"]: 524 | arguments.append("-Pn") 525 | if options["whois"]: 526 | scripts += "asn-query", "fcrdns,whois-ip", "whois-domain" 527 | if scripts: 528 | arguments.append("--script=" + ",".join(scripts)) 529 | options["nmap_arguments"] = " ".join(arguments) 530 | 531 | 532 | def execute_command(cmd, options, logfile=False, keep_endings=True): 533 | """Execute system command. 534 | 535 | If logfile is provided, will add the command as well as stdout and stderr 536 | to the logfile. 537 | 538 | Args: 539 | cmd (str): Command to execute 540 | options (dictionary): Options 541 | logfile (str): Name of logfile 542 | 543 | Returns: 544 | Result value, stdout, stderr (tuple) 545 | """ 546 | stdout = "" 547 | stderr = "" 548 | result = False 549 | logging.debug(" ".join(cmd)) 550 | if options["dry_run"]: 551 | return True, stdout, stderr 552 | try: 553 | process = subprocess.run(cmd, encoding="utf-8", text=True, capture_output=True) 554 | # For easier processing, split string into lines 555 | stdout = process.stdout.splitlines(keep_endings) 556 | stderr = process.stderr.splitlines(keep_endings) 557 | result = not process.returncode 558 | except OSError as exception: 559 | logging.error("Error while executing %s: %s", cmd, exception) 560 | except Exception as exception: 561 | logging.error("Exception while executing %s: %s", cmd, exception) 562 | if logfile: 563 | append_logs(logfile, options, " ".join(cmd), "") 564 | append_logs(logfile, options, stdout, stderr) 565 | return result, stdout, stderr 566 | 567 | 568 | def download_cert(host, port, options, logfile): 569 | """Download an SSL certificate and append it to the logfile.""" 570 | try: 571 | cert = ssl.get_server_certificate((host, port)) 572 | append_logs(logfile, options, cert, "") 573 | except ssl.SSLError: 574 | pass 575 | 576 | 577 | def append_logs(logfile, options, stdout, stderr): 578 | """Append unicode text strings to unicode type logfile.""" 579 | if options["dry_run"]: 580 | return 581 | try: 582 | if stdout: 583 | with io.open(logfile, encoding="utf-8", mode="a+") as open_file: 584 | open_file.write(compact_strings(stdout, options)) 585 | if stderr: 586 | with io.open(logfile, encoding="utf-8", mode="a+") as open_file: 587 | open_file.write(compact_strings(stderr, options)) 588 | except IOError: 589 | logging.error("Could not write to %s", logfile) 590 | 591 | 592 | def append_file(logfile, options, input_file): 593 | """Append content from input_file to logfile, and delete input_file.""" 594 | if options["dry_run"]: 595 | return 596 | try: 597 | if os.path.isfile(input_file) and os.stat(input_file).st_size: 598 | with open(input_file, "r") as read_file: 599 | logging.debug("Appending input_file to logfile") 600 | append_logs(logfile, options, read_file.read().splitlines(True), []) 601 | os.remove(input_file) 602 | except (IOError, OSError) as exception: 603 | logging.error("FAILED: Could not read %s (%s)", input_file, exception) 604 | 605 | 606 | def compact_strings(lines, options): 607 | """Remove empty and remarked lines.""" 608 | if options["compact"]: 609 | return "".join([x for x in lines if not (x == "\n") and not x.startswith("#")]) 610 | return "".join(lines) 611 | 612 | 613 | def check_trace(host, port, options, logfile, host_results): 614 | """Check for HTTP TRACE method.""" 615 | if options["trace"]: 616 | command = [ 617 | get_binary("curl"), 618 | "-sIA", 619 | "'{0}'".format(options["user_agent"]), 620 | "--connect-timeout", 621 | str(options["timeout"]), 622 | "-X", 623 | "TRACE", 624 | f"{host}:{port}", 625 | ] 626 | _result, _stdout, _stderr = execute_command( 627 | command, options, logfile 628 | ) # pylint: disable=unused-variable 629 | 630 | 631 | def do_droopescan(url, port, cms, options, logfile, host_results): 632 | """Perform a droopescan of type cms.""" 633 | if options["droopescan"]: 634 | logging.debug("Performing %s droopescan on %s", cms, url) 635 | command = [get_binary("droopescan"), "scan", cms, "--quiet", "--url", url] 636 | _result, _stdout, _stderr = execute_command( 637 | command, options, logfile 638 | ) # pylint: disable=unused-variable 639 | 640 | 641 | def do_nikto(host, port, options, logfile, host_results): 642 | """Perform a nikto scan.""" 643 | command = [ 644 | get_binary("nikto"), 645 | "-ask", 646 | "no", 647 | "-host", 648 | f"{host}:{port}", 649 | "-maxtime", 650 | f'{options["maxtime"]}s', 651 | "-nointeractive", 652 | "-vhost", 653 | f"{host}", 654 | ] 655 | if port == 443: 656 | command.append("-ssl") 657 | if options["proxy"]: 658 | command += ["-useproxy", f'https://{options["proxy"]}'] 659 | elif options["proxy"]: 660 | command += ["-useproxy", f'http://{options["proxy"]}'] 661 | if options["username"] and options["password"]: 662 | command += ["-id", f'{options["username"]}:{options["password"]}'] 663 | parameters = read_parameters(options["settings"], host, port) 664 | if "nikto_output" in parameters: 665 | command += "-output", parameters["nikto_output"] 666 | if "nikto_plugins" in parameters: 667 | command += "-Plugins", parameters["nikto_plugins"] 668 | if "nikto_tuning" in parameters: 669 | command += "-Tuning", parameters["nikto_tuning"] 670 | if options["timeout"]: 671 | command = [get_binary("timeout"), str(options["maxtime"])] + command 672 | logging.info("%s Starting nikto on port %s", host, port) 673 | _result, stdout, _stderr = execute_command( 674 | command, options, logfile 675 | ) # pylint: disable=unused-variable 676 | check_strings_for_alerts(stdout, NIKTO_ALERTS, host_results, host, port, options) 677 | 678 | 679 | def read_parameters(settings, host, port): 680 | """Read tuning dictionary per host-port combination from the settings dictionary.""" 681 | parameters = {} 682 | if ( 683 | "targets" in settings 684 | and host in settings["targets"] 685 | and "ports" in settings["targets"][host] 686 | ): 687 | parameters = next( 688 | ( 689 | item 690 | for item in settings["targets"][host]["ports"] 691 | if item["port"] == port 692 | ), 693 | {}, 694 | ) 695 | return parameters 696 | 697 | 698 | def do_portscan(host, options, logfile, stop_event, host_results): 699 | """Perform a portscan. 700 | 701 | Args: 702 | host: Target host. 703 | options: Dictionary object containing options. 704 | logfile: Filename where logfile will be written to. 705 | stop_event: Event handler for stop event 706 | host_results: Host results dictionary 707 | 708 | Returns: 709 | A list with tuples of open ports and the protocol. 710 | """ 711 | ports = [] 712 | open_ports = [] 713 | if not options["nmap"]: 714 | if options["port"]: 715 | ports = [int(port) for port in options["port"].split(",") if port.isdigit()] 716 | return zip(ports, ["unknown"] * len(ports)) 717 | return ALLPORTS 718 | if ":" in host: 719 | options["nmap_arguments"] += " -6" 720 | logging.info("%s Starting nmap", host) 721 | logging.log(COMMAND, "nmap %s %s", options["nmap_arguments"], host) 722 | if options["dry_run"]: 723 | return ALLPORTS 724 | try: 725 | temp_file = f"nmap-{host}-{next(tempfile._get_candidate_names())}" # pylint: disable=protected-access 726 | scanner = nmap.PortScanner() 727 | scanner.scan( 728 | hosts=host, 729 | arguments=f"{options['nmap_arguments']} -oN {temp_file}", 730 | ) 731 | for ip_address in [ 732 | x for x in scanner.all_hosts() if scanner[x] and scanner[x].state() == "up" 733 | ]: 734 | ports = [ 735 | port 736 | for port in scanner[ip_address].all_tcp() 737 | if scanner[ip_address]["tcp"][port]["state"] == "open" 738 | ] 739 | for port in ports: 740 | open_ports.append([port, scanner[ip_address]["tcp"][port]["name"]]) 741 | check_nmap_log_for_alerts(temp_file, host_results, host, options) 742 | append_file(logfile, options, temp_file) 743 | if open_ports: 744 | logging.info("%s Found open TCP ports %s", host, open_ports) 745 | else: 746 | # Format logmessage as info message, so that it ends up in logfile 747 | logging.log(LOGS, "[*] %s No open ports found", host) 748 | except (AssertionError, nmap.PortScannerError) as exception: 749 | if stop_event.is_set(): 750 | logging.debug("%s nmap interrupted", host) 751 | else: 752 | logging.log( 753 | STATUS, 754 | "%s Issue with nmap %s: %s", 755 | host, 756 | options["nmap_arguments"], 757 | exception, 758 | ) 759 | open_ports = [UNKNOWN] 760 | finally: 761 | if os.path.isfile(temp_file): 762 | os.remove(temp_file) 763 | host_results["ports"] = ports 764 | return open_ports 765 | 766 | 767 | def check_nmap_log_for_alerts(logfile, host_results, host, options): 768 | """Check for keywords in logfile and log them as alert.""" 769 | try: 770 | if os.path.isfile(logfile) and os.stat(logfile).st_size: 771 | with open(logfile, "r") as read_file: 772 | log = read_file.read().splitlines() 773 | port = 0 774 | for line in log: # Highly inefficient 'brute-force' check 775 | # Grab the last open port number to use that for the alert 776 | if ( 777 | " open " in line 778 | and "/" in line[:7] 779 | and line[: (line.index("/"))].isdecimal() 780 | ): 781 | port = int(line[: (line.index("/"))]) 782 | for keyword in NMAP_INFO: 783 | if f"{keyword}: " in line: 784 | add_item(host_results, host, port, options, line, logging.INFO) 785 | for keyword in NMAP_ALERTS: 786 | if keyword in line: 787 | add_item(host_results, host, port, options, line, ALERT) 788 | except (IOError, OSError) as exception: 789 | logging.error("FAILED: Could not read %s (%s)", logfile, exception) 790 | 791 | 792 | def check_strings_for_alerts( 793 | strings, keywords, host_results, host, port, options, negate=[] 794 | ): 795 | """Check for keywords in strings and log them as alerts.""" 796 | for line in strings: # Highly inefficient 'brute-force' check 797 | for keyword in keywords: 798 | if keyword in line: 799 | if not negate: 800 | add_item(host_results, host, port, options, line, ALERT) 801 | else: 802 | for item in negate: 803 | if item in line: 804 | line = "" 805 | if line: 806 | add_item(host_results, host, port, options, line, ALERT) 807 | 808 | 809 | def add_item(host_results, host, port, options, line, logging_type): 810 | """Log item, and add line to the corresponding key in host_results. 811 | Set options["exit"] code when alerts are found. 812 | 813 | logging_type can be INFO or ALERT. 814 | """ 815 | # Don't log anything if no port has been set 816 | if not port: 817 | return 818 | filtered_line = re.sub(REMOVE_PREPEND_LINE, "", line).strip() 819 | if logging_type == logging.INFO: 820 | key = "info" 821 | else: 822 | key = "alerts" 823 | if options["exit_code"]: 824 | options["exit"] = 1 825 | if key not in host_results: 826 | host_results[key] = {} 827 | if port not in host_results[key]: 828 | host_results[key][port] = [filtered_line] 829 | else: 830 | host_results[key][port].append(filtered_line) 831 | logging.log(logging_type, f"{host}:{port} {filtered_line}") 832 | 833 | 834 | def get_binary(tool): 835 | """Convert tool command to its environment variable, if it is set.""" 836 | if tool.split(".")[0].upper() in os.environ: 837 | tool = os.environ[tool.split(".")[0].upper()] 838 | return tool 839 | 840 | 841 | def do_testssl(host, port, protocol, options, logfile, host_results): 842 | """Check SSL/TLS configuration and vulnerabilities.""" 843 | # --color 0 Don't use color escape codes 844 | # --warnings off Skip connection warnings 845 | # --quiet Don't output the banner 846 | # 847 | # The following settings are default, and can be overwritten using the 'testssl' parameter 848 | # --each-cipher Checks each local cipher remotely 849 | # --fs Check (perfect) forward secrecy settings 850 | # --protocols Check TLS/SSL protocols 851 | # --server-defaults Display the server's default picks and certificate info 852 | # --starttls protocol Use starttls protocol 853 | # --vulnerable Test for all vulnerabilities 854 | if not options["testssl.sh"]: 855 | return 856 | command = [ 857 | get_binary("testssl.sh"), 858 | "--color", 859 | "0", 860 | "--quiet", 861 | "--warnings", 862 | "off", 863 | ] 864 | parameters = read_parameters(options["settings"], host, port) 865 | if "testssl" in parameters: 866 | for parameter in parameters["testssl"]: 867 | command.append(parameter) 868 | else: 869 | command += [ 870 | "--each-cipher", 871 | "--fs", 872 | "--protocols", 873 | "--server-defaults", 874 | "--vulnerable", 875 | ] 876 | if options["timeout"]: 877 | command = [get_binary("timeout"), str(options["maxtime"])] + command 878 | if "smtp" in protocol: 879 | command += ["--starttls", "smtp"] 880 | logging.info("%s Starting testssl.sh on port %s", host, port) 881 | _result, stdout, _stderr = execute_command( 882 | command + [f"{host}:{port}"], # pylint: disable=unused-variable 883 | options, 884 | logfile, 885 | ) 886 | negate = [] 887 | if "testssl_untrusted" in parameters and bool(parameters["testssl_untrusted"]): 888 | negate = TESTSSL_UNTRUSTED 889 | check_strings_for_alerts( 890 | stdout, TESTSSL_ALERTS, host_results, host, port, options, negate 891 | ) 892 | 893 | 894 | def do_wpscan(url, port, options, logfile): 895 | """Run WPscan.""" 896 | if options["wpscan"]: 897 | logging.info("Starting WPscan on " + url) 898 | command = [ 899 | get_binary("wpscan"), 900 | "--format", 901 | "cli-no-color", 902 | "--no-banner", 903 | "--update", 904 | "--ignore-main-redirect", 905 | "--url", 906 | url, 907 | ] 908 | _result, _stdout, _stderr = execute_command( 909 | command, options, logfile 910 | ) # pylint: disable=unused-variable 911 | 912 | 913 | def prepare_queue(options): 914 | """Prepare a file which holds all hosts (targets) to scan.""" 915 | expanded = False 916 | try: 917 | if not options["inputfile"]: 918 | expanded = next( 919 | tempfile._get_candidate_names() 920 | ) # pylint: disable=protected-access 921 | with open(expanded, "a") as inputfile: 922 | inputfile.write(options["target"]) 923 | options["inputfile"] = expanded 924 | with open(options["inputfile"], "r") as inputfile: 925 | targets = [] 926 | for host in [ 927 | line for line in inputfile.read().splitlines() if line.strip() 928 | ]: 929 | if options["dry_run"] or not re.match(r".*[\.:].*[-/][0-9]+", host): 930 | targets.append(host) 931 | else: 932 | arguments = "-nsL" 933 | scanner = nmap.PortScanner() 934 | scanner.scan(hosts=f"{host}", arguments=arguments) 935 | if "." in scanner.all_hosts(): 936 | targets += sorted( 937 | scanner.all_hosts(), 938 | key=lambda x: tuple(map(int, x.split("."))), 939 | ) 940 | else: 941 | targets += scanner.all_hosts() 942 | with open(options["queuefile"], "a") as queuefile: 943 | for target in targets: 944 | queuefile.write(target + "\n") 945 | if expanded: 946 | os.remove(expanded) 947 | except IOError as exception: 948 | abort_program(f"Could not read/write file: {exception}") 949 | 950 | 951 | def remove_from_queue(finished_queue, options, stop_event): 952 | """Remove a host from the queue file.""" 953 | while not stop_event.is_set() or finished_queue.qsize(): 954 | try: 955 | host = finished_queue.get(block=False) 956 | with open(options["queuefile"], "r+") as queuefile: 957 | hosts = queuefile.read().splitlines() 958 | queuefile.seek(0) 959 | for i in hosts: 960 | if i != host: 961 | queuefile.write(i + "\n") 962 | queuefile.truncate() 963 | if not os.stat(options["queuefile"]).st_size: 964 | os.remove(options["queuefile"]) 965 | finished_queue.task_done() 966 | logging.debug("%s Removed from queue", host) 967 | except queue.Empty: 968 | time.sleep(1) 969 | logging.debug("Exiting remove_from_queue thread") 970 | 971 | 972 | def process_host( 973 | options, host_queue, output_queue, finished_queue, stop_event, results 974 | ): 975 | """ 976 | Worker thread: Process each host atomic, add output files to output_queue, 977 | and finished hosts to finished_queue. 978 | """ 979 | while host_queue.qsize() and not stop_event.wait(0.01): 980 | try: 981 | host = host_queue.get() 982 | host_logfile = ( 983 | host + "-" + next(tempfile._get_candidate_names()) 984 | ) # pylint: disable=protected-access 985 | logging.debug( 986 | "%s Processing (%s items left in host queue)", host, host_queue.qsize() 987 | ) 988 | host_results = {} 989 | open_ports = do_portscan( 990 | host, options, host_logfile, stop_event, host_results 991 | ) 992 | expected_ports = ALLOWED_OPEN_PORTS 993 | if ( 994 | "targets" in options["settings"] 995 | and host in options["settings"]["targets"] 996 | and "allowed_ports" in options["settings"]["targets"][host] 997 | ): 998 | expected_ports = options["settings"]["targets"][host]["allowed_ports"] 999 | if open_ports: 1000 | if UNKNOWN in open_ports: 1001 | logging.info("%s Scan interrupted ?", host) 1002 | else: 1003 | for port, protocol in open_ports: 1004 | if stop_event.is_set(): 1005 | logging.info("%s Scan interrupted ?", host) 1006 | break 1007 | # Sometimes nmap detects webserver as 'ssl/ssl' 1008 | if port not in expected_ports: 1009 | add_item( 1010 | host_results, 1011 | host, 1012 | port, 1013 | options, 1014 | "Unexpected open port found", 1015 | ALERT, 1016 | ) 1017 | if "http" in protocol or "ssl" in protocol: 1018 | http_checks( 1019 | host, 1020 | port, 1021 | protocol, 1022 | options, 1023 | host_logfile, 1024 | host_results, 1025 | ) 1026 | if ( 1027 | "ssl" in protocol 1028 | or port in SSL_PORTS 1029 | or options["force_ssl"] 1030 | ): 1031 | tls_checks( 1032 | host, 1033 | port, 1034 | protocol, 1035 | options, 1036 | host_logfile, 1037 | host_results, 1038 | ) 1039 | if os.path.isfile(host_logfile): 1040 | if os.stat(host_logfile).st_size: 1041 | with open(host_logfile, "r") as read_file: 1042 | output_queue.put(read_file.read()) 1043 | os.remove(host_logfile) 1044 | if not stop_event.is_set(): # Do not flag host as being done 1045 | results["results"][host] = host_results 1046 | finished_queue.put(host) 1047 | host_queue.task_done() 1048 | except queue.Empty: 1049 | break 1050 | logging.debug( 1051 | "Exiting process_host thread, queue contains %s items", host_queue.qsize() 1052 | ) 1053 | 1054 | 1055 | def process_output(output_queue, stop_event): 1056 | """Convert logged items in output_queue atomically to log items.""" 1057 | while not stop_event.is_set() or output_queue.qsize(): 1058 | try: 1059 | item = output_queue.get(block=False) 1060 | logging.debug("Processing output item") 1061 | logging.log(LOGS, item) 1062 | output_queue.task_done() 1063 | except queue.Empty: 1064 | time.sleep(1) 1065 | except UnicodeDecodeError as exception: 1066 | logging.error("Having issues decoding %s: %s", item, exception) 1067 | # Flag the issue ready, regardless 1068 | output_queue.task_done() 1069 | logging.debug( 1070 | "Exiting process_output thread, queue contains %s items", output_queue.qsize() 1071 | ) 1072 | 1073 | 1074 | def loop_hosts(options, target_list, results): 1075 | """Iterate all hosts in target_list and perform requested actions.""" 1076 | stop_event = threading.Event() 1077 | work_queue = queue.Queue() 1078 | output_queue = queue.Queue() 1079 | finished_queue = queue.Queue() 1080 | 1081 | def stop_gracefully(signum, frame): # pylint: disable=unused-argument 1082 | """Handle interrupt (gracefully).""" 1083 | logging.error("Caught Ctrl-C - exiting gracefully (please be patient)") 1084 | stop_event.set() 1085 | 1086 | signal.signal(signal.SIGINT, stop_gracefully) 1087 | for target in target_list: 1088 | work_queue.put(target) 1089 | threads = [ 1090 | threading.Thread( 1091 | target=process_host, 1092 | args=( 1093 | options, 1094 | work_queue, 1095 | output_queue, 1096 | finished_queue, 1097 | stop_event, 1098 | results, 1099 | ), 1100 | ) 1101 | for _ in range(min(options["threads"], work_queue.qsize())) 1102 | ] 1103 | threads.append( 1104 | threading.Thread( 1105 | target=remove_from_queue, args=(finished_queue, options, stop_event) 1106 | ) 1107 | ) 1108 | threads[-1].daemon = True 1109 | threads.append( 1110 | threading.Thread(target=process_output, args=(output_queue, stop_event)) 1111 | ) 1112 | threads[-1].daemon = True 1113 | logging.debug("Starting %s threads", len(threads)) 1114 | for thread in threads: 1115 | thread.start() 1116 | while work_queue.qsize() and not stop_event.wait(1): 1117 | try: 1118 | time.sleep(0.0001) 1119 | except IOError: 1120 | pass 1121 | if not stop_event.is_set(): 1122 | work_queue.join() # block until the queue is empty 1123 | logging.debug("Work queue is empty - waiting for threads to finish") 1124 | while not stop_event.is_set() and ( 1125 | not output_queue.empty() or not finished_queue.empty() 1126 | ): 1127 | logging.debug( 1128 | "%s threads running. %s items in output and %s items in finished queue", 1129 | threading.activeCount(), 1130 | output_queue.qsize(), 1131 | finished_queue.qsize(), 1132 | ) 1133 | time.sleep(1) 1134 | 1135 | 1136 | def read_settings(filename): 1137 | """Return a list of settings parsed as an object.""" 1138 | settings = {} 1139 | if os.path.isfile(filename): 1140 | try: 1141 | with open(filename) as yamlfile: 1142 | settings = yaml.safe_load(yamlfile) 1143 | except yaml.scanner.ScannerError as exception: 1144 | logging.error(f"Could not parse settings file {filename}: {exception}") 1145 | return settings 1146 | 1147 | 1148 | def read_targets(filename): 1149 | """Return a list of targets.""" 1150 | target_list = [] 1151 | try: 1152 | with open(filename, "r") as queuefile: 1153 | target_list = [ 1154 | line for line in queuefile.read().splitlines() if line.strip() 1155 | ] 1156 | except IOError: 1157 | logging.error("Could not read %s", filename) 1158 | return target_list 1159 | 1160 | 1161 | def parse_arguments(banner): 1162 | """Parse command line arguments.""" 1163 | parser = argparse.ArgumentParser( 1164 | description=__doc__, 1165 | formatter_class=argparse.RawDescriptionHelpFormatter, 1166 | fromfile_prefix_chars="@", 1167 | ) 1168 | parser.add_argument( 1169 | "target", 1170 | nargs="?", 1171 | type=str, 1172 | help="""[TARGET] can be a single (IP) address, an IP 1173 | range, or multiple comma-separated addressess""", 1174 | ) 1175 | parser.add_argument("--version", action="store_true", help="Show version and exit") 1176 | parser.add_argument( 1177 | "--dry-run", 1178 | action="store_true", 1179 | help="Only show commands, don't actually do anything", 1180 | ) 1181 | parser.add_argument( 1182 | "-i", 1183 | "--inputfile", 1184 | action="store", 1185 | type=str, 1186 | help="A file containing targets, one per line", 1187 | ) 1188 | parser.add_argument( 1189 | "-o", 1190 | "--output-file", 1191 | action="store", 1192 | type=str, 1193 | default="analyze_hosts.output", 1194 | help="""output file containing all scanresults 1195 | (default %(default)s)""", 1196 | ) 1197 | parser.add_argument( 1198 | "--compact", 1199 | action="store_true", 1200 | help="Only log raw logfiles and alerts to file", 1201 | ) 1202 | parser.add_argument( 1203 | "--queuefile", 1204 | action="store", 1205 | default="analyze_hosts.queue", 1206 | help="the queuefile", 1207 | ) 1208 | parser.add_argument( 1209 | "--resume", action="store_true", help="Resume working on the queue" 1210 | ) 1211 | parser.add_argument( 1212 | "--settings", 1213 | action="store", 1214 | default="analyze_hosts.yml", 1215 | help="Name of settings file to use (default %(default)s)", 1216 | ) 1217 | parser.add_argument( 1218 | "--exit-code", 1219 | action="store_true", 1220 | help="When supplied, return exit code 1 when alerts are discovered", 1221 | ) 1222 | parser.add_argument( 1223 | "--force", action="store_true", help="Ignore / overwrite the queuefile" 1224 | ) 1225 | parser.add_argument("--debug", action="store_true", help="Show debug information") 1226 | parser.add_argument("-v", "--verbose", action="store_true", help="Be more verbose") 1227 | parser.add_argument( 1228 | "-q", 1229 | "--quiet", 1230 | action="store_true", 1231 | help="Do not show scan outputs on the console", 1232 | ) 1233 | parser.add_argument( 1234 | "--allports", 1235 | action="store_true", 1236 | help="Run a full-blown nmap scan on all ports", 1237 | ) 1238 | parser.add_argument( 1239 | "-n", "--no-portscan", action="store_true", help="Do NOT run a nmap portscan" 1240 | ) 1241 | parser.add_argument("-p", "--port", action="store", help="Specific port(s) to scan") 1242 | parser.add_argument( 1243 | "--up", 1244 | action="store_true", 1245 | help="Assume host is up (do not rely on ping probe)", 1246 | ) 1247 | parser.add_argument( 1248 | "--udp", action="store_true", help="Check for open UDP ports as well" 1249 | ) 1250 | 1251 | parser.add_argument( 1252 | "--framework", action="store_true", help="Analyze the website and run webscans" 1253 | ) 1254 | parser.add_argument( 1255 | "--http", 1256 | action="store_true", 1257 | help="""Check for various HTTP vulnerabilities (compression, 1258 | headers, trace)""", 1259 | ) 1260 | parser.add_argument( 1261 | "--compression", action="store_true", help="Check for webserver compression" 1262 | ) 1263 | parser.add_argument( 1264 | "--headers", action="store_true", help="Check for various HTTP headers" 1265 | ) 1266 | parser.add_argument( 1267 | "--trace", action="store_true", help="Check webserver for HTTP TRACE method" 1268 | ) 1269 | parser.add_argument( 1270 | "--redirect", action="store_true", help="Check for insecure redirect" 1271 | ) 1272 | parser.add_argument( 1273 | "--force-ssl", 1274 | action="store_true", 1275 | help="Enforce SSL/TLS check on all open ports", 1276 | ) 1277 | parser.add_argument( 1278 | "--json", action="store", type=str, help="Save output in JSON file" 1279 | ) 1280 | parser.add_argument( 1281 | "--ssl", action="store_true", help="Check for various SSL/TLS vulnerabilities" 1282 | ) 1283 | parser.add_argument("--nikto", action="store_true", help="Run a nikto scan") 1284 | parser.add_argument( 1285 | "--sslcert", action="store_true", help="Download SSL certificate" 1286 | ) 1287 | 1288 | parser.add_argument( 1289 | "-w", "--whois", action="store_true", help="Perform a whois lookup" 1290 | ) 1291 | parser.add_argument("--proxy", action="store", help="Use proxy server (host:port)") 1292 | parser.add_argument( 1293 | "--timeout", 1294 | action="store", 1295 | default="10", 1296 | type=int, 1297 | help="Timeout for requests in seconds (default %(default)s)", 1298 | ) 1299 | parser.add_argument( 1300 | "--threads", 1301 | action="store", 1302 | type=int, 1303 | default=5, 1304 | help="Maximum number of threads (default %(default)s)", 1305 | ) 1306 | parser.add_argument( 1307 | "--user-agent", 1308 | action="store", 1309 | default="analyze_hosts", 1310 | help="Custom User-Agent to use (default %(default)s)", 1311 | ) 1312 | parser.add_argument( 1313 | "--password", action="store", help="Password for HTTP basic host authentication" 1314 | ) 1315 | parser.add_argument( 1316 | "--username", action="store", help="Username for HTTP basic host authentication" 1317 | ) 1318 | parser.add_argument( 1319 | "--maxtime", 1320 | action="store", 1321 | default="600", 1322 | type=int, 1323 | help="Timeout for scans in seconds (default %(default)s)", 1324 | ) 1325 | args = parser.parse_args() 1326 | if args.version: 1327 | print(banner) 1328 | sys.exit(0) 1329 | if not (args.inputfile or args.target or args.resume): 1330 | parser.error("Specify either a target or input file") 1331 | options = vars(parser.parse_args()) 1332 | options["testssl.sh"] = args.ssl 1333 | options["curl"] = args.trace 1334 | options["wpscan"] = args.framework 1335 | options["droopescan"] = args.framework 1336 | return options 1337 | 1338 | 1339 | def setup_logging(options): 1340 | """Set up loghandlers according to options.""" 1341 | logger = logging.getLogger() 1342 | logger.setLevel(0) 1343 | try: 1344 | logfile = logging.FileHandler(options["output_file"], encoding="utf-8") 1345 | except IOError: 1346 | print(f"[-] Could not log to {options['output_file']}, exiting") 1347 | sys.exit(-1) 1348 | logfile.setFormatter(LogFormatter()) 1349 | logfile.setLevel(COMMAND) 1350 | logger.addHandler(logfile) 1351 | # Don't log the asynchronous commands in the logfile 1352 | logfile.addFilter(LogFilter([COMMAND, STATUS])) 1353 | console = logging.StreamHandler(stream=sys.stdout) 1354 | console.setFormatter(LogFormatter()) 1355 | # Set up a stderr loghandler which only shows error message 1356 | errors = logging.StreamHandler(stream=sys.stderr) 1357 | errors.setFormatter(LogFormatter()) 1358 | errors.setLevel(logging.ERROR) 1359 | console.addFilter(LogFilter([logging.ERROR])) 1360 | console.addFilter(LogFilter([logging.CRITICAL])) 1361 | if options["debug"]: 1362 | console.setLevel(logging.DEBUG) 1363 | elif options["verbose"]: 1364 | console.setLevel(logging.INFO) 1365 | elif options["dry_run"]: 1366 | console.setLevel(COMMAND) 1367 | else: 1368 | console.setLevel(STATUS) 1369 | logger.addHandler(console) 1370 | logger.addHandler(errors) 1371 | if options["compact"]: 1372 | logfile.setLevel(LOGS) 1373 | if options["quiet"]: 1374 | console.addFilter(LogFilter([COMMAND, LOGS])) 1375 | # make sure requests library is, erm, less verbose 1376 | # pylint: disable=E1101 1377 | logging.getLogger("requests.packages.urllib3.connectionpool").setLevel( 1378 | logging.ERROR 1379 | ) 1380 | 1381 | 1382 | def init_results(options): 1383 | """Initialize the results object with basic scan information.""" 1384 | # For now, no support for resumed scans 1385 | results = {} 1386 | results["arguments"] = options 1387 | results["date_start"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") 1388 | results["results"] = {} 1389 | return results 1390 | 1391 | 1392 | def write_json(results, options): 1393 | """Write results to JSON file.""" 1394 | if options["json"]: 1395 | results["date_finish"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") 1396 | json_results = json.dumps(results) 1397 | # Truncating the file, no check yet on whether it exists 1398 | with io.open(options["json"], encoding="utf-8", mode="w+") as open_file: 1399 | open_file.write(json_results) 1400 | logging.log(STATUS, "JSON results saved to %s", options["json"]) 1401 | 1402 | 1403 | def main(): 1404 | """Main program loop.""" 1405 | banner = f"{NAME} version {__version__}" 1406 | options = parse_arguments(banner) 1407 | options["exit"] = 0 1408 | setup_logging(options) 1409 | logging.log(STATUS, "%s starting", banner) 1410 | preflight_checks(options) 1411 | prepare_nmap_arguments(options) 1412 | if not options["resume"]: 1413 | prepare_queue(options) 1414 | options["settings"] = read_settings(options["settings"]) 1415 | logging.debug(options) 1416 | results = init_results(options) 1417 | loop_hosts(options, read_targets(options["queuefile"]), results) 1418 | write_json(results, options) 1419 | if not options["dry_run"]: 1420 | logging.log(STATUS, "Output saved to %s", options["output_file"]) 1421 | sys.exit(options["exit"]) 1422 | 1423 | 1424 | if __name__ == "__main__": 1425 | main() 1426 | -------------------------------------------------------------------------------- /analyze_hosts.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # analyze_hosts - Scans one or more hosts on security vulnerabilities 4 | # 5 | # Copyright (C) 2012-2014 Peter Mosmans 6 | # 7 | # 8 | # This source code (shell script) is subject to the terms of the Mozilla Public 9 | # License, v. 2.0. If a copy of the MPL was not distributed with this 10 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 11 | 12 | # TODO: - add: option to only list commands, don't execute them 13 | # - add: make logging of output default 14 | # - add: grep on errors of ssh script output 15 | # - add: check installation (whether all tools are present) 16 | # - add: generic set tool check 17 | # - change: first do some portlogic before executing tool 18 | # - change: refactor looping of ports 19 | # - change: iterate --ssl commands per port instead of per tool 20 | # - change: better output for issues (grepable) 21 | # - change: move issues to issue tracker 22 | # - change: move changelog to real changelog 23 | # - refactor: git part/version header info 24 | 25 | 26 | # since 0.88: basic starttls xmpp support (port 5222) 27 | # 0.89: whois scan (-w) is default option if nothing is selected 28 | # 0.90: added SSLv3 to the list of dangerous protocols 29 | # 0.91: added check on DNS version string 30 | # 0.92: added AECDH to the list of dangerous ciphers 31 | # 0.93: check for open secure redirect 32 | 33 | 34 | unset CDPATH 35 | 36 | NAME="analyze_hosts" 37 | VERSION="0.93.1" 38 | 39 | # statuses 40 | declare ERROR=-1 41 | declare UNKNOWN=0 42 | declare OPEN=1 UP=1 NONEWLINE=1 BASIC=1 43 | declare ADVANCED=2 44 | declare ALTERNATIVE=4 45 | 46 | # logging and verboseness 47 | declare NOLOGFILE=-1 48 | declare QUIET=1 49 | declare STDOUT=2 50 | declare VERBOSE=4 51 | declare LOGFILE=8 52 | declare RAWLOGS=16 53 | declare SEPARATELOGS=32 54 | 55 | # the global variable message keeps track of the current message type 56 | declare INFO=0 57 | declare OK=1 58 | declare OKTEXT="OK: " 59 | declare WARNING=2 60 | declare WARNINGTEXT="WARNING: " 61 | declare -i defaultmessage=$INFO 62 | 63 | # scantypes 64 | declare -i dnstest=$UNKNOWN fingerprint=$UNKNOWN nikto=$UNKNOWN 65 | declare -i portscan=$UNKNOWN sshscan=$UNKNOWN sslscan=$UNKNOWN 66 | declare -i redirect=$UNKNOWN 67 | declare -i trace=$UNKNOWN whois=$UNKNOWN webscan=$UNKNOWN 68 | 69 | # defaults 70 | declare cipherscan="cipherscan" 71 | declare openssl="openssl" 72 | declare gitsource=https://github.com/PeterMosmans/security-scripts.git 73 | declare -i loglevel=$STDOUT 74 | # timeout for program, eg. cipherscan 75 | declare -i timeout=60 76 | # timeout for single requests 77 | declare -i requesttimeout=10 78 | declare webports=80,443,8080 79 | declare DEFAULTSSLPORTS=443,465,993,995,3389 80 | declare sslports=$DEFAULTSSLPORTS 81 | 82 | # statuses 83 | declare -i hoststatus=$UNKNOWN portstatus=$UNKNOWN 84 | datestring=$(date +%Y-%m-%d) 85 | workdir=. 86 | 87 | # colours 88 | declare BLUE='\E[1;49;96m' LIGHTBLUE='\E[2;49;96m' 89 | declare RED='\E[1;49;31m' LIGHTRED='\E[2;49;31m' 90 | declare GREEN='\E[1;49;32m' LIGHTGREEN='\E[2;49;32m' 91 | declare RESETSCREEN='\E[0m' 92 | 93 | trap abortscan INT 94 | trap cleanup QUIT 95 | 96 | # define functions 97 | prettyprint() { 98 | (($loglevel&$QUIET)) && return 99 | [[ -z $nocolor ]] && echo -ne $2 100 | if [[ "$3" == "$NONEWLINE" ]]; then 101 | echo -n "$1" 102 | else 103 | echo "$1" 104 | fi 105 | [[ -z $nocolor ]] && echo -ne ${RESETSCREEN} 106 | } 107 | 108 | usage() { 109 | local realpath=$(dirname $(readlink -f $0)) 110 | if [[ -d $realpath/.git ]]; then 111 | pushd $realpath 1>/dev/null 2>&1 112 | local branch=$(git rev-parse --abbrev-ref HEAD) 113 | local commit=$(git log|head -1|awk '{print $2}'|cut -c -10) 114 | popd 1>/dev/null 115 | prettyprint "$NAME (git) from ${branch} branch commit ${commit}" $BLUE 116 | else 117 | prettyprint "$NAME version $VERSION" $BLUE 118 | fi 119 | prettyprint " (c) 2012-2015 Peter Mosmans [Go Forward]" $LIGHTBLUE 120 | prettyprint " Licensed under the Mozilla Public License 2.0" $LIGHTBLUE 121 | echo "" 122 | echo " usage: $0 [OPTION]... [HOST]" 123 | echo "" 124 | echo "Scanning options:" 125 | echo " -a, --all perform all basic scans" 126 | echo " --max perform all advanced scans (more thorough)" 127 | echo " -b, --basic perform basic scans (fingerprint, ssl, trace)" 128 | echo " results of HOST matches regexp FILTER" 129 | echo " --dns test for recursive query and version string" 130 | echo " -f perform web fingerprinting (all webports)" 131 | echo " --fingerprint perform all web fingerprinting methods" 132 | echo " -h, --header show webserver headers (all webports)" 133 | echo " -n, --nikto nikto webscan (all webports)" 134 | echo " -p nmap portscan (top 1000 TCP ports)" 135 | echo " --ports nmap portscan (all ports, TCP and UDP)" 136 | echo " --redirect test for open secure redirect" 137 | echo " -s check SSL configuration" 138 | echo " --ssl perform all SSL configuration checks" 139 | echo " --sslcert show details of SSL certificate" 140 | echo " --timeout=SECONDS change timeout for tools (default ${timeout})" 141 | echo " --ssh perform SSH configuration checks" 142 | echo " -t check webserver for HTTP TRACE method" 143 | echo " --trace perform all HTTP TRACE method checks" 144 | echo " -w, --whois perform WHOIS lookup for (hostname and) IP address" 145 | echo " -W confirm WHOIS results before continuing scan" 146 | echo " --filter=FILTER only proceed with scan of HOST if WHOIS" 147 | echo " --wordlist=filename scan webserver for existence of files in filename" 148 | echo "" 149 | echo "Port selection (comma separated list):" 150 | echo " --webports=PORTS use PORTS for web scans (default $webports)" 151 | echo " --sslports=PORTS use PORTS for ssl scans (default $sslports)" 152 | echo "" 153 | echo "Logging and input file:" 154 | echo " -d, --directory=DIR location of temporary files (default /tmp)" 155 | echo " -i, --inputfile=FILE use a file containing hostnames" 156 | echo " -l, --log log each scan in a separate logfile" 157 | echo " --nocolor don't use fancy colors in screen output" 158 | echo " -o, --output=FILE concatenate all OK and WARNING messages into FILE" 159 | echo " -q, --quiet quiet" 160 | echo " -v, --verbose show server responses" 161 | echo "" 162 | echo "Default programs:" 163 | echo " --cipherscan=FILE location of cipherscan (default ${cipherscan})" 164 | echo " --openssl=FILE location of openssl (default ${openssl})" 165 | echo "" 166 | echo " -u update this script (if it's a cloned repository)" 167 | echo " --update force update (overwrite all local modifications)" 168 | echo " --version print version information and exit" 169 | echo "" 170 | prettyprint " BLUE: INFO, status messages" $BLUE 171 | prettyprint " GREEN: OK, secure settings" $GREEN 172 | prettyprint " RED: WARNING, possible vulnerabilities" $RED 173 | echo "" 174 | echo " [HOST] can be a single (IP) address, an IP range, eg. 127.0.0.1-255" 175 | echo " or multiple comma-separated addressess" 176 | echo "" 177 | echo "example: $0 -a --filter Amazon www.google.com" 178 | echo "" 179 | } 180 | 181 | # starttool (name) 182 | # GLOBAL: logfile 183 | # resultsfile 184 | starttool() { 185 | checkfortool $1 186 | tool=$(basename $1) 187 | logfile=$workdir/${target}_${tool}_${datestring}.txt 188 | resultsfile=$workdir/${target}_${tool}_${datestring}_results.txt 189 | } 190 | 191 | ################################################################################ 192 | # Checks whether a program exists 193 | # 194 | # Parameters: tool 195 | ################################################################################ 196 | checkfortool() { 197 | if [ ! $(which $1 2>/dev/null) ] && [[ ! ("$1 --version") ]] ; then 198 | showstatus "ERROR: The program $1 could not be found" $RED 199 | tool=$ERROR 200 | exit 201 | fi 202 | } 203 | 204 | ################################################################################ 205 | # Purges the current logfile and resets message parameter to default message 206 | # 207 | # Parameters: [LOGLEVEL] if LOGLEVEL == VERBOSE then show log on screen 208 | ################################################################################ 209 | purgelogs() { 210 | local currentloglevel=$loglevel 211 | if [[ ! -z $1 ]]; then let "loglevel=loglevel|$1"; fi 212 | if [[ ! -z "$$logfile" ]] && [[ -f "$logfile" ]]; then 213 | if (($loglevel&$VERBOSE)); then 214 | if [[ -s "$logfile" ]]; then 215 | showstatus "$(grep -v '^#' $logfile)" 216 | # cat $logfile | grep -v '^#' 217 | # showstatus "" 218 | fi 219 | fi 220 | if (($loglevel&$RAWLOGS)); then 221 | grep -v '^[#%]' $logfile >> $outputfile 222 | fi 223 | if !(($loglevel&$SEPARATELOGS)); then rm -f $logfile 1>/dev/null 2>&1; fi 224 | fi 225 | loglevel=$currentloglevel 226 | message=$defaultmessage 227 | rm -f $resultsfile 228 | } 229 | 230 | # clears logfiles 231 | clearlogs() { 232 | rm -f $logfile 233 | rm -f $resultsfile 234 | } 235 | 236 | endtool() { 237 | purgelogs 238 | tool=$ERROR 239 | } 240 | 241 | 242 | ################################################################################ 243 | # Shows a status message on the screen 244 | # 245 | # Parameters: message [COLOR] [LOGFILE|NOLOGFILE|NONEWLINE] 246 | # COLOR: color of message 247 | # LOGFILE: only write contents to logfile 248 | # NOLOGFILE: don't log contents to logfile 249 | # NONEWLINE: don't echo new line character, and save message 250 | ################################################################################ 251 | showstatus() { 252 | if [[ ! -z "$2" ]]; then 253 | case "$2" in 254 | $LOGFILE) 255 | (($loglevel&$LOGFILE)) && echo "${linebuffer}$1" >> $outputfile 256 | linebuffer="";; 257 | $NOLOGFILE) 258 | !(($loglevel&$QUIET)) && echo "$1" 259 | linebuffer="";; 260 | $NONEWLINE) 261 | linebuffer="$1" 262 | !(($loglevel&$QUIET)) && echo -n "$1" 263 | (($loglevel&$LOGFILE)) && echo -n "$1" >> $outputfile;; 264 | (*) 265 | prettyprint "$1" $2 $3 266 | (($loglevel&$LOGFILE)) && echo "${linebuffer}${1}" >> $outputfile 267 | linebuffer="";; 268 | esac 269 | else 270 | !(($loglevel&$QUIET)) && echo "$1" 271 | (($loglevel&$LOGFILE)) && echo "$1"|grep "." >> $outputfile 272 | fi 273 | } 274 | 275 | ################################################################################ 276 | # Updates the script from the git repository 277 | # 278 | # Parameters: [1] force update 279 | ################################################################################ 280 | do_update() { 281 | local force=false 282 | [[ ! -z "$1" ]] && local force=true 283 | local realpath=$(dirname $(readlink -f $0)) 284 | local branch="unkown" 285 | local commit="unknown" 286 | if [[ -d $realpath/.git ]]; then 287 | starttool "git" 288 | pushd $realpath 1>/dev/null 2>&1 289 | local status=$UNKNOWN 290 | branch=$(git rev-parse --abbrev-ref HEAD) 291 | commit=$(git rev-parse --short HEAD) 292 | showstatus "current version: ${branch} branch, commit ${commit}" 293 | if $force; then 294 | showstatus "forcing update, overwriting local changes" 295 | git fetch origin master 1>$logfile 2>&1 296 | git reset --hard FETCH_HEAD 1>>$logfile 2>&1 297 | else 298 | git pull 1>$logfile 2>&1 299 | fi 300 | commit=$(git rev-parse --short HEAD) 301 | grep -Eq "error: |Permission denied" $logfile && status=$ERROR 302 | grep -q "Already up-to-date." $logfile && status=$OPEN 303 | case $status in 304 | $ERROR) showstatus "error updating $0" $RED 305 | showstatus "" 306 | showstatus "use --update to force an update, overwriting local changes";; 307 | $UNKNOWN) showstatus "succesfully updated to latest version (commit ${commit})" $GREEN 308 | showstatus "$(git log --oneline -n 1)";; 309 | $OPEN) showstatus "already running latest version" $BLUE;; 310 | esac 311 | popd 1>/dev/null 2>&1 312 | endtool 313 | else 314 | if $force; then 315 | git clone $gitsource 316 | else 317 | showstatus "Sorry, this doesn't seem to be a git archive - cannot update" $RED 318 | showstatus "Please clone the repository using the following command: " 319 | showstatus "git clone ${gitsource}" 320 | showstatus "" 321 | showstatus "use --update to do this automatically, creating an archive in ${realpath}" 322 | fi 323 | fi 324 | exit 0 325 | } 326 | 327 | startup() { 328 | # always log the startup message 329 | message=$OK 330 | flag=$OPEN 331 | trap cleanup EXIT 332 | showstatus "$NAME version $VERSION starting on $(date +%d-%m-%Y' at '%R)" 333 | if (($loglevel&$LOGFILE)); then 334 | if [[ -n $appendfile ]]; then 335 | showstatus "appending to existing file $outputfile" 336 | else 337 | showstatus "logging to $outputfile" 338 | fi 339 | fi 340 | showstatus "scanparameters: $options" $LOGFILE 341 | # set the default message status 342 | message=$defaultmessage 343 | [[ -n "$workdir" ]] && pushd $workdir 1>/dev/null 2>&1 344 | if ! [ -w $(pwd) ]; then 345 | showstatus "ERROR: cannot write to directory $(pwd)" $RED 346 | showstatus " please specify writable directory using -d" 347 | exit 348 | fi 349 | } 350 | 351 | version() { 352 | curl --version 353 | echo "" 354 | nikto -Version 355 | echo "" 356 | nmap -V 357 | echo "" 358 | prettyprint "$NAME version $VERSION" $BLUE 359 | prettyprint " (c) 2013-2015 Peter Mosmans [Go Forward]" $LIGHTBLUE 360 | prettyprint " Licensed under the Mozilla Public License 2.0" $LIGHTBLUE 361 | echo "" 362 | } 363 | 364 | checkifportopen() { 365 | portstatus=$UNKNOWN 366 | if [[ -s "$portselection" ]]; then 367 | portstatus=$ERROR 368 | grep -q " $1/open/" $portselection && portstatus=$OPEN 369 | fi 370 | } 371 | 372 | 373 | do_redirect() { 374 | starttool "curl" 375 | status=$UNKNOWN 376 | showstatus "trying open secure redirect... " $NONEWLINE 377 | curl -sIH "Host: vuln" http://$target/|grep -q "Location: https\?://vuln/" && status=$OPEN 378 | if (($status==$OPEN)); then 379 | message=$WARNING 380 | showstatus "open secure redirect" $RED 381 | else 382 | message=$OK 383 | showstatus "no open secure redirect" $GREEN 384 | fi 385 | endtool 386 | } 387 | 388 | 389 | do_dnstest() { 390 | starttool "dig" 391 | local status=$UNKNOWN 392 | local ports=53 393 | showstatus "trying recursive dig... " $NONEWLINE 394 | dig google.com @$target 1>$logfile 2>&1 $logfile 2>&1 $resultsfile 407 | if [[ -s $resultsfile ]] ; then 408 | showstatus "version string shown: $(cat $resultsfile)" $RED 409 | else 410 | showstatus "no version string shown" $GREEN 411 | fi 412 | endtool 413 | starttool "nmap" 414 | showstatus "trying to retrieve version string using nmap... " $NONEWLINE 415 | nmap -sSU -p 53 --script dns-nsid -oN $logfile $target 1>/dev/null 2>&1 $resultsfile 418 | if [[ -s $resultsfile ]]; then 419 | showstatus "version string shown: $(cat $resultsfile)" $RED 420 | else 421 | showstatus "no version string shown" $GREEN 422 | fi 423 | else 424 | showstatus "no version string received" 425 | fi 426 | endtool 427 | } 428 | 429 | do_fingerprint() { 430 | if (($fingerprint>=$BASIC)); then 431 | starttool "whatweb" 432 | for port in ${webports//,/ }; do 433 | starttool "whatweb" 434 | showstatus "performing whatweb fingerprinting on $target port $port... " $NONEWLINE 435 | if [[ ! $sslports =~ $port ]]; then 436 | whatweb -a3 --color never http://$target:$port --log-brief $logfile 1>/dev/null 2>&1 437 | else 438 | whatweb -a3 --color never https://$target:$port --log-brief $logfile 1>/dev/null 2>&1 439 | fi 440 | if [[ -s $logfile ]]; then 441 | message=$OK 442 | showstatus "connected" $GREEN 443 | else 444 | showstatus "could not connect" $BLUE 445 | fi 446 | purgelogs $VERBOSE 447 | done 448 | endtool 449 | fi 450 | 451 | if (($fingerprint>=$ADVANCED)); then 452 | starttool "curl" 453 | for port in ${webports//,/ }; do 454 | showstatus "retrieving headers from $target port $port... " $NONEWLINE 455 | if [[ ! $sslports =~ $port ]]; then 456 | curl -A "$NAME" -q --insecure -m 10 --dump-header $logfile http://$target:$port 1>/dev/null 2>&1 457 | else 458 | curl -A "$NAME" -q --insecure -m 10 --dump-header $logfile https://$target:$port 1>/dev/null 2>&1 459 | fi 460 | if [[ -s $logfile ]]; then 461 | message=$OK 462 | showstatus "connected" $GREEN 463 | else 464 | showstatus "could not connect" $BLUE 465 | fi 466 | purgelogs $VERBOSE 467 | done 468 | endtool 469 | 470 | starttool "nmap" 471 | # get a combination of open and closed ports for best results 472 | # currently top 15 open TCP ports plus bottom 3 ports 473 | ports=80,23,443,21,22,25,3389,110,445,139,143,53,135,3306,8080,117,116,108 474 | showstatus "performing nmap OSfingerprinting using ports ${ports}... " 475 | nmap -Pn -O -p$ports --open --script banner.nse,sshv1.nse,ssh-hostkey.nse,ssh2-enum-algos.nse -oN $logfile $target 1>/dev/null 2>&1 /dev/null 2>&1 =$ADVANCED)); then 510 | showstatus "performing advanced nmap portscan on $target (TCP, UDP, all ports)... " $NONEWLINE 511 | nmap --open -p- -sS -sU -sV -sC -oN $logfile -oG $portselection $target 1>/dev/null 2>&1 /dev/null 2>&1 =$BASIC)); then 529 | starttool "nmap" 530 | local portstatus=$UNKNOWN 531 | local ports=22 532 | showstatus "trying nmap SSH scan on $target port $ports... " $NONEWLINE 533 | nmap -Pn -p $ports --open --script banner.nse,sshv1.nse,ssh-hostkey.nse,ssh2-enum-algos.nse -oN $logfile $target 1>/dev/null 2>&1 =$BASIC)); then 564 | starttool "${cipherscan}" 565 | #TODO: needs to check for openssl as well 566 | for port in ${sslports//,/ }; do 567 | showstatus "performing cipherscan on $target port $port... " $NONEWLINE 568 | # TODO refactor this 569 | if [[ "$port" == "5222" ]]; then 570 | extracmd="-starttls xmpp" 571 | else 572 | extracmd="" 573 | fi 574 | # cipherscan wants (kn)own options first, openssl options last 575 | "${cipherscan}" -o "${openssl}" ${extracmd} -servername ${target} ${target}:${port} 1>${logfile} 576 | if [[ -s $logfile ]] ; then 577 | # Check if cipherscan was able to connect to the server 578 | failedstring="Certificate: UNTRUSTED, bit, signature" 579 | grep -q "$failedstring" $logfile && portstatus=$ERROR 580 | if ((portstatus!=$ERROR)); then 581 | message=$OK 582 | showstatus "connected" $GREEN 583 | awk '/^[0-9].*(ADH|AECDH|RC4|IDEA|SSLv2|SSLv3|EXP|MD5|NULL| 40| 56)/{print $2,$3}' $logfile > $resultsfile 584 | if [[ -s $resultsfile ]]; then 585 | message=$WARNING 586 | showstatus "${WARNINGTEXT}Weak/insecure SSL/TLS ciphers/protocols supported" $RED 587 | showstatus "$(cat $resultsfile)" $RED 588 | fi 589 | purgelogs 590 | parse_cert $target $port 591 | fi 592 | else 593 | portstatus=$ERROR 594 | fi 595 | if (($portstatus==$ERROR)); then 596 | showstatus "could not connect" $BLUE 597 | clearlogs 598 | else 599 | purgelogs 600 | fi 601 | done 602 | endtool 603 | rm -f $resultsfile 604 | fi 605 | 606 | if (($sslscan>=$ADVANCED)); then 607 | starttool "nmap" 608 | showstatus "performing nmap sslscan on $target ports $sslports..." 609 | nmap -p $sslports --script ssl-enum-ciphers,ssl-heartbleed,rdp-enum-encryption --open -oN $logfile $target 1>/dev/null 2>&1 $resultsfile 612 | if [[ -s $resultsfile ]]; then 613 | message=$WARNING 614 | showstatus "${WARNINGTEXT}Weak/insecure SSL/TLS ciphers/protocols supported" $RED 615 | showstatus "$(cat $resultsfile)" $RED 616 | fi 617 | else 618 | showstatus "could not connect to $target ports $sslports" $BLUE 619 | fi 620 | endtool 621 | fi 622 | } 623 | 624 | do_trace() { 625 | starttool "curl" 626 | for port in ${webports//,/ }; do 627 | local prefix="http://" 628 | [[ $sslports =~ $port ]] && prefix="--insecure https://" 629 | showstatus "trying $target port $port... " $NONEWLINE 630 | curl -Iqs -A "$NAME" -i -m 30 -X TRACE -o $logfile $prefix$target:$port/ 1>/dev/null 2>&1 631 | if [[ -s $logfile ]]; then 632 | status=$(awk 'NR==1 {print $2}' $logfile) 633 | if (($status==200)); then 634 | message=$WARNING 635 | showstatus "TRACE enabled on port $port" $RED 636 | else 637 | message=$OK 638 | showstatus "disabled (HTTP $status)" $GREEN 639 | fi 640 | else 641 | showstatus "could not connect" $BLUE 642 | fi 643 | purgelogs 644 | done 645 | endtool 646 | 647 | if (($trace>=$ADVANCED)); then 648 | starttool "nmap" 649 | showstatus "trying nmap TRACE method on $target ports $webports... " $NONEWLINE 650 | nmap -p$webports --open --script http-trace -oN $logfile $target 1>/dev/null 2>&1 =$BASIC)); then 698 | local nomatch= 699 | local ip= 700 | starttool "whois" 701 | if [[ $target =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then 702 | ip=$target 703 | local reverse=$(host $target|awk '{print $5}'|sed s'/[.]$//') 704 | if [[ "$reverse" == "3(NXDOMAIN)" ]] ; then 705 | showstatus "$target does not resolve to a PTR record" 706 | else 707 | showstatus "$target resolves to " $NONEWLINE 708 | showstatus $reverse $BLUE 709 | fi 710 | else 711 | whois ${target#*.} > $logfile 712 | if [ -z $logfile ]; then 713 | grep -q "No match for" $logfile && whois ${target%%*.} > $logfile 714 | # not all whois servers use the same formatting 715 | showstatus "$(grep -iE '^(registra|date|admin |tech|name server)(.*):(.*)[^ ]$' $logfile)" 716 | showstatus "$(awk '/Registrar( Technical Contacts)*:[ ]*$|(Domain )*[Nn]ameservers:[ ]*$|Technical:[ ]*$/{s=1}s; /^$/{s=0}' $logfile)" 717 | fi 718 | ip=$(host -c IN $target|awk '/address/{print $4}'|head -1) 719 | if [[ ! -n "$ip" ]]; then 720 | message=$WARNING 721 | showstatus "$target does not resolve to an IP address - aborting scans" $RED 722 | purgelogs 723 | return 724 | else 725 | showstatus "$target resolves to $ip" 726 | fi 727 | fi 728 | purgelogs 729 | # not all versions of whois support -H (hide legal disclaimer) 730 | whois -H $ip 1>$logfile 2>/dev/null 731 | showstatus "$(grep -iE '^(inetnum|netrange|netname|nettype|descr|orgname|orgid|originas|country|origin):(.*)[^ ]$' $logfile)" 732 | if [[ -n "$filter" ]]; then 733 | if grep -qiE "^(inetnum|netrange|netname|nettype|descr|orgname|orgid|originas|country|origin):.*($filter)" $logfile; then 734 | message=$OK 735 | showstatus "WHOIS info matches $filter - continuing scans" $GREEN 736 | else 737 | message=$WARNING 738 | showstatus "WHOIS info doesn't match $filter - aborting scans on $target" $RED 739 | purgelogs 740 | return 741 | fi 742 | fi 743 | 744 | (($whois&$ADVANCED)) && read -p "press ENTER to continue: " failsafe < /dev/stdin 745 | purgelogs 746 | endtool 747 | fi 748 | 749 | (($portscan>=$BASIC)) && do_portscan 750 | (($redirect>=$BASIC)) && do_redirect 751 | (($dnstest>=$BASIC)) && do_dnstest 752 | (($fingerprint>=$BASIC)) && do_fingerprint 753 | (($nikto>=$BASIC)) && do_nikto 754 | (($sshscan>=$BASIC)) && do_sshscan 755 | (($sslscan>=$BASIC)) && do_sslscan 756 | (($trace>=$BASIC)) && do_trace 757 | (($webscan>=$BASIC)) && do_webscan 758 | } 759 | 760 | looptargets() { 761 | if [[ -s "$inputfile" ]]; then 762 | total=$(grep -c . $inputfile) 763 | local counter=1 764 | while read target; do 765 | if [[ ! -z "$target" ]]; then 766 | showstatus "" 767 | showstatus "working on " $NONEWLINE 768 | showstatus "$target" $BLUE $NONEWLINE 769 | showstatus " ($counter of $total)" 770 | let counter=$counter+1 771 | execute_all 772 | fi 773 | done < "$inputfile" 774 | else 775 | showstatus "" 776 | showstatus "working on " $NONEWLINE 777 | showstatus "$target" $BLUE 778 | execute_all 779 | fi 780 | } 781 | 782 | abortscan() { 783 | flag=$ERROR 784 | if [[ "$tool" != "$ERROR" ]]; then 785 | showstatus "" 786 | showstatus "interrupted $tool while working on $target..." $RED 787 | purgelogs 788 | prettyprint "press Ctrl-C again to abort scan, or wait 10 seconds to resume" $BLUE 789 | sleep 10 && flag=$OPEN 790 | fi 791 | ((flag==$ERROR)) && exit 1 792 | } 793 | 794 | cleanup() { 795 | trap '' EXIT INT QUIT 796 | if [[ ! -z $tool ]] && [[ "$ERROR" != "$tool" ]]; then 797 | message=$WARNING 798 | showstatus "$tool interrupted..." $RED 799 | purgelogs 800 | fi 801 | showstatus "cleaning up temporary files..." 802 | [[ -e "$logfile" ]] && rm "$logfile" 803 | [[ -e "$portselection" ]] && rm $portselection 1>/dev/null 2>&1 804 | [[ -e "$resultsfile" ]] && rm "$resultsfile" 805 | [[ -n "$workdir" ]] && popd 1>/dev/null 806 | [[ -e "$tmpfile" ]] && rm "$tmpfile" 807 | (($loglevel&$LOGFILE)) && showstatus "logged to $outputfile" $NOLOGFILE 808 | # always log the end message 809 | message=$OK 810 | showstatus "ended on $(date +%d-%m-%Y' at '%R)" 811 | exit 812 | } 813 | 814 | timeoutstring() { 815 | # workaround for systems that don't have GNU timeout (eg. Windows) 816 | if timeout --version >/dev/null 2>&1; then 817 | echo "timeout $1" 818 | else 819 | echo "" 820 | fi 821 | } 822 | 823 | ################################################################################ 824 | # Retrieves a x.509 certificate and shows whether the dates are valid 825 | # 826 | # Arguments: 1 target 827 | # 2 port 828 | # 829 | # GLOBAL message: OK when certificate could be retrieved 830 | # WARNING when certificate could be retrieved but isn't valid 831 | ################################################################################ 832 | parse_cert() { 833 | local target=$1 834 | local port=$2 835 | # TODO refactor this 836 | if [[ "$port" == "5222" ]]; then 837 | extracmd="-starttls xmpp" 838 | else 839 | extracmd="" 840 | fi 841 | starttool $openssl 842 | if [[ "$tool" != "$ERROR" ]]; then 843 | timeoutcmd=$(timeoutstring $requesttimeout) 844 | showstatus "trying to retrieve SSL x.509 certificate on ${target}:${port}... " $NONEWLINE 845 | certificate=$(mktemp -q $NAME.XXXXXXX) 846 | echo Q | $timeoutcmd $openssl s_client ${extracmd} -connect $target:$port -servername $target 1>$certificate 2>/dev/null 847 | if [[ -s $certificate ]] && $openssl x509 -in $certificate -noout 1>/dev/null 2>&1; then 848 | message=$OK 849 | showstatus "received" $GREEN 850 | showstatus "$($openssl x509 -noout -issuer -subject -nameopt multiline -in $certificate 2>/dev/null)" 851 | startdate=$($openssl x509 -noout -startdate -in $certificate 2>/dev/null|cut -d= -f 2) 852 | enddate=$($openssl x509 -noout -enddate -in $certificate 2>/dev/null|cut -d= -f 2) 853 | parsedstartdate=$(date --date="$startdate" +%Y%m%d) 854 | parsedenddate=$(date --date="$enddate" +%Y%m%d) 855 | localizedstartdate=$(date --date="$startdate" +%d-%m-%Y) 856 | localizedenddate=$(date --date="$enddate" +%d-%m-%Y) 857 | if [[ $parsedstartdate -gt $(date +%Y%m%d) ]]; then 858 | message=$WARNING 859 | showstatus "${WARNINGTEXT}certificate is not valid yet, valid from ${localizedstartdate} until ${localizedenddate}" $RED 860 | else 861 | if [[ $parsedenddate -lt $(date +%Y%m%d) ]]; then 862 | message=$WARNING 863 | showstatus "${WARNINGTEXT}certificate has expired on ${localizedenddate}" $RED 864 | else 865 | showstatus "${OKTEXT}certificate is valid between ${localizedstartdate} and ${localizedenddate}" $GREEN 866 | fi 867 | fi 868 | # check if certificate is self-signed 869 | if [[ "$(openssl x509 -noout -subject_hash -in $certificate 2>/dev/null)" == "$(openssl x509 -noout -issuer_hash -in $certificate 2>/dev/null)" ]]; then 870 | message=$WARNING 871 | showstatus "${WARNINGTEXT}self-signed certificate" $RED 872 | fi 873 | # check if certificate is in any way authoritative 874 | # ignore any purpose CA flag, since it's always true 875 | if openssl x509 -noout -purpose -in $certificate 2>/dev/null|grep -v '^Any Purpose'|grep -q ' CA : Yes'; then 876 | message=$WARNING 877 | showstatus "${WARNINGTEXT}certificate has Certificate Authority purposes set" $RED 878 | showstatus "$($openssl x509 -noout -purpose -in $certificate 2>/dev/null|grep 'CA : Yes')" $RED 879 | fi 880 | else 881 | showstatus "failed" $BLUE 882 | fi 883 | rm -f $certificate 1>/dev/null 884 | fi 885 | endtool 886 | } 887 | 888 | # if [ -x /usr/bin/tput ] && tput setaf 1 >&/dev/null; then 889 | 890 | if ! options=$(getopt -o ad:fhi:lno:pqstuvwWy -l cipherscan:,dns,directory:,filter:,fingerprint,header,inputfile:,log,max,nikto,nocolor,openssl:,output:,ports,quiet,redirect,ssh,ssl,sslcert,sslports:,timeout:,trace,update,version,webports:,whois,wordlist: -- "$@") ; then 891 | usage 892 | exit 1 893 | fi 894 | 895 | eval set -- $options 896 | if [[ "$#" -le 1 ]]; then 897 | usage 898 | exit 1 899 | fi 900 | 901 | # set default option if only a target (or targetfile) is specified 902 | if [[ "$#" -eq 2 ]] && ! [[ $2 =~ ^- ]]; then 903 | whois=$BASIC 904 | fi 905 | 906 | while [[ $# -gt 0 ]]; do 907 | case $1 in 908 | -a|--all) 909 | dnstest=$BASIC 910 | fingerprint=$BASIC 911 | nikto=$BASIC 912 | portscan=$BASIC 913 | redirect=$BASIC 914 | sshscan=$BASIC 915 | sslscan=$BASIC 916 | trace=$BASIC 917 | whois=$BASIC;; 918 | --allports) portscan=$ADVANCED;; 919 | --cipherscan) 920 | cipherscan=$2 921 | [[ ! $cipherscan =~ ^/ ]] && cipherscan="$(pwd)/$cipherscan" 922 | if [[ ! -s "$cipherscan" ]]; then 923 | echo "error: cannot find $cipherscan" 924 | exit 1 925 | fi 926 | shift;; 927 | --dns) dnstest=$ADVANCED;; 928 | -f) fingerprint=$BASIC;; 929 | --fingerprint) fingerprint=$ADVANCED;; 930 | -h|--header) fingerprint=$ALTERNATIVE;; 931 | -d|--directory) workdir=$2 932 | shift ;; 933 | --filter) filter="$2" 934 | let "whois=whois|$BASIC" 935 | shift ;; 936 | -i|--inputfile) inputfile="$2" 937 | [[ ! $inputfile =~ ^/ ]] && inputfile=$(pwd)/$inputfile 938 | if [[ ! -s "$inputfile" ]]; then 939 | echo "error: cannot find $inputfile" 940 | exit 1 941 | fi 942 | shift ;; 943 | -l) log="TRUE";; 944 | --max) 945 | dnstest=$ADVANCED 946 | fingerprint=$ADVANCED 947 | nikto=$ADVANCED 948 | portscan=$ADVANCED 949 | redirect=$ADVANCED 950 | sshscan=$ADVANCED 951 | sslscan=$ADVANCED 952 | trace=$ADVANCED 953 | whois=$ADVANCED;; 954 | -n) nikto=$BASIC;; 955 | --nikto) nikto=$ADVANCED;; 956 | --nocolor) nocolor=TRUE;; 957 | -o|--output) 958 | let "loglevel=loglevel|$LOGFILE" 959 | outputfile=$2 960 | [[ ! $outputfile =~ ^/ ]] && outputfile=$(pwd)/$outputfile 961 | [[ -s $outputfile ]] && appendfile=1 962 | shift ;; 963 | --openssl) 964 | openssl=$2 965 | shift;; 966 | -p) portscan=$BASIC;; 967 | --ports) portscan=$ADVANCED;; 968 | --webports) webports=$2 969 | shift ;; 970 | --sslports) sslports=$2 971 | shift ;; 972 | -q|--quiet) let "loglevel=loglevel|$QUIET";; 973 | --redirect) redirect=$BASIC;; 974 | -s) let "sslscan=sslscan|$BASIC";; 975 | --ssh) sshscan=$BASIC;; 976 | --ssl) let "sslscan=sslscan|$ADVANCED";; 977 | --sslcert) let "sslscan=sslscan|$ALTERNATIVE";; 978 | -t) trace=$BASIC;; 979 | --timeout) timeout=$2 980 | shift ;; 981 | --trace) trace=$ADVANCED;; 982 | -u) do_update && exit 0;; 983 | --update) do_update 1 && exit 0;; 984 | -v) let "loglevel=loglevel|$VERBOSE";; 985 | --version) version; 986 | exit 0;; 987 | -w|--whois) whois=$BASIC;; 988 | -W) let "whois=whois|$ADVANCED";; 989 | --wordlist) let "webscan=webscan|$BASIC" 990 | wordlist=$2 991 | [[ ! $wordlist =~ ^/ ]] && wordlist=$(pwd)/$wordlist 992 | shift ;; 993 | (--) shift; 994 | break;; 995 | (-*) echo "$0: unrecognized option $1" 1>&2; exit 1;; 996 | (*) break;; 997 | esac 998 | shift 999 | done 1000 | 1001 | #if ! type nmap >/dev/null 2>&1; then 1002 | # prettyprint "ERROR: the program nmap is needed but could not be found" $RED 1003 | # exit 1004 | #fi 1005 | 1006 | if [[ ! -r "$inputfile" ]]; then 1007 | if [[ -z "$1" ]]; then 1008 | echo "Nothing to do... no target specified" 1009 | exit 1010 | fi 1011 | umask 177 1012 | if [[ -n "$workdir" ]]; then 1013 | [[ -d $workdir ]] || mkdir $workdir 1>/dev/null 2>&1 1014 | fi 1015 | tmpfile=$(mktemp -q $workdir/$NAME.XXXXXXX) 1016 | if [[ $1 =~ -.*[0-9]$ ]]; then 1017 | starttool "nmap" 1018 | nmap -nsL $1 2>/dev/null|awk '/scan report/{print $5}' >$tmpfile 1019 | endtool 1020 | inputfile=$tmpfile 1021 | fi 1022 | if [[ $1 =~ , ]]; then 1023 | for targets in ${1//,/ }; do 1024 | echo $targets >> $tmpfile 1025 | done 1026 | inputfile=$tmpfile 1027 | fi 1028 | fi 1029 | 1030 | target=$1 1031 | startup 1032 | looptargets 1033 | -------------------------------------------------------------------------------- /display_results.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import ipaddress 5 | import json 6 | import sys 7 | import textwrap 8 | 9 | try: 10 | from colorama import Fore, Style 11 | except ImportError as exception: 12 | print( 13 | f"[-] Please install required modules, e.g. pip3 install -r requirements.txt: {exception}", 14 | file=sys.stderr, 15 | ) 16 | 17 | NAME = "display_results" 18 | VERSION = "0.3.0" 19 | ALLOWED_PORTS = [80, 443] 20 | 21 | 22 | def parse_arguments(banner): 23 | """Parse command line arguments.""" 24 | parser = argparse.ArgumentParser( 25 | formatter_class=argparse.RawDescriptionHelpFormatter, 26 | description=textwrap.dedent( 27 | banner 28 | + """\ 29 | - displays scan results nicely 30 | 31 | Copyright (C) 2020 Peter Mosmans [Go Forward] 32 | This program is free software: you can redistribute it and/or modify 33 | it under the terms of the GNU General Public License as published by 34 | the Free Software Foundation, either version 3 of the License, or 35 | (at your option) any later version.""" 36 | ), 37 | ) 38 | parser.add_argument( 39 | "inputfile", 40 | nargs="?", 41 | action="store", 42 | type=str, 43 | help="A JSON file containing scan results", 44 | ) 45 | parser.add_argument( 46 | "--empty", action="store_true", help="Show hosts without any results", 47 | ) 48 | parser.add_argument( 49 | "--host", action="store", help="Filter on specific host", 50 | ) 51 | parser.add_argument( 52 | "--info", 53 | action="store_true", 54 | help="Show also informational items", 55 | default=False, 56 | ) 57 | parser.add_argument( 58 | "--ports", 59 | action="store_true", 60 | help=f"Show ports other than {ALLOWED_PORTS} that are open", 61 | ) 62 | parser.add_argument("--version", action="store_true", help="Show version and exit") 63 | args = parser.parse_args() 64 | if args.version: 65 | print(banner) 66 | sys.exit(0) 67 | if not args.inputfile: 68 | parser.error("Specify a JSON input file") 69 | return vars(parser.parse_args()) 70 | 71 | 72 | def sorted_hosts(unsorted): 73 | """Return a list of hosts sorted on IP address.""" 74 | return map(str, sorted([ipaddress.ip_address(x) for x in unsorted])) 75 | 76 | 77 | def format_alert(alert): 78 | """Return a string formatted as an alert.""" 79 | return f"{Fore.GREEN}{alert}{Fore.RESET}" 80 | 81 | 82 | def display_json(filename, **options): 83 | """Display filename sorted on IP address.""" 84 | try: 85 | with open(filename, mode="r", encoding="utf-8") as json_file: 86 | results = json.load(json_file) 87 | print( 88 | f"Scan of {results['arguments']['target']} was started at {Style.BRIGHT}{results['date_start']}{Style.NORMAL}" 89 | ) 90 | hosts = results["results"] 91 | targets = sorted_hosts(hosts) 92 | for target in targets: 93 | if options["host"] and target != options["host"]: 94 | continue 95 | result = {} 96 | for port in hosts[target]["ports"]: 97 | result[str(port)] = [] 98 | if "info" in hosts[target] and options["info"]: 99 | for port in hosts[target]["info"]: 100 | for item in hosts[target]["info"][port]: 101 | result[port].append(item) 102 | if "alerts" in hosts[target]: 103 | for port in hosts[target]["alerts"]: 104 | for item in hosts[target]["alerts"][port]: 105 | result[port].append(format_alert(item)) 106 | if options["empty"] or len(result) > 1: 107 | print(f"{Style.BRIGHT}{target}{Style.NORMAL}") 108 | for port, items in result.items(): 109 | if ( 110 | not len(items) 111 | and options["ports"] 112 | and (int(port) not in ALLOWED_PORTS) 113 | ): 114 | print(f" {Fore.MAGENTA}{port:>5}{Fore.RESET} OPEN PORT") 115 | for item in items: 116 | print(f" {Fore.MAGENTA}{port:>5}{Fore.RESET} {item}") 117 | port = "" # Only show port number for the first row 118 | print("") 119 | except FileNotFoundError as exception: 120 | print(f"File {filename} could not be found: Exception {exception}") 121 | except KeyError as exception: 122 | print(f"File {filename} not in expected format: Exception {exception}") 123 | 124 | 125 | def main(): 126 | """Main program loop.""" 127 | banner = "{0} version {1}".format(NAME, VERSION) 128 | options = parse_arguments(banner) 129 | display_json(options["inputfile"], **options) 130 | sys.exit(0) 131 | 132 | 133 | if __name__ == "__main__": 134 | main() 135 | -------------------------------------------------------------------------------- /fours.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Fours - Snap Shot Security Scanner 4 | # Perform a security scan given a set of parameters, and report in HTML 5 | 6 | # Copyright (C) 2023, Peter Mosmans 7 | # SPDX-License-Identifier: GPL-3.0-only 8 | 9 | IMAGE=gofwd/analyze_hosts 10 | TIMEZONE=Europe/Amsterdam 11 | TARGETS=targets.txt 12 | SCANLOG=scan-results.txt 13 | SCANRESULTS=scan-results.json 14 | HTMLRESULTS=scan-results.html 15 | 16 | if ! which docker &>/dev/null; then 17 | echo "Docker not found or not installed - exiting prematurely" 18 | exit 1 19 | fi 20 | 21 | if [[ ! -f $TARGETS ]]; then 22 | echo "Target file $TARGETS not found - exiting prematurely" 23 | exit 1 24 | fi 25 | 26 | echo "Fours v0.1 - Snap Shot Security Scanner - (c) 2023 Peter Mosmans" 27 | if [[ -f analyze_hosts.queue ]]; then 28 | echo -n "It looks like a scan is still active: " 29 | state=$(docker ps --format '{{.Image}} Created {{.RunningFor}} - {{.Status}}' | grep $IMAGE) 30 | echo "$state" 31 | if [[ -z "$state" ]]; then 32 | echo "Hmm... it seems $IMAGE is not running (anymore)" 33 | echo "You might want to manually remove analyze_hosts.queue and restart the script" 34 | fi 35 | echo "Exiting prematurely" 36 | exit 0 37 | fi 38 | 39 | echo "Starting scan of $(wc -l $TARGETS) target(s)" 40 | if [[ ! -f $SCANLOG ]]; then 41 | echo "Appending scan log to $SCANLOG" 42 | fi 43 | 44 | docker run --rm --volume "$PWD":/workdir -e TZ=$TIMEZONE $IMAGE \ 45 | --compact \ 46 | --http \ 47 | --nikto \ 48 | --quiet \ 49 | --settings analyze_hosts.yml \ 50 | --ssl \ 51 | --threads 10 \ 52 | -i $TARGETS \ 53 | -o $SCANLOG \ 54 | --json $SCANRESULTS 55 | 56 | if [[ ! -f $SCANRESULTS ]]; then 57 | echo "$SCANRESULTS not created - exiting prematurely" 58 | exit 1 59 | fi 60 | 61 | echo "Converting scan results into HTML file $HTMLRESULTS" 62 | docker run --rm --volume "$PWD":/workdir --entrypoint '' gofwd/analyze_hosts \ 63 | results_to_html.py $SCANRESULTS > $HTMLRESULTS 64 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | colorama 2 | droopescan>=1.41.2 3 | jinja2 4 | python-nmap>=0.5.0.post1 5 | python-wappalyzer>=0.2.2 6 | pyyaml 7 | requests>=2.9.1 8 | typer 9 | -------------------------------------------------------------------------------- /results_to_html.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """results_to_html - Convert JSON results to a simple HTML file 4 | 5 | Copyright (C) 2023 Peter Mosmans [Go Forward] 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | """ 11 | 12 | import ipaddress 13 | import json 14 | import sys 15 | from jinja2 import Environment, FileSystemLoader 16 | import typer 17 | 18 | app = typer.Typer() 19 | 20 | 21 | def sorted_hosts(unsorted): 22 | """Return a list of hosts sorted on IP address (when possible).""" 23 | try: 24 | results = map(str, sorted([ipaddress.ip_address(x) for x in unsorted])) 25 | except: 26 | results = sorted(unsorted) 27 | return results 28 | 29 | 30 | @app.command() 31 | def convert(filename: str, templates="/usr/share/templates"): 32 | """Convert JSON results to HTML.""" 33 | try: 34 | with open(filename, encoding="utf-8") as handle: 35 | results = json.load(handle) 36 | hosts = sorted_hosts(results["results"]) 37 | environment = Environment(loader=FileSystemLoader(templates)) 38 | template = environment.get_template("results.html") 39 | print(template.render(results=results, hosts=hosts)) 40 | except Exception as e: 41 | print(f"Something went wrong: {e}", file=sys.stderr) 42 | sys.exit(-1) 43 | 44 | 45 | if __name__ == "__main__": 46 | app() 47 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=security-scripts -------------------------------------------------------------------------------- /templates/results.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 23 | 35 | 36 | Scan results from {{ results["date_start"]|e }} 37 | 38 | 39 | 40 |

41 | The scan was performed at {{ results["date_start"]|e }} 42 |

43 |

44 | 45 | 47 | 49 |

50 |
51 | 52 | {% for key, value in results["arguments"].items() %} 53 | 54 | 57 | 66 | 67 | {% endfor %} 68 |
55 | {{ key|e }} 56 | 58 | {% if value is iterable and (value is not string and value is not mapping) %} 59 | {% for item in value %} 60 | {{ item|e }}
61 | {% endfor %} 62 | {% else %} 63 | {{ value|e }} 64 | {% endif %} 65 |
69 |
70 | 71 | 72 | {% for host in hosts %} 73 | {% set result = results["results"][host] %} 74 | {% set class = "alert" if "alerts" in result else "info" %} 75 | 76 | 79 | 119 | 120 | 121 | {% endfor %} 122 |
77 | {{ host|e }} 78 | 80 | {% if host in results["arguments"]["settings"]["targets"] %} 81 |
82 | {{ results["arguments"]["settings"]["targets"][host] }} 83 |
84 | {% endif %} 85 | {% if "info" in result %} 86 |
    87 | {% for port in result["info"] %} 88 |
  • 89 | {{ port|e }} 90 |
      91 | {% for issue in result["info"][port] %} 92 |
    • 93 | {{ issue|e }} 94 |
    • 95 | {% endfor %} 96 |
    97 | {% endfor %} 98 |
  • 99 |
100 | {% endif %} 101 | 102 | {% if "alerts" in result %} 103 |
    104 | {% for port in result["alerts"] %} 105 |
  • 106 | {{ port|e }} 107 |
      108 | {% for issue in result["alerts"][port] %} 109 |
    • 110 | {{ issue|e }} 111 |
    • 112 | {% endfor %} 113 |
    114 | {% endfor %} 115 |
  • 116 |
117 | {% endif %} 118 |
123 | 124 | 125 | -------------------------------------------------------------------------------- /test_ssl_handshake.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # test_ssl_handshake - Tests SSL/TLS handshakes 4 | # 5 | # Copyright (C) 2014 Peter Mosmans 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | 20 | NAME="test_ssl_handshake" 21 | 22 | 23 | BUGSTRING='(ssl handshake failure|sslv3 alert unexpected message|sslv3 alert illegal parameter)' 24 | 25 | #statuses 26 | declare SUCCESS=0 27 | declare ERR_PREREQUISITES=100 28 | declare ERR_NOTDETECTED=101 29 | declare NONEWLINE=1 30 | 31 | # logging and verboseness (v1.0) 32 | declare NOLOGFILE=-1 33 | declare QUIET=1 34 | declare STDOUT=2 35 | declare VERBOSE=4 36 | declare LOGFILE=8 37 | declare RAWLOGS=16 38 | declare SEPARATELOGS=32 39 | 40 | #defaults 41 | DEFAULTSTART=125 42 | DEFAULTSTRING="ALL:!PSK:!SRP" 43 | DEFAULTPROTOCOL="-ssl2 -ssl3 -tls1" 44 | EXTRAPARMS="-quiet -verify 0 -connect" 45 | 46 | # colours (v1.0) 47 | declare BLUE='\E[1;49;96m' LIGHTBLUE='\E[2;49;96m' 48 | declare RED='\E[1;49;31m' LIGHTRED='\E[2;49;31m' 49 | declare GREEN='\E[1;49;32m' LIGHTGREEN='\E[2;49;32m' 50 | declare RESETSCREEN='\E[0m' 51 | 52 | # temporary file - just in case it's needed for debugging purposes 53 | STATUSREPORT=${TMP}/status 54 | 55 | # global variables 56 | cipherfile= 57 | cipherlist= 58 | defaultoption=true 59 | faultyciphers= 60 | force=false 61 | iterate=false 62 | loglevel=0 63 | start=0 64 | total=0 65 | 66 | # bug tests 67 | bug128=false 68 | bugrsa=false 69 | bugintolerant=false 70 | 71 | # define functions: first the bug tests 72 | 73 | # Handshake fails when the tls1_2 protocol and 128 or more ciphers are specified, 74 | # but is successful with tls1_2 and 127 or less ciphers 75 | bug_128_cipherlimit() { 76 | load_ciphers "-tls1" "" 77 | prettyprint "testing for 128 cipherlimit" $BLUE 78 | local bug=128 79 | local protocol="-tls1_2" 80 | local start=126 81 | local try=10 82 | let end=$(echo ${cipherlist} | tr ':' ' ' | wc -w) 83 | [[ ${end} -gt $((start+try)) ]] && let end=$((start+try)) 84 | if [[ ${end} -le ${bug} ]]; then 85 | prettyprint "FAILED TEST: need at least ${bug} ciphersuites to test" $GREEN 86 | return ${ERR_PREREQUISITES} 87 | fi 88 | add_ciphers ${start} ${cipherlist} ${protocol} ${end} 89 | successful=$? 90 | if [[ ${successful} -ne ${bug} ]]; then 91 | prettyprint "FAILED TEST: tried ${successful} ciphers" $GREEN 92 | return ${ERR_NOTDETECTED} 93 | fi 94 | echo "shuffling order of ciphers" 95 | cipherlist=$(echo ${cipherlist} | tr ':' '\n'| shuf | tr '\n' ':'| sed -e 's/:$//' 1>/dev/stdout) 96 | add_ciphers ${start} ${cipherlist} ${protocol} ${end} 97 | successful=$? 98 | if [[ ${successful} -ne ${bug} ]]; then 99 | prettyprint "FAILED TEST: tried ${successful} ciphers" $GREEN 100 | return ${ERR_NOTDETECTED} 101 | fi 102 | prettyprint "SUCCESS: 128 cipherlimit detected" $RED 103 | } 104 | 105 | # Handshake fails when aRSA ciphers are specified first, 106 | # but is successful with the default ordering 107 | bug_rsa_order() { 108 | load_ciphers "" 'ALL' 109 | local protocol="-ssl3" 110 | local tests=3 111 | prettyprint "testing for RSA order sensitivity" $BLUE 112 | echo "test 1 of ${tests} - using cipherstring ALL" 113 | echo Q | $openssl s_client ${protocol} -cipher ${cipherlist} ${EXTRAPARMS} ${host} 1>/dev/null 2>${STATUSREPORT} 114 | parse_status ${STATUSREPORT} 115 | if [[ $? -ne ${SUCCESS} ]]; then 116 | prettyprint "FAILED TEST: handshake failed" $GREEN 117 | return ${ERR_NOTDETECTED} 118 | fi 119 | load_ciphers "" 'ALL:+aRSA' 120 | echo "test 2 of ${tests} - using cipherstring ALL:+aRSA" 121 | echo Q | $openssl s_client ${protocol} -cipher ${cipherlist} ${EXTRAPARMS} ${host} 1>/dev/null 2>${STATUSREPORT} 122 | parse_status ${STATUSREPORT} 123 | if [[ $? -eq ${SUCCESS} ]]; then 124 | prettyprint "FAILED TEST: handshake successful" $GREEN 125 | return ${ERR_NOTDETECTED} 126 | fi 127 | load_ciphers "" 'ALL:aRSA' 128 | echo "test 3 of ${tests} - using cipherstring ALL:aRSA" 129 | echo Q | $openssl s_client ${protocol} -cipher ${cipherlist} ${EXTRAPARMS} ${host} 1>/dev/null 2>${STATUSREPORT} 130 | parse_status ${STATUSREPORT} 131 | if [[ $? -ne ${SUCCESS} ]]; then 132 | prettyprint 'FAILED TEST: handshake failed when testing cipherstring ALL:aRSA' $GREEN 133 | return ${ERR_NOTDETECTED} 134 | fi 135 | prettyprint "SUCCESS: RSA order sensitivity detected" $RED 136 | return 0 137 | } 138 | 139 | # Handshake fails without connecting without specifying TLS protocol, 140 | # but is successful when a TLS protocol is specified 141 | bug_intolerant() { 142 | local protocols="-tls1_2 -tls1_1 -tls1 -ssl3 -ssl2" 143 | local succeeded="" 144 | local counter=1 145 | local tests=$(($(echo ${protocols}|wc -w)+1)) 146 | prettyprint "testing for version intolerant server (using previously loaded cipherstring)" $BLUE 147 | echo -n "test ${counter} of ${tests} - connect without a protocol specified " 148 | # check if default connection fails 149 | echo Q | $openssl s_client ${protocol} -cipher ${cipherlist} ${EXTRAPARMS} ${host} 1>/dev/null 2>${STATUSREPORT} 150 | _=$(parse_status ${STATUSREPORT}) 151 | if [[ $? -eq ${SUCCESS} ]]; then 152 | echo "" 153 | prettyprint "FAILED TEST: handshake successful" $GREEN 154 | return ${ERR_NOTDETECTED} 155 | fi 156 | echo "handshake failed" 157 | # check if connection is successful with one of the protocols 158 | for protocol in ${protocols}; do 159 | let counter=${counter}+1 160 | echo -n "test ${counter} of ${tests} - connect with ${protocol} " 161 | echo Q | $openssl s_client ${protocol} -cipher ${cipherlist} ${EXTRAPARMS} ${host} 1>/dev/null 2>${STATUSREPORT} 162 | _=$(parse_status ${STATUSREPORT}) 163 | if [[ $? -eq ${SUCCESS} ]]; then 164 | succeeded=${protocol} 165 | prettyprint "SUCCESS: handshake successful" $RED 166 | else 167 | echo "handshake failed" 168 | fi 169 | done 170 | if [ -z ${succeeded} ]; then 171 | prettyprint "FAILED TEST: could not connect with any of the protocols ${protocols}" $GREEN 172 | return ${ERR_NOTDETECTED} 173 | fi 174 | prettyprint "SUCCESS: version intolerant server detected" $RED 175 | } 176 | 177 | # ...then the general functions 178 | cleanup() { 179 | rm -f ${STATUSREPORT} 1>/dev/null 180 | if [[ ! -z ${faultyciphers} ]]; then 181 | echo "faulty cipherlist: " 182 | echo ${faultyciphers} 183 | fi 184 | } 185 | 186 | # returns number of successful ciphers 187 | add_ciphers() { 188 | local start=$1 189 | local cipherlist=$2 190 | local protocol=$3 191 | local finish=${4:-$(echo ${cipherlist} | tr ':' ' ' | wc -w)} 192 | 193 | echo "Adding ${start} to ${finish} ciphersuites" 194 | for ((c=${start}; c<=${finish}; c++ )); do 195 | [[ $c -gt $start ]] && echo "total number of ciphers ${c} - cipher added: $(echo ${cipherlist} | cut --delimiter=":" -f$c)" 196 | (($loglevel&$VERBOSE)) && echo "${openssl} s_client -cipher $(echo ${cipherlist} | cut --delimiter=":" -f1-$c) ${protocol} ${EXTRAPARMS} ${host}" 197 | echo Q | $openssl s_client -cipher $(echo ${cipherlist} | cut --delimiter=":" -f1-$c) ${protocol} ${EXTRAPARMS} ${host} 1>/dev/null 2>${STATUSREPORT} 198 | parse_status ${STATUSREPORT} 199 | [[ $? -ne 0 ]] && return $c 200 | done 201 | return $((c-1)) 202 | } 203 | 204 | # returns number of successful ciphers 205 | iterate_ciphers() { 206 | 207 | local start=$1 208 | local cipherlist=$2 209 | local total=$(echo ${cipherlist} | tr ':' ' ' | wc -w) 210 | echo "${start} ${cipherlist} ${total}" 211 | for ((c=${start}; c<=${total}; c++ )); do 212 | local cipher=$(echo ${cipherlist} | cut --delimiter=":" -f$c) 213 | echo -n "testing cipher ${c} - ${cipher} " 214 | rm -f ${STATUSREPORT} 1>/dev/null 215 | (($loglevel&$VERBOSE)) && echo "$openssl s_client -cipher ${cipher} ${EXTRAPARMS} ${host}" 216 | echo Q | $openssl s_client -cipher ${cipher} ${EXTRAPARMS} ${host} 1>/dev/null 2>${STATUSREPORT} 217 | parse_status ${STATUSREPORT} 218 | if [[ $? -ne ${SUCCESS} ]]; then 219 | [[ ${counter} == ${bug} ]] && return ${counter} 220 | [[ ! ${force} ]] && return ${counter} 221 | prettyprint "handshake failed" $RED $NONEWLINE 222 | echo ", adding ${cipher} to the list of faulty ciphers" 223 | [[ ! -z ${faultyciphers} ]] && faultyciphers="${faultyciphers}:" 224 | faultyciphers="${faultyciphers}${cipher}" 225 | else 226 | prettyprint "handshake successful" $GREEN 227 | fi 228 | done 229 | return $((c-1)) 230 | } 231 | 232 | load_ciphers() { 233 | local protocol={$1:-$DEFAULTPROTOCOL} 234 | local cipherstring=${2:-$DEFAULTSTRING} 235 | if [[ ! -z $2 ]]; then 236 | (($loglevel&$VERBOSE)) && echo "loading custom cipherstring ${2}" 237 | fi 238 | if [[ ! -z ${cipherfile} ]] && [[ -f ${cipherfile} ]]; then 239 | # cipherstring expects : as delimiters, check if they're present.. 240 | if grep -vq ":" ${cipherfile}; then 241 | # is it a multiple-column file ? if so, only use first column 242 | if grep -q "=" ${cipherfile}; then 243 | cipherlist=$(awk '{print $1}' ${cipherfile} | tr '\n' ':' | sed -e 's/:$//' 1>/dev/stdout) 244 | else 245 | # replace newline characters with : 246 | cipherlist=$(tr '\n' ':' < ${cipherfile} | sed -e 's/:$//') 247 | fi 248 | else 249 | cipherlist=$(cat ${cipherfile}) 250 | fi 251 | else 252 | (($loglevel&$VERBOSE)) && echo "reading cipherlist from ${openssl} and cipherstring ${cipherstring}" 253 | cipherlist=$(${openssl} ciphers ${protocol} -l ${cipherstring}) 254 | fi 255 | totalciphers=$(echo ${cipherlist} | tr ':' ' ' | wc -w) 256 | (($loglevel&$VERBOSE)) && echo "loaded ${totalciphers} ciphers" 257 | } 258 | 259 | main() { 260 | startup "$@" 261 | load_ciphers "${protocol}" "${cipherstring}" 262 | test_connection ${cipherlist} ${protocol} 263 | ${bugintolerant} && bug_intolerant 264 | ${bug128} && bug_128_cipherlimit 265 | ${bugrsa} && bug_rsa_order 266 | if ${iterate}; then 267 | iterate_ciphers "${start}" "${cipherlist}" 268 | echo ${faultyciphers} 269 | fi 270 | # else 271 | # add_ciphers 272 | # fi 273 | } 274 | 275 | parse_status() { 276 | (($loglevel&$VERBOSE)) && show_statusreport $1 277 | if grep -qiE "no ciphers available" $1; then 278 | echo "cipher not supported by server" 279 | return 0 280 | fi 281 | if grep -qiE "no cipher match" $1; then 282 | echo "cipher locally not supported by ${openssl}" 283 | return 0 284 | fi 285 | if grep -qiE "ssl handshake failure" $1; then 286 | echo "SSL handshake error detected" 287 | (($loglevel&$VERBOSE)) && show_statusreport $1 288 | return 1 289 | fi 290 | if grep -qiE "${BUGSTRING}" $1; then 291 | echo "SSL error detected" 292 | show_statusreport $1 293 | return 1 294 | fi 295 | } 296 | 297 | # prettyprint (v1.0) 298 | prettyprint() { 299 | (($loglevel&$QUIET)) && return 300 | [[ -z $nocolor ]] && echo -ne $2 301 | if [[ "$3" == "$NONEWLINE" ]]; then 302 | echo -n "$1" 303 | else 304 | echo "$1" 305 | fi 306 | [[ -z $nocolor ]] && echo -ne ${RESETSCREEN} 307 | } 308 | 309 | show_statusreport() { 310 | echo "status report from server:" 311 | grep -iv "loading" $1|awk 'FS=":"{print $6}' 312 | grep -iv "loading" $1 313 | } 314 | 315 | startup() { 316 | prettyprint "$NAME starting on $(date +%d-%m-%Y' at '%R)" $BLUE 317 | prettyprint "BETA VERSION - bugs are present and not all features are correctly implemented" $RED 318 | trap cleanup EXIT QUIT 319 | if ! options=$(getopt -o :fv -l 128,ciphers:,cipherstring:,force,intolerant,iterate,openssl:,rsa,start:,verbose -- "$@") ; then 320 | usage 321 | exit 1 322 | fi 323 | 324 | eval set -- $options 325 | if [[ "$#" -le 1 ]]; then 326 | usage 327 | exit 1 328 | fi 329 | while [[ $# -gt 0 ]]; do 330 | case $1 in 331 | --128) 332 | bug128=true 333 | defaultoption=false;; 334 | --ciphers) 335 | cipherfile=$2 336 | shift;; 337 | --cipherstring) 338 | cipherstring=$2 339 | shift;; 340 | -f|--force) 341 | force=true;; 342 | --intolerant) 343 | bugintolerant=true 344 | defaultoption=false;; 345 | --iterate) 346 | iterate=true 347 | defaultoption=false;; 348 | --openssl) 349 | openssl=$2 350 | shift;; 351 | --rsa) 352 | bugrsa=true 353 | defaultoption=false;; 354 | --start) 355 | start=$2 356 | shift;; 357 | -v|--verbose) 358 | let "loglevel=loglevel|$VERBOSE";; 359 | (--) shift; 360 | break;; 361 | (-*) echo "$0: unrecognized option $1" 1>&2; exit 1;; 362 | (*) break;; 363 | esac 364 | shift 365 | done 366 | openssl=${openssl:-$(which openssl)} 367 | if ! [[ -f ${openssl} ]]; then 368 | echo "could not find ${openssl}... exiting" 369 | exit 1 370 | fi 371 | 372 | if [ -z $1 ]; then 373 | usage 374 | exit 0 375 | fi 376 | 377 | # add default port number ? 378 | if [[ $1 =~ .*:[0-9]+$ ]]; then 379 | host=$1 380 | else 381 | host=$1:443 382 | fi 383 | 384 | if ${defaultoption}; then 385 | bug128=true 386 | bugrsa=true 387 | bugintolerant=true 388 | fi 389 | } 390 | 391 | test_connection() { 392 | local cipherlist=$1 393 | local protocol=$2 394 | echo "first trying to connect to ${host} using ${totalciphers} ciphers..." 395 | echo Q | ${openssl} s_client -cipher ${cipherlist} ${protocol} ${EXTRAPARMS} ${host} 1>/dev/null 2>${STATUSREPORT} 396 | if grep -qiE "(connect:errno|ssl handhake failure|no cipher match)" ${STATUSREPORT}; then 397 | echo "could not connect to ${host} with all ciphers... exiting" 398 | exit 1 399 | fi 400 | parse_status ${STATUSREPORT} 401 | if [[ $? -eq 0 ]]; then 402 | echo "no issues detected" 403 | fi 404 | } 405 | 406 | usage() { 407 | local realpath=$(dirname $(readlink -f $0)) 408 | if [[ -d $realpath/.git ]]; then 409 | pushd $realpath 1>/dev/null 2>&1 410 | local branch=$(git rev-parse --abbrev-ref HEAD) 411 | local commit=$(git log|head -1|awk '{print $2}'|cut -c -10) 412 | popd 1>/dev/null 413 | prettyprint "$NAME (git)" $BLUE $NONEWLINE 414 | echo " from ${branch} branch commit ${commit}" 415 | else 416 | prettyprint "$NAME version $VERSION" $BLUE 417 | fi 418 | prettyprint " (c) 2014 Peter Mosmans [Go Forward]" $LIGHTBLUE 419 | prettyprint " Licensed under the GPL 3.0" $LIGHTBLUE 420 | echo "" 421 | echo "tests SSL/TLS handshakes (for known bugs)" 422 | echo "" 423 | echo "usage: $0 target[:port]" 424 | echo "" 425 | echo " --start=NUMBER number of ciphers to start with" 426 | echo " --ciphers=FILE a file containing a list which ciphers to use" 427 | echo " --cipherstring=CIPHERSTRING" 428 | echo " cipherstring (default ${cipherstring})" 429 | echo " -f | --force continue even though the error has been detected" 430 | echo " --total=NUMBER number of ciphers to test" 431 | echo " --iterate iterate through all the ciphers instead of adding" 432 | echo " --openssl=FILE location of openssl (default ${openssl})" 433 | echo " -v | --verbose be more verbose, please" 434 | echo "" 435 | echo " tests:" 436 | echo " --128 test for 128 cipherlimit" 437 | echo " --intolerant test for version intolerant server" 438 | echo " --rsa test for RSA order sensitivity" 439 | echo "" 440 | echo " by default, all tests will be performed" 441 | echo "" 442 | echo "BETA VERSION - bugs are present and not all features are correctly implemented" 443 | } 444 | 445 | main "$@" 446 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 1.6 3 | skipsdist = true 4 | envlist = py27,pylint 5 | 6 | [testenv] 7 | setenv = VIRTUAL_ENV={envdir} 8 | deps = -r{toxinidir}/requirements.txt 9 | commands = python analyze_hosts.py --help 10 | 11 | [testenv:pylint] 12 | deps = pylint 13 | commands = - pylint analyze_hosts.py --------------------------------------------------------------------------------