Skip to content

Commit 280e16c

Browse files
committed
Land rapid7#3677 - Updated shodan_search for new API
2 parents 6920c71 + 0063933 commit 280e16c

File tree

1 file changed

+103
-101
lines changed

1 file changed

+103
-101
lines changed

modules/auxiliary/gather/shodan_search.rb

Lines changed: 103 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55

66
require 'msf/core'
77
require 'rex'
8-
require 'net/dns'
8+
require 'net/https'
9+
require 'uri'
910

1011
class Metasploit4 < Msf::Auxiliary
1112

@@ -16,170 +17,171 @@ def initialize(info = {})
1617
super(update_info(info,
1718
'Name' => 'Shodan Search',
1819
'Description' => %q{
19-
This module uses the SHODAN API to query the database and
20-
returns the first 50 IPs. SHODAN accounts are free & output
21-
can be sent to a file for use by another program. Results
22-
can also populated into the services table in the database.
23-
NOTE: SHODAN filters (port, hostname, os, geo, city) can be
24-
used in queries, but the free API does not allow net, country,
25-
before, and after filters. An unlimited API key can be
26-
purchased from the Shodan site to use those queries. The 50
27-
result limit can also be raised to 10,000 for a small fee.
28-
API: http://www.shodanhq.com/api_doc
29-
FILTERS: http://www.shodanhq.com/help/filters
20+
This module uses the Shodan API to search Shodan. Accounts are free
21+
and an API key is required to used this module. Output from the module
22+
is displayed to the screen and can be saved to a file or the MSF database.
23+
NOTE: SHODAN filters (i.e. port, hostname, os, geo, city) can be used in
24+
queries, but there are limitations when used with a free API key. Please
25+
see the Shodan site for more information.
26+
Shodan website: https://www.shodan.io/
27+
API: https://developer.shodan.io/api
3028
},
3129
'Author' =>
3230
[
33-
'John Sawyer <johnhsawyer[at]gmail.com>', #sploitlab.com
34-
'sinn3r' #Metasploit-fu plus other features
31+
'John H Sawyer <john[at]sploitlab.com>', # InGuardians, Inc.
32+
'sinn3r' # Metasploit-fu plus other features
3533
],
3634
'License' => MSF_LICENSE
37-
))
35+
)
36+
)
3837

39-
# disabling all the unnecessary options that someone might set to break our query
40-
deregister_options('RPORT','RHOST', 'DOMAIN',
41-
'DigestAuthIIS', 'SSLVersion', 'NTLM::SendLM', 'NTLM::SendNTLM',
42-
'NTLM::SendSPN', 'NTLM::UseLMKey', 'NTLM::UseNTLM2_session',
43-
'NTLM::UseNTLMv2','SSL')
38+
deregister_options('RHOST', 'DOMAIN', 'DigestAuthIIS', 'NTLM::SendLM',
39+
'NTLM::SendNTLM', 'VHOST', 'RPORT', 'NTLM::SendSPN', 'NTLM::UseLMKey',
40+
'NTLM::UseNTLM2_session', 'NTLM::UseNTLMv2')
4441

4542
register_options(
4643
[
47-
OptString.new('SHODAN_APIKEY', [true, "The SHODAN API key"]),
48-
OptString.new('QUERY', [true, "Keywords you want to search for"]),
49-
OptString.new('OUTFILE', [false, "A filename to store the list of IPs"]),
50-
OptBool.new('DATABASE', [false, "Add search results to the database", false]),
51-
OptInt.new('MAXPAGE', [true, "Max amount of pages to collect", 1]),
52-
OptString.new('FILTER', [false, 'Search for a specific IP/City/Country/Hostname']),
53-
OptString.new('VHOST', [true, 'The virtual host name to use in requests', 'www.shodanhq.com']),
44+
OptString.new('SHODAN_APIKEY', [true, 'The SHODAN API key']),
45+
OptString.new('QUERY', [true, 'Keywords you want to search for']),
46+
OptString.new('OUTFILE', [false, 'A filename to store the list of IPs']),
47+
OptBool.new('DATABASE', [false, 'Add search results to the database', false]),
48+
OptInt.new('MAXPAGE', [true, 'Max amount of pages to collect', 1]),
49+
OptRegexp.new('REGEX', [true, 'Regex search for a specific IP/City/Country/Hostname', '.*'])
50+
5451
], self.class)
5552
end
5653

