recog 3.1.1 → 3.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/Gemfile +6 -0
  4. data/Rakefile +7 -5
  5. data/lib/recog/db.rb +67 -68
  6. data/lib/recog/db_manager.rb +22 -21
  7. data/lib/recog/fingerprint/regexp_factory.rb +10 -13
  8. data/lib/recog/fingerprint/test.rb +9 -8
  9. data/lib/recog/fingerprint.rb +252 -262
  10. data/lib/recog/fingerprint_parse_error.rb +3 -1
  11. data/lib/recog/formatter.rb +41 -39
  12. data/lib/recog/match_reporter.rb +82 -83
  13. data/lib/recog/matcher.rb +37 -40
  14. data/lib/recog/matcher_factory.rb +7 -6
  15. data/lib/recog/nizer.rb +218 -224
  16. data/lib/recog/verifier.rb +30 -28
  17. data/lib/recog/verify_reporter.rb +69 -73
  18. data/lib/recog/version.rb +3 -1
  19. data/lib/recog.rb +2 -0
  20. data/recog/bin/recog_match +21 -20
  21. data/recog/xml/apache_modules.xml +2 -0
  22. data/recog/xml/dhcp_vendor_class.xml +1 -1
  23. data/recog/xml/favicons.xml +133 -1
  24. data/recog/xml/ftp_banners.xml +1 -1
  25. data/recog/xml/html_title.xml +140 -1
  26. data/recog/xml/http_cookies.xml +20 -2
  27. data/recog/xml/http_servers.xml +38 -17
  28. data/recog/xml/http_wwwauth.xml +17 -4
  29. data/recog/xml/mdns_device-info_txt.xml +49 -15
  30. data/recog/xml/sip_banners.xml +0 -2
  31. data/recog/xml/sip_user_agents.xml +1 -1
  32. data/recog/xml/snmp_sysdescr.xml +1 -2
  33. data/recog/xml/ssh_banners.xml +8 -0
  34. data/recog/xml/telnet_banners.xml +3 -2
  35. data/recog/xml/tls_jarm.xml +1 -1
  36. data/recog/xml/x11_banners.xml +1 -0
  37. data/recog/xml/x509_issuers.xml +1 -1
  38. data/recog/xml/x509_subjects.xml +0 -1
  39. data/recog.gemspec +14 -13
  40. data/spec/lib/recog/db_spec.rb +37 -36
  41. data/spec/lib/recog/fingerprint/regexp_factory_spec.rb +19 -20
  42. data/spec/lib/recog/fingerprint_spec.rb +44 -42
  43. data/spec/lib/recog/formatter_spec.rb +20 -18
  44. data/spec/lib/recog/match_reporter_spec.rb +35 -30
  45. data/spec/lib/recog/nizer_spec.rb +85 -101
  46. data/spec/lib/recog/verify_reporter_spec.rb +45 -44
  47. data/spec/spec_helper.rb +2 -1
  48. data.tar.gz.sig +1 -3
  49. metadata +3 -3
  50. metadata.gz.sig +0 -0
data/lib/recog/nizer.rb CHANGED
@@ -1,125 +1,127 @@
1
- module Recog
2
- class Nizer
3
-
4
- # Default certainty ratings where none are specified in the fingerprint itself
5
- DEFAULT_OS_CERTAINTY = 0.85 # Most frequent weights are 0.9, 1.0, and 0.5
6
- DEFAULT_SERVICE_CERTAINTY = 0.85 # Most frequent weight is 0.85
7
-
8
- # Non-weighted host attributes that can be extracted from fingerprint matches
9
- HOST_ATTRIBUTES = %W{
10
- host.domain
11
- host.ip
12
- host.mac
13
- host.name
14
- host.time
15
- hw.device
16
- hw.family
17
- hw.serial_number
18
- hw.product
19
- hw.vendor
20
- }
21
-
22
- @@db_manager = nil
23
- @@db_sorted = false
24
-
25
-
26
- #
27
- # Load fingerprints from a specific file or directory
28
- # This will not preserve any fingerprints that have already been loaded
29
- # @param path [String] Path to file or directory of XML fingerprints
30
- def self.load_db(path = nil)
31
- if path
32
- @@db_manager = Recog::DBManager.new(path)
33
- else
34
- @@db_manager = Recog::DBManager.new
35
- end
1
+ # frozen_string_literal: true
36
2
 
