ssh_scan 0.0.21 → 0.0.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.travis.yml +9 -1
- data/README.md +2 -13
- data/bin/ssh_scan +1 -2
- data/config/policies/mozilla_intermediate.yml +0 -3
- data/config/policies/mozilla_modern.yml +0 -3
- data/lib/ssh_scan.rb +2 -0
- data/lib/ssh_scan/banner.rb +2 -2
- data/lib/ssh_scan/client.rb +37 -25
- data/lib/ssh_scan/grader.rb +32 -0
- data/lib/ssh_scan/policy.rb +1 -1
- data/lib/ssh_scan/policy_manager.rb +27 -18
- data/lib/ssh_scan/result.rb +271 -0
- data/lib/ssh_scan/scan_engine.rb +101 -75
- data/lib/ssh_scan/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a161adf0202c747f948c9161675b567c6b715b4f
|
4
|
+
data.tar.gz: 5533118de4e0f7ef6eb26bc60adb915ffbde9cae
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1c7f1fc49e71437001aa5e6cd8f8d63cadc127dde727d469e6d94002533a3f525eb69ce364d40dcf28cce05dcaf17dcb757bb51615a36f5b6497d6a289d6529d
|
7
|
+
data.tar.gz: 654a3c0ec780433fadd09db4bb0eb77f18bc60dc088c28e6073a8876d2f3fe717dd568180347bcc31ce8834fd5e30c4b83a19cff8ba01979587f16f40aacc403
|
data/.travis.yml
CHANGED
@@ -34,6 +34,14 @@ matrix:
|
|
34
34
|
- bundle install
|
35
35
|
- chmod 755 ./spec/ssh_scan/integration.sh
|
36
36
|
- ./spec/ssh_scan/integration.sh
|
37
|
+
- rvm: 2.3.0
|
38
|
+
env:
|
39
|
+
- LABEL=docker_integration_tests
|
40
|
+
services:
|
41
|
+
- docker
|
42
|
+
script:
|
43
|
+
- docker build -t mozilla/ssh_scan .
|
44
|
+
- docker run -it mozilla/ssh_scan /app/spec/ssh_scan/integration.sh
|
37
45
|
- rvm: 2.3.0
|
38
46
|
env:
|
39
47
|
- LABEL=docker_build_push
|
@@ -42,7 +50,7 @@ matrix:
|
|
42
50
|
script:
|
43
51
|
- docker build -t mozilla/ssh_scan .
|
44
52
|
- >
|
45
|
-
if [ "$TRAVIS_PULL_REQUEST" == "false" ]; then \
|
53
|
+
if [ [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_BRANCH" == "master" ] ]; then \
|
46
54
|
docker login -u="$DOCKER_USER" -p="$DOCKER_PASS" ;\
|
47
55
|
docker push mozilla/ssh_scan:latest ;\
|
48
56
|
else \
|
data/README.md
CHANGED
@@ -28,7 +28,7 @@ To run from a docker container, type:
|
|
28
28
|
|
29
29
|
```bash
|
30
30
|
docker pull mozilla/ssh_scan
|
31
|
-
docker run -it mozilla/ssh_scan /app/bin/ssh_scan -t
|
31
|
+
docker run -it mozilla/ssh_scan /app/bin/ssh_scan -t sshscan.rubidus.com
|
32
32
|
```
|
33
33
|
|
34
34
|
To install and run from source, type:
|
@@ -38,17 +38,6 @@ To install and run from source, type:
|
|
38
38
|
git clone https://github.com/mozilla/ssh_scan.git
|
39
39
|
cd ssh_scan
|
40
40
|
|
41
|
-
# install rvm,
|
42
|
-
# you might have to provide root to install missing packages
|
43
|
-
gpg2 --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3
|
44
|
-
curl -sSL https://get.rvm.io | bash -s stable
|
45
|
-
|
46
|
-
# install Ruby 2.3.1 with rvm,
|
47
|
-
# again, you might have to install missing devel packages
|
48
|
-
rvm install 2.3.1
|
49
|
-
rvm use 2.3.1
|
50
|
-
|
51
|
-
# resolve dependencies
|
52
41
|
gem install bundler
|
53
42
|
bundle install
|
54
43
|
|
@@ -60,7 +49,7 @@ bundle install
|
|
60
49
|
Run `ssh_scan -h` to get this
|
61
50
|
|
62
51
|
```bash
|
63
|
-
ssh_scan v0.0.
|
52
|
+
ssh_scan v0.0.21 (https://github.com/mozilla/ssh_scan)
|
64
53
|
|
65
54
|
Usage: ssh_scan [options]
|
66
55
|
-t, --target [IP/Range/Hostname] IP/Ranges/Hostname to scan
|
data/bin/ssh_scan
CHANGED
@@ -261,8 +261,7 @@ puts JSON.pretty_generate(results)
|
|
261
261
|
|
262
262
|
if options["unit_test"] == true
|
263
263
|
results.each do |result|
|
264
|
-
if result
|
265
|
-
result["compliance"][:compliant] == false
|
264
|
+
if result.compliant == false
|
266
265
|
exit 1 #non-zero means a false
|
267
266
|
else
|
268
267
|
exit 0 #non-zero means pass
|
data/lib/ssh_scan.rb
CHANGED
data/lib/ssh_scan/banner.rb
CHANGED
@@ -21,8 +21,8 @@ module SSHScan
|
|
21
21
|
# or "SSH-number" then return the number, else return
|
22
22
|
# "unknown"
|
23
23
|
def ssh_version()
|
24
|
-
if
|
25
|
-
return
|
24
|
+
if match = @string.match(/SSH-(\d+[\.\d+]+)/)
|
25
|
+
return match[1].to_f
|
26
26
|
else
|
27
27
|
return "unknown"
|
28
28
|
end
|
data/lib/ssh_scan/client.rb
CHANGED
@@ -6,19 +6,36 @@ require 'ssh_scan/error'
|
|
6
6
|
|
7
7
|
module SSHScan
|
8
8
|
class Client
|
9
|
-
def initialize(
|
10
|
-
@
|
9
|
+
def initialize(ip, port, timeout = 3)
|
10
|
+
@ip = ip
|
11
11
|
@timeout = timeout
|
12
12
|
|
13
|
-
@port = port
|
13
|
+
@port = port.to_i
|
14
14
|
@client_banner = SSHScan::Constants::DEFAULT_CLIENT_BANNER
|
15
15
|
@server_banner = nil
|
16
16
|
@kex_init_raw = SSHScan::Constants::DEFAULT_KEY_INIT.to_binary_s
|
17
17
|
end
|
18
18
|
|
19
|
+
def ip
|
20
|
+
@ip
|
21
|
+
end
|
22
|
+
|
23
|
+
def port
|
24
|
+
@port
|
25
|
+
end
|
26
|
+
|
27
|
+
def banner
|
28
|
+
@server_banner
|
29
|
+
end
|
30
|
+
|
19
31
|
def connect()
|
32
|
+
@error = nil
|
33
|
+
|
20
34
|
begin
|
21
|
-
@sock = Socket.tcp(@
|
35
|
+
@sock = Socket.tcp(@ip, @port, connect_timeout: @timeout)
|
36
|
+
rescue SocketError => e
|
37
|
+
@error = SSHScan::Error::ConnectionRefused.new(e.message)
|
38
|
+
@sock = nil
|
22
39
|
rescue Errno::ETIMEDOUT => e
|
23
40
|
@error = SSHScan::Error::ConnectTimeout.new(e.message)
|
24
41
|
@sock = nil
|
@@ -53,58 +70,53 @@ module SSHScan
|
|
53
70
|
end
|
54
71
|
end
|
55
72
|
|
56
|
-
def
|
57
|
-
|
58
|
-
|
59
|
-
result[:ssh_scan_version] = SSHScan::VERSION
|
60
|
-
result[:ip] = @target
|
61
|
-
result[:port] = @port
|
73
|
+
def error?
|
74
|
+
!@error.nil?
|
75
|
+
end
|
62
76
|
|
77
|
+
def error
|
78
|
+
@error
|
79
|
+
end
|
80
|
+
|
81
|
+
def get_kex_result(kex_init_raw = @kex_init_raw)
|
63
82
|
if !@sock
|
64
|
-
|
65
|
-
return
|
83
|
+
@error = "Socket is no longer valid"
|
84
|
+
return nil
|
66
85
|
end
|
67
86
|
|
68
|
-
# Assemble and print results
|
69
|
-
result[:server_banner] = @server_banner.to_s
|
70
|
-
result[:ssh_version] = @server_banner.ssh_version
|
71
|
-
result[:os] = @server_banner.os_guess.common
|
72
|
-
result[:os_cpe] = @server_banner.os_guess.cpe
|
73
|
-
result[:ssh_lib] = @server_banner.ssh_lib_guess.common
|
74
|
-
result[:ssh_lib_cpe] = @server_banner.ssh_lib_guess.cpe
|
75
|
-
|
76
87
|
begin
|
77
88
|
@sock.write(kex_init_raw)
|
78
89
|
resp = @sock.read(4)
|
79
90
|
|
80
91
|
if resp.nil?
|
81
|
-
|
92
|
+
@error = SSHScan::Error::NoKexResponse.new(
|
82
93
|
"service did not respond to our kex init request"
|
83
94
|
)
|
84
95
|
@sock = nil
|
85
|
-
return
|
96
|
+
return nil
|
86
97
|
end
|
87
98
|
|
88
99
|
resp += @sock.read(resp.unpack("N").first)
|
89
100
|
@sock.close
|
90
101
|
|
91
102
|
kex_exchange_init = SSHScan::KeyExchangeInit.read(resp)
|
92
|
-
result.merge!(kex_exchange_init.to_hash)
|
93
103
|
rescue Errno::ETIMEDOUT => e
|
94
104
|
@error = SSHScan::Error::ConnectTimeout.new(e.message)
|
95
105
|
@sock = nil
|
106
|
+
return nil
|
96
107
|
rescue Errno::ECONNREFUSED,
|
97
108
|
Errno::ENETUNREACH,
|
98
109
|
Errno::ECONNRESET,
|
99
110
|
Errno::EACCES,
|
100
111
|
Errno::EHOSTUNREACH
|
101
|
-
|
112
|
+
@error = SSHScan::Error::NoKexResponse.new(
|
102
113
|
"service did not respond to our kex init request"
|
103
114
|
)
|
104
115
|
@sock = nil
|
116
|
+
return nil
|
105
117
|
end
|
106
118
|
|
107
|
-
return
|
119
|
+
return kex_exchange_init.to_hash
|
108
120
|
end
|
109
121
|
end
|
110
122
|
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module SSHScan
|
2
|
+
# A very crude means of translating # of compliance recommendations into a a grade
|
3
|
+
# Basic formula is 100 - (# of recommendations * 10)
|
4
|
+
class Grader
|
5
|
+
GRADE_MAP = {
|
6
|
+
91..100 => "A",
|
7
|
+
81..90 => "B",
|
8
|
+
71..80 => "C",
|
9
|
+
61..70 => "D",
|
10
|
+
0..60 => "F",
|
11
|
+
}
|
12
|
+
|
13
|
+
def initialize(result)
|
14
|
+
@result = result
|
15
|
+
end
|
16
|
+
|
17
|
+
def grade
|
18
|
+
score = 100
|
19
|
+
|
20
|
+
if @result.compliance_recommendations.each do |recommendation|
|
21
|
+
score -= 10
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
GRADE_MAP.each do |score_range,grade|
|
26
|
+
if score_range.include?(score)
|
27
|
+
return grade
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/ssh_scan/policy.rb
CHANGED
@@ -15,7 +15,7 @@ module SSHScan
|
|
15
15
|
@compression = opts['compression'] || []
|
16
16
|
@references = opts['references'] || []
|
17
17
|
@auth_methods = opts['auth_methods'] || []
|
18
|
-
@ssh_version = opts['ssh_version'] ||
|
18
|
+
@ssh_version = opts['ssh_version'] || nil
|
19
19
|
end
|
20
20
|
|
21
21
|
# Generate a {SSHScan::Policy} object from YAML file.
|
@@ -7,9 +7,10 @@ module SSHScan
|
|
7
7
|
end
|
8
8
|
|
9
9
|
def out_of_policy_encryption
|
10
|
+
return [] if @policy.encryption.empty?
|
10
11
|
target_encryption =
|
11
|
-
@result
|
12
|
-
@result
|
12
|
+
@result.encryption_algorithms_client_to_server |
|
13
|
+
@result.encryption_algorithms_server_to_client
|
13
14
|
outliers = []
|
14
15
|
target_encryption.each do |target_enc|
|
15
16
|
outliers << target_enc unless @policy.encryption.include?(target_enc)
|
@@ -18,9 +19,10 @@ module SSHScan
|
|
18
19
|
end
|
19
20
|
|
20
21
|
def missing_policy_encryption
|
22
|
+
return [] if @policy.encryption.empty?
|
21
23
|
target_encryption =
|
22
|
-
@result
|
23
|
-
@result
|
24
|
+
@result.encryption_algorithms_client_to_server |
|
25
|
+
@result.encryption_algorithms_server_to_client
|
24
26
|
outliers = []
|
25
27
|
@policy.encryption.each do |encryption|
|
26
28
|
if target_encryption.include?(encryption) == false
|
@@ -31,9 +33,10 @@ module SSHScan
|
|
31
33
|
end
|
32
34
|
|
33
35
|
def out_of_policy_macs
|
36
|
+
return [] if @policy.macs.empty?
|
34
37
|
target_macs =
|
35
|
-
@result
|
36
|
-
@result
|
38
|
+
@result.mac_algorithms_server_to_client |
|
39
|
+
@result.mac_algorithms_client_to_server
|
37
40
|
outliers = []
|
38
41
|
target_macs.each do |target_mac|
|
39
42
|
outliers << target_mac unless @policy.macs.include?(target_mac)
|
@@ -42,9 +45,10 @@ module SSHScan
|
|
42
45
|
end
|
43
46
|
|
44
47
|
def missing_policy_macs
|
48
|
+
return [] if @policy.macs.empty?
|
45
49
|
target_macs =
|
46
|
-
@result
|
47
|
-
@result
|
50
|
+
@result.mac_algorithms_server_to_client |
|
51
|
+
@result.mac_algorithms_client_to_server
|
48
52
|
outliers = []
|
49
53
|
|
50
54
|
@policy.macs.each do |mac|
|
@@ -56,7 +60,8 @@ module SSHScan
|
|
56
60
|
end
|
57
61
|
|
58
62
|
def out_of_policy_kex
|
59
|
-
|
63
|
+
return [] if @policy.kex.empty?
|
64
|
+
target_kexs = @result.key_algorithms
|
60
65
|
outliers = []
|
61
66
|
target_kexs.each do |target_kex|
|
62
67
|
outliers << target_kex unless @policy.kex.include?(target_kex)
|
@@ -65,7 +70,8 @@ module SSHScan
|
|
65
70
|
end
|
66
71
|
|
67
72
|
def missing_policy_kex
|
68
|
-
|
73
|
+
return [] if @policy.kex.empty?
|
74
|
+
target_kex = @result.key_algorithms
|
69
75
|
outliers = []
|
70
76
|
|
71
77
|
@policy.kex.each do |kex|
|
@@ -77,9 +83,10 @@ module SSHScan
|
|
77
83
|
end
|
78
84
|
|
79
85
|
def out_of_policy_compression
|
86
|
+
return [] if @policy.compression.empty?
|
80
87
|
target_compressions =
|
81
|
-
@result
|
82
|
-
@result
|
88
|
+
@result.compression_algorithms_server_to_client |
|
89
|
+
@result.compression_algorithms_client_to_server
|
83
90
|
outliers = []
|
84
91
|
target_compressions.each do |target_compression|
|
85
92
|
outliers << target_compression unless
|
@@ -89,9 +96,10 @@ module SSHScan
|
|
89
96
|
end
|
90
97
|
|
91
98
|
def missing_policy_compression
|
99
|
+
return [] if @policy.compression.empty?
|
92
100
|
target_compressions =
|
93
|
-
@result
|
94
|
-
@result
|
101
|
+
@result.compression_algorithms_server_to_client |
|
102
|
+
@result.compression_algorithms_client_to_server
|
95
103
|
outliers = []
|
96
104
|
|
97
105
|
@policy.compression.each do |compression|
|
@@ -103,9 +111,9 @@ module SSHScan
|
|
103
111
|
end
|
104
112
|
|
105
113
|
def out_of_policy_auth_methods
|
106
|
-
return [] if @
|
107
|
-
|
108
|
-
target_auth_methods = @result
|
114
|
+
return [] if @policy.auth_methods.empty?
|
115
|
+
return [] if @result.auth_methods.empty?
|
116
|
+
target_auth_methods = @result.auth_methods
|
109
117
|
outliers = []
|
110
118
|
|
111
119
|
if not @policy.auth_methods.empty?
|
@@ -119,7 +127,8 @@ module SSHScan
|
|
119
127
|
end
|
120
128
|
|
121
129
|
def out_of_policy_ssh_version
|
122
|
-
|
130
|
+
return false if @policy.ssh_version.nil?
|
131
|
+
target_ssh_version = @result.ssh_version
|
123
132
|
if @policy.ssh_version
|
124
133
|
if target_ssh_version < @policy.ssh_version
|
125
134
|
return true
|
@@ -0,0 +1,271 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'ssh_scan/banner'
|
3
|
+
require 'ipaddr'
|
4
|
+
require 'string_ext'
|
5
|
+
require 'set'
|
6
|
+
|
7
|
+
module SSHScan
|
8
|
+
class Result
|
9
|
+
def initialize()
|
10
|
+
@version = SSHScan::VERSION
|
11
|
+
@fingerprints = nil
|
12
|
+
@duplicate_host_key_ips = Set.new()
|
13
|
+
@compliance = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def version
|
17
|
+
@version
|
18
|
+
end
|
19
|
+
|
20
|
+
def ip
|
21
|
+
@ip
|
22
|
+
end
|
23
|
+
|
24
|
+
def ip=(ip)
|
25
|
+
unless ip.is_a?(String) && ip.ip_addr?
|
26
|
+
raise ArgumentError, "Invalid attempt to set IP to a non-IP address value"
|
27
|
+
end
|
28
|
+
|
29
|
+
@ip = ip
|
30
|
+
end
|
31
|
+
|
32
|
+
def port
|
33
|
+
@port
|
34
|
+
end
|
35
|
+
|
36
|
+
def port=(port)
|
37
|
+
unless port.is_a?(Integer) && port > 0 && port <= 65535
|
38
|
+
raise ArgumentError, "Invalid attempt to set port to a non-port value"
|
39
|
+
end
|
40
|
+
|
41
|
+
@port = port
|
42
|
+
end
|
43
|
+
|
44
|
+
def banner()
|
45
|
+
@banner || SSHScan::Banner.new("")
|
46
|
+
end
|
47
|
+
|
48
|
+
def hostname=(hostname)
|
49
|
+
@hostname = hostname
|
50
|
+
end
|
51
|
+
|
52
|
+
def hostname()
|
53
|
+
@hostname || ""
|
54
|
+
end
|
55
|
+
|
56
|
+
def banner=(banner)
|
57
|
+
unless banner.is_a?(SSHScan::Banner)
|
58
|
+
raise ArgumentError, "Invalid attempt to set banner with a non-banner object"
|
59
|
+
end
|
60
|
+
|
61
|
+
@banner = banner
|
62
|
+
end
|
63
|
+
|
64
|
+
def ssh_version
|
65
|
+
self.banner.ssh_version
|
66
|
+
end
|
67
|
+
|
68
|
+
def os_guess_common
|
69
|
+
self.banner.os_guess.common
|
70
|
+
end
|
71
|
+
|
72
|
+
def os_guess_cpe
|
73
|
+
self.banner.os_guess.cpe
|
74
|
+
end
|
75
|
+
|
76
|
+
def ssh_lib_guess_common
|
77
|
+
self.banner.ssh_lib_guess.common
|
78
|
+
end
|
79
|
+
|
80
|
+
def ssh_lib_guess_cpe
|
81
|
+
self.banner.ssh_lib_guess.cpe
|
82
|
+
end
|
83
|
+
|
84
|
+
def cookie
|
85
|
+
@cookie || ""
|
86
|
+
end
|
87
|
+
|
88
|
+
def key_algorithms
|
89
|
+
@hex_result_hash ? @hex_result_hash[:key_algorithms] : []
|
90
|
+
end
|
91
|
+
|
92
|
+
def server_host_key_algorithms
|
93
|
+
@hex_result_hash ? @hex_result_hash[:server_host_key_algorithms] : []
|
94
|
+
end
|
95
|
+
|
96
|
+
def encryption_algorithms_client_to_server
|
97
|
+
@hex_result_hash ? @hex_result_hash[:encryption_algorithms_client_to_server] : []
|
98
|
+
end
|
99
|
+
|
100
|
+
def encryption_algorithms_server_to_client
|
101
|
+
@hex_result_hash ? @hex_result_hash[:encryption_algorithms_server_to_client] : []
|
102
|
+
end
|
103
|
+
|
104
|
+
def mac_algorithms_client_to_server
|
105
|
+
@hex_result_hash ? @hex_result_hash[:mac_algorithms_client_to_server] : []
|
106
|
+
end
|
107
|
+
|
108
|
+
def mac_algorithms_server_to_client
|
109
|
+
@hex_result_hash ? @hex_result_hash[:mac_algorithms_server_to_client] : []
|
110
|
+
end
|
111
|
+
|
112
|
+
def compression_algorithms_client_to_server
|
113
|
+
@hex_result_hash ? @hex_result_hash[:compression_algorithms_client_to_server] : []
|
114
|
+
end
|
115
|
+
|
116
|
+
def compression_algorithms_server_to_client
|
117
|
+
@hex_result_hash ? @hex_result_hash[:compression_algorithms_server_to_client] : []
|
118
|
+
end
|
119
|
+
|
120
|
+
def languages_client_to_server
|
121
|
+
@hex_result_hash ? @hex_result_hash[:languages_client_to_server] : []
|
122
|
+
end
|
123
|
+
|
124
|
+
def languages_server_to_client
|
125
|
+
@hex_result_hash ? @hex_result_hash[:languages_server_to_client] : []
|
126
|
+
end
|
127
|
+
|
128
|
+
def set_kex_result(kex_result)
|
129
|
+
@hex_result_hash = kex_result.to_hash
|
130
|
+
end
|
131
|
+
|
132
|
+
def set_start_time
|
133
|
+
@start_time = Time.now
|
134
|
+
end
|
135
|
+
|
136
|
+
def start_time
|
137
|
+
@start_time
|
138
|
+
end
|
139
|
+
|
140
|
+
def set_end_time
|
141
|
+
@end_time = Time.now
|
142
|
+
end
|
143
|
+
|
144
|
+
def scan_duration
|
145
|
+
if start_time.nil?
|
146
|
+
raise "Cannot calculate scan duration without start_time set"
|
147
|
+
end
|
148
|
+
|
149
|
+
if end_time.nil?
|
150
|
+
raise "Cannot calculate scan duration without end_time set"
|
151
|
+
end
|
152
|
+
|
153
|
+
end_time - start_time
|
154
|
+
end
|
155
|
+
|
156
|
+
def end_time
|
157
|
+
@end_time
|
158
|
+
end
|
159
|
+
|
160
|
+
def auth_methods=(auth_methods)
|
161
|
+
@auth_methods = auth_methods
|
162
|
+
end
|
163
|
+
|
164
|
+
def fingerprints=(fingerprints)
|
165
|
+
@fingerprints = fingerprints
|
166
|
+
end
|
167
|
+
|
168
|
+
def fingerprints
|
169
|
+
@fingerprints
|
170
|
+
end
|
171
|
+
|
172
|
+
def duplicate_host_key_ips=(duplicate_host_key_ips)
|
173
|
+
@duplicate_host_key_ips = duplicate_host_key_ips
|
174
|
+
end
|
175
|
+
|
176
|
+
def duplicate_host_key_ips
|
177
|
+
@duplicate_host_key_ips
|
178
|
+
end
|
179
|
+
|
180
|
+
def auth_methods()
|
181
|
+
@auth_methods || []
|
182
|
+
end
|
183
|
+
|
184
|
+
def set_compliance=(compliance)
|
185
|
+
@compliance = compliance
|
186
|
+
end
|
187
|
+
|
188
|
+
def compliance_policy
|
189
|
+
@compliance[:policy]
|
190
|
+
end
|
191
|
+
|
192
|
+
def compliant?
|
193
|
+
@compliance[:compliant]
|
194
|
+
end
|
195
|
+
|
196
|
+
def compliance_references
|
197
|
+
@compliance[:references]
|
198
|
+
end
|
199
|
+
|
200
|
+
def compliance_recommendations
|
201
|
+
@compliance[:recommendations]
|
202
|
+
end
|
203
|
+
|
204
|
+
def set_client_attributes(client)
|
205
|
+
self.ip = client.ip
|
206
|
+
self.port = client.port || 22
|
207
|
+
self.banner = client.banner || SSHScan::Banner.new("")
|
208
|
+
end
|
209
|
+
|
210
|
+
def error=(error)
|
211
|
+
@error = error.to_s
|
212
|
+
end
|
213
|
+
|
214
|
+
def error?
|
215
|
+
!@error.nil?
|
216
|
+
end
|
217
|
+
|
218
|
+
def error
|
219
|
+
@error
|
220
|
+
end
|
221
|
+
|
222
|
+
def grade=(grade)
|
223
|
+
@compliance_grade = grade
|
224
|
+
end
|
225
|
+
|
226
|
+
def grade
|
227
|
+
@compliance_grade
|
228
|
+
end
|
229
|
+
|
230
|
+
def to_hash
|
231
|
+
hashed_object = {
|
232
|
+
"ssh_scan_version" => self.version,
|
233
|
+
"ip" => self.ip,
|
234
|
+
"hostname" => self.hostname,
|
235
|
+
"port" => self.port,
|
236
|
+
"server_banner" => self.banner.to_s,
|
237
|
+
"ssh_version" => self.ssh_version,
|
238
|
+
"os" => self.os_guess_common,
|
239
|
+
"os_cpe" => self.os_guess_cpe,
|
240
|
+
"ssh_lib" => self.ssh_lib_guess_common,
|
241
|
+
"ssh_lib_cpe" => self.ssh_lib_guess_cpe,
|
242
|
+
"key_algorithms" => self.key_algorithms,
|
243
|
+
"encryption_algorithms_client_to_server" => self.encryption_algorithms_client_to_server,
|
244
|
+
"encryption_algorithms_server_to_client" => self.encryption_algorithms_server_to_client,
|
245
|
+
"mac_algorithms_client_to_server" => self.mac_algorithms_client_to_server,
|
246
|
+
"mac_algorithms_server_to_client" => self.mac_algorithms_server_to_client,
|
247
|
+
"compression_algorithms_client_to_server" => self.compression_algorithms_client_to_server,
|
248
|
+
"compression_algorithms_server_to_client" => self.compression_algorithms_server_to_client,
|
249
|
+
"languages_client_to_server" => self.languages_client_to_server,
|
250
|
+
"languages_server_to_client" => self.languages_server_to_client,
|
251
|
+
"auth_methods" => self.auth_methods,
|
252
|
+
"fingerprints" => self.fingerprints,
|
253
|
+
"duplicate_host_key_ips" => self.duplicate_host_key_ips,
|
254
|
+
"compliance" => @compliance,
|
255
|
+
"start_time" => self.start_time,
|
256
|
+
"end_time" => self.end_time,
|
257
|
+
"scan_duration_seconds" => self.scan_duration,
|
258
|
+
}
|
259
|
+
|
260
|
+
if self.error?
|
261
|
+
hashed_object.error = self.error
|
262
|
+
end
|
263
|
+
|
264
|
+
hashed_object
|
265
|
+
end
|
266
|
+
|
267
|
+
def to_json
|
268
|
+
self.to_hash.to_json
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
data/lib/ssh_scan/scan_engine.rb
CHANGED
@@ -4,6 +4,7 @@ require 'ssh_scan/crypto'
|
|
4
4
|
#require 'ssh_scan/fingerprint_database'
|
5
5
|
require 'net/ssh'
|
6
6
|
require 'logger'
|
7
|
+
require 'open3'
|
7
8
|
|
8
9
|
module SSHScan
|
9
10
|
# Handle scanning of targets.
|
@@ -19,41 +20,65 @@ module SSHScan
|
|
19
20
|
port = 22
|
20
21
|
end
|
21
22
|
timeout = opts["timeout"]
|
22
|
-
|
23
|
+
|
24
|
+
result = SSHScan::Result.new()
|
25
|
+
result.port = port.to_i
|
23
26
|
|
24
|
-
|
27
|
+
# Start the scan timer
|
28
|
+
result.set_start_time
|
25
29
|
|
26
30
|
if target.fqdn?
|
31
|
+
result.hostname = target
|
32
|
+
|
33
|
+
# If doesn't resolve as IPv6, we'll try IPv4
|
27
34
|
if target.resolve_fqdn_as_ipv6.nil?
|
28
35
|
client = SSHScan::Client.new(
|
29
36
|
target.resolve_fqdn_as_ipv4.to_s, port, timeout
|
30
37
|
)
|
31
38
|
client.connect()
|
32
|
-
result
|
33
|
-
|
34
|
-
|
39
|
+
result.set_client_attributes(client)
|
40
|
+
kex_result = client.get_kex_result()
|
41
|
+
result.set_kex_result(kex_result) unless kex_result.nil?
|
42
|
+
result.error = client.error if client.error?
|
43
|
+
# If it does resolve as IPv6, we're try IPv6
|
35
44
|
else
|
36
45
|
client = SSHScan::Client.new(
|
37
46
|
target.resolve_fqdn_as_ipv6.to_s, port, timeout
|
38
47
|
)
|
39
48
|
client.connect()
|
40
|
-
result
|
41
|
-
|
49
|
+
result.set_client_attributes(client)
|
50
|
+
kex_result = client.get_kex_result()
|
51
|
+
result.set_kex_result(kex_result) unless kex_result.nil?
|
52
|
+
result.error = client.error if client.error?
|
53
|
+
|
54
|
+
# If resolves as IPv6, but somehow we get an client error, fall-back to IPv4
|
55
|
+
if result.error?
|
42
56
|
client = SSHScan::Client.new(
|
43
57
|
target.resolve_fqdn_as_ipv4.to_s, port, timeout
|
44
58
|
)
|
45
59
|
client.connect()
|
46
|
-
result
|
47
|
-
|
48
|
-
|
60
|
+
result.set_client_attributes(client)
|
61
|
+
kex_result = client.get_kex_result()
|
62
|
+
result.set_kex_result(kex_result) unless kex_result.nil?
|
63
|
+
result.error = client.error if client.error?
|
49
64
|
end
|
50
65
|
end
|
51
66
|
else
|
52
67
|
client = SSHScan::Client.new(target, port, timeout)
|
53
68
|
client.connect()
|
54
|
-
result
|
55
|
-
|
56
|
-
|
69
|
+
result.set_client_attributes(client)
|
70
|
+
kex_result = client.get_kex_result()
|
71
|
+
result.set_kex_result(kex_result)
|
72
|
+
|
73
|
+
# Attempt to suppliment a hostname that wasn't provided
|
74
|
+
result.hostname = target.resolve_ptr
|
75
|
+
|
76
|
+
result.error = client.error if client.error?
|
77
|
+
end
|
78
|
+
|
79
|
+
if result.error?
|
80
|
+
result.set_end_time
|
81
|
+
return result
|
57
82
|
end
|
58
83
|
|
59
84
|
# Connect and get results (Net-SSH)
|
@@ -69,57 +94,54 @@ module SSHScan
|
|
69
94
|
net_ssh_session, :auth_methods => ["none"]
|
70
95
|
)
|
71
96
|
auth_session.authenticate("none", "test", "test")
|
72
|
-
result
|
97
|
+
result.auth_methods = auth_session.allowed_auth_methods
|
73
98
|
net_ssh_session.close
|
74
99
|
rescue Net::SSH::ConnectionTimeout => e
|
75
|
-
result
|
76
|
-
result[:error] = SSHScan::Error::ConnectTimeout.new(e.message)
|
100
|
+
result.error = SSHScan::Error::ConnectTimeout.new(e.message)
|
77
101
|
rescue Net::SSH::Disconnect => e
|
78
|
-
result
|
79
|
-
result[:error] = SSHScan::Error::Disconnected.new(e.message)
|
102
|
+
result.error = SSHScan::Error::Disconnected.new(e.message)
|
80
103
|
rescue Net::SSH::Exception => e
|
81
104
|
if e.to_s.match(/could not settle on/)
|
82
|
-
result
|
105
|
+
result.error = e
|
83
106
|
else
|
84
107
|
raise e
|
85
108
|
end
|
86
|
-
|
87
|
-
result['fingerprints'] = {}
|
88
|
-
host_keys = `ssh-keyscan -t rsa,dsa #{target} 2>/dev/null`.split
|
89
|
-
host_keys_len = host_keys.length - 1
|
90
|
-
|
91
|
-
for i in 0..host_keys_len
|
92
|
-
if host_keys[i].eql? "ssh-dss"
|
93
|
-
pkey = SSHScan::Crypto::PublicKey.new(host_keys[i + 1])
|
94
|
-
result['fingerprints'].merge!({
|
95
|
-
"dsa" => {
|
96
|
-
"known_bad" => pkey.bad_key?.to_s,
|
97
|
-
"md5" => pkey.fingerprint_md5,
|
98
|
-
"sha1" => pkey.fingerprint_sha1,
|
99
|
-
"sha256" => pkey.fingerprint_sha256,
|
100
|
-
}
|
101
|
-
})
|
102
|
-
end
|
109
|
+
end
|
103
110
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
111
|
+
# Figure out what rsa or dsa fingerprints exist
|
112
|
+
fingerprints = {}
|
113
|
+
|
114
|
+
host_keys = `ssh-keyscan -t rsa,dsa #{target} 2>/dev/null`.split
|
115
|
+
host_keys_len = host_keys.length - 1
|
116
|
+
|
117
|
+
for i in 0..host_keys_len
|
118
|
+
if host_keys[i].eql? "ssh-dss"
|
119
|
+
pkey = SSHScan::Crypto::PublicKey.new(host_keys[i + 1])
|
120
|
+
fingerprints.merge!({
|
121
|
+
"dsa" => {
|
122
|
+
"known_bad" => pkey.bad_key?.to_s,
|
123
|
+
"md5" => pkey.fingerprint_md5,
|
124
|
+
"sha1" => pkey.fingerprint_sha1,
|
125
|
+
"sha256" => pkey.fingerprint_sha256,
|
126
|
+
}
|
127
|
+
})
|
128
|
+
end
|
129
|
+
|
130
|
+
if host_keys[i].eql? "ssh-rsa"
|
131
|
+
pkey = SSHScan::Crypto::PublicKey.new(host_keys[i + 1])
|
132
|
+
fingerprints.merge!({
|
133
|
+
"rsa" => {
|
134
|
+
"known_bad" => pkey.bad_key?.to_s,
|
135
|
+
"md5" => pkey.fingerprint_md5,
|
136
|
+
"sha1" => pkey.fingerprint_sha1,
|
137
|
+
"sha256" => pkey.fingerprint_sha256,
|
138
|
+
}
|
139
|
+
})
|
115
140
|
end
|
116
141
|
end
|
117
142
|
|
118
|
-
|
119
|
-
|
120
|
-
result['start_time'] = start_time.to_s
|
121
|
-
result['end_time'] = end_time.to_s
|
122
|
-
result['scan_duration_seconds'] = end_time - start_time
|
143
|
+
result.fingerprints = fingerprints
|
144
|
+
result.set_end_time
|
123
145
|
|
124
146
|
return result
|
125
147
|
end
|
@@ -156,13 +178,14 @@ module SSHScan
|
|
156
178
|
opts['fingerprint_database']
|
157
179
|
)
|
158
180
|
results.each do |result|
|
159
|
-
fingerprint_db.clear_fingerprints(result
|
160
|
-
|
161
|
-
|
181
|
+
fingerprint_db.clear_fingerprints(result.ip)
|
182
|
+
|
183
|
+
if result.fingerprints
|
184
|
+
result.fingerprints.values.each do |host_key_algo|
|
162
185
|
host_key_algo.each do |fingerprint|
|
163
186
|
key, value = fingerprint
|
164
187
|
next if key == "known_bad"
|
165
|
-
fingerprint_db.add_fingerprint(value, result
|
188
|
+
fingerprint_db.add_fingerprint(value, result.ip)
|
166
189
|
end
|
167
190
|
end
|
168
191
|
end
|
@@ -170,45 +193,48 @@ module SSHScan
|
|
170
193
|
|
171
194
|
# Decorate all the results with duplicate keys
|
172
195
|
results.each do |result|
|
173
|
-
if result
|
174
|
-
ip = result
|
175
|
-
result
|
176
|
-
result
|
196
|
+
if result.fingerprints
|
197
|
+
ip = result.ip
|
198
|
+
result.duplicate_host_key_ips = []
|
199
|
+
result.fingerprints.values.each do |host_key_algo|
|
177
200
|
host_key_algo.each do |fingerprint|
|
178
201
|
key, value = fingerprint
|
179
202
|
next if key == "known_bad"
|
180
203
|
fingerprint_db.find_fingerprints(value).each do |other_ip|
|
181
204
|
next if ip == other_ip
|
182
|
-
result
|
205
|
+
result.duplicate_host_key_ips << other_ip
|
183
206
|
end
|
184
207
|
end
|
185
208
|
end
|
186
|
-
result
|
209
|
+
result.duplicate_host_key_ips
|
187
210
|
end
|
188
211
|
end
|
189
212
|
|
190
213
|
# Decorate all the results with compliance information
|
191
214
|
results.each do |result|
|
192
215
|
# Do this only when we have all the information we need
|
193
|
-
if
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
!result[:languages_client_to_server].nil? &&
|
203
|
-
!result[:languages_server_to_client].nil?
|
216
|
+
if opts["policy"] &&
|
217
|
+
result.key_algorithms.any? &&
|
218
|
+
result.server_host_key_algorithms.any? &&
|
219
|
+
result.encryption_algorithms_client_to_server.any? &&
|
220
|
+
result.encryption_algorithms_server_to_client.any? &&
|
221
|
+
result.mac_algorithms_client_to_server.any? &&
|
222
|
+
result.mac_algorithms_server_to_client.any? &&
|
223
|
+
result.compression_algorithms_client_to_server.any? &&
|
224
|
+
result.compression_algorithms_server_to_client.any?
|
204
225
|
|
205
226
|
policy = SSHScan::Policy.from_file(opts["policy"])
|
206
227
|
policy_mgr = SSHScan::PolicyManager.new(result, policy)
|
207
|
-
result
|
228
|
+
result.set_compliance = policy_mgr.compliance_results
|
229
|
+
|
230
|
+
if result.compliance_policy
|
231
|
+
grader = SSHScan::Grader.new(result)
|
232
|
+
result.grade = grader.grade
|
233
|
+
end
|
208
234
|
end
|
209
235
|
end
|
210
236
|
|
211
|
-
return results
|
237
|
+
return results.map {|r| r.to_hash}
|
212
238
|
end
|
213
239
|
end
|
214
240
|
end
|
data/lib/ssh_scan/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ssh_scan
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.22
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jonathan Claudius
|
@@ -12,7 +12,7 @@ authors:
|
|
12
12
|
autorequire:
|
13
13
|
bindir: bin
|
14
14
|
cert_chain: []
|
15
|
-
date: 2017-
|
15
|
+
date: 2017-06-08 00:00:00.000000000 Z
|
16
16
|
dependencies:
|
17
17
|
- !ruby/object:Gem::Dependency
|
18
18
|
name: bindata
|
@@ -351,6 +351,7 @@ files:
|
|
351
351
|
- lib/ssh_scan/error/no_banner.rb
|
352
352
|
- lib/ssh_scan/error/no_kex_response.rb
|
353
353
|
- lib/ssh_scan/fingerprint_database.rb
|
354
|
+
- lib/ssh_scan/grader.rb
|
354
355
|
- lib/ssh_scan/os.rb
|
355
356
|
- lib/ssh_scan/os/centos.rb
|
356
357
|
- lib/ssh_scan/os/cisco.rb
|
@@ -366,6 +367,7 @@ files:
|
|
366
367
|
- lib/ssh_scan/policy.rb
|
367
368
|
- lib/ssh_scan/policy_manager.rb
|
368
369
|
- lib/ssh_scan/protocol.rb
|
370
|
+
- lib/ssh_scan/result.rb
|
369
371
|
- lib/ssh_scan/scan_engine.rb
|
370
372
|
- lib/ssh_scan/ssh_lib.rb
|
371
373
|
- lib/ssh_scan/ssh_lib/ciscossh.rb
|