Merge pull request #3559 from dmaloney-r7/feature/MSP-10230/snmp_login

MSP-10230 #land
bug/bundler_fix
Samuel Huckins 2014-07-23 13:59:37 -05:00
commit ffd7d28bc6
2 changed files with 133 additions and 251 deletions

View File

@ -0,0 +1,74 @@
require 'metasploit/framework/credential'
module Metasploit
module Framework
# This class is responsible for taking datastore options from the snmp_login module
# and yielding appropriate {Metasploit::Framework::Credential}s to the {Metasploit::Framework::LoginScanner::SNMP}.
# This one has to be different from {credentialCollection} as it will only have a {Metasploit::Framework::Credential#public}
# It may be slightly confusing that the attribues are called password and pass_file, because this is what the legacy
# module used. However, community Strings are now considered more to be public credentials than private ones.
class CommunityStringCollection
# @!attribute pass_file
# Path to a file containing passwords, one per line
# @return [String]
attr_accessor :pass_file
# @!attribute password
# @return [String]
attr_accessor :password
# @!attribute prepended_creds
# List of credentials to be tried before any others
#
# @see #prepend_cred
# @return [Array<Credential>]
attr_accessor :prepended_creds
# @option opts [String] :pass_file See {#pass_file}
# @option opts [String] :password See {#password}
# @option opts [Array<Credential>] :prepended_creds ([]) See {#prepended_creds}
def initialize(opts = {})
opts.each do |attribute, value|
public_send("#{attribute}=", value)
end
self.prepended_creds ||= []
end
# Combines all the provided credential sources into a stream of {Credential}
# objects, yielding them one at a time
#
# @yieldparam credential [Metasploit::Framework::Credential]
# @return [void]
def each
begin
if pass_file.present?
pass_fd = File.open(pass_file, 'r:binary')
pass_fd.each_line do |line|
line.chomp!
yield Metasploit::Framework::Credential.new(public: line, paired: false)
end
end
if password.present?
yield Metasploit::Framework::Credential.new(public: password, paired: false)
end
ensure
pass_fd.close if pass_fd && !pass_fd.closed?
end
end
# Add {Credential credentials} that will be yielded by {#each}
#
# @see prepended_creds
# @param cred [Credential]
# @return [self]
def prepend_cred(cred)
prepended_creds.unshift cred
self
end
end
end
end

View File