37
- # Sort the databases, no behavior or result change for those calling
38
- # Nizer.match or Nizer.multi_match as they have a single DB
39
- @@db_manager.databases.sort! { |a, b| b.preference <=> a.preference }
40
- @@db_sorted = true
41
- end
3
+ module Recog
4
+ class Nizer
5
+ # Default certainty ratings where none are specified in the fingerprint itself
6
+ DEFAULT_OS_CERTAINTY = 0.85 # Most frequent weights are 0.9, 1.0, and 0.5
7
+ DEFAULT_SERVICE_CERTAINTY = 0.85 # Most frequent weight is 0.85
8
+
9
+ # Non-weighted host attributes that can be extracted from fingerprint matches
10
+ HOST_ATTRIBUTES = %w[
11
+ host.domain
12
+ host.ip
13
+ host.mac
14
+ host.name
15
+ host.time
16
+ hw.device
17
+ hw.family
18
+ hw.serial_number
19
+ hw.product
20
+ hw.vendor
21
+ ].freeze
42
22
 
43
- #
44
- # Destroy the current DBManager object
45
- def self.unload_db
46
23
  @@db_manager = nil
47
24
  @@db_sorted = false
48
- end
49
25
 
50
- #
51
- # Display the fingerprint databases in the order in which they will be used
52
- # to match banners. This is useful for fingerprint tuning and debugging.
53
- def self.display_db_order
54
- self.load_db unless @@db_manager
26
+ #
27
+ # Load fingerprints from a specific file or directory
28
+ # This will not preserve any fingerprints that have already been loaded
29
+ # @param path [String] Path to file or directory of XML fingerprints
30
+ def self.load_db(path = nil)
31
+ @@db_manager = if path
32
+ Recog::DBManager.new(path)
33
+ else
34
+ Recog::DBManager.new
35
+ end
36
+
37
+ # Sort the databases, no behavior or result change for those calling
38
+ # Nizer.match or Nizer.multi_match as they have a single DB
39
+ @@db_manager.databases.sort! { |a, b| b.preference <=> a.preference }
40
+ @@db_sorted = true
41
+ end
55
42
 
56
- puts format('%s %-22s %-8s %s', 'Preference', 'Database', 'Type', 'Protocol')
57
- @@db_manager.databases.each do |db|
58
- puts format('%10.3f %-22s %-8s %s', db.preference, db.match_key,
59
- db.database_type, db.protocol)
43
+ #
44
+ # Destroy the current DBManager object
45
+ def self.unload_db
46
+ @@db_manager = nil
47
+ @@db_sorted = false
60
48
  end
61
- end
62
49
 
63
- #
64
- # 2016.11 - Rewritten to be wrapper around #match_db_all, functionality
65
- # and results must remain unchanged.
66
- #
67
- # Locate a database that corresponds with the `match_key` and attempt to
68
- # find a matching {Fingerprint fingerprint}, stopping at the first hit.
69
- # Returns `nil` when no matching database or fingerprint is found.
70
- #
71
- # @param match_key [String] Fingerprint DB name, e.g. 'smb.native_os'
72
- # @param match_string [String] String to match
73
- # @return (see Fingerprint#match) or nil
74
- def self.match(match_key, match_string)
75
- filter = { match_key: match_key, multi_match: false }
76
- matches = self.match_all_db(match_string, filter)
77
-
78
- matches[0]
79
- end
50
+ #
51
+ # Display the fingerprint databases in the order in which they will be used
52
+ # to match banners. This is useful for fingerprint tuning and debugging.
53
+ def self.display_db_order
54
+ load_db unless @@db_manager
55
+
56
+ puts format('%s %-22s %-8s %s', 'Preference', 'Database', 'Type', 'Protocol')
57
+ @@db_manager.databases.each do |db|
58
+ puts format('%10.3f %-22s %-8s %s', db.preference, db.match_key,
59
+ db.database_type, db.protocol)
60
+ end
61
+ end
80
62
 
