recog 3.1.1 → 3.1.2

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.
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