From 4bda42fd78e4f0b4e13386ec29494f5dd7cab461 Mon Sep 17 00:00:00 2001 From: Jonathan Claudius Date: Thu, 17 Jan 2019 15:22:58 -0500 Subject: [PATCH] Refactor fingerprints for keys --- lib/ssh_scan/crypto.rb | 60 ------------------- lib/ssh_scan/public_key.rb | 59 +++++++++++++++++++ lib/ssh_scan/result.rb | 12 ++-- lib/ssh_scan/scan_engine.rb | 111 ++++++++++++++++++----------------- spec/public_key_spec.rb | 78 ++++++++++++++++++++++++ spec/ssh_scan/result_spec.rb | 2 + 6 files changed, 202 insertions(+), 120 deletions(-) delete mode 100644 lib/ssh_scan/crypto.rb create mode 100644 lib/ssh_scan/public_key.rb create mode 100644 spec/public_key_spec.rb diff --git a/lib/ssh_scan/crypto.rb b/lib/ssh_scan/crypto.rb deleted file mode 100644 index 454ea493..00000000 --- a/lib/ssh_scan/crypto.rb +++ /dev/null @@ -1,60 +0,0 @@ -require 'openssl' -require 'sshkey' -require 'base64' - -module SSHScan - # All cryptography related methods. - module Crypto - # House methods helpful in analysing SSH public keys. - class PublicKey - def initialize(key) - @key = key - end - - # Is the current key known to be in our known bad key list - # @return [Boolean] true if this {SSHScan::Crypto::PublicKey} - # instance's key is also in {SSHScan::Crypto}'s - # bad_public_keys, otherwise false - def bad_key? - SSHScan::Crypto.bad_public_keys.each do |other_key| - if self.fingerprint_sha256 == other_key.fingerprint_sha256 - return true - end - end - - return false - end - - # Generate MD5 fingerprint for this {SSHScan::Crypto::PublicKey} instance. - # @return [String] formatted MD5 fingerprint - def fingerprint_md5 - OpenSSL::Digest::MD5.hexdigest(::Base64.decode64(@key)).scan(/../).join(':') - end - - # Generate SHA1 fingerprint for this {SSHScan::Crypto::PublicKey} instance. - # @return [String] formatted SHA1 fingerprint - def fingerprint_sha1 - OpenSSL::Digest::SHA1.hexdigest(::Base64.decode64(@key)).scan(/../).join(':') - end - - # Generate SHA256 fingerprint for this {SSHScan::Crypto::PublicKey} instance. - # @return [String] formatted SHA256 fingerprint - def fingerprint_sha256 - OpenSSL::Digest::SHA256.hexdigest(::Base64.decode64(@key)).scan(/../).join(':') - end - end - - def self.bad_public_keys - bad_keys = [] - - Dir.glob("data/ssh-badkeys/host/*.key").each do |file_path| - file = File.read(File.expand_path(file_path)) - key = SSHKey.new(file) - bad_keys << SSHScan::Crypto::PublicKey.new(key.ssh_public_key.split[1]) - end - - return bad_keys - end - - end -end diff --git a/lib/ssh_scan/public_key.rb b/lib/ssh_scan/public_key.rb new file mode 100644 index 00000000..ba71a481 --- /dev/null +++ b/lib/ssh_scan/public_key.rb @@ -0,0 +1,59 @@ +require 'openssl' +require 'sshkey' +require 'base64' + +module SSHScan + # All cryptography related methods. + module Crypto + # House methods helpful in analysing SSH public keys. + class PublicKey + def initialize(key_string) + @key_string = key_string + end + + def valid? + SSHKey.valid_ssh_public_key?(@key_string) + end + + def type + if @key_string.start_with?("ssh-rsa") + return "rsa" + elsif @key_string.start_with?("ssh-dss") + return "dsa" + else + return "unknown" + end + end + + def length + SSHKey.ssh_public_key_bits(@key_string) + end + + def fingerprint_md5 + SSHKey.fingerprint(@key_string) + end + + def fingerprint_sha1 + SSHKey.sha1_fingerprint(@key_string) + end + + def fingerprint_sha256 + SSHKey.sha256_fingerprint(@key_string) + end + + def to_hash + { + self.type => { + "raw" => @key_string, + "length" => self.length, + "fingerprints" => { + "md5" => self.fingerprint_md5, + "sha1" => self.fingerprint_sha1, + "sha256" => self.fingerprint_sha256 + } + } + } + end + end + end +end diff --git a/lib/ssh_scan/result.rb b/lib/ssh_scan/result.rb index c0484598..dac68552 100644 --- a/lib/ssh_scan/result.rb +++ b/lib/ssh_scan/result.rb @@ -8,7 +8,7 @@ module SSHScan class Result def initialize() @version = SSHScan::VERSION - @fingerprints = nil + @keys = nil @duplicate_host_key_ips = Set.new() @compliance = {} end @@ -157,12 +157,12 @@ def auth_methods=(auth_methods) @auth_methods = auth_methods end - def fingerprints=(fingerprints) - @fingerprints = fingerprints + def keys=(keys) + @keys = keys end - def fingerprints - @fingerprints + def keys + @keys end def duplicate_host_key_ips=(duplicate_host_key_ips) @@ -249,7 +249,7 @@ def to_hash "languages_client_to_server" => self.languages_client_to_server, "languages_server_to_client" => self.languages_server_to_client, "auth_methods" => self.auth_methods, - "fingerprints" => self.fingerprints, + "keys" => self.keys, "duplicate_host_key_ips" => self.duplicate_host_key_ips, "compliance" => @compliance, "start_time" => self.start_time, diff --git a/lib/ssh_scan/scan_engine.rb b/lib/ssh_scan/scan_engine.rb index 7526e10f..1f1dbdaf 100644 --- a/lib/ssh_scan/scan_engine.rb +++ b/lib/ssh_scan/scan_engine.rb @@ -1,6 +1,6 @@ require 'socket' require 'ssh_scan/client' -require 'ssh_scan/crypto' +require 'ssh_scan/public_key' require 'ssh_scan/fingerprint_database' require 'ssh_scan/subprocess' require 'net/ssh' @@ -119,7 +119,7 @@ def scan_target(socket, opts) end # Figure out what rsa or dsa fingerprints exist - fingerprints = {} + keys = {} output = "" @@ -136,31 +136,34 @@ def scan_target(socket, opts) for i in 0..host_keys_len if host_keys[i].eql? "ssh-dss" - pkey = SSHScan::Crypto::PublicKey.new(host_keys[i + 1]) - fingerprints.merge!({ - "dsa" => { - "known_bad" => pkey.bad_key?.to_s, - "md5" => pkey.fingerprint_md5, - "sha1" => pkey.fingerprint_sha1, - "sha256" => pkey.fingerprint_sha256, - } - }) + key = SSHScan::Crypto::PublicKey.new([host_keys[i], host_keys[i + 1]].join(" ")) + keys.merge!(key.to_hash) + # fingerprints.merge!({ + # "dsa" => { + # "known_bad" => pkey.bad_key?.to_s, + # "md5" => pkey.fingerprint_md5, + # "sha1" => pkey.fingerprint_sha1, + # "sha256" => pkey.fingerprint_sha256, + # } + # }) end if host_keys[i].eql? "ssh-rsa" - pkey = SSHScan::Crypto::PublicKey.new(host_keys[i + 1]) - fingerprints.merge!({ - "rsa" => { - "known_bad" => pkey.bad_key?.to_s, - "md5" => pkey.fingerprint_md5, - "sha1" => pkey.fingerprint_sha1, - "sha256" => pkey.fingerprint_sha256, - } - }) + key = SSHScan::Crypto::PublicKey.new([host_keys[i], host_keys[i + 1]].join(" ")) + keys.merge!(key.to_hash) + # pkey = SSHScan::Crypto::PublicKey.new(host_keys[i + 1]) + # fingerprints.merge!({ + # "rsa" => { + # "known_bad" => pkey.bad_key?.to_s, + # "md5" => pkey.fingerprint_md5, + # "sha1" => pkey.fingerprint_sha1, + # "sha256" => pkey.fingerprint_sha256, + # } + # }) end end - result.fingerprints = fingerprints + result.keys = keys result.set_end_time return result @@ -194,41 +197,41 @@ def scan(opts) 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.fingerprints - result.fingerprints.values.each do |host_key_algo| - host_key_algo.each do |fingerprint| - key, value = fingerprint - next if key == "known_bad" - fingerprint_db.add_fingerprint(value, result.ip) - end - end - end - end + # fingerprint_db = SSHScan::FingerprintDatabase.new( + # opts['fingerprint_database'] + # ) + # results.each do |result| + # fingerprint_db.clear_fingerprints(result.ip) + + # if result.fingerprints + # result.fingerprints.values.each do |host_key_algo| + # host_key_algo.each do |fingerprint| + # key, value = fingerprint + # next if key == "known_bad" + # fingerprint_db.add_fingerprint(value, result.ip) + # end + # end + # end + # end # Decorate all the results with duplicate keys - results.each do |result| - if result.fingerprints - ip = result.ip - result.duplicate_host_key_ips = [] - result.fingerprints.values.each do |host_key_algo| - host_key_algo.each do |fingerprint| - key, value = fingerprint - next if key == "known_bad" - fingerprint_db.find_fingerprints(value).each do |other_ip| - next if ip == other_ip - result.duplicate_host_key_ips << other_ip - end - end - end - result.duplicate_host_key_ips - end - end + # results.each do |result| + # if result.fingerprints + # ip = result.ip + # result.duplicate_host_key_ips = [] + # result.fingerprints.values.each do |host_key_algo| + # host_key_algo.each do |fingerprint| + # key, value = fingerprint + # next if key == "known_bad" + # fingerprint_db.find_fingerprints(value).each do |other_ip| + # next if ip == other_ip + # result.duplicate_host_key_ips << other_ip + # end + # end + # end + # result.duplicate_host_key_ips + # end + # end # Decorate all the results with compliance information results.each do |result| diff --git a/spec/public_key_spec.rb b/spec/public_key_spec.rb new file mode 100644 index 00000000..88b14492 --- /dev/null +++ b/spec/public_key_spec.rb @@ -0,0 +1,78 @@ +require 'spec_helper' +require 'rspec' +require 'ssh_scan/public_key' + +describe SSHScan::Crypto::PublicKey do + context "when parsing an RSA key string" do + it "should parse it and have the right values for each attribute" do + key_string = "ssh-rsa " + + "AAAAB3NzaC1yc2EAAAADAQABAAABAQCl/BNLUxR49+3AKqhf6sWKr" + + "h8XXzqXV00bEPFtcJFWxyRqC5pPWo9zRRiS2jitIcqljIQVohEEZH" + + "t48vZaA1hniVfe/FmrFzuCOuQOIP2fuRgLSNHu+lWVScsHoX/MuYX" + + "EIxj6aW7UpFn4lD01mvPtazXFO/tJ+LRs49YBP7UvL1smIS2xoyuH" + + "7kZDN17QG08YwbIB2fApMl8rXH+2Rpj5hlv+7rcZ1dqCGtmXmvsv8" + + "fKGYd7BxRy0s/d7EY4e/DeDxA1qTNV9BrBTNn6jAKIedTE5s4GNRb" + + "N/Q20mP2qmw70PiTGROw6xp9SBFA7N9hjjOT7iutK/pa7y1joXKjeJ" + key = SSHScan::Crypto::PublicKey.new(key_string) + expect(key).to be_kind_of SSHScan::Crypto::PublicKey + expect(key.valid?).to be true + expect(key.type).to eq("rsa") + expect(key.length).to be 2048 + expect(key.fingerprint_md5).to eq("fc:c5:5b:0d:f0:c6:fd:fe:80:18:62:2c:05:38:20:8a") + expect(key.fingerprint_sha1).to eq("e1:3c:71:49:80:37:87:32:b5:0c:e3:86:41:ef:2e:2a:2f:14:e3:58") + expect(key.fingerprint_sha256).to eq("aH0wN2cs6x5Ktf9PvIzoQeFVqDBC4I484wq6vNv9XFA=") + expect(key.to_hash).to eq( + { + "rsa" => { + "fingerprints" => { + "md5"=>"fc:c5:5b:0d:f0:c6:fd:fe:80:18:62:2c:05:38:20:8a", + "sha1"=>"e1:3c:71:49:80:37:87:32:b5:0c:e3:86:41:ef:2e:2a:2f:14:e3:58", + "sha256"=>"aH0wN2cs6x5Ktf9PvIzoQeFVqDBC4I484wq6vNv9XFA=" + }, + "length" => 2048, + "raw" => "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCl/BNLUxR49+3AKqhf6sWKrh8XXzqXV00bEPFtcJFWxyRqC5pPWo9zRRiS2jitIcqljIQVohEEZHt48vZaA1hniVfe/FmrFzuCOuQOIP2fuRgLSNHu+lWVScsHoX/MuYXEIxj6aW7UpFn4lD01mvPtazXFO/tJ+LRs49YBP7UvL1smIS2xoyuH7kZDN17QG08YwbIB2fApMl8rXH+2Rpj5hlv+7rcZ1dqCGtmXmvsv8fKGYd7BxRy0s/d7EY4e/DeDxA1qTNV9BrBTNn6jAKIedTE5s4GNRbN/Q20mP2qmw70PiTGROw6xp9SBFA7N9hjjOT7iutK/pa7y1joXKjeJ", + } + } + ) + end + end + + context "when parsing an DSA key string" do + it "should parse it and have the right values for each attribute" do + key_string = "ssh-dss " + + "AAAAB3NzaC1kc3MAAACBAOXOC6kuB7xDMgHS79KFQITNeAT9tMKd2oK1" + + "c6bQEHRgTSMP3sWZ1cntWVFKl5u6MEuEBBT9PZKWsy7vRE525Wwt+NbR" + + "IBso3vYFF1MtxZKpAsF+gbGI7y+aZcIceXrHkkY2bz3oGb9I9MZ2DSu2" + + "9crW11YHCmuOJ2FJiDcx7dV9AAAAFQC+Ws9e0KJaAsN8cj75DbTQumrd" + + "JQAAAIBjn5EA5JvQg7xu8TRcNmZWhuyBLoOZczU6nk2h4i+x4pbpVMVr" + + "Ch5Lr8wsH60w7IW4yKg6JvPlzmQW0ZRZAwnU9sC3YO64H1RFQg8tnmRr" + + "w0I9oi6wKPEe5rLgbdr9jYHePs9tiV+ZFfUKmXh0s7srr/dwmX/gHCPI" + + "whLEVa+dLQAAAIEAn/+dSyf6KXdfKNyx9MYc1l2/2YUhVuxClF26PNQX" + + "0CZhcSoDyUXU/eAqaS7S6EYqtM/8FK1OZY1tzM5Nm4GWY2LLF22Q2YkK" + + "ItkhfS3GaD5JeuTQ+HK0F+wQjmpqt2pUulVQXQAjvE1qoRFQ4/yeVrvh" + + "VqCzFICnariQP7tMYEo=" + key = SSHScan::Crypto::PublicKey.new(key_string) + expect(key).to be_kind_of SSHScan::Crypto::PublicKey + expect(key.valid?).to be true + expect(key.type).to eq("dsa") + expect(key.length).to be 1024 + expect(key.fingerprint_md5).to eq("6b:5f:8d:57:be:2e:55:7f:e3:d7:15:d1:66:17:d8:8c") + expect(key.fingerprint_sha1).to eq("49:84:7f:d7:9d:84:2a:20:61:72:10:3f:2c:b1:16:9b:12:5b:e7:07") + expect(key.fingerprint_sha256).to eq("sWZzgrGxzs/aMmcU2w6FyET/Iihd6HL1qNyDZnO0NDw=") + expect(key.to_hash).to eq( + { + "dsa" => { + "fingerprints" => { + "md5"=>"6b:5f:8d:57:be:2e:55:7f:e3:d7:15:d1:66:17:d8:8c", + "sha1"=>"49:84:7f:d7:9d:84:2a:20:61:72:10:3f:2c:b1:16:9b:12:5b:e7:07", + "sha256"=>"sWZzgrGxzs/aMmcU2w6FyET/Iihd6HL1qNyDZnO0NDw=" + }, + "length" => 1024, + "raw" => "ssh-dss AAAAB3NzaC1kc3MAAACBAOXOC6kuB7xDMgHS79KFQITNeAT9tMKd2oK1c6bQEHRgTSMP3sWZ1cntWVFKl5u6MEuEBBT9PZKWsy7vRE525Wwt+NbRIBso3vYFF1MtxZKpAsF+gbGI7y+aZcIceXrHkkY2bz3oGb9I9MZ2DSu29crW11YHCmuOJ2FJiDcx7dV9AAAAFQC+Ws9e0KJaAsN8cj75DbTQumrdJQAAAIBjn5EA5JvQg7xu8TRcNmZWhuyBLoOZczU6nk2h4i+x4pbpVMVrCh5Lr8wsH60w7IW4yKg6JvPlzmQW0ZRZAwnU9sC3YO64H1RFQg8tnmRrw0I9oi6wKPEe5rLgbdr9jYHePs9tiV+ZFfUKmXh0s7srr/dwmX/gHCPIwhLEVa+dLQAAAIEAn/+dSyf6KXdfKNyx9MYc1l2/2YUhVuxClF26PNQX0CZhcSoDyUXU/eAqaS7S6EYqtM/8FK1OZY1tzM5Nm4GWY2LLF22Q2YkKItkhfS3GaD5JeuTQ+HK0F+wQjmpqt2pUulVQXQAjvE1qoRFQ4/yeVrvhVqCzFICnariQP7tMYEo=", + } + } + ) + end + end + +end \ No newline at end of file diff --git a/spec/ssh_scan/result_spec.rb b/spec/ssh_scan/result_spec.rb index 53c2824b..2cf78111 100644 --- a/spec/ssh_scan/result_spec.rb +++ b/spec/ssh_scan/result_spec.rb @@ -1,5 +1,6 @@ require 'spec_helper' require 'rspec' +require 'ssh_scan/version' require 'ssh_scan/result' describe SSHScan::Result do @@ -32,6 +33,7 @@ expect(result.start_time).to be_nil expect(result.end_time).to be_nil expect(result.scan_duration).to be_nil + expect(result.keys).to be_nil end context "when setting IP" do