81
- #
82
- # @param match_key [String] Fingerprint DB name, e.g. 'smb.native_os'
83
- # @param match_string [String] String to match
84
- # @return [Array] Array of Fingerprint#match or empty array
85
- def self.multi_match(match_key, match_string)
86
- filter = { match_key: match_key, multi_match: true }
87
- self.match_all_db(match_string, filter)
88
- end
63
+ #
64
+ # 2016.11 - Rewritten to be wrapper around #match_db_all, functionality
65
+ # and results must remain unchanged.
66
+ #
67
+ # Locate a database that corresponds with the `match_key` and attempt to
68
+ # find a matching {Fingerprint fingerprint}, stopping at the first hit.
69
+ # Returns `nil` when no matching database or fingerprint is found.
70
+ #
71
+ # @param match_key [String] Fingerprint DB name, e.g. 'smb.native_os'
72
+ # @param match_string [String] String to match
73
+ # @return (see Fingerprint#match) or nil
74
+ def self.match(match_key, match_string)
75
+ filter = { match_key: match_key, multi_match: false }
76
+ matches = match_all_db(match_string, filter)
77
+
78
+ matches[0]
79
+ end
80
+
81
+ #
82
+ # @param match_key [String] Fingerprint DB name, e.g. 'smb.native_os'
83
+ # @param match_string [String] String to match
84
+ # @return [Array] Array of Fingerprint#match or empty array
85
+ def self.multi_match(match_key, match_string)
86
+ filter = { match_key: match_key, multi_match: true }
87
+ match_all_db(match_string, filter)
88
+ end
89
+
90
+ #
91
+ # Search all fingerprint dbs and attempt to find matching
92
+ # {Fingerprint fingerprint}s. It will return the first match found
93
+ # unless the :multi_match option is used to request all matches.
94
+ # Returns an array of all matching fingerprints or an empty array.
95
+ #
96
+ # @param match_string [String] Service banner to match
97
+ # @param [Hash] filters This hash contains filters used to limit the
98
+ # results to just those from specific types of fingerprints.
99
+ # The values that these filters match come from the 'fingerprints' top
100
+ # level element in the fingerprint DB XML or, in the case of 'protocol',
101
+ # this value can be overridden at the individual fingerprint level by
102
+ # setting a value for 'service.protocol'
103
+ #
104
+ # With the exception of 'match_key', the filters below match the
105
+ # 'fingerprints' attributes with the same name.
106
+ # @option filters [String] :match_key Value from XML 'matches' or file name
107
+ # @option filters [String] :database_type fprint db type: service, util.os, etc.
108
+ # @option filters [String] :protocol Protocol (ftp, smtp, etc.)
109
+ # @option filters [Boolean] :multi_match Return all matches instead of first
110
+ # @return [Array] Array of Fingerprint#match or empty array
111
+ def self.match_all_db(match_string, filters = {})
112
+ match_string = match_string.to_s.unpack('C*').pack('C*')
113
+ matches = [] # array to hold all fingerprint matches
114
+
115
+ load_db unless @@db_manager
116
+
117
+ @@db_manager.databases.each do |db|
118
+ next if filters[:match_key] && !filters[:match_key].eql?(db.match_key)
119
+ next if filters[:database_type] && !filters[:database_type].eql?(db.database_type)
120
+
121
+ db.fingerprints.each do |fp|
122
+ m = fp.match(match_string)
123
+ next unless m
89
124
 