@ -5,8 +5,8 @@
require 'msf/core'
require 'openssl'
require 'snmp'
require 'metasploit/framework/community_string_collection'
require 'metasploit/framework/login_scanner/snmp'
class Metasploit3 < Msf::Auxiliary
@ -49,260 +49,68 @@ class Metasploit3 < Msf::Auxiliary
# Operate on an entire batch of hosts at once
def run_batch(batch)
@found = {}
@tried = []
begin
udp_sock = nil
idx = 0
# Create an unbound UDP socket if no CHOST is specified, otherwise
# create a UDP socket bound to CHOST (in order to avail of pivoting)
udp_sock = Rex::Socket::Udp.create( { 'LocalHost' => datastore['CHOST'] || nil, 'Context' => {'Msf' => framework, 'MsfExploit' => self} })
add_socket(udp_sock)
each_user_pass do |user, pass|
comm = pass
data1 = create_probe_snmp1(comm)
data2 = create_probe_snmp2(comm)
batch.each do |ip|
fq_pass = [ip,pass]
next if @tried.include? fq_pass
@tried << fq_pass
vprint_status "#{ip}:#{datastore['RPORT']} - SNMP - Trying #{(pass.nil? || pass.empty?) ? "<BLANK>" : pass}..."
begin
udp_sock.sendto(data1, ip, datastore['RPORT'].to_i, 0)
udp_sock.sendto(data2, ip, datastore['RPORT'].to_i, 0)
rescue ::Interrupt
raise $!
rescue ::Rex::HostUnreachable, ::Rex::ConnectionTimeout, ::Rex::ConnectionRefused
nil
end
if (idx % 10 == 0)
while (r = udp_sock.recvfrom(65535, 0.25) and r[1])
parse_reply(r)
end
end
idx += 1
end
end
idx = 0
while (r = udp_sock.recvfrom(65535, 3) and r[1] and idx < 500)
parse_reply(r)
idx += 1
end
if @found.keys.length > 0
print_status("Validating scan results from #{@found.keys.length} hosts...")
end
# Review all successful communities and determine write access
@found.keys.sort.each do |host|
fake_comm = Rex::Text.rand_text_alphanumeric(8)
anycomm_ro = false
anycomm_rw = false
comms_ro = []
comms_rw = []
finished = false
versions = ["1", "2"]
versions.each do |version|
comms_todo = @found[host].keys.sort
comms_todo.unshift(fake_comm)
comms_todo.each do |comm|
begin
sval = nil
snmp = snmp_client(host, datastore['RPORT'].to_i, version, udp_sock, comm)
resp = snmp.get("sysName.0")
resp.each_varbind { |var| sval = var.value }
next if not sval
svar = ::SNMP::VarBind.new("1.3.6.1.2.1.1.5.0", ::SNMP::OctetString.new(sval))
resp = snmp.set(svar)
if resp.error_status == :noError
comms_rw << comm
print_status("Host #{host} provides READ-WRITE access with community '#{comm}'")
if comm == fake_comm
anycomm_rw = true
finished = true
break
end
else
comms_ro << comm
print_status("Host #{host} provides READ-ONLY access with community '#{comm}'")
if comm == fake_comm
anycomm_ro = true
finished = true
break
end
end
# Used to flag whether this version was compatible
finished = true
rescue ::SNMP::UnsupportedPduTag, ::SNMP::InvalidPduTag, ::SNMP::ParseError,
::SNMP::InvalidErrorStatus, ::SNMP::InvalidTrapVarbind, ::SNMP::InvalidGenericTrap,
::SNMP::BER::OutOfData, ::SNMP::BER::InvalidLength, ::SNMP::BER::InvalidTag,
::SNMP::BER::InvalidObjectId, ::SNMP::MIB::ModuleNotLoadedError,
::SNMP::UnsupportedValueTag
next
rescue ::SNMP::UnsupportedVersion
break
rescue ::SNMP::RequestTimeout
next
end
end
break if finished
end
# Report on the results
comms_ro = ["anything"] if anycomm_ro
comms_rw = ["anything"] if anycomm_rw
comms_rw.each do |comm|
report_auth_info(
:host => host,
:port => datastore['RPORT'].to_i,
:proto => 'udp',
:sname => 'snmp',
:user => '',
:pass => comm,
:duplicate_ok => true,
:active => true,
:source_type => "user_supplied",
:type => "password"
)
end
comms_ro.each do |comm|
report_auth_info(
:host => host,
:port => datastore['RPORT'].to_i,
:proto => 'udp',
:sname => 'snmp',
:user => '',
:pass => comm,
:duplicate_ok => true,
:active => true,
:source_type => "user_supplied",
:type => "password_ro"
)
end
end
rescue ::Interrupt
raise $!
rescue ::Exception => e
print_error("Unknown error: #{e.class} #{e}")
end
end
#
# Allocate a SNMP client using the existing socket
#
def snmp_client(host, port, version, socket, community)
version = :SNMPv1 if version == "1"
version = :SNMPv2c if version == "2c"
snmp = ::SNMP::Manager.new(
:Host => host,
:Port => port,
:Community => community,
:Version => version,
:Timeout => 1,
:Retries => 2,
:Transport => SNMP::RexUDPTransport,
:Socket => socket
)
end
#
# The response parsers
#
def parse_reply(pkt)
return if not pkt[1]
if(pkt[1] =~ /^::ffff:/)
pkt[1] = pkt[1].sub(/^::ffff:/, '')
end
asn = OpenSSL::ASN1.decode(pkt[0]) rescue nil
return if not asn
snmp_error = asn.value[0].value rescue nil
snmp_comm = asn.value[1].value rescue nil
snmp_data = asn.value[2].value[3].value[0] rescue nil
snmp_oid = snmp_data.value[0].value rescue nil
snmp_info = snmp_data.value[1].value rescue nil
return if not (snmp_error and snmp_comm and snmp_data and snmp_oid and snmp_info)
snmp_info = snmp_info.to_s.gsub(/\s+/, ' ')
inf = snmp_info
com = snmp_comm
if(com)
@found[pkt[1]]||={}
if(not @found[pkt[1]][com])
print_good("SNMP: #{pkt[1]} community string: '#{com}' info: '#{inf}'")
@found[pkt[1]][com] = inf
end
report_service(
:host => pkt[1],
:port => pkt[2],
:proto => 'udp',
:name => 'snmp',
:info => inf,
:state => "open"
batch.each do |ip|
collection = Metasploit::Framework::CommunityStringCollection.new(
pass_file: datastore['PASS_FILE'],
password: datastore['PASSWORD']
)
scanner = Metasploit::Framework::LoginScanner::SNMP.new(
host: ip,
port: rport,
cred_details: collection,
stop_on_success: datastore['STOP_ON_SUCCESS'],
connection_timeout: 2
)
service_data = {
address: ip,
port: rport,
service_name: 'snmp',
protocol: 'udp',
workspace_id: myworkspace_id
}
scanner.scan! do |result|
if result.success?
credential_data = {
module_fullname: self.fullname,
origin_type: :service,
username: result.credential.public
}
credential_data.merge!(service_data)
credential_core = create_credential(credential_data)
login_data = {
core: credential_core,
last_attempted_at: DateTime.now,
status: Metasploit::Model::Login::Status::SUCCESSFUL
}
login_data.merge!(service_data)
create_credential_login(login_data)
print_good "#{ip}:#{rport} - LOGIN SUCCESSFUL: #{result.credential}"
else
invalidate_data = {
public: result.credential.public,
private: result.credential.private,
realm_key: result.credential.realm_key,
realm_value: result.credential.realm,
status: result.status
} .merge(service_data)
invalidate_login(invalidate_data)
print_status "#{ip}:#{rport} - LOGIN FAILED: #{result.credential} (#{result.status}: #{result.proof})"
end
end
end
end
def create_probe_snmp1(name)
xid = rand(0x100000000)
pdu =
"\x02\x01\x00" +
"\x04" + [name.length].pack('c') + name +
"\xa0\x1c" +
"\x02\x04" + [xid].pack('N') +
"\x02\x01\x00" +
"\x02\x01\x00" +
"\x30\x0e\x30\x0c\x06\x08\x2b\x06\x01\x02\x01" +
"\x01\x01\x00\x05\x00"
head = "\x30" + [pdu.length].pack('C')
data = head + pdu
data
def rport
datastore['RPORT']
end
def create_probe_snmp2(name)
xid = rand(0x100000000)
pdu =
"\x02\x01\x01" +
"\x04" + [name.length].pack('c') + name +
"\xa1\x19" +
"\x02\x04" + [xid].pack('N') +
"\x02\x01\x00" +
"\x02\x01\x00" +
"\x30\x0b\x30\x09\x06\x05\x2b\x06\x01\x02\x01" +
"\x05\x00"
head = "\x30" + [pdu.length].pack('C')
data = head + pdu
data
end
end