Skip to content

Commit ffd7d28

Browse files
committed
Merge pull request rapid7#3559 from dmaloney-r7/feature/MSP-10230/snmp_login
MSP-10230 #land
2 parents b1d1e4f + e54f5e8 commit ffd7d28

File tree

2 files changed

+128
-246
lines changed

2 files changed

+128
-246
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
require 'metasploit/framework/credential'
2+
3+
module Metasploit
4+
module Framework
5+
6+
# This class is responsible for taking datastore options from the snmp_login module
7+
# and yielding appropriate {Metasploit::Framework::Credential}s to the {Metasploit::Framework::LoginScanner::SNMP}.
8+
# This one has to be different from {credentialCollection} as it will only have a {Metasploit::Framework::Credential#public}
9+
# It may be slightly confusing that the attribues are called password and pass_file, because this is what the legacy
10+
# module used. However, community Strings are now considered more to be public credentials than private ones.
11+
class CommunityStringCollection
12+
# @!attribute pass_file
13+
# Path to a file containing passwords, one per line
14+
# @return [String]
15+
attr_accessor :pass_file
16+
17+
# @!attribute password
18+
# @return [String]
19+
attr_accessor :password
20+
21+
# @!attribute prepended_creds
22+
# List of credentials to be tried before any others
23+
#
24+
# @see #prepend_cred
25+
# @return [Array<Credential>]
26+
attr_accessor :prepended_creds
27+
28+
# @option opts [String] :pass_file See {#pass_file}
29+
# @option opts [String] :password See {#password}
30+
# @option opts [Array<Credential>] :prepended_creds ([]) See {#prepended_creds}
31+
def initialize(opts = {})
32+
opts.each do |attribute, value|
33+
public_send("#{attribute}=", value)
34+
end
35+
self.prepended_creds ||= []
36+
end
37+
38+
# Combines all the provided credential sources into a stream of {Credential}
39+
# objects, yielding them one at a time
40+
#
41+
# @yieldparam credential [Metasploit::Framework::Credential]
42+
# @return [void]
43+
def each
44+
begin
45+
if pass_file.present?
46+
pass_fd = File.open(pass_file, 'r:binary')
47+
pass_fd.each_line do |line|
48+
line.chomp!
49+
yield Metasploit::Framework::Credential.new(public: line, paired: false)
50+
end
51+
end
52+
53+
if password.present?
54+
yield Metasploit::Framework::Credential.new(public: password, paired: false)
55+
end
56+
57+
ensure
58+
pass_fd.close if pass_fd && !pass_fd.closed?
59+
end
60+
end
61+
62+
# Add {Credential credentials} that will be yielded by {#each}
63+
#
64+
# @see prepended_creds
65+
# @param cred [Credential]
66+
# @return [self]
67+
def prepend_cred(cred)
68+
prepended_creds.unshift cred
69+
self
70+
end
71+
72+
end
73+
end
74+
end

modules/auxiliary/scanner/snmp/snmp_login.rb

Lines changed: 54 additions & 246 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55

66

77
require 'msf/core'
8-
require 'openssl'
9-
require 'snmp'
8+
require 'metasploit/framework/community_string_collection'
9+
require 'metasploit/framework/login_scanner/snmp'
1010

1111
class Metasploit3 < Msf::Auxiliary
1212

@@ -49,260 +49,68 @@ def run_batch_size
4949
# Operate on an entire batch of hosts at once
5050
def run_batch(batch)
5151