90
- #
91
- # Search all fingerprint dbs and attempt to find matching
92
- # {Fingerprint fingerprint}s. It will return the first match found
93
- # unless the :multi_match option is used to request all matches.
94
- # Returns an array of all matching fingerprints or an empty array.
95
- #
96
- # @param match_string [String] Service banner to match
97
- # @param [Hash] filters This hash contains filters used to limit the
98
- # results to just those from specific types of fingerprints.
99
- # The values that these filters match come from the 'fingerprints' top
100
- # level element in the fingerprint DB XML or, in the case of 'protocol',
101
- # this value can be overridden at the individual fingerprint level by
102
- # setting a value for 'service.protocol'
103
- #
104
- # With the exception of 'match_key', the filters below match the
105
- # 'fingerprints' attributes with the same name.
106
- # @option filters [String] :match_key Value from XML 'matches' or file name
107
- # @option filters [String] :database_type fprint db type: service, util.os, etc.
108
- # @option filters [String] :protocol Protocol (ftp, smtp, etc.)
109
- # @option filters [Boolean] :multi_match Return all matches instead of first
110
- # @return [Array] Array of Fingerprint#match or empty array
111
- def self.match_all_db(match_string, filters = {})
112
- match_string = match_string.to_s.unpack('C*').pack('C*')
113
- matches = Array.new # array to hold all fingerprint matches
114
-
115
- self.load_db unless @@db_manager
116
-
117
- @@db_manager.databases.each do |db|
118
- next if filters[:match_key] && !filters[:match_key].eql?(db.match_key)
119
- next if filters[:database_type] && !filters[:database_type].eql?(db.database_type)
120
- db.fingerprints.each do |fp|
121
- m = fp.match(match_string)
122
- if m
123
125
  # Filter on protocol after match since each individual fp
124
126
  # can contain its own 'protocol' value that overrides the
125
127
  # one set at the DB level.
@@ -127,140 +129,132 @@ class Nizer
127
129
  return matches unless filters[:multi_match]
128
130
  end
129
131
  end
130
- end
131
132
 
132
- matches
133
- end
133
+ matches
134
+ end
134
135
 
135
- #
136
- # Consider an array of match outputs, choose the best result, taking into
137
- # account the granularity of OS vs Version vs SP vs Language. Only consider
138
- # fields relevant to the host (OS, name, mac address, etc).
139
- #
140
- def self.best_os_match(matches)
136
+ #
137
+ # Consider an array of match outputs, choose the best result, taking into
138
+ # account the granularity of OS vs Version vs SP vs Language. Only consider
139
+ # fields relevant to the host (OS, name, mac address, etc).
140
+ #
141
+ def self.best_os_match(matches)
142
+ # The result hash we return to the caller
143
+ result = {}
144
+
145
+ # Certain attributes should be evaluated separately
146
+ host_attrs = {}
147
+
148
+ # Bucket matches into matched OS product names
149
+ os_products = {}
150
+
151
+ matches.each do |m|
152
+ # Count how many times each host attribute value is asserted
153
+ (HOST_ATTRIBUTES & m.keys).each do |ha|
154
+ host_attrs[ha] ||= {}
155
+ host_attrs[ha][m[ha]] ||= 0
156
+ host_attrs[ha][m[ha]] += 1
157
+ end
141
158
 
142
- # The result hash we return to the caller
143
- result = {}
159
+ next unless m.key?('os.product')
144
160
 
145
- # Certain attributes should be evaluated separately
146
- host_attrs = {}
161
+ # Group matches by OS product and normalize certainty
162
+ cm = m.dup
163
+ cm['os.certainty'] = (m['os.certainty'] || DEFAULT_OS_CERTAINTY).to_f
164
+ os_products[cm['os.product']] ||= []
165
+ os_products[cm['os.product']] << cm
166
+ end
147
167
 
148
- # Bucket matches into matched OS product names
149
- os_products = {}
168
+ #
169
+ # Select the best host attribute value by highest frequency
170
+ #
171
+ host_attrs.each_key do |hk|
172
+ ranked_attr = host_attrs[hk].keys.sort do |a, b|
173
+ host_attrs[hk][b] <=> host_attrs[hk][a]
174
+ end
175
+ result[hk] = ranked_attr.first
176
+ end
150
177
 
151
- matches.each do |m|
152
- # Count how many times each host attribute value is asserted
153
- (HOST_ATTRIBUTES & m.keys).each do |ha|
154
- host_attrs[ha] ||= {}
155
- host_attrs[ha][m[ha]] ||= 0
156
- host_attrs[ha][m[ha]] += 1
178
+ # Unable to guess the OS without OS matches
179
+ return result unless os_products.keys.length > 0
180
+
181
+ #
182
+ # Select the best operating system name by combined certainty of all
183
+ # matches within an os.product group. Multiple weak matches can
184
+ # outweigh a single strong match by design.
185
+ #
186
+ ranked_os = os_products.keys.sort do |a, b|
187
+ os_products[b].map { |r| r['os.certainty'] }.inject(:+) <=>
188
+ os_products[a].map { |r| r['os.certainty'] }.inject(:+)
157
189
  end
