Class: SSHScan::ScanEngine

Inherits:
Object
  • Object
show all
Defined in:
lib/ssh_scan/scan_engine.rb

Overview

Handle scanning of targets.

Instance Method Summary collapse

Instance Method Details

#scan(opts) ⇒ Hash

Utilize multiple threads to scan multiple targets, combine results and check for compliance.

Parameters:

  • opts (Hash)

    options (sockets, threads …)

Returns:

  • (Hash)

    results



170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
# File 'lib/ssh_scan/scan_engine.rb', line 170

def scan(opts)
  sockets = opts["sockets"]
  threads = opts["threads"] || 5
  logger = opts["logger"] || Logger.new(STDOUT)

  results = []

  work_queue = Queue.new

  sockets.each {|x| work_queue.push x }
  workers = (0...threads).map do
    Thread.new do
      begin
        while socket = work_queue.pop(true)
          results << scan_target(socket, opts)
        end
      rescue ThreadError => e
        raise e unless e.to_s.match(/queue empty/)
      end
    end
  end
  workers.map(&:join)

  # Add all the fingerprints to our peristent FingerprintDatabase
  fingerprint_db = SSHScan::FingerprintDatabase.new(
    opts['fingerprint_database']
  )
  results.each do |result|
    fingerprint_db.clear_fingerprints(result.ip)

    if result.keys
      result.keys.values.each do |host_key_algo|
        host_key_algo['fingerprints'].values.each do |fingerprint|
          fingerprint_db.add_fingerprint(fingerprint, result.ip)
        end
      end
    end
  end

  # Decorate all the results with duplicate keys
  results.each do |result|
    if result.keys
      ip = result.ip
      result.duplicate_host_key_ips = []
      result.keys.values.each do |host_key_algo|
        host_key_algo["fingerprints"].values.each do |fingerprint|
          fingerprint_db.find_fingerprints(fingerprint).each do |other_ip|
            next if ip == other_ip
            result.duplicate_host_key_ips << other_ip
          end
        end
      end
    end
  end

  # Decorate all the results with SSHFP records
  sshfp = SSHScan::SshFp.new()
  results.each do |result|
    if !result.hostname.empty?
      dns_keys = sshfp.query(result.hostname)
      result.dns_keys = dns_keys
    end
  end

  # Decorate all the results with compliance information
  results.each do |result|
    # Do this only when we have all the information we need
    if opts["policy"] &&
       result.key_algorithms.any? &&
       result.server_host_key_algorithms.any? &&
       result.encryption_algorithms_client_to_server.any? &&
       result.encryption_algorithms_server_to_client.any? &&
       result.mac_algorithms_client_to_server.any? &&
       result.mac_algorithms_server_to_client.any? &&
       result.compression_algorithms_client_to_server.any? &&
       result.compression_algorithms_server_to_client.any?

      policy = SSHScan::Policy.from_file(opts["policy"])
      policy_mgr = SSHScan::PolicyManager.new(result, policy)
      result.set_compliance = policy_mgr.compliance_results

      if result.compliance_policy
        result.grade = SSHScan::Grader.new(result).grade
      end
    end
  end

  return results.map {|r| r.to_hash}
end

#scan_target(socket, opts) ⇒ Hash

Scan a single target.

Parameters:

  • socket (String)

    ip:port specification

  • opts (Hash)

    options (timeout, …)

Returns:

  • (Hash)

    result



19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/ssh_scan/scan_engine.rb', line 19