5754
# create our Shodan query function that performs the actual web request
5855
def shodan_query(query, apikey, page)
5956
# send our query to Shodan
60-
uri = "/api/search?&q=" + Rex::Text.uri_encode(query) + "&key=" + apikey + "&page=" + page.to_s
61-
res = send_request_raw(
62-
{
63-
'rhost' => shodan_rhost,
64-
'rport' => shodan_rport,
65-
'vhost' => vhost,
66-
'method' => 'GET',
67-
'uri' => uri
68-
}, 25)
69-
70-
# Check if we got a response, parse the JSON, and return it
71-
if (res)
57+
uri = URI.parse('https://api.shodan.io/shodan/host/search?query=' +
58+
Rex::Text.uri_encode(query) + '&key=' + apikey + '&page=' + page.to_s)
59+
http = Net::HTTP.new(uri.host, uri.port)
60+
http.use_ssl = true
61+
request = Net::HTTP::Get.new(uri.request_uri)
62+
res = http.request(request)
63+
64+
if res and res.body =~ /<title>401 Unauthorized<\/title>/
65+
fail_with(Failure::BadConfig, '401 Unauthorized. Your SHODAN_APIKEY is invalid')
66+
end
67+
68+
# Check if we can resolve host, got a response,
69+
# then parse the JSON, and return it
70+
if res
7271
results = ActiveSupport::JSON.decode(res.body)
7372
return results
7473
else
75-
return 'server_error'
74+
return 'server_response_error'
7675
end
7776
end
7877

78+
# save output to file
7979
def save_output(data)
80-
f = ::File.open(datastore['OUTFILE'], "wb")
81-
f.write(data)
82-
f.close
83-
print_status("Save results in #{datastore['OUTFILE']}")
80+
::File.open(datastore['OUTFILE'], 'wb') do |f|
81+
f.write(data)
82+
print_status("Saved results in #{datastore['OUTFILE']}")
83+
end
8484
end
8585

86+
# Check to see if api.shodan.io resolves properly
8687
def shodan_rhost
87-
@res = Net::DNS::Resolver.new()
88-
dns_query = @res.query("#{datastore['VHOST']}", "A")
88+
@res = Net::DNS::Resolver.new
89+
dns_query = @res.query('api.shodan.io', 'A')
8990
if dns_query.answer.length == 0
90-
print_error("Could not resolve #{datastore['VHOST']}")
91-
raise ::Rex::ConnectError(vhost, shodan_port)
91+
print_error('Could not resolve api.shodan.io')
92+
raise ::Rex::ConnectError('api.shodan.io', '443')
9293
end
9394
dns_query.answer[0].to_s.split(/[\s,]+/)[4]
9495
end
9596

96-
def shodan_rport
97-
80
98-
end
99-
10097
def run
98+
# check to ensure api.shodan.io is resolvable
99+
shodan_rhost
101100

102101
# create our Shodan request parameters
103102
query = datastore['QUERY']
104103
apikey = datastore['SHODAN_APIKEY']
105-
106104
page = 1
105+
maxpage = datastore['MAXPAGE']
107106

108107
# results gets our results from shodan_query
109108
results = []
110109
results[page] = shodan_query(query, apikey, page)
111110

112-
if results[page].empty?
113-
print_error("No Results Found!")
114-
return
111+
if results[page]['total'].nil? || results[page]['total'] == 0
112+
print_error('No Results Found!')
115113
end
116114

117115
# Determine page count based on total results
118-
if results[page]['total']%50 == 0
119-
tpages = results[page]['total']/50
116+
if results[page]['total'] % 100 == 0
117+
tpages = results[page]['total'] / 100
120118
else
121-
tpages = results[page]['total']/50 + 1
119+
tpages = results[page]['total'] / 100 + 1
120+
maxpage = tpages if datastore['MAXPAGE'] > tpages
122121
end
123122

