#!/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