52-
@found = {}
53-
@tried = []
54-
55-
begin
56-
udp_sock = nil
57-
idx = 0
58-
59-
# Create an unbound UDP socket if no CHOST is specified, otherwise
60-
# create a UDP socket bound to CHOST (in order to avail of pivoting)
61-
udp_sock = Rex::Socket::Udp.create( { 'LocalHost' => datastore['CHOST'] || nil, 'Context' => {'Msf' => framework, 'MsfExploit' => self} })
62-
add_socket(udp_sock)
63-
64-
each_user_pass do |user, pass|
65-
comm = pass
66-
67-
data1 = create_probe_snmp1(comm)
68-
data2 = create_probe_snmp2(comm)
69-
70-
batch.each do |ip|
71-
fq_pass = [ip,pass]
72-
next if @tried.include? fq_pass
73-
@tried << fq_pass
74-
vprint_status "#{ip}:#{datastore['RPORT']} - SNMP - Trying #{(pass.nil? || pass.empty?) ? "<BLANK>" : pass}..."
75-
76-
begin
77-
udp_sock.sendto(data1, ip, datastore['RPORT'].to_i, 0)
78-
udp_sock.sendto(data2, ip, datastore['RPORT'].to_i, 0)
79-
rescue ::Interrupt
80-
raise $!
81-
rescue ::Rex::HostUnreachable, ::Rex::ConnectionTimeout, ::Rex::ConnectionRefused
82-
nil
83-
end
84-
85-
if (idx % 10 == 0)
86-
while (r = udp_sock.recvfrom(65535, 0.25) and r[1])
87-
parse_reply(r)
88-
end
89-
end
90-
91-
idx += 1
92-
93-
end
94-
end
95-
96-
idx = 0
97-
while (r = udp_sock.recvfrom(65535, 3) and r[1] and idx < 500)
98-
parse_reply(r)
99-
idx += 1
100-
end
101-
102-
if @found.keys.length > 0
103-
print_status("Validating scan results from #{@found.keys.length} hosts...")
104-
end
105-
106-
# Review all successful communities and determine write access
107-
@found.keys.sort.each do |host|
108-
fake_comm = Rex::Text.rand_text_alphanumeric(8)
109-
anycomm_ro = false
110-
anycomm_rw = false
111-
comms_ro = []
112-
comms_rw = []
113-
finished = false
114-
versions = ["1", "2"]
115-
116-
versions.each do |version|
117-
comms_todo = @found[host].keys.sort
118-
comms_todo.unshift(fake_comm)
119-
120-
comms_todo.each do |comm|
121-
begin
122-
sval = nil
123-
snmp = snmp_client(host, datastore['RPORT'].to_i, version, udp_sock, comm)
124-
resp = snmp.get("sysName.0")
125-
resp.each_varbind { |var| sval = var.value }
126-
next if not sval
127-
128-
svar = ::SNMP::VarBind.new("1.3.6.1.2.1.1.5.0", ::SNMP::OctetString.new(sval))
129-
resp = snmp.set(svar)
130-
131-
if resp.error_status == :noError
132-
comms_rw << comm
133-
print_status("Host #{host} provides READ-WRITE access with community '#{comm}'")
134-
if comm == fake_comm
135-
anycomm_rw = true
136-
finished = true
137-
break
138-
end
139-
else
140-
comms_ro << comm
141-
print_status("Host #{host} provides READ-ONLY access with community '#{comm}'")
142-
if comm == fake_comm
143-
anycomm_ro = true
144-
finished = true
145-
break
146-
end
147-
end
148-
149-
# Used to flag whether this version was compatible
150-
finished = true
151-
152-
rescue ::SNMP::UnsupportedPduTag, ::SNMP::InvalidPduTag, ::SNMP::ParseError,
153-
::SNMP::InvalidErrorStatus, ::SNMP::InvalidTrapVarbind, ::SNMP::InvalidGenericTrap,
154-
::SNMP::BER::OutOfData, ::SNMP::BER::InvalidLength, ::SNMP::BER::InvalidTag,
155-
::SNMP::BER::InvalidObjectId, ::SNMP::MIB::ModuleNotLoadedError,
156-
::SNMP::UnsupportedValueTag
157-
next
158-
159-
rescue ::SNMP::UnsupportedVersion
160-
break
161-
rescue ::SNMP::RequestTimeout
162-
next
163-
end
164-
end
165-
166-
break if finished
167-
end
168-
169-
# Report on the results
170-
comms_ro = ["anything"] if anycomm_ro
171-
comms_rw = ["anything"] if anycomm_rw
52+
batch.each do |ip|
53+
collection = Metasploit::Framework::CommunityStringCollection.new(
54+
pass_file: datastore['PASS_FILE'],
55+
password: datastore['PASSWORD']
56+
)
17257

173-
comms_rw.each do |comm|
174-
report_auth_info(
175-
:host => host,
176-
:port => datastore['RPORT'].to_i,
177-
:proto => 'udp',
178-
:sname => 'snmp',
179-
:user => '',
180-
:pass => comm,
181-
:duplicate_ok => true,
182-
:active => true,
183-
:source_type => "user_supplied",
184-
:type => "password"
185-
)
186-
end
58+
scanner = Metasploit::Framework::LoginScanner::SNMP.new(
59+
host: ip,
60+
port: rport,
61+
cred_details: collection,
62+
stop_on_success: datastore['STOP_ON_SUCCESS'],
63+
connection_timeout: 2
64+
)
18765

188-
comms_ro.each do |comm|
189-
report_auth_info(
190-
:host => host,
191-
:port => datastore['RPORT'].to_i,
192-
:proto => 'udp',
193-
:sname => 'snmp',
194-
:user => '',
195-
:pass => comm,
196-
:duplicate_ok => true,
197-
:active => true,
198-
:source_type => "user_supplied",
199-
:type => "password_ro"
200-
)
66+
service_data = {
67+
address: ip,
68+
port: rport,
69+
service_name: 'snmp',
70+
protocol: 'udp',
71+
workspace_id: myworkspace_id
72+
}
73+
74+
scanner.scan! do |result|
75+
if result.success?
76+
credential_data = {
77+
module_fullname: self.fullname,
78+
origin_type: :service,
79+
username: result.credential.public
80+
}
81+
credential_data.merge!(service_data)
82+
83+
credential_core = create_credential(credential_data)
84+
85+
login_data = {
86+
core: credential_core,
87+
last_attempted_at: DateTime.now,
88+
status: Metasploit::Model::Login::Status::SUCCESSFUL
89+
}
90+
login_data.merge!(service_data)
91+
92+
create_credential_login(login_data)
93+
print_good "#{ip}:#{rport} - LOGIN SUCCESSFUL: #{result.credential}"
94+
else
95+
invalidate_data = {
96+
public: result.credential.public,
97+
private: result.credential.private,
98+
realm_key: result.credential.realm_key,
99+
realm_value: result.credential.realm,
100+
status: result.status
101+
} .merge(service_data)
102+
invalidate_login(invalidate_data)
103+
print_status "#{ip}:#{rport} - LOGIN FAILED: #{result.credential} (#{result.status}: #{result.proof})"
201104
end
202105
end
203-
204-
rescue ::Interrupt
205-
raise $!
206-
rescue ::Exception => e
207-
print_error("Unknown error: #{e.class} #{e}")
208106
end
209-
210107
end
211108

212-
#
213-
# Allocate a SNMP client using the existing socket
214-
#
215-
def snmp_client(host, port, version, socket, community)
216-
version = :SNMPv1 if version == "1"
217-
version = :SNMPv2c if version == "2c"
218-
219-
snmp = ::SNMP::Manager.new(
220-
:Host => host,
221-
:Port => port,
222-
:Community => community,
223-
:Version => version,
224-
:Timeout => 1,
225-
:Retries => 2,
226-
:Transport => SNMP::RexUDPTransport,
227-
:Socket => socket
228-
)
109+
def rport
110+
datastore['RPORT']
229111
end
230112

231-
#
232-
# The response parsers
233-
#
234-
def parse_reply(pkt)
235-
236-
return if not pkt[1]
237-
238-
if(pkt[1] =~ /^::ffff:/)
239-
pkt[1] = pkt[1].sub(/^::ffff:/, '')
240-
end
241-
242-
asn = OpenSSL::ASN1.decode(pkt[0]) rescue nil
243-
return if not asn
244113

245-
snmp_error = asn.value[0].value rescue nil
246-
snmp_comm = asn.value[1].value rescue nil
247-
snmp_data = asn.value[2].value[3].value[0] rescue nil
248-
snmp_oid = snmp_data.value[0].value rescue nil
249-
snmp_info = snmp_data.value[1].value rescue nil
250114

251-
return if not (snmp_error and snmp_comm and snmp_data and snmp_oid and snmp_info)
252-
snmp_info = snmp_info.to_s.gsub(/\s+/, ' ')
253-
254-
inf = snmp_info
255-
com = snmp_comm
256-
257-
if(com)
258-
@found[pkt[1]]||={}
259-
if(not @found[pkt[1]][com])
260-
print_good("SNMP: #{pkt[1]} community string: '#{com}' info: '#{inf}'")
261-
@found[pkt[1]][com] = inf
262-
end
263-
264-
report_service(
265-
:host => pkt[1],
266-
:port => pkt[2],
267-
:proto => 'udp',
268-
:name => 'snmp',
269-
:info => inf,
270-
:state => "open"
271-
)
272-
end
273-
end
274-
275-
276-
def create_probe_snmp1(name)
277-
xid = rand(0x100000000)
278-
pdu =
279-
"\x02\x01\x00" +
280-
"\x04" + [name.length].pack('c') + name +
281-
"\xa0\x1c" +
282-
"\x02\x04" + [xid].pack('N') +
283-
"\x02\x01\x00" +
284-
"\x02\x01\x00" +
285-
"\x30\x0e\x30\x0c\x06\x08\x2b\x06\x01\x02\x01" +
286-
"\x01\x01\x00\x05\x00"
287-
head = "\x30" + [pdu.length].pack('C')
288-
data = head + pdu
289-
data
290-
end
291-
292-
def create_probe_snmp2(name)
293-
xid = rand(0x100000000)
294-
pdu =
295-
"\x02\x01\x01" +
296-
"\x04" + [name.length].pack('c') + name +
297-
"\xa1\x19" +
298-
"\x02\x04" + [xid].pack('N') +
299-
"\x02\x01\x00" +
300-
"\x02\x01\x00" +
301-
"\x30\x0b\x30\x09\x06\x05\x2b\x06\x01\x02\x01" +
302-
"\x05\x00"
303-
head = "\x30" + [pdu.length].pack('C')
304-
data = head + pdu
305-
data
306-
end
307115

308116
end

0 commit comments

Comments
 (0)