def scan_target(socket, opts)
  target, port = socket.chomp.split(':')
  if port.nil?
    port = 22
  end

  timeout = opts["timeout"]
  
  result = SSHScan::Result.new()
  result.port = port.to_i

  # Start the scan timer
  result.set_start_time

  if target.fqdn?
    result.hostname = target

    # If doesn't resolve as IPv6, we'll try IPv4
    if target.resolve_fqdn_as_ipv6.nil?
      client = SSHScan::Client.new(
        target.resolve_fqdn_as_ipv4.to_s, port, timeout
      )
      client.connect
      result.set_client_attributes(client)
      kex_result = client.get_kex_result()
      client.close
      result.set_kex_result(kex_result) unless kex_result.nil?
      result.error = client.error if client.error?
    # If it does resolve as IPv6, we're try IPv6
    else
      client = SSHScan::Client.new(
        target.resolve_fqdn_as_ipv6.to_s, port, timeout
      )
      client.connect
      result.set_client_attributes(client)
      kex_result = client.get_kex_result()
      client.close
      result.set_kex_result(kex_result) unless kex_result.nil?
      result.error = client.error if client.error?

      # If resolves as IPv6, but somehow we get an client error, fall-back to IPv4
      if result.error?
        result.unset_error
        client = SSHScan::Client.new(
          target.resolve_fqdn_as_ipv4.to_s, port, timeout
        )
        client.connect()
        result.set_client_attributes(client)
        kex_result = client.get_kex_result()
        client.close
        result.set_kex_result(kex_result) unless kex_result.nil?
        result.error = client.error if client.error?
      end
    end
  else
    client = SSHScan::Client.new(target, port, timeout)
    client.connect()
    result.set_client_attributes(client)
    kex_result = client.get_kex_result()
    client.close

    unless kex_result.nil?
      result.set_kex_result(kex_result)
    end

    # Attempt to suppliment a hostname that wasn't provided
    result.hostname = target.resolve_ptr

    result.error = client.error if client.error?
  end

  if result.error?
    result.set_end_time
    return result
  end

  # Connect and get results (Net-SSH)
  begin
    net_ssh_session = Net::SSH::Transport::Session.new(
                        target,
                        :port => port,
                        :timeout => timeout,
                        :verify_host_key => :never
                      )
    raise SSHScan::Error::ClosedConnection.new if net_ssh_session.closed?
    auth_session = Net::SSH::Authentication::Session.new(
      net_ssh_session, :auth_methods => ["none"]
    )
    auth_session.authenticate("none", "test", "test")
    result.auth_methods = auth_session.allowed_auth_methods
    net_ssh_session.close
  rescue Net::SSH::ConnectionTimeout => e
    result.error = SSHScan::Error::ConnectTimeout.new(e.message)
  rescue Net::SSH::Disconnect, Errno::ECONNRESET => e
    result.error = SSHScan::Error::Disconnected.new(e.message)
  rescue Net::SSH::Exception => e
    if e.to_s.match(/could not settle on/)
      result.error = e
    else
      raise e
    end
  end

  # Figure out what rsa or dsa fingerprints exist
  keys = {}

  output = ""

  cmd = ['ssh-keyscan', '-t', 'rsa,dsa,ecdsa,ed25519', '-p', port.to_s, target].join(" ")

  Utils::Subprocess.new(cmd) do |stdout, stderr, thread|
    if stdout
      output += stdout
    end
  end

  host_keys = output.split
  host_keys_len = host_keys.length - 1

  for i in 0..host_keys_len
    if host_keys[i].eql? "ssh-dss"
      key = SSHScan::Crypto::PublicKey.new([host_keys[i], host_keys[i + 1]].join(" "))
      keys.merge!(key.to_hash)
    end

    if host_keys[i].eql? "ssh-rsa"
      key = SSHScan::Crypto::PublicKey.new([host_keys[i], host_keys[i + 1]].join(" "))
      keys.merge!(key.to_hash)
    end

    if host_keys[i].eql? "ecdsa-sha2-nistp256"
      key = SSHScan::Crypto::PublicKey.new([host_keys[i], host_keys[i + 1]].join(" "))
      keys.merge!(key.to_hash)
    end

    if host_keys[i].eql? "ssh-ed25519"
      key = SSHScan::Crypto::PublicKey.new([host_keys[i], host_keys[i + 1]].join(" "))
      keys.merge!(key.to_hash)
    end
  end

  result.keys = keys
  result.set_end_time

  return result
end