158
190
 
159
- next unless m.has_key?('os.product')
191
+ # Within the best match group, try to fill in missing attributes
192
+ os_name = ranked_os.first
160
193
 
161
- # Group matches by OS product and normalize certainty
162
- cm = m.dup
163
- cm['os.certainty'] = ( m['os.certainty'] || DEFAULT_OS_CERTAINTY ).to_f
164
- os_products[ cm['os.product'] ] ||= []
165
- os_products[ cm['os.product'] ] << cm
166
- end
194
+ # Find the best match within the winning group
195
+ ranked_os_matches = os_products[os_name].sort do |a, b|
196
+ b['os.certainty'] <=> a['os.certainty']
197
+ end
167
198
 
168
- #
169
- # Select the best host attribute value by highest frequency
170
- #
171
- host_attrs.keys.each do |hk|
172
- ranked_attr = host_attrs[hk].keys.sort do |a,b|
173
- host_attrs[hk][b] <=> host_attrs[hk][a]
199
+ # Fill in missing result values in descending order of best match
200
+ ranked_os_matches.each do |rm|
201
+ rm.each_pair do |k, v|
202
+ result[k] ||= v
203
+ end
174
204
  end
175
- result[hk] = ranked_attr.first
176
- end
177
205
 
178
- # Unable to guess the OS without OS matches
179
- unless os_products.keys.length > 0
180
- return result
206
+ result
181
207
  end
182
208
 
183
209
  #
184
- # Select the best operating system name by combined certainty of all
185
- # matches within an os.product group. Multiple weak matches can
186
- # outweigh a single strong match by design.
210
+ # Consider an array of match outputs, choose the best result, taking into
211
+ # account the granularity of service. Only consider fields relevant to the
212
+ # service.
187
213
  #
188
- ranked_os = os_products.keys.sort do |a,b|
189
- os_products[b].map{ |r| r['os.certainty'] }.inject(:+) <=>
190
- os_products[a].map{ |r| r['os.certainty'] }.inject(:+)
191
- end
192
-
193
- # Within the best match group, try to fill in missing attributes
194
- os_name = ranked_os.first
195
-
196
- # Find the best match within the winning group
197
- ranked_os_matches = os_products[os_name].sort do |a,b|
198
- b['os.certainty'] <=> a['os.certainty']
199
- end
200
-
201
- # Fill in missing result values in descending order of best match
202
- ranked_os_matches.each do |rm|
203
- rm.each_pair do |k,v|
204
- result[k] ||= v
214
+ def self.best_service_match(matches)
215
+ # The result hash we return to the caller
216
+ result = {}
217
+
218
+ # Bucket matches into matched service product names
219
+ service_products = {}
220
+
221
+ matches.select { |m| m.key?('service.product') }.each do |m|
222
+ # Group matches by product and normalize certainty
223
+ cm = m.dup
224
+ cm['service.certainty'] = (m['service.certainty'] || DEFAULT_SERVICE_CERTAINTY).to_f
225
+ service_products[cm['service.product']] ||= []
226
+ service_products[cm['service.product']] << cm
205
227
  end
206
- end
207
-
208
- result
209
- end
210
228
 