124123
# start printing out our query statistics
125-
print_status("Total: #{results[page]['total']} on #{tpages} pages. Showing: #{datastore['MAXPAGE']}")
126-
print_status("Country Statistics:")
127-
results[page]['countries'].each { |ctry|
128-
print_status "\t#{ctry['name']} (#{ctry['code']}): #{ctry['count']}"
129-
}
130-
131-
# If search results greater than 50, loop & get all results
132-
print_status("Collecting data, please wait...")
133-
if (results[page]['total'] > 50)
124+
print_status("Total: #{results[page]['total']} on #{tpages} " +
125+
"pages. Showing: #{maxpage} page(s)")
126+
127+
# If search results greater than 100, loop & get all results
128+
print_status('Collecting data, please wait...')
129+
if results[page]['total'] > 100
134130
page += 1
135-
while page <= tpages
136-
results[page] = shodan_query(query, apikey, page)
137-
page +=1
131+
while page <= maxpage
138132
break if page > datastore['MAXPAGE']
133+
results[page] = shodan_query(query, apikey, page)
134+
page += 1
139135
end
140136
end
141137

142138
# Save the results to this table
143139
tbl = Rex::Ui::Text::Table.new(
144-
'Header' => 'IP Results',
140+
'Header' => 'Search Results',
145141
'Indent' => 1,
146-
'Columns' => ['IP', 'City', 'Country', 'Hostname']
142+
'Columns' => ['IP:Port', 'City', 'Country', 'Hostname']
147143
)
148144

149-
# Organize results and put them into the table
150-
page = 1
151-
my_filter = datastore['FILTER']
152-
for i in page..tpages
153-
next if results[i].nil? or results[i]['matches'].nil?
154-
results[i]['matches'].each { |host|
155-
156-
city = host['city'] || 'N/A'
157-
ip = host['ip'] || 'N/A'
145+
# Organize results and put them into the table and database
146+
p = 1
147+
regex = datastore['REGEX'] if datastore['REGEX']
148+
while p <= maxpage
149+
break if p > maxpage
150+
results[p]['matches'].each do |host|
151+
city = host['location']['city'] || 'N/A'
152+
ip = host['ip_str'] || 'N/A'
158153
port = host['port'] || ''
159-
country = host['country_name'] || 'N/A'
154+
country = host['location']['country_name'] || 'N/A'
160155
hostname = host['hostnames'][0]
161156
data = host['data']
162157

163-
if ip =~ /#{my_filter}/ or
164-
city =~ /#{my_filter}/i or
165-
country =~ /#{my_filter}/i or
166-
hostname =~ /#{my_filter}/i or
167-
data =~ /#{my_filter}/i
168-
# Unfortunately we cannot display the banner properly,
169-
# because it messes with our output format
170-
tbl << ["#{ip}:#{port}", city, country, hostname]
158+
report_host(:host => ip,
159+
:name => hostname,
160+
:comments => 'Added from Shodan',
161+
:info => host['info']
162+
) if datastore['DATABASE']
163+
164+
report_service(:host => ip,
165+
:port => port,
166+
:info => 'Added from Shodan'
167+
) if datastore['DATABASE']
168+
169+
if ip =~ regex ||
170+
city =~ regex ||
171+
country =~ regex ||
172+
hostname =~ regex ||
173+
data =~ regex
174+
# Unfortunately we cannot display the banner properly,
175+
# because it messes with our output format
176+
tbl << ["#{ip}:#{port}", city, country, hostname]
171177
end
172-
}
178+
end
179+
p += 1
173180
end
174181

175-
#Show data and maybe save it if needed
176-
print_line("\n#{tbl.to_s}")
177-
178-
report_note(
179-
:type => 'shodan',
180-
:data => tbl.to_csv
181-
) if datastore['DATABASE']
182-
183-
save_output(tbl.to_s) if not datastore['OUTFILE'].nil?
182+
# Show data and maybe save it if needed
183+
print_line
184+
print_line("#{tbl}")
185+
save_output(tbl) if datastore['OUTFILE']
184186
end
185187
end

0 commit comments

Comments
 (0)