# frozen_string_literal: true require 'json' require 'uri' module PWN module Plugins # This plugin is used for interacting w/ Shodan's REST API using # the 'rest' browser type of PWN::Plugins::TransparentBrowser. # This is based on the following Shodan API Specification: # https://developer.shodan.io/api module Shodan @@logger = PWN::Plugins::PWNLogger.create # Supported Method Parameters:: # valid_uri?( # search_results_hash: 'required - iteration of search results' # ) private_class_method def self.valid_uri?(opts = {}) uri = URI.parse(opts[:uri].to_s) %w[http https].include?(uri.scheme) rescue URI::BadURIError, URI::InvalidURIError, URI::InvalidComponentError false rescue StandardError => e raise e end # Supported Method Parameters:: # extract_uris( # search_results_hash: 'required - iteration of search results' # ) private_class_method def self.extract_uris(opts = {}) search_results_hash = opts[:search_results_hash] search_results_hash.each_with_object([]) do |value, uris| valid_uris_from_value = *URI.extract(value.to_s).select do |uri| valid_uri?(uri: uri) end uris.push(valid_uris_from_value) end rescue URI::BadURIError, URI::InvalidURIError, URI::InvalidComponentError false rescue StandardError => e raise e end # Supported Method Parameters:: # shodan_rest_call( # api_key: 'required - shodan api key', # http_method: 'optional HTTP method (defaults to GET) # rest_call: 'required rest call to make per the schema', # params: 'optional params passed in the URI or HTTP Headers', # http_body: 'optional HTTP body sent in HTTP methods that support it e.g. POST' # ) private_class_method def self.shodan_rest_call(opts = {}) shodan_obj = opts[:shodan_obj] http_method = if opts[:http_method].nil? :get else opts[:http_method].to_s.scrub.to_sym end rest_call = opts[:rest_call].to_s.scrub params = opts[:params] http_body = opts[:http_body].to_s.scrub base_shodan_api_uri = 'https://api.shodan.io' api_key = opts[:api_key] browser_obj = PWN::Plugins::TransparentBrowser.open(browser_type: :rest) rest_client = browser_obj[:browser]::Request case http_method when :get response = rest_client.execute( method: :get, url: "#{base_shodan_api_uri}/#{rest_call}", headers: { content_type: 'application/json; charset=UTF-8', params: params }, verify_ssl: false ) when :post response = rest_client.execute( method: :post, url: "#{base_shodan_api_uri}/#{rest_call}", headers: { content_type: 'application/json; charset=UTF-8', params: params }, payload: http_body, verify_ssl: false ) else raise @@logger.error("Unsupported HTTP Method #{http_method} for #{self} Plugin") end JSON.parse(response.scrub, symbolize_names: true) rescue RestClient::TooManyRequests print 'Too many requests. Sleeping 10s...' sleep 10 retry rescue StandardError => e case e.message when '400 Bad Request', '404 Resource Not Found' "#{e.message}: #{e.response}" else raise e end end # Supported Method Parameters:: # services_by_ips = PWN::Plugins::Shodan.services_by_ips( # api_key: 'required shodan api key', # target_ips: 'required - comma-delimited list of ip addresses to target' # ) public_class_method def self.services_by_ips(opts = {}) api_key = opts[:api_key].to_s.scrub target_ips = opts[:target_ips].to_s.scrub.gsub(/\s/, '').split(',') services_by_ips = [] params = { key: api_key } target_ips.each do |target_ip| response = shodan_rest_call( api_key: api_key, rest_call: "shodan/host/#{target_ip}", params: params ) services_by_ips.push(response) rescue StandardError => e services_by_ips.push(error: e.message) next end services_by_ips rescue StandardError => e raise e end # Supported Method Parameters:: # query_result_totals = PWN::Plugins::Shodan.query_result_totals( # api_key: 'required shodan api key', # query: 'required - shodan search query', # facets: 'optional - comma-separated list of properties to get summary information' # ) public_class_method def self.query_result_totals(opts = {}) api_key = opts[:api_key].to_s.scrub query = opts[:query].to_s.scrub facets = opts[:facets].to_s.scrub if facets params = { key: api_key, query: query, facets: facets } else params = { key: api_key, query: query } end shodan_rest_call( api_key: api_key, rest_call: 'shodan/host/count', params: params ) rescue StandardError => e raise e end # Supported Method Parameters:: # search_results = PWN::Plugins::Shodan.search( # api_key: 'required shodan api key', # query: 'required - shodan search query', # facets: 'optional - comma-separated list of properties to get summary information' # ) public_class_method def self.search(opts = {}) api_key = opts[:api_key].to_s.scrub query = opts[:query].to_s.scrub facets = opts[:facets].to_s.scrub if facets params = { key: api_key, query: query, facets: facets } else params = { key: api_key, query: query } end shodan_rest_call( api_key: api_key, rest_call: 'shodan/host/search', params: params ) rescue StandardError => e raise e end # Supported Method Parameters:: # tokens_result = PWN::Plugins::Shodan.tokens( # api_key: 'required shodan api key', # query: 'required - shodan search query', # ) public_class_method def self.tokens(opts = {}) api_key = opts[:api_key].to_s.scrub query = opts[:query].to_s.scrub params = { key: api_key, query: query } shodan_rest_call( api_key: api_key, rest_call: 'shodan/host/search/tokens', params: params ) rescue StandardError => e raise e end # Supported Method Parameters:: # ports_shodan_crawls = PWN::Plugins::Shodan.ports_shodan_crawls( # api_key: 'required shodan api key' # ) public_class_method def self.ports_shodan_crawls(opts = {}) api_key = opts[:api_key].to_s.scrub params = { key: api_key } shodan_rest_call( api_key: api_key, rest_call: 'shodan/ports', params: params ) rescue StandardError => e raise e end # Supported Method Parameters:: # protocols = PWN::Plugins::Shodan.list_on_demand_scan_protocols( # api_key: 'required shodan api key' # ) public_class_method def self.list_on_demand_scan_protocols(opts = {}) api_key = opts[:api_key].to_s.scrub params = { key: api_key } shodan_rest_call( api_key: api_key, rest_call: 'shodan/protocols', params: params ) rescue StandardError => e raise e end # Supported Method Parameters:: # scan__networkresponse = PWN::Plugins::Shodan.scan_network( # api_key: 'required shodan api key', # target_ips: 'required - comma-delimited list of ip addresses to target' # ) public_class_method def self.scan_network(opts = {}) api_key = opts[:api_key].to_s.scrub target_ips = opts[:target_ips].to_s.scrub.gsub(/\s/, '') params = { key: api_key } http_body = "ips=#{target_ips}" shodan_rest_call( http_method: :post, api_key: api_key, rest_call: 'shodan/scan', params: params, http_body: http_body ) rescue StandardError => e raise e end # Supported Method Parameters:: # scan_internet_response = PWN::Plugins::Shodan.scan_internet( # api_key: 'required shodan api key', # port: 'required - port to scan (see #ports_shodan_crawls for list)', # protocol: 'required - supported shodan protocol (see #list_on_demand_scan_protocols for list)' # ) public_class_method def self.scan_internet(opts = {}) api_key = opts[:api_key].to_s.scrub port = opts[:port].to_i protocol = opts[:protocol].to_s.scrub params = { key: api_key } http_body = "port=#{port}&protocol=#{protocol}" shodan_rest_call( http_method: :post, api_key: api_key, rest_call: 'shodan/scan/internet', params: params, http_body: http_body ) rescue StandardError => e raise e end # Supported Method Parameters:: # scan_status_result = PWN::Plugins::Shodan.scan_status( # api_key: 'required shodan api key', # scan_id: 'required - unique ID returned by #scan_network', # ) public_class_method def self.scan_status(opts = {}) api_key = opts[:api_key].to_s.scrub scan_id = opts[:scan_id].to_s.scrub params = { key: api_key } shodan_rest_call( api_key: api_key, rest_call: "shodan/scan/status/#{scan_id}", params: params ) rescue StandardError => e raise e end # Supported Method Parameters:: # services_shodan_crawls = PWN::Plugins::Shodan.services_shodan_crawls( # api_key: 'required shodan api key' # ) public_class_method def self.services_shodan_crawls(opts = {}) api_key = opts[:api_key].to_s.scrub params = { key: api_key } shodan_rest_call( api_key: api_key, rest_call: 'shodan/services', params: params ) rescue StandardError => e raise e end # Supported Method Parameters:: # saved_search_queries_result = PWN::Plugins::Shodan.saved_search_queries( # api_key: 'required shodan api key', # page: 'optional - page number to iterate over results (each page contains 10 items)', # sort: 'optional - sort results by available parameters :votes|:timestamp', # order: 'optional - sort :asc|:desc (ascending or descending)' # ) public_class_method def self.saved_search_queries(opts = {}) api_key = opts[:api_key].to_s.scrub page = opts[:page].to_i sort = opts[:sort].to_sym order = opts[:order].to_sym params = { key: api_key, page: page, sort: sort.to_s, order: order.to_s } shodan_rest_call( api_key: api_key, rest_call: 'shodan/query', params: params ) rescue StandardError => e raise e end # Supported Method Parameters:: # most_popular_tags_result = PWN::Plugins::Shodan.most_popular_tags( # api_key: 'required shodan api key', # result_count: 'optional - number of results to return (defaults to 10)' # ) public_class_method def self.most_popular_tags(opts = {}) api_key = opts[:api_key].to_s.scrub result_count = opts[:result_count].to_i if result_count params = { key: api_key, size: result_count } else params = { key: api_key } end shodan_rest_call( api_key: api_key, rest_call: 'shodan/query/tags', params: params ) rescue StandardError => e raise e end # Supported Method Parameters:: # my_profile = PWN::Plugins::Shodan.my_profile( # api_key: 'required shodan api key' # ) public_class_method def self.my_profile(opts = {}) api_key = opts[:api_key].to_s.scrub params = { key: api_key } shodan_rest_call( api_key: api_key, rest_call: 'account/profile', params: params ) rescue StandardError => e raise e end # Supported Method Parameters:: # my_pub_ip = PWN::Plugins::Shodan.my_pub_ip( # api_key: 'required shodan api key' # ) public_class_method def self.my_pub_ip(opts = {}) api_key = opts[:api_key].to_s.scrub params = { key: api_key } shodan_rest_call( api_key: api_key, rest_call: 'tools/myip', params: params ) rescue StandardError => e raise e end # Supported Method Parameters:: # api_info = PWN::Plugins::Shodan.api_info( # api_key: 'required shodan api key' # ) public_class_method def self.api_info(opts = {}) api_key = opts[:api_key].to_s.scrub params = { key: api_key } shodan_rest_call( api_key: api_key, rest_call: 'api-info', params: params ) rescue StandardError => e raise e end # Supported Method Parameters:: # honeypot_probability_scores = PWN::Plugins::Shodan.honeypot_probability_scores( # api_key: 'required shodan api key', # target_ips: 'required - comma-delimited list of ip addresses to target' # ) public_class_method def self.honeypot_probability_scores(opts = {}) api_key = opts[:api_key].to_s.scrub target_ips = opts[:target_ips].to_s.scrub.gsub(/\s/, '').split(',') honeypot_probability_scores = [] params = { key: api_key } target_ips.each do |target_ip| response = shodan_rest_call( api_key: api_key, rest_call: "labs/honeyscore/#{target_ip}", params: params ) honeypot_probability_scores.push("#{target_ip} => #{response}") end honeypot_probability_scores rescue StandardError => e raise e end # Supported Method Parameters:: # uri_arr = PWN::Plugins::Shodan.get_uris( # search_results: 'required - search_results object returned from #search method' # ) public_class_method def self.get_uris(opts = {}) search_results = opts[:search_results] search_results.map do |search_results_hash| extract_uris( search_results_hash: search_results_hash ) end.flatten rescue StandardError => e raise e end # Author(s):: 0day Inc. public_class_method def self.authors "AUTHOR(S): 0day Inc. " end # Display Usage for this Module public_class_method def self.help puts "USAGE: services_by_ips = #{self}.services_by_ips( api_key: 'required - shodan api key', target_ips: 'required - comma-delimited list of ip addresses to target' ) query_result_totals = PWN::Plugins::Shodan.query_result_totals( api_key: 'required shodan api key', query: 'required - shodan search query', facets: 'optional - comma-separated list of properties to get summary information' ) search_results = #{self}.search( api_key: 'required shodan api key', query: 'required - shodan search query', facets: 'optional - comma-separated list of properties to get summary information' ) tokens_result = #{self}.tokens( api_key: 'required shodan api key', query: 'required - shodan search query', ) ports_shodan_crawls = #{self}.ports_shodan_crawls( api_key: 'required shodan api key' ) protocols = #{self}.list_on_demand_scan_protocols( api_key: 'required shodan api key' ) scan_network_response = #{self}.scan_network( api_key: 'required shodan api key', target_ips: 'required - comma-delimited list of ip addresses to target' ) scan_internet_response = #{self}.scan_internet( api_key: 'required shodan api key', port: 'required - port to scan (see #ports_shodan_crawls for list)', protocol: 'required - supported shodan protocol (see #list_on_demand_scan_protocols for list)' ) scan_status_result = #{self}.scan_status( api_key: 'required shodan api key', scan_id: 'required - unique ID returned by #scan_network', ) services_shodan_crawls = #{self}.services_shodan_crawls( api_key: 'required shodan api key' ) saved_search_queries_result = #{self}.saved_search_queries( api_key: 'required shodan api key', page: 'optional - page number to iterate over results (each page contains 10 items)', sort: 'optional - sort results by available parameters :votes|:timestamp', order: 'optional - sort :asc|:desc (ascending or descending)' ) most_popular_tags_result = #{self}.most_popular_tags( api_key: 'required shodan api key', result_count: 'optional - number of results to return (defaults to 10)' ) my_profile = #{self}.my_profile( api_key: 'required shodan api key' ) my_pub_ip = #{self}.my_pub_ip( api_key: 'required shodan api key' ) api_info = #{self}.api_info( api_key: 'required shodan api key' ) honeypot_probability_scores = #{self}.honeypot_probability_scores( api_key: 'required shodan api key', target_ips: 'required - comma-delimited list of ip addresses to target' ) uri_arr = #{self}.get_uris( search_results: 'required - search_results object returned from #search method' ) #{self}.authors " end end end end