diff --git a/lib/metasploit/framework/community_string_collection.rb b/lib/metasploit/framework/community_string_collection.rb new file mode 100644 index 0000000000..fdeddb4a96 --- /dev/null +++ b/lib/metasploit/framework/community_string_collection.rb @@ -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] + attr_accessor :prepended_creds + + # @option opts [String] :pass_file See {#pass_file} + # @option opts [String] :password See {#password} + # @option opts [Array] :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 diff --git a/modules/auxiliary/scanner/snmp/snmp_login.rb b/modules/auxiliary/scanner/snmp/snmp_login.rb index d16e1cfbe6..5f5e25ef66 100644 --- a/modules/auxiliary/scanner/snmp/snmp_login.rb +++ b/modules/auxiliary/scanner/snmp/snmp_login.rb @@ -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?) ? "" : 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