CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/lib/metasploit/framework/login_scanner/snmp.rb
Views: 1904
1
# -*- coding: binary -*-
2
3
require 'snmp'
4
require 'metasploit/framework/login_scanner/base'
5
6
module Metasploit
7
module Framework
8
module LoginScanner
9
# This is the LoginScanner class for dealing with SNMP.
10
# It is responsible for taking a single target, and a list of credentials
11
# and attempting them. It then saves the results.
12
class SNMP
13
include Metasploit::Framework::LoginScanner::Base
14
15
DEFAULT_TIMEOUT = 2
16
DEFAULT_PORT = 161
17
DEFAULT_PROTOCOL = 'udp'.freeze
18
DEFAULT_VERSION = '1'.freeze
19
DEFAULT_QUEUE_SIZE = 100
20
LIKELY_PORTS = [ 161, 162 ].freeze
21
LIKELY_SERVICE_NAMES = [ 'snmp' ].freeze
22
PRIVATE_TYPES = [ :password ].freeze
23
REALM_KEY = nil
24
25
attr_accessor :queued_credentials, :queued_results, :sock # :nodoc: # :nodoc: # :nodoc:
26
27
# The SNMP version to scan
28
# @return [String]
29
attr_accessor :version
30
31
# The SNMP protocol to use
32
# @return [String]
33
attr_accessor :protocol
34
35
# The number of logins to try in each batch
36
# @return [Integer]
37
attr_accessor :queue_size
38
39
validates :version,
40
presence: true,
41
inclusion: {
42
in: ['1', '2c', 'all']
43
}
44
45
validates :protocol,
46
presence: true,
47
inclusion: {
48
in: ['udp', 'tcp']
49
}
50
51
validates :queue_size,
52
presence: true,
53
numericality: {
54
only_integer: true,
55
greater_than_or_equal_to: 0
56
}
57
58
# This method returns an array of versions to scan
59
# @return [Array] An array of versions
60
def versions
61
case version
62
when '1'
63
[:SNMPv1]
64
when '2c'
65
[:SNMPv2c]
66
when 'all'
67
%i[SNMPv1 SNMPv2c]
68
end
69
end
70
71
# Attempt to login with every {Credential credential} in # #cred_details.
72
#
73
# @yieldparam result [Result] The {Result} object for each attempt
74
# @yieldreturn [void]
75
# @return [void]
76
def scan!
77
valid!
78
79
# Keep track of connection errors.
80
# If we encounter too many, we will stop.
81
consecutive_error_count = 0
82
total_error_count = 0
83
84
successful_users = Set.new
85
first_attempt = true
86
87
# Create a socket for the initial login tests (read-only)
88
configure_socket
89
90
# Create a map of community name to credential object
91
credential_map = {}
92
93
begin
94
each_credential do |credential|
95
# Track the credentials by community string
96
credential_map[credential.public] = credential
97
98
# Skip users for whom we've have already found a password
99
if successful_users.include?(credential.public)
100
# For Pro bruteforce Reuse and Guess we need to note that we
101
# skipped an attempt.
102
if credential.parent.respond_to?(:skipped)
103
credential.parent.skipped = true
104
credential.parent.save!
105
end
106
next
107
end
108
# Queue and trigger authentication if queue size is reached
109
versions.each do |version|
110
process_logins(community: credential.public, type: 'read', version: version)
111
end
112
113
# Exit early if we already have a positive result
114
if stop_on_success && !queued_results.empty?
115
break
116
end
117
end
118
rescue Errno::ECONNREFUSED
119
# Exit early if we get an ICMP port unreachable
120
return
121
end
122
123
# Handle any unprocessed responses
124
process_logins(final: true)
125
126
# Create a non-duplicated set of credentials
127
found_credentials = queued_results.uniq
128
129
# Reset the queued results for our write test
130
self.queued_results = []
131
132
# Grab a new socket to avoid stale replies
133
configure_socket
134
135
# Try to write back the originally received values
136
found_credentials.each do |result|
137
process_logins(
138
version: result[:snmp_version],
139
community: result[:community],
140
type: 'write',
141
data: result[:proof]
142
)
143
end
144
145
# Catch any stragglers
146
process_logins(final: true)
147
148
# Mark any results from our write scan as read-write in our found credentials
149
queued_results.select { |r| [0, 17].include? r[:snmp_error] }.map { |r| r[:community] }.uniq.each do |c|
150
found_credentials.select { |r| r[:community] == c }.each do |result|
151
result[:access_level] = 'read-write'
152
end
153
end
154
155
# Iterate the results
156
found_credentials.each do |result_options|
157
# Scrub the SNMP version & error code from the tracked result
158
result_options.delete(:snmp_version)
159
result_options.delete(:snmp_error)
160
161
# Associate the community with the original credential
162
result_options[:credential] = credential_map[result_options.delete(:community)]
163
164
# In the rare chance that we got a result for a community we didn't scan...
165
next unless result_options[:credential]
166
167
# Create, freeze, and yield the result
168
result = ::Metasploit::Framework::LoginScanner::Result.new(result_options)
169
result.freeze
170
yield result if block_given?
171
end
172
173
nil
174
ensure
175
shutdown_socket
176
end
177
178
# Queue up and possibly send any requests, based on the queue limit and final flag
179
def process_logins(opts = {})
180
self.queued_results ||= []
181
self.queued_credentials ||= []
182
183
unless opts[:final] || self.queued_credentials.length > queue_size
184
self.queued_credentials.push [ opts[:type], opts[:community], opts[:version], opts[:data] ]
185
return
186
end
187
188
return if self.queued_credentials.empty?
189
190
process_responses(0.01)
191
192
until self.queued_credentials.empty?
193
action, community, version, data = self.queued_credentials.pop
194
case action
195
when 'read'
196
send_snmp_read_request(version, community)
197
when 'write'
198
send_snmp_write_request(version, community, data)
199
end
200
sleep_between_attempts
201
end
202
process_responses(1.0)
203
end
204
205
def recv_wrapper(sock, max_size, timeout)
206
res = nil
207
if protocol == 'udp'
208
res = sock.recvfrom(max_size, timeout)
209
elsif protocol == 'tcp'
210
ready = ::IO.select([sock], nil, nil, timeout)
211
if ready
212
res = sock.recv_nonblock(max_size)
213
# Put into an array to mimic recvfrom
214
res = [res, host, port]
215
end
216
end
217
218
res
219
end
220
221
# Process any responses on the UDP socket and queue the results
222
def process_responses(timeout = 1.0)
223
queue = []
224
while (res = recv_wrapper(sock, 65535, timeout))
225
226
# Ignore invalid responses
227
break if !(res[1])
228
229
# Ignore empty responses
230
next if !(res[0] && !res[0].empty?)
231
232
# Trim the IPv6-compat prefix off if needed
233
shost = res[1].sub(/^::ffff:/, '')
234
235
response = parse_snmp_response(res[0])
236
next unless response
237
238
self.queued_results << {
239
community: response[:community],
240
host: host,
241
port: port,
242
protocol: protocol,
243
service_name: 'snmp',
244
proof: response[:proof],
245
status: Metasploit::Model::Login::Status::SUCCESSFUL,
246
access_level: 'read-only',
247
snmp_version: response[:version],
248
snmp_error: response[:error]
249
}
250
end
251
end
252
253
# Create and send a SNMP read request for sys.sysDescr.0
254
def send_snmp_read_request(version, community)
255
send_snmp_request(
256
create_snmp_read_sys_descr_request(version, community)
257
)
258
end
259
260
# Create and send a SNMP write request for sys.sysDescr.0
261
def send_snmp_write_request(version, community, data)
262
send_snmp_request(
263
create_snmp_write_sys_descr_request(version, community, data)
264
)
265
end
266
267
def send_wrapper(sock, pkt, host, port, flags)
268
if protocol == 'tcp'
269
return sock.send(pkt, flags)
270
end
271
272
if protocol == 'udp'
273
return sock.sendto(pkt, host, port, 0)
274
end
275
end
276
277
# Send a SNMP request on the existing socket
278
def send_snmp_request(pkt)
279
resend_count = 0
280
281
begin
282
send_wrapper(sock, pkt, host, port, 0)
283
rescue ::Errno::ENOBUFS
284
resend_count += 1
285
if resend_count > MAX_RESEND_COUNT
286
return false
287
end
288
289
::IO.select(nil, nil, nil, 0.25)
290
retry
291
rescue ::Rex::ConnectionError
292
# This fires for host unreachable, net unreachable, and broadcast sends
293
# We can safely ignore all of these for UDP sends
294
end
295
end
296
297
# Create a SNMP request that tries to read from sys.sysDescr.0
298
def create_snmp_read_sys_descr_request(version_str, community)
299
version = version_str == :SNMPv1 ? 1 : 2
300
OpenSSL::ASN1::Sequence([
301
OpenSSL::ASN1::Integer(version - 1),
302
OpenSSL::ASN1::OctetString(community),
303
OpenSSL::ASN1::Set.new([
304
OpenSSL::ASN1::Integer(rand(0x80000000)),
305
OpenSSL::ASN1::Integer(0),
306
OpenSSL::ASN1::Integer(0),
307
OpenSSL::ASN1::Sequence([
308
OpenSSL::ASN1::Sequence([
309
OpenSSL::ASN1.ObjectId('1.3.6.1.2.1.1.1.0'),
310
OpenSSL::ASN1.Null(nil)
311
])
312
]),
313
], 0, :IMPLICIT)
314
]).to_der
315
end
316
317
# Create a SNMP request that tries to write to sys.sysDescr.0
318
def create_snmp_write_sys_descr_request(version_str, community, data)
319
version = version_str == :SNMPv1 ? 1 : 2
320
snmp_write = OpenSSL::ASN1::Sequence([
321
OpenSSL::ASN1::Integer(version - 1),
322
OpenSSL::ASN1::OctetString(community),
323
OpenSSL::ASN1::Set.new([
324
OpenSSL::ASN1::Integer(rand(0x80000000)),
325
OpenSSL::ASN1::Integer(0),
326
OpenSSL::ASN1::Integer(0),
327
OpenSSL::ASN1::Sequence([
328
OpenSSL::ASN1::Sequence([
329
OpenSSL::ASN1.ObjectId('1.3.6.1.2.1.1.1.0'),
330
OpenSSL::ASN1::OctetString(data)
331
])
332
]),
333
], 3, :IMPLICIT)
334
]).to_der
335
end
336
337
# Parse a SNMP reply from a packet and return a response hash or nil
338
def parse_snmp_response(pkt)
339
asn = begin
340
OpenSSL::ASN1.decode(pkt)
341
rescue StandardError
342
nil
343
end
344
return if !asn
345
346
snmp_vers = begin
347
asn.value[0].value.to_i
348
rescue StandardError
349
nil
350
end
351
snmp_comm = begin
352
asn.value[1].value
353
rescue StandardError
354
nil
355
end
356
snmp_error = begin
357
asn.value[2].value[1].value.to_i
358
rescue StandardError
359
nil
360
end
361
snmp_data = begin
362
asn.value[2].value[3].value[0]
363
rescue StandardError
364
nil
365
end
366
snmp_oid = begin
367
snmp_data.value[0].value
368
rescue StandardError
369
nil
370
end
371
snmp_info = begin
372
snmp_data.value[1].value.to_s
373
rescue StandardError
374
nil
375
end
376
377
return if !(snmp_error && snmp_comm && snmp_data && snmp_oid && snmp_info)
378
379
snmp_vers = snmp_vers == 0 ? '1' : '2c'
380
381
{ error: snmp_error, community: snmp_comm, proof: snmp_info, version: snmp_vers }
382
end
383
384
# Create a new socket for this scanner
385
def configure_socket
386
shutdown_socket if sock
387
388
self.sock = ::Rex::Socket.create({
389
'PeerHost' => host,
390
'PeerPort' => port,
391
'Proto' => protocol,
392
'Timeout' => connection_timeout,
393
'Context' =>
394
{ 'Msf' => framework, 'MsfExploit' => framework_module }
395
})
396
end
397
398
# Close any open socket if it exists
399
def shutdown_socket
400
sock.close if sock
401
self.sock = nil
402
end
403
404
# Sets the SNMP parameters if not specified
405
def set_sane_defaults
406
self.connection_timeout = DEFAULT_TIMEOUT if connection_timeout.nil?
407
self.protocol = DEFAULT_PROTOCOL if protocol.nil?
408
self.port = DEFAULT_PORT if port.nil?
409
self.version = DEFAULT_VERSION if version.nil?
410
self.queue_size = DEFAULT_QUEUE_SIZE if queue_size.nil?
411
end
412
413
end
414
end
415
end
416
end
417
418