5
5
6
6
require 'msf/core'
7
7
require 'rex'
8
- require 'net/dns'
8
+ require 'net/https'
9
+ require 'uri'
9
10
10
11
class Metasploit4 < Msf ::Auxiliary
11
12
@@ -16,170 +17,167 @@ def initialize(info = {})
16
17
super ( update_info ( info ,
17
18
'Name' => 'Shodan Search' ,
18
19
'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
30
28
} ,
31
29
'Author' =>
32
30
[
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
35
33
] ,
36
34
'License' => MSF_LICENSE
37
- ) )
35
+ )
36
+ )
38
37
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' )
44
41
45
42
register_options (
46
43
[
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
+ OptString . new ( 'FILTER' , [ false , 'Search for a specific IP/City/Country/Hostname' ] )
54
50
] , self . class )
55
51
end
56
52
57
53
# create our Shodan query function that performs the actual web request
58
54
def shodan_query ( query , apikey , page )
59
55
# 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 )
56
+
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
+ # Check if we can resolve host, got a response,
65
+ # then parse the JSON, and return it
66
+ if res
72
67
results = ActiveSupport ::JSON . decode ( res . body )
73
68
return results
74
69
else
75
- return 'server_error '
70
+ return 'server_response_error '
76
71
end
77
72
end
78
73
74
+ # save output to file
79
75
def save_output ( data )
80
- f = ::File . open ( datastore [ 'OUTFILE' ] , "wb" )
76
+ f = ::File . open ( datastore [ 'OUTFILE' ] , 'wb' )
81
77
f . write ( data )
82
78
f . close
83
79
print_status ( "Save results in #{ datastore [ 'OUTFILE' ] } " )
84
80
end
85
81
82
+ # Check to see if api.shodan.io resolves properly
86
83
def shodan_rhost
87
- @res = Net ::DNS ::Resolver . new ( )
88
- dns_query = @res . query ( " #{ datastore [ 'VHOST' ] } " , "A" )
84
+ @res = Net ::DNS ::Resolver . new
85
+ dns_query = @res . query ( 'api.shodan.io' , 'A' )
89
86
if dns_query . answer . length == 0
90
- print_error ( " Could not resolve #{ datastore [ 'VHOST' ] } " )
91
- raise ::Rex ::ConnectError ( vhost , shodan_port )
87
+ print_error ( ' Could not resolve api.shodan.io' )
88
+ raise ::Rex ::ConnectError ( 'api.shodan.io' , '443' )
92
89
end
93
90
dns_query . answer [ 0 ] . to_s . split ( /[\s ,]+/ ) [ 4 ]
94
91
end
95
92
96
- def shodan_rport
97
- 80
98
- end
99
-
100
93
def run
94
+ # check to ensure api.shodan.io is resolvable
95
+ shodan_rhost
101
96
102
97
# create our Shodan request parameters
103
98
query = datastore [ 'QUERY' ]
104
99
apikey = datastore [ 'SHODAN_APIKEY' ]
105
-
106
100
page = 1
101
+ maxpage = datastore [ 'MAXPAGE' ]
107
102
108
103
# results gets our results from shodan_query
109
104
results = [ ]
110
105
results [ page ] = shodan_query ( query , apikey , page )
111
106
112
- if results [ page ] . empty?
113
- print_error ( " No Results Found!" )
107
+ if results [ page ] [ 'total' ] == 0
108
+ print_error ( ' No Results Found!' )
114
109
return
115
110
end
116
111
117
112
# Determine page count based on total results
118
- if results [ page ] [ 'total' ] % 50 == 0
119
- tpages = results [ page ] [ 'total' ] / 50
113
+ if results [ page ] [ 'total' ] % 100 == 0
114
+ tpages = results [ page ] [ 'total' ] / 100
120
115
else
121
- tpages = results [ page ] [ 'total' ] /50 + 1
116
+ tpages = results [ page ] [ 'total' ] / 100 + 1
117
+ maxpage = tpages if datastore [ 'MAXPAGE' ] > tpages
122
118
end
123
119
124
120
# 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 )
121
+ print_status ( "Total: #{ results [ page ] [ 'total' ] } on #{ tpages } " \
122
+ "pages. Showing: #{ maxpage } page(s)" )
123
+
124
+ # If search results greater than 100, loop & get all results
125
+ print_status ( 'Collecting data, please wait...' )
126
+ if results [ page ] [ 'total' ] > 100
134
127
page += 1
135
128
while page <= tpages
136
- results [ page ] = shodan_query ( query , apikey , page )
137
- page +=1
138
129
break if page > datastore [ 'MAXPAGE' ]
130
+ results [ page ] = shodan_query ( query , apikey , page )
131
+ page += 1
139
132
end
140
133
end
141
134
142
135
# Save the results to this table
143
136
tbl = Rex ::Ui ::Text ::Table . new (
144
- 'Header' => 'IP Results' ,
137
+ 'Header' => 'Search Results' ,
145
138
'Indent' => 1 ,
146
- 'Columns' => [ 'IP' , 'City' , 'Country' , 'Hostname' ]
139
+ 'Columns' => [ 'IP:Port ' , 'City' , 'Country' , 'Hostname' ]
147
140
)
148
141
149
- # Organize results and put them into the table
142
+ # Organize results and put them into the table and database
150
143
page = 1
151
144
my_filter = datastore [ 'FILTER' ]
152
145
for i in page ..tpages
153
- next if results [ i ] . nil? or results [ i ] [ 'matches' ] . nil?
154
- results [ i ] [ 'matches' ] . each { |host |
146
+ next if results [ i ] . nil? || results [ i ] [ 'matches' ] . nil?
147
+ results [ i ] [ 'matches' ] . each do |host |
155
148
156
- city = host [ 'city' ] || 'N/A'
157
- ip = host [ 'ip ' ] || 'N/A'
149
+ city = host [ 'location' ] [ ' city'] || 'N/A'
150
+ ip = host [ 'ip_str ' ] || 'N/A'
158
151
port = host [ 'port' ] || ''
159
- country = host [ 'country_name' ] || 'N/A'
152
+ country = host [ 'location' ] [ ' country_name'] || 'N/A'
160
153
hostname = host [ 'hostnames' ] [ 0 ]
161
154
data = host [ 'data' ]
162
155
163
- if ip =~ /#{ my_filter } / or
164
- city =~ /#{ my_filter } /i or
165
- country =~ /#{ my_filter } /i or
166
- hostname =~ /#{ my_filter } /i or
156
+ report_host ( :host => ip ,
157
+ :name => hostname ,
158
+ :comments => 'Added from Shodan' ,
159
+ :info => host [ 'info' ]
160
+ ) if datastore [ 'DATABASE' ]
161
+
162
+ report_service ( :host => ip ,
163
+ :port => port ,
164
+ :info => 'Added from Shodan'
165
+ ) if datastore [ 'DATABASE' ]
166
+
167
+ if ip =~ /#{ my_filter } / ||
168
+ city =~ /#{ my_filter } /i ||
169
+ country =~ /#{ my_filter } /i ||
170
+ hostname =~ /#{ my_filter } /i ||
167
171
data =~ /#{ my_filter } /i
168
172
# Unfortunately we cannot display the banner properly,
169
173
# because it messes with our output format
170
174
tbl << [ "#{ ip } :#{ port } " , city , country , hostname ]
171
175
end
172
- }
176
+ end
173
177
end
174
178
175
- #Show data and maybe save it if needed
179
+ # Show data and maybe save it if needed
176
180
print_line ( "\n #{ tbl . to_s } " )
177
-
178
- report_note (
179
- :type => 'shodan' ,
180
- :data => tbl . to_csv
181
- ) if datastore [ 'DATABASE' ]
182
-
183
181
save_output ( tbl . to_s ) if not datastore [ 'OUTFILE' ] . nil?
184
182
end
185
183
end
0 commit comments