Mirror of metasploit
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

shodan_search.rb 6.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. ##
  2. # This module requires Metasploit: http://metasploit.com/download
  3. # Current source: https://github.com/rapid7/metasploit-framework
  4. ##
  5. require 'msf/core'
  6. require 'rex'
  7. require 'net/https'
  8. require 'uri'
  9. class MetasploitModule < Msf::Auxiliary
  10. include Msf::Exploit::Remote::HttpClient
  11. include Msf::Auxiliary::Report
  12. def initialize(info = {})
  13. super(update_info(info,
  14. 'Name' => 'Shodan Search',
  15. 'Description' => %q{
  16. This module uses the Shodan API to search Shodan. Accounts are free
  17. and an API key is required to used this module. Output from the module
  18. is displayed to the screen and can be saved to a file or the MSF database.
  19. NOTE: SHODAN filters (i.e. port, hostname, os, geo, city) can be used in
  20. queries, but there are limitations when used with a free API key. Please
  21. see the Shodan site for more information.
  22. Shodan website: https://www.shodan.io/
  23. API: https://developer.shodan.io/api
  24. },
  25. 'Author' =>
  26. [
  27. 'John H Sawyer <john[at]sploitlab.com>', # InGuardians, Inc.
  28. 'sinn3r' # Metasploit-fu plus other features
  29. ],
  30. 'License' => MSF_LICENSE
  31. )
  32. )
  33. deregister_options('RHOST', 'DOMAIN', 'DigestAuthIIS', 'NTLM::SendLM',
  34. 'NTLM::SendNTLM', 'VHOST', 'RPORT', 'NTLM::SendSPN', 'NTLM::UseLMKey',
  35. 'NTLM::UseNTLM2_session', 'NTLM::UseNTLMv2')
  36. register_options(
  37. [
  38. OptString.new('SHODAN_APIKEY', [true, 'The SHODAN API key']),
  39. OptString.new('QUERY', [true, 'Keywords you want to search for']),
  40. OptString.new('OUTFILE', [false, 'A filename to store the list of IPs']),
  41. OptBool.new('DATABASE', [false, 'Add search results to the database', false]),
  42. OptInt.new('MAXPAGE', [true, 'Max amount of pages to collect', 1]),
  43. OptRegexp.new('REGEX', [true, 'Regex search for a specific IP/City/Country/Hostname', '.*'])
  44. ], self.class)
  45. end
  46. # create our Shodan query function that performs the actual web request
  47. def shodan_query(query, apikey, page)
  48. # send our query to Shodan
  49. uri = URI.parse('https://api.shodan.io/shodan/host/search?query=' +
  50. Rex::Text.uri_encode(query) + '&key=' + apikey + '&page=' + page.to_s)
  51. http = Net::HTTP.new(uri.host, uri.port)
  52. http.use_ssl = true
  53. request = Net::HTTP::Get.new(uri.request_uri)
  54. res = http.request(request)
  55. if res and res.body =~ /<title>401 Unauthorized<\/title>/
  56. fail_with(Failure::BadConfig, '401 Unauthorized. Your SHODAN_APIKEY is invalid')
  57. end
  58. # Check if we can resolve host, got a response,
  59. # then parse the JSON, and return it
  60. if res
  61. results = ActiveSupport::JSON.decode(res.body)
  62. return results
  63. else
  64. return 'server_response_error'
  65. end
  66. end
  67. # save output to file
  68. def save_output(data)
  69. ::File.open(datastore['OUTFILE'], 'wb') do |f|
  70. f.write(data)
  71. print_status("Saved results in #{datastore['OUTFILE']}")
  72. end
  73. end
  74. # Check to see if api.shodan.io resolves properly
  75. def shodan_resolvable?
  76. begin
  77. Rex::Socket.resolv_to_dotted("api.shodan.io")
  78. rescue RuntimeError, SocketError
  79. return false
  80. end
  81. true
  82. end
  83. def run
  84. # check to ensure api.shodan.io is resolvable
  85. unless shodan_resolvable?
  86. print_error("Unable to resolve api.shodan.io")
  87. return
  88. end
  89. # create our Shodan request parameters
  90. query = datastore['QUERY']
  91. apikey = datastore['SHODAN_APIKEY']
  92. page = 1
  93. maxpage = datastore['MAXPAGE']
  94. # results gets our results from shodan_query
  95. results = []
  96. results[page] = shodan_query(query, apikey, page)
  97. if results[page]['total'].nil? || results[page]['total'] == 0
  98. msg = "No results."
  99. if results[page]['error'].to_s.length > 0
  100. msg << " Error: #{results[page]['error']}"
  101. end
  102. print_error(msg)
  103. return
  104. end
  105. # Determine page count based on total results
  106. if results[page]['total'] % 100 == 0
  107. tpages = results[page]['total'] / 100
  108. else
  109. tpages = results[page]['total'] / 100 + 1
  110. maxpage = tpages if datastore['MAXPAGE'] > tpages
  111. end
  112. # start printing out our query statistics
  113. print_status("Total: #{results[page]['total']} on #{tpages} " +
  114. "pages. Showing: #{maxpage} page(s)")
  115. # If search results greater than 100, loop & get all results
  116. print_status('Collecting data, please wait...')
  117. if results[page]['total'] > 100
  118. page += 1
  119. while page <= maxpage
  120. break if page > datastore['MAXPAGE']
  121. results[page] = shodan_query(query, apikey, page)
  122. page += 1
  123. end
  124. end
  125. # Save the results to this table
  126. tbl = Rex::Text::Table.new(
  127. 'Header' => 'Search Results',
  128. 'Indent' => 1,
  129. 'Columns' => ['IP:Port', 'City', 'Country', 'Hostname']
  130. )
  131. # Organize results and put them into the table and database
  132. p = 1
  133. regex = datastore['REGEX'] if datastore['REGEX']
  134. while p <= maxpage
  135. break if p > maxpage
  136. results[p]['matches'].each do |host|
  137. city = host['location']['city'] || 'N/A'
  138. ip = host['ip_str'] || 'N/A'
  139. port = host['port'] || ''
  140. country = host['location']['country_name'] || 'N/A'
  141. hostname = host['hostnames'][0]
  142. data = host['data']
  143. report_host(:host => ip,
  144. :name => hostname,
  145. :comments => 'Added from Shodan',
  146. :info => host['info']
  147. ) if datastore['DATABASE']
  148. report_service(:host => ip,
  149. :port => port,
  150. :info => 'Added from Shodan'
  151. ) if datastore['DATABASE']
  152. if ip =~ regex ||
  153. city =~ regex ||
  154. country =~ regex ||
  155. hostname =~ regex ||
  156. data =~ regex
  157. # Unfortunately we cannot display the banner properly,
  158. # because it messes with our output format
  159. tbl << ["#{ip}:#{port}", city, country, hostname]
  160. end
  161. end
  162. p += 1
  163. end
  164. # Show data and maybe save it if needed
  165. print_line
  166. print_line("#{tbl}")
  167. save_output(tbl) if datastore['OUTFILE']
  168. end
  169. end