Skip to content

Commit 808fc58

Browse files
authored
Land rapid7#20027, adds support for Shodan facets
Shodan facets
2 parents 77c2b0c + 1f3f5db commit 808fc58

File tree

1 file changed

+90
-65
lines changed

1 file changed

+90
-65
lines changed

modules/auxiliary/gather/shodan_search.rb

Lines changed: 90 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ def initialize(info = {})
2222
see the Shodan site for more information.
2323
Shodan website: https://www.shodan.io/
2424
API: https://developer.shodan.io/api
25+
Filters: https://www.shodan.io/search/filters
26+
Facets: https://www.shodan.io/search/facet (from the scrollbox)
2527
},
2628
'Author' =>
2729
[
@@ -36,6 +38,7 @@ def initialize(info = {})
3638
[
3739
OptString.new('SHODAN_APIKEY', [true, 'The SHODAN API key']),
3840
OptString.new('QUERY', [true, 'Keywords you want to search for']),
41+
OptString.new('FACETS', [false, 'List of facets']),
3942
OptString.new('OUTFILE', [false, 'A filename to store the list of IPs']),
4043
OptBool.new('DATABASE', [false, 'Add search results to the database', false]),
4144
OptInt.new('MAXPAGE', [true, 'Max amount of pages to collect', 1]),
@@ -55,7 +58,7 @@ def initialize(info = {})
5558
end
5659

5760
# create our Shodan query function that performs the actual web request
58-
def shodan_query(apikey, query, page)
61+
def shodan_query(apikey, query, facets, page)
5962
# send our query to Shodan
6063
res = send_request_cgi({
6164
'method' => 'GET',
@@ -66,6 +69,7 @@ def shodan_query(apikey, query, page)
6669
'vars_get' => {
6770
'key' => apikey,
6871
'query' => query,
72+
'facets' => facets,
6973
'page' => page.to_s
7074
}
7175
})
@@ -117,92 +121,113 @@ def run
117121

118122
# create our Shodan request parameters
119123
query = datastore['QUERY']
124+
facets = datastore['FACETS']
120125
apikey = datastore['SHODAN_APIKEY']
121126
maxpage = datastore['MAXPAGE']
122127

123128
# results gets our results from shodan_query
124129
results = []
125-
results[0] = shodan_query(apikey, query, 1)
130+
first_page = 0
131+
results[first_page] = shodan_query(apikey, query, facets, 1)
126132

127-
if results[0]['total'].nil? || results[0]['total'] == 0
133+
if results[first_page]['total'].nil? || results[first_page]['total'] == 0
128134
msg = "No results."
129-
if results[0]['error'].to_s.length > 0
130-
msg << " Error: #{results[0]['error']}"
135+
if results[first_page]['error'].to_s.length > 0
136+
msg << " Error: #{results[first_page]['error']}"
131137
end
132138
print_error(msg)
133139
return
134140
end
135141

136142
# Determine page count based on total results
137-
if results[0]['total'] % 100 == 0
138-
tpages = results[0]['total'] / 100
143+
if results[first_page]['total'] % 100 == 0
144+
tpages = results[first_page]['total'] / 100
139145
else
140-
tpages = results[0]['total'] / 100 + 1
146+
tpages = results[first_page]['total'] / 100 + 1
141147
end
142148
maxpage = tpages if datastore['MAXPAGE'] > tpages
143149

144-
# start printing out our query statistics
145-
print_status("Total: #{results[0]['total']} on #{tpages} " +
146-
"pages. Showing: #{maxpage} page(s)")
147-
148-
# If search results greater than 100, loop & get all results
149-
print_status('Collecting data, please wait...')
150-
151-
if results[0]['total'] > 100
152-
page = 1
153-
while page < maxpage
154-
page_result = shodan_query(apikey, query, page+1)
155-
if page_result['matches'].nil?
156-
next
150+
if facets
151+
facets_tbl = Rex::Text::Table.new(
152+
'Header' => 'Facets',
153+
'Indent' => 1,
154+
'Columns' => ['Facet', 'Name', 'Count']
155+
)
156+
print_status("Total: #{results[first_page]['total']} on #{tpages} " \
157+
'pages. Showing facets')
158+
facet = results.dig(first_page,'facets')
159+
facet.each do |name, list|
160+
list.each do |f|
161+
facets_tbl << [name.to_s, (f['value']).to_s, (f['count']).to_s]
157162
end
158-
results[page] = page_result
159-
page += 1
160163
end
161-
end
162-
163-
# Save the results to this table
164-
tbl = Rex::Text::Table.new(
165-
'Header' => 'Search Results',
166-
'Indent' => 1,
167-
'Columns' => ['IP:Port', 'City', 'Country', 'Hostname']
168-
)
164+
else
165+
# start printing out our query statistics
166+
print_status("Total: #{results[first_page]['total']} on #{tpages} " +
167+
"pages. Showing: #{maxpage} page(s)")
168+
169+
# If search results greater than 100, loop & get all results
170+
print_status('Collecting data, please wait...')
171+
172+
if results[first_page]['total'] > 100
173+
page = 1
174+
while page < maxpage
175+
page_result = shodan_query(apikey, query, facets, page+1)
176+
if page_result['matches'].nil?
177+
next
178+
end
179+
results[page] = page_result
180+
page += 1
181+
end
182+
end
183+
# Save the results to this table
184+
tbl = Rex::Text::Table.new(
185+
'Header' => 'Search Results',
186+
'Indent' => 1,
187+
'Columns' => ['IP:Port', 'City', 'Country', 'Hostname']
188+
)
169189

170-
# Organize results and put them into the table and database
171-
regex = datastore['REGEX'] if datastore['REGEX']
172-
results.each do |page|
173-
page['matches'].each do |host|
174-
city = host['location']['city'] || 'N/A'
175-
ip = host['ip_str'] || 'N/A'
176-
port = host['port'] || ''
177-
country = host['location']['country_name'] || 'N/A'
178-
hostname = host['hostnames'][0]
179-
data = host['data']
180-
181-
report_host(:host => ip,
182-
:name => hostname,
183-
:comments => 'Added from Shodan',
184-
:info => host['info']
185-
) if datastore['DATABASE']
186-
187-
report_service(:host => ip,
188-
:port => port,
189-
:info => 'Added from Shodan'
190-
) if datastore['DATABASE']
191-
192-
if ip =~ regex ||
193-
city =~ regex ||
194-
country =~ regex ||
195-
hostname =~ regex ||
196-
data =~ regex
197-
# Unfortunately we cannot display the banner properly,
198-
# because it messes with our output format
199-
tbl << ["#{ip}:#{port}", city, country, hostname]
190+
# Organize results and put them into the table and database
191+
regex = datastore['REGEX'] if datastore['REGEX']
192+
results.each do |page|
193+
page['matches'].each do |host|
194+
city = host.dig('location','city') || 'N/A'
195+
ip = host.fetch('ip_str', 'N/A')
196+
port = host.fetch('port', '')
197+
country = host.dig('location','country_name') || 'N/A'
198+
hostname = host.dig('hostnames',0)
199+
data = host.dig('data')
200+
201+
report_host(:host => ip,
202+
:name => hostname,
203+
:comments => 'Added from Shodan',
204+
:info => host.dig('info')
205+
) if datastore['DATABASE']
206+
207+
report_service(:host => ip,
208+
:port => port,
209+
:info => 'Added from Shodan'
210+
) if datastore['DATABASE']
211+
212+
if ip =~ regex ||
213+
city =~ regex ||
214+
country =~ regex ||
215+
hostname =~ regex ||
216+
data =~ regex
217+
# Unfortunately we cannot display the banner properly,
218+
# because it messes with our output format
219+
tbl << ["#{ip}:#{port}", city, country, hostname]
220+
end
200221
end
201222
end
223+
#Show data and maybe save it if needed
224+
print_line()
225+
print_line("#{tbl}")
226+
save_output(tbl) if datastore['OUTFILE']
227+
end
228+
if datastore['FACETS']
229+
print_line(facets_tbl.to_s)
230+
save_output(facets_tbl) if datastore['OUTFILE']
202231
end
203-
#Show data and maybe save it if needed
204-
print_line()
205-
print_line("#{tbl}")
206-
save_output(tbl) if datastore['OUTFILE']
207232
end
208233
end

0 commit comments

Comments
 (0)