ssh_scan 0.0.21 → 0.0.22
Sign up to get free protection for your applications and to get access to all the features.
- 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
|