211
- #
212
- # Consider an array of match outputs, choose the best result, taking into
213
- # account the granularity of service. Only consider fields relevant to the
214
- # service.
215
- #
216
- def self.best_service_match(matches)
217
-
218
- # The result hash we return to the caller
219
- result = {}
220
-
221
- # Bucket matches into matched service product names
222
- service_products = {}
223
-
224
- matches.select{ |m| m.has_key?('service.product') }.each do |m|
225
- # Group matches by product and normalize certainty
226
- cm = m.dup
227
- cm['service.certainty'] = ( m['service.certainty'] || DEFAULT_SERVICE_CERTAINTY ).to_f
228
- service_products[ cm['service.product'] ] ||= []
229
- service_products[ cm['service.product'] ] << cm
230
- end
231
-
232
- # Unable to guess the service without service matches
233
- unless service_products.keys.length > 0
234
- return result
235
- end
236
-
237
- #
238
- # Select the best service name by combined certainty of all matches
239
- # within an service.product group. Multiple weak matches can
240
- # outweigh a single strong match by design.
241
- #
242
- ranked_service = service_products.keys.sort do |a,b|
243
- service_products[b].map{ |r| r['service.certainty'] }.inject(:+) <=>
244
- service_products[a].map{ |r| r['service.certainty'] }.inject(:+)
245
- end
229
+ # Unable to guess the service without service matches
230
+ return result unless service_products.keys.length > 0
231
+
232
+ #
233
+ # Select the best service name by combined certainty of all matches
234
+ # within an service.product group. Multiple weak matches can
235
+ # outweigh a single strong match by design.
236
+ #
237
+ ranked_service = service_products.keys.sort do |a, b|
238
+ service_products[b].map { |r| r['service.certainty'] }.inject(:+) <=>
239
+ service_products[a].map { |r| r['service.certainty'] }.inject(:+)
240
+ end
246
241
 
247
- # Within the best match group, try to fill in missing attributes
248
- service_name = ranked_service.first
242
+ # Within the best match group, try to fill in missing attributes
243
+ service_name = ranked_service.first
249
244
 
250
- # Find the best match within the winning group
251
- ranked_service_matches = service_products[service_name].sort do |a,b|
252
- b['service.certainty'] <=> a['service.certainty']
253
- end
245
+ # Find the best match within the winning group
246
+ ranked_service_matches = service_products[service_name].sort do |a, b|
247
+ b['service.certainty'] <=> a['service.certainty']
248
+ end
254
249
 
255
- # Fill in missing service values in descending order of best match
256
- ranked_service_matches.each do |rm|
257
- rm.keys.select{ |k| k.index('service.') == 0 }.each do |k|
258
- result[k] ||= rm[k]
250
+ # Fill in missing service values in descending order of best match
251
+ ranked_service_matches.each do |rm|
252
+ rm.keys.select { |k| k.index('service.') == 0 }.each do |k|
253
+ result[k] ||= rm[k]
254
+ end
259
255
  end
260
- end
261
256
 
262
- result
257
+ result
258
+ end
263
259
  end
264
-
265
- end
266
260
  end
@@ -1,39 +1,41 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Recog
2
- class Verifier
3
- attr_reader :db, :reporter
4
+ class Verifier
5
+ attr_reader :db, :reporter
4
6
 
5
- def initialize(db, reporter)
6
- @db = db
7
- @reporter = reporter
8
- end
7
+ def initialize(db, reporter)
8
+ @db = db
9
+ @reporter = reporter
10
+ end
9
11
 
10
- def verify
11
- reporter.report(db.fingerprints.count) do
12
- db.fingerprints.each do |fp|
13
- reporter.print_name fp
12
+ def verify
13
+ reporter.report(db.fingerprints.count) do
14
+ db.fingerprints.each do |fp|
15
+ reporter.print_name fp
14
16
 
15
- fp.verify_params do |status, message|
16
- case status
17
- when :warn
18
- reporter.warning message, fp.line
19
- when :fail
20
- reporter.failure message, fp.line
21
- when :success
22
- reporter.success(message)
17
+ fp.verify_params do |status, message|
18
+ case status
19
+ when :warn
20
+ reporter.warning message, fp.line
21
+ when :fail
22
+ reporter.failure message, fp.line
23
+ when :success
24
+ reporter.success(message)
25
+ end
23
26
  end
24
- end
25
- fp.verify_tests do |status, message|
26
- case status
27
- when :warn
28
- reporter.warning message, fp.line
29
- when :fail
30
- reporter.failure message, fp.line
31
- when :success
32
- reporter.success(message)
27
+ fp.verify_tests do |status, message|
28
+ case status
29
+ when :warn
30
+ reporter.warning message, fp.line
31
+ when :fail
32
+ reporter.failure message, fp.line
33
+ when :success
34
+ reporter.success(message)
35
+ end
33
36
  end
34
37
  end
35
38
  end
36
39
  end
37
40
  end
38
41
  end
39
- end