## # This file is part of the Metasploit Framework and may be subject to # redistribution and commercial restrictions. Please see the Metasploit # web site for more information on licensing and terms of use. # http://metasploit.com/ ## require 'msf/core' require 'rex' class Metasploit4 < Msf::Auxiliary include Msf::Exploit::Remote::HttpClient include Msf::Auxiliary::Report def initialize(info = {}) super(update_info(info, 'Name' => 'Shodan Search', 'Description' => %q{ This module uses the SHODAN API to query the database and returns the first 50 IPs. SHODAN accounts are free & output can be sent to a file for use by another program. Results can also populated into the services table in the database. NOTE: SHODAN filters (port, hostname, os, geo, city) can be used in queries, but the free API does not allow net, country, before, and after filters. An unlimited API key can be purchased from the Shodan site to use those queries. The 50 result limit can also be raised to 10,000 for a small fee. API: http://www.shodanhq.com/api_doc FILTERS: http://www.shodanhq.com/help/filters }, 'Author' => [ 'John Sawyer ', #sploitlab.com 'sinn3r' #Metasploit-fu plus other features ], 'License' => MSF_LICENSE )) # disabling all the unnecessary options that someone might set to break our query deregister_options('RPORT','RHOST', 'DOMAIN', 'DigestAuthIIS', 'SSLVersion', 'NTLM::SendLM', 'NTLM::SendNTLM', 'NTLM::SendSPN', 'NTLM::UseLMKey', 'NTLM::UseNTLM2_session', 'NTLM::UseNTLMv2', 'SSL') register_options( [ OptString.new('SHODAN_APIKEY', [true, "The SHODAN API key"]), OptString.new('QUERY', [true, "Keywords you want to search for"]), OptString.new('OUTFILE', [false, "A filename to store the list of IPs"]), OptBool.new('DATABASE', [false, "Add search results to the database", false]), OptInt.new('MAXPAGE', [true, "Max amount of pages to collect", 1]), OptString.new('FILTER', [false, 'Search for a specific IP/City/Country/Hostname']), OptString.new('VHOST', [true, 'The virtual host name to use in requests', 'www.shodanhq.com']), ], self.class) end # create our Shodan query function that performs the actual web request def shodan_query(query, apikey, page) # send our query to Shodan uri = "/api/search?&q=" + Rex::Text.uri_encode(query) + "&key=" + apikey + "&page=" + page.to_s res = send_request_raw( { 'method' => 'GET', 'uri' => uri }, 25) # Check if we got a response, parse the JSON, and return it if (res) results = ActiveSupport::JSON.decode(res.body) return results else return 'server_error' end end def save_output(data) f = ::File.open(datastore['OUTFILE'], "wb") f.write(data) f.close print_status("Save results in #{datastore['OUTFILE']}") end def cleanup datastore['RHOST'] = @old_rhost datastore['RPORT'] = @old_rport end def run # create our Shodan request parameters query = datastore['QUERY'] apikey = datastore['SHODAN_APIKEY'] @res = Net::DNS::Resolver.new() dns_query = @res.query("#{datastore['VHOST']}", "A") if dns_query.answer.length == 0 print_error("Could not resolve #{datastore['VHOST']}") return else # Make a copy of the original rhost @old_rhost = datastore['RHOST'] @old_rport = datastore['RPORT'] datastore['RHOST'] = dns_query.answer[0].to_s.split(/[\s,]+/)[4] datastore['RPORT'] = 80 end page = 1 # results gets our results from shodan_query results = [] results[page] = shodan_query(query, apikey, page) if results[page].empty? print_error("No Results Found!") return end # Determine page count based on total results if results[page]['total']%50 == 0 tpages = results[page]['total']/50 else tpages = results[page]['total']/50 + 1 end # start printing out our query statistics print_status("Total: #{results[page]['total']} on #{tpages} pages. Showing: #{datastore['MAXPAGE']}") print_status("Country Statistics:") results[page]['countries'].each { |ctry| print_status "\t#{ctry['name']} (#{ctry['code']}): #{ctry['count']}" } # If search results greater than 50, loop & get all results print_status("Collecting data, please wait...") if (results[page]['total'] > 50) page += 1 while page <= tpages results[page] = shodan_query(query, apikey, page) page +=1 break if page > datastore['MAXPAGE'] end end # Save the results to this table tbl = Rex::Ui::Text::Table.new( 'Header' => 'IP Results', 'Indent' => 1, 'Columns' => ['IP', 'City', 'Country', 'Hostname'] ) # Organize results and put them into the table page = 1 my_filter = datastore['FILTER'] for i in page..tpages next if results[i].nil? or results[i]['matches'].nil? results[i]['matches'].each { |host| city = host['city'] || 'N/A' ip = host['ip'] || 'N/A' port = host['port'] || '' country = host['country_name'] || 'N/A' hostname = host['hostnames'][0] data = host['data'] if ip =~ /#{my_filter}/ or city =~ /#{my_filter}/i or country =~ /#{my_filter}/i or hostname =~ /#{my_filter}/i or data =~ /#{my_filter}/i # Unfortunately we cannot display the banner properly, # because it messes with our output format tbl << ["#{ip}:#{port}", city, country, hostname] end } end #Show data and maybe save it if needed print_line("\n#{tbl.to_s}") report_note( :type => 'shodan', :data => tbl.to_csv ) if datastore['DATABASE'] save_output(tbl.to_s) if not datastore['OUTFILE'].nil? end end