metasploit-framework/modules/auxiliary/gather/shodan_search.rb

193 lines
6.1 KiB
Ruby
Raw Normal View History

##
2017-07-24 13:26:21 +00:00
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
2014-08-20 04:48:13 +00:00
require 'net/https'
require 'uri'
2016-03-08 13:02:44 +00:00
class MetasploitModule < Msf::Auxiliary
2013-08-30 21:28:54 +00:00
include Msf::Exploit::Remote::HttpClient
include Msf::Auxiliary::Report
def initialize(info = {})
super(update_info(info,
'Name' => 'Shodan Search',
'Description' => %q{
2014-08-20 04:48:13 +00:00
This module uses the Shodan API to search Shodan. Accounts are free
2017-08-27 01:01:10 +00:00
and an API key is required to use this module. Output from the module
2014-08-20 04:48:13 +00:00
is displayed to the screen and can be saved to a file or the MSF database.
NOTE: SHODAN filters (i.e. port, hostname, os, geo, city) can be used in
queries, but there are limitations when used with a free API key. Please
see the Shodan site for more information.
Shodan website: https://www.shodan.io/
API: https://developer.shodan.io/api
2013-08-30 21:28:54 +00:00
},
'Author' =>
[
2014-08-20 04:48:13 +00:00
'John H Sawyer <john[at]sploitlab.com>', # InGuardians, Inc.
'sinn3r' # Metasploit-fu plus other features
2013-08-30 21:28:54 +00:00
],
'License' => MSF_LICENSE
2014-08-20 04:48:13 +00:00
)
)
2013-08-30 21:28:54 +00:00
2014-08-20 04:48:13 +00:00
deregister_options('RHOST', 'DOMAIN', 'DigestAuthIIS', 'NTLM::SendLM',
'NTLM::SendNTLM', 'VHOST', 'RPORT', 'NTLM::SendSPN', 'NTLM::UseLMKey',
'NTLM::UseNTLM2_session', 'NTLM::UseNTLMv2')
2013-08-30 21:28:54 +00:00
register_options(
[
2014-08-20 04:48:13 +00:00
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]),
OptRegexp.new('REGEX', [true, 'Regex search for a specific IP/City/Country/Hostname', '.*'])
])
2013-08-30 21:28:54 +00:00
end
# create our Shodan query function that performs the actual web request
def shodan_query(query, apikey, page)
# send our query to Shodan
2014-08-20 04:48:13 +00:00
uri = URI.parse('https://api.shodan.io/shodan/host/search?query=' +
Rex::Text.uri_encode(query) + '&key=' + apikey + '&page=' + page.to_s)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Get.new(uri.request_uri)
res = http.request(request)
if res and res.body =~ /<title>401 Unauthorized<\/title>/
fail_with(Failure::BadConfig, '401 Unauthorized. Your SHODAN_APIKEY is invalid')
end
2014-08-20 04:48:13 +00:00
# Check if we can resolve host, got a response,
# then parse the JSON, and return it
if res
2013-08-30 21:28:54 +00:00
results = ActiveSupport::JSON.decode(res.body)
return results
else
2014-08-20 04:48:13 +00:00
return 'server_response_error'
2013-08-30 21:28:54 +00:00
end
end
2014-08-20 04:48:13 +00:00
# save output to file
2013-08-30 21:28:54 +00:00
def save_output(data)
2014-08-23 04:08:27 +00:00
::File.open(datastore['OUTFILE'], 'wb') do |f|
f.write(data)
print_status("Saved results in #{datastore['OUTFILE']}")
end
2013-08-30 21:28:54 +00:00
end
2014-08-20 04:48:13 +00:00
# Check to see if api.shodan.io resolves properly
def shodan_resolvable?
begin
Rex::Socket.resolv_to_dotted("api.shodan.io")
rescue RuntimeError, SocketError
return false
2014-04-15 02:16:40 +00:00
end
true
2014-04-15 02:16:40 +00:00
end
2013-08-30 21:28:54 +00:00
def run
2014-08-20 04:48:13 +00:00
# check to ensure api.shodan.io is resolvable
unless shodan_resolvable?
print_error("Unable to resolve api.shodan.io")
return
end
2014-04-15 02:16:40 +00:00
2013-08-30 21:28:54 +00:00
# create our Shodan request parameters
query = datastore['QUERY']
apikey = datastore['SHODAN_APIKEY']
page = 1
2014-08-20 04:48:13 +00:00
maxpage = datastore['MAXPAGE']
2013-08-30 21:28:54 +00:00
# results gets our results from shodan_query
results = []
results[page] = shodan_query(query, apikey, page)
if results[page]['total'].nil? || results[page]['total'] == 0
2015-09-10 21:05:40 +00:00
msg = "No results."
if results[page]['error'].to_s.length > 0
msg << " Error: #{results[page]['error']}"
end
print_error(msg)
return
2013-08-30 21:28:54 +00:00
end
# Determine page count based on total results
2014-08-20 04:48:13 +00:00
if results[page]['total'] % 100 == 0
tpages = results[page]['total'] / 100
2013-08-30 21:28:54 +00:00
else
2014-08-20 04:48:13 +00:00
tpages = results[page]['total'] / 100 + 1
maxpage = tpages if datastore['MAXPAGE'] > tpages
2013-08-30 21:28:54 +00:00
end
# start printing out our query statistics
print_status("Total: #{results[page]['total']} on #{tpages} " +
2014-08-20 04:48:13 +00:00
"pages. Showing: #{maxpage} page(s)")
# If search results greater than 100, loop & get all results
print_status('Collecting data, please wait...')
if results[page]['total'] > 100
2013-08-30 21:28:54 +00:00
page += 1
while page <= maxpage
2013-08-30 21:28:54 +00:00
break if page > datastore['MAXPAGE']
2014-08-20 04:48:13 +00:00
results[page] = shodan_query(query, apikey, page)
page += 1
2013-08-30 21:28:54 +00:00
end
end
# Save the results to this table
tbl = Rex::Text::Table.new(
2014-08-20 04:48:13 +00:00
'Header' => 'Search Results',
2013-08-30 21:28:54 +00:00
'Indent' => 1,
2014-08-20 04:48:13 +00:00
'Columns' => ['IP:Port', 'City', 'Country', 'Hostname']
2013-08-30 21:28:54 +00:00
)
2014-08-20 04:48:13 +00:00
# Organize results and put them into the table and database
p = 1
regex = datastore['REGEX'] if datastore['REGEX']
while p <= maxpage
break if p > maxpage
results[p]['matches'].each do |host|
2014-08-20 04:48:13 +00:00
city = host['location']['city'] || 'N/A'
ip = host['ip_str'] || 'N/A'
2013-08-30 21:28:54 +00:00
port = host['port'] || ''
2014-08-20 04:48:13 +00:00
country = host['location']['country_name'] || 'N/A'
2013-08-30 21:28:54 +00:00
hostname = host['hostnames'][0]
data = host['data']
report_host(:host => ip,
:name => hostname,
2014-08-20 04:48:13 +00:00
:comments => 'Added from Shodan',
:info => host['info']
2014-08-20 04:48:13 +00:00
) if datastore['DATABASE']
report_service(:host => ip,
2014-08-20 04:48:13 +00:00
:port => port,
:info => 'Added from Shodan'
) if datastore['DATABASE']
if ip =~ regex ||
city =~ regex ||
country =~ regex ||
hostname =~ regex ||
data =~ regex
2014-08-23 04:08:27 +00:00
# Unfortunately we cannot display the banner properly,
# because it messes with our output format
tbl << ["#{ip}:#{port}", city, country, hostname]
2013-08-30 21:28:54 +00:00
end
2014-08-20 04:48:13 +00:00
end
p += 1
2013-08-30 21:28:54 +00:00
end
2014-08-20 04:48:13 +00:00
# Show data and maybe save it if needed
2014-08-23 04:08:27 +00:00
print_line
print_line("#{tbl}")
save_output(tbl) if datastore['OUTFILE']
2013-08-30 21:28:54 +00:00
end
end