Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/usr/bin/env ruby
- #
- # Shodan API Search Assistant
- # By: Hood3dRob1n
- #
- ########### ENTER API KEY HERE ###########
- APIKEY='YOURAPIKEYNEEDS2GORIGHTINHEREYO!' #
- ###########################################
- ##### STD GEMS #######
- require 'fileutils' #
- require 'optparse' #
- require 'resolv' #
- #### NON-STD GEMS ####
- require 'rubygems' #
- require 'colorize' #
- require 'curb' #
- require 'json' #
- require 'nokogiri' #
- ######################
- HOME=File.expand_path(File.dirname(__FILE__))
- RESULTS = HOME + '/results/'
- # Banner
- def banner
- puts
- puts "Shodan API Search Assistant".light_green
- puts "By".light_green + ": Hood3dRob1n".white
- end
- # Clear Terminal
- def cls
- if RUBY_PLATFORM =~ /win32|win64|\.NET|windows|cygwin|mingw32/i
- system('cls')
- else
- system('clear')
- end
- end
- # Custom ShodanAPI Class :)
- # The pre-built option is broken and doesn't work in several places....
- # So we re-wrote it!
- class ShodanAPI
- # Initialize ShodanAPI via passed API Key
- def initialize(apikey)
- @url="http://www.shodanhq.com/api/"
- if shodan_connect(apikey)
- @key=apikey
- end
- end
- # Check API Key against API Info Query
- # Return True on success, False on Error or Failure
- def shodan_connect(apikey)
- url = @url + "info?key=#{apikey}"
- begin
- c = Curl::Easy.perform(url)
- if c.body_str =~ /"unlocked_left": \d+, "telnet": .+, "plan": ".+", "https": .+, "unlocked": .+/i
- results = JSON.parse(c.body_str)
- @plan = results['plan']
- @unlocked = results['unlocked']
- @unlocks = results['unlocked_left']
- @https = results['https']
- @telnet = results['telnet']
- return true
- elsif c.body_str =~ /"error": "API access denied"/i
- puts "Access Denied using API Key '#{apikey}'".light_red + "!".white
- puts "Check Key & Try Again".light_red + "....".white
- return false
- else
- puts "Unknown Problem with Connection to Shodan API".light_green + "!".white
- return false
- end
- rescue => e
- puts "Problem with Connection to Shodan API".light_red + "!".white
- puts "\t=> #{e}"
- return false
- end
- end
- # Just checks our key is working (re-using shodan_connect so updates @unlocks)
- # Returns True or False
- def connected?
- if shodan_connect(@key)
- return true
- else
- return false
- end
- end
- # Return the number of unlocks remaining
- def unlocks
- if shodan_connect(@key)
- return @unlocks.to_i
- else
- return nil
- end
- end
- # Check if HTTPS is Enabled
- def https?
- if shodan_connect(@key)
- if @https
- return true
- else
- return false
- end
- else
- return false
- end
- end
- # Check if Telnet is Enabled
- def telnet?
- if shodan_connect(@key)
- if @telnet
- return true
- else
- return false
- end
- else
- return false
- end
- end
- # Actually display Basic Info for current API Key
- def info
- url = @url + 'info?key=' + @key
- begin
- c = Curl::Easy.perform(url)
- results = JSON.parse(c.body_str)
- puts
- puts "Shodan API Key Confirmed".light_green + "!".white
- puts "API Key".light_green + ": #{@key}".white
- puts "Plan Type".light_green + ": #{results['plan']}".white
- puts "Unlocked".light_green + ": #{results['unlocked']}".white
- puts "Unlocks Remaining".light_green + ": #{results['unlocked_left']}".white
- puts "HTTPS Enabled".light_green + ": #{results['https']}".white
- puts "Telnet Enabled".light_green + ": #{results['telnet']}".white
- return true
- rescue => e
- puts "Problem with Connection to Shodan API".light_red + "!".white
- puts "\t=> #{e}".white
- return false
- end
- end
- # Lookup all available information for a specific IP address
- # Returns results hash or nil
- def host(ip)
- url = @url + 'host?ip=' + ip + '&key=' + @key
- begin
- c = Curl::Easy.perform(url)
- results = JSON.parse(c.body_str)
- return results
- rescue => e
- puts "Problem running Host Search".light_red + "!".white
- puts "\t=> #{e}".white
- return nil
- end
- end
- # Returns the number of devices that a search query found
- # Unrestricted usage of all advanced filters
- # Return results count or nil on failure
- def count(string)
- url = @url + 'count?q=' + string + '&key=' + @key
- begin
- c = Curl::Easy.perform(url)
- results = JSON.parse(c.body_str)
- return results['total']
- rescue => e
- puts "Problem grabbing results count".light_red + "!".white
- puts "\t=> #{e}".white
- return nil
- end
- end
- # Search Shodan for devices using a search query
- # Returns results hash or nil
- def search(string, filters={})
- prem_filters = [ 'city', 'country', 'geo', 'net', 'before', 'after', 'org', 'isp', 'title', 'html' ]
- cheap_filters = [ 'hostname', 'os', 'port' ]
- url = @url + 'search?q=' + string
- if not filters.empty?
- filters.each do |k, v|
- if cheap_filters.include?(k)
- url += ' ' + k + ":\"#{v}\""
- end
- if prem_filters.include?(k)
- if @unlocks.to_i > 1
- url += ' ' + k + ":\"#{v}\""
- @unlocks = @unlocks.to_i - 1 # Remove an unlock for use of filter
- else
- puts "Not Enough Unlocks Left to run Premium Filter Search".light_red + "!".white
- puts "Try removing '#{k}' filter and trying again".light_red + "....".white
- return nil
- end
- end
- end
- end
- url += '&key=' + @key
- begin
- c = Curl::Easy.perform(url)
- results = JSON.parse(c.body_str)
- return results
- rescue => e
- puts "Problem running Shodan Search".light_red + "!".white
- puts "\t=> #{e}".white
- return nil
- end
- end
- # Quick Search Shodan for devices using a search query
- # Results are limited to only the IP addresses
- # Returns results array or nil
- def quick_search(string, filters={})
- prem_filters = [ 'city', 'country', 'geo', 'net', 'before', 'after', 'org', 'isp', 'title', 'html' ]
- cheap_filters = [ 'hostname', 'os', 'port' ]
- url = @url + 'search?q=' + string
- if not filters.empty?
- filters.each do |k, v|
- if cheap_filters.include?(k)
- url += ' ' + k + ":\"#{v}\""
- end
- if prem_filters.include?(k)
- if @unlocks.to_i > 1
- url += ' ' + k + ":\"#{v}\""
- @unlocks = @unlocks.to_i - 1
- else
- puts "Not Enough Unlocks Left to run Premium Filter Search".light_red + "!".white
- puts "Try removing '#{k}' filter and trying again".light_red + "....".white
- return nil
- end
- end
- end
- end
- url += '&key=' + @key
- begin
- ips=[]
- c = Curl::Easy.perform(url)
- results = JSON.parse(c.body_str)
- results['matches'].each do |host|
- ips << host['ip']
- end
- return ips
- rescue => e
- puts "Problem running Shodan Quick Search".light_red + "!".white
- puts "\t=> #{e}".white
- return nil
- end
- end
- # Perform Shodan Exploit Search as done on Web
- # Provide Search String and source
- # Source can be: metasploit, exploitdb, or cve
- # Returns results hash array on success: { downloadID => { link => description } }
- # Returns nil on failure
- def sploit_search(string, source)
- sources = [ "metasploit", "exploitdb", "cve" ]
- if sources.include?(source.downcase)
- sploits = 'https://exploits.shodan.io/?q=' + string + ' source:"' + source.downcase + '"'
- begin
- results={}
- c = Curl::Easy.perform(sploits)
- page = Nokogiri::HTML(c.body_str) # Parsable doc object now
- # Enumerate target section, parse out link & description
- page.css('div[class="search-result well"]').each do |linematch|
- if linematch.to_s =~ /<div class="search-result well">\s+<a href="(.+)"\s/
- link=$1
- end
- if linematch.to_s =~ /class="title">(.+)\s+<\/a>/
- desc=$1.gsub('<em>', '').gsub('</em>', '')
- end
- case source.downcase
- when 'cve'
- dl_id = 'N/A for CVE Search'
- when 'exploitdb'
- dl_id = link.split('/')[-1] unless link.nil?
- when 'metasploit'
- dl_id = link.sub('http://www.metasploit.com/', '').sub(/\/$/, '') unless link.nil?
- end
- results.store(dl_id, { link => desc}) unless (link.nil? or link == '') or (desc.nil? or desc == '') or (dl_id.nil? or dl_id == 'N/A for CVE Search')
- end
- return results
- rescue Curl::Err::ConnectionFailedError => e
- puts "Shitty connection yo".light_red + ".....".white
- return nil
- rescue => e
- puts "Unknown connection problem".light_red + ".....".white
- puts "\t=> #{e}".white
- return nil
- end
- else
- puts "Invalid Search Source Requested".light_red + "!".white
- return nil
- end
- end
- # Download Exploit Code from Exploit-DB or MSF Github Page
- # By passing in the Download ID (which can be seen in sploit_search() results)
- # Return { 'Download' => dl_link, 'Viewing' => v_link, 'Exploit' => c.body_str }
- # or nil on failure
- def sploit_download(id, source)
- sources = [ "metasploit", "exploitdb" ]
- if sources.include?(source.downcase)
- case source.downcase
- when 'exploitdb'
- dl_link = "http://www.exploit-db.com/download/#{id}/"
- v_link = "http://www.exploit-db.com/exploits/#{id}/"
- when 'metasploit'
- dl_link = "https://raw.github.com/rapid7/metasploit-framework/master/#{id.sub('/exploit/', '/exploits/')}.rb"
- v_link = "http://www.rapid7.com/db/#{id}/"
- end
- begin
- c = Curl::Easy.perform(dl_link)
- page = Nokogiri::HTML(c.body_str) # Parsable doc object now
- results = { 'Download' => dl_link, 'Viewing' => v_link, 'Exploit' => c.body_str }
- return results
- rescue Curl::Err::ConnectionFailedError => e
- puts "Shitty connection yo".light_red + ".....".white
- return false
- rescue => e
- puts "Unknown connection problem".light_red + ".....".white
- puts "#{e}".light_red
- return false
- end
- else
- puts "Invalid Download Source Requested".light_red + "!".white
- return false
- end
- end
- end
- ### MAIN ###
- options = {}
- optparse = OptionParser.new do |opts|
- opts.banner = "Usage:".light_green + "#{$0} ".white + "[".light_green + "OPTIONS".white + "]".light_green
- opts.separator ""
- opts.separator "EX:".light_green + " #{$0} -s cisco-ios".white
- opts.separator "EX:".light_green + " #{$0} -h 217.140.75.46".white
- opts.separator "EX:".light_green + " #{$0} --quick-search IIS/5.1".white
- opts.separator "EX:".light_green + " #{$0} -S exploitdb -x udev".white
- opts.separator "EX:".light_green + " #{$0} -d 8678 -S exploitdb".white
- opts.separator "EX:".light_green + " #{$0} --source metasploit --exploit-search udev".white
- opts.separator "EX:".light_green + " #{$0} -S metasploit -d modules/exploit/linux/local/udev_netlink".white
- opts.separator ""
- opts.separator "Options: ".light_green
- opts.on('-q', '--quick-search STRING', "\n\tShodan Quick Search".white) do |search_str|
- options[:method] = 3 # 1=> Normal, 2=> IP, 3=> Quick, 4=>Exploit Search, 5=>Exploit Download
- options[:search] = search_str.chomp
- end
- opts.on('-s', '--shodan-search STRING', "\n\tShodan Search".white) do |search_str|
- options[:method] = 1 # 1=> Normal, 2=> IP, 3=> Quick, 4=>Exploit Search, 5=>Exploit Download
- options[:search] = search_str.chomp
- end
- opts.on('-h', '--host-search HOST', "\n\tShodan Host Search against IP".white) do |search_str|
- options[:method] = 2 # 1=> Normal, 2=> IP, 3=> Quick, 4=>Exploit Search, 5=>Exploit Download
- if search_str.chomp =~ /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/
- options[:search] = search_str.chomp
- else
- begin
- ip = Resolv.getaddress(search_str.chomp) # Resolve Host Domain to IP
- options[:search] = ip
- rescue Resolv::ResolvError => e
- cls
- banner
- puts
- puts "Unable to Resolve Host to IP".light_red + "!".white
- puts
- puts opts
- puts
- exit 69;
- end
- end
- end
- opts.on('-S', '--source SOURCE', "\n\tSet Exploit Source: exploitdb or metasploit".white) do |source|
- sources=["metasploit", "exploitdb"]
- if sources.include?(source.downcase.chomp)
- options[:source] = source.downcase.chomp
- else
- cls
- banner
- puts
- puts "Invalid Search Source Requested".light_red + "!".white
- puts "\t=> #{source}".light_red
- puts
- puts opts
- puts
- exit 69;
- end
- end
- opts.on('-x', '--exploit-search STRING', "\n\tShodan Exploit Search for String (requires -S)".white) do |search_str|
- options[:method] = 4 # 1=> Normal, 2=> IP, 3=> Quick, 4=>Exploit Search, 5=>Exploit Download
- options[:search] = search_str.chomp
- end
- opts.on('-d', '--download-id ID', "\n\tDownload Exploit by Exploit ID (requires -S)".white) do |search_str|
- options[:method] = 5 # 1=> Normal, 2=> IP, 3=> Quick, 4=>Exploit-DB Search, 5=>Exploit-DB Download
- options[:search] = search_str.chomp
- end
- opts.on('-H', '--help', "\n\tHelp Menu".white) do
- cls
- banner
- puts
- puts opts
- puts
- exit 69;
- end
- end
- begin
- foo = ARGV[0] || ARGV[0] = "-H"
- optparse.parse!
- mandatory = [:method,:search]
- missing = mandatory.select{ |param| options[param].nil? }
- if not missing.empty?
- cls
- banner
- puts
- puts "Missing options: ".red + " #{missing.join(', ')}".white
- puts optparse
- exit 666;
- end
- rescue OptionParser::InvalidOption, OptionParser::MissingArgument, OptionParser::AmbiguousOption
- cls
- banner
- puts
- puts $!.to_s.red
- puts
- puts optparse
- puts
- exit 666;
- end
- banner
- shodan = ShodanAPI.new(APIKEY)
- if shodan.connected?
- # Display Basic API Key Info
- shodan.info
- puts
- # Create Results Dir if it doesnt exist
- Dir.mkdir(RESULTS) unless File.exists?(RESULTS) and File.directory?(RESULTS)
- # Now run as requested....
- case options[:method].to_i
- when 1
- results = shodan.search(options[:search].to_s)
- if not results.nil?
- puts "Shodan Search".light_green + ": #{options[:search].to_s}".white
- f=File.open(RESULTS + "shodan_search_results.txt", 'w+')
- f.puts "Shodan Search: #{options[:search].to_s}"
- puts "Total Results Found".light_green + ": #{results['total']}".white
- f.puts "Total Results Found: #{results['total']}"
- results['countries'].each do |country|
- puts " #{country['name']}".light_green + ": #{country['count']}".white
- f.puts " #{country['name']}: #{country['count']}"
- end
- puts
- f.puts
- results['matches'].each do |host|
- puts "Host IP".light_green + ": #{host['ip']}".white
- f.puts "Host IP: #{host['ip']}"
- puts "#{host['data']}".white
- f.puts host['data']
- end
- f.puts
- f.close
- else
- puts "No Results Found for ".light_red + "#{string}".white + " via Shodan Search".light_red + "!".white
- end
- puts
- when 2
- # Check Host Results
- results = shodan.host(options[:search].to_s)
- if not results.nil?
- f=File.open(RESULTS + "shodan_host_search_results.txt", 'w+')
- puts "Host IP".light_green + ": #{results['ip']}".white unless results['ip'].nil?
- f.puts "Host IP: #{results['ip']}" unless results['ip'].nil?
- puts "ISP".light_green + ": #{results['data'][0]['isp']}".white unless results['data'][0]['isp'].nil?
- f.puts "ISP: #{results['data'][0]['isp']}" unless results['data'][0]['isp'].nil?
- puts "Hostname(s)".light_green + ": #{results['hostnames'].join(',')}".white unless results['hostnames'].empty?
- f.puts "Hostname(s): #{results['hostnames'].join(',')}" unless results['hostnames'].empty?
- puts "Host OS".light_green + ": #{results['os']}".white unless results['os'].nil?
- f.puts "Host OS: #{results['os']}" unless results['os'].nil?
- puts "Country".light_green + ": #{results['country_name']}".white unless results['country_name'].nil?
- f.puts "Country: #{results['country_name']}" unless results['country_name'].nil?
- puts "City".light_green + ": #{results['city']}".white unless results['city'].nil?
- f.puts "City: #{results['city']}" unless results['city'].nil?
- puts "Longitude".light_green + ": #{results['longitude']}".white unless results['longitude'].nil? or results['longitude'].nil?
- f.puts "Longitude: #{results['longitude']}" unless results['longitude'].nil? or results['longitude'].nil?
- puts "Latitude".light_green + ": #{results['latitude']}".white unless results['longitude'].nil? or results['longitude'].nil?
- f.puts "Latitude: #{results['latitude']}" unless results['longitude'].nil? or results['longitude'].nil?
- f.puts
- puts
- # We need to split and re-pair up the ports & banners as ports comes after banners in results iteration
- ban=nil
- port_banners={}
- results['data'][0].each do |k, v|
- if k == 'port'
- port=v
- if not ban.nil?
- port_banners.store(port, ban) # store them in hash so we pair them up properly
- ban=nil
- end
- elsif k == 'banner'
- ban=v
- end
- end
- # Now we can display them in proper pairs
- port_banners.each do |port, ban|
- puts "Port".light_green + ": #{port}".white
- f.puts "Port: #{port}"
- puts "Banner".light_green + ": \n#{ban}".white
- f.puts "Banner: \n#{ban}"
- end
- f.puts
- f.close
- else
- puts "No results found for host".light_red + "!".white
- end
- puts
- when 3
- # Perform Quick Shodan Search
- string = options[:search].to_s
- ips = shodan.quick_search(string)
- if not ips.nil?
- puts "Shodan Search".light_green + ": #{string}".white
- puts "Total Results".light_green + ": #{ips.size}".white
- puts "IP Addresses Returned".light_green + ": ".white
- f=File.open(RESULTS + 'quick_search-ips.lst', 'w+')
- ips.each {|x| puts " #{x}".white; f.puts x }
- f.close
- else
- puts "No Results Found for ".light_red + "#{string}".white + " via Shodan Quick Search".light_red + "!".white
- end
- puts
- when 4
- # Search for Exploits
- string = options[:search].to_s
- source = options[:source].to_s
- results = shodan.sploit_search(string, source)
- if not results.nil?
- f=File.open(RESULTS + "shodan_#{source}_search_results.txt", 'w+')
- puts "Shodan Exploit Search".light_green + ": #{string}".white
- f.puts "Shodan Exploit Search: #{string}"
- results.each do |id, stuff|
- puts "ID".light_green + ": #{id}".white unless id.nil?
- f.puts "ID: #{id}" unless id.nil?
- stuff.each do |link, desc|
- puts "View".light_green + ": #{link.sub('http://www.metasploit.com/', 'http://www.rapid7.com/db/')}".white unless link.nil?
- f.puts "View: #{link.sub('http://www.metasploit.com/', 'http://www.rapid7.com/db/')}" unless link.nil?
- if not link.nil? and source.downcase == 'metasploit'
- puts "Github Link".light_green + ": https://raw.github.com/rapid7/metasploit-framework/master/#{link.sub('http://www.metasploit.com/', '').sub('/exploit/', '/exploits/').sub(/\/$/, '')}.rb".white
- f.puts "Github Link: https://raw.github.com/rapid7/metasploit-framework/master/#{link.sub('http://www.metasploit.com/', '').sub('/exploit/', '/exploits/').sub(/\/$/, '')}.rb"
- end
- puts "Exploit Description".light_green + ": \n#{desc}".white unless desc.nil?
- f.puts "Exploit Description: \n#{desc}" unless desc.nil?
- f.puts
- puts
- end
- end
- f.close
- else
- puts "No Results Found for ".light_red + "#{string}".white + " via Shodan Exploit Search".light_red + "!".white
- end
- puts
- when 5
- # Now download one of the exploits you found....
- id=options[:search].to_s
- source = options[:source].to_s
- results = shodan.sploit_download(id, source)
- if not results.nil?
- downloads = RESULTS + 'downloads/'
- Dir.mkdir(downloads) unless File.exists?(downloads) and File.directory?(downloads)
- f=File.open(downloads + "#{source}-#{id}.code", 'w+')
- results.each do |k, v|
- if k == 'Exploit'
- puts "Saved to".light_green + ": #{downloads}#{source}-#{id}.code".white
- puts "#{k}".light_green + ": \n#{v}".white
- f.puts v
- else
- puts "#{k}".light_green + ": #{v}".white
- end
- end
- f.close
- else
- puts "No Download Results Found for ID".light_red + "#: #{id}".white
- end
- end
- else
- exit 666;
- end
- #EOF
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement