aquatone 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/CHANGELOG.md +18 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +168 -0
  7. data/Rakefile +10 -0
  8. data/aquatone.gemspec +29 -0
  9. data/aquatone.js +164 -0
  10. data/bin/console +14 -0
  11. data/bin/setup +8 -0
  12. data/exe/aquatone-discover +129 -0
  13. data/exe/aquatone-gather +55 -0
  14. data/exe/aquatone-scan +76 -0
  15. data/lib/aquatone.rb +43 -0
  16. data/lib/aquatone/assessment.rb +40 -0
  17. data/lib/aquatone/browser.rb +18 -0
  18. data/lib/aquatone/browser/drivers/nightmare.rb +52 -0
  19. data/lib/aquatone/collector.rb +106 -0
  20. data/lib/aquatone/collectors/dictionary.rb +20 -0
  21. data/lib/aquatone/collectors/dnsdb.rb +45 -0
  22. data/lib/aquatone/collectors/gtr.rb +58 -0
  23. data/lib/aquatone/collectors/hackertarget.rb +24 -0
  24. data/lib/aquatone/collectors/netcraft.rb +48 -0
  25. data/lib/aquatone/collectors/shodan.rb +45 -0
  26. data/lib/aquatone/collectors/threatcrowd.rb +25 -0
  27. data/lib/aquatone/collectors/virustotal.rb +24 -0
  28. data/lib/aquatone/command.rb +152 -0
  29. data/lib/aquatone/commands/discover.rb +187 -0
  30. data/lib/aquatone/commands/gather.rb +167 -0
  31. data/lib/aquatone/commands/scan.rb +108 -0
  32. data/lib/aquatone/domain.rb +33 -0
  33. data/lib/aquatone/http_client.rb +5 -0
  34. data/lib/aquatone/key_store.rb +72 -0
  35. data/lib/aquatone/port_lists.rb +36 -0
  36. data/lib/aquatone/report.rb +88 -0
  37. data/lib/aquatone/resolver.rb +47 -0
  38. data/lib/aquatone/thread_pool.rb +31 -0
  39. data/lib/aquatone/url_maker.rb +27 -0
  40. data/lib/aquatone/validation.rb +22 -0
  41. data/lib/aquatone/version.rb +3 -0
  42. data/subdomains.lst +8214 -0
  43. data/templates/default.html.erb +225 -0
  44. metadata +159 -0
@@ -0,0 +1,106 @@
1
+ module Aquatone
2
+ class Collector
3
+ class Error < StandardError; end
4
+ class InvalidMetadataError < Error; end
5
+ class MetadataNotSetError < Error; end
6
+ class MissingKeyRequirement < Error; end
7
+
8
+ attr_reader :domain, :hosts
9
+
10
+ DEFAULT_PRIORITY = 1
11
+
12
+ def self.meta
13
+ @meta || fail(MetadataNotSetError, "Metadata has not been set")
14
+ end
15
+
16
+ def self.meta=(meta)
17
+ validate_metadata(meta)
18
+ @meta = meta
19
+ end
20
+
21
+ def self.descendants
22
+ collectors = ObjectSpace.each_object(Class).select { |klass| klass < self }
23
+ collectors.sort { |x, y| x.priority <=> y.priority }
24
+ end
25
+
26
+ def self.sluggified_name
27
+ return meta[:slug].downcase if meta[:slug]
28
+ meta[:name].strip.downcase.gsub(/[^a-z0-9]+/, '-').gsub("--", "-")
29
+ end
30
+
31
+ def initialize(domain)
32
+ check_key_requirements!
33
+ @domain = domain
34
+ @hosts = []
35
+ end
36
+
37
+ def run
38
+ fail NotImplementedError
39
+ end
40
+
41
+ def execute!
42
+ run
43
+ hosts
44
+ end
45
+
46
+ def self.priority
47
+ meta[:priority] || DEFAULT_PRIORITY
48
+ end
49
+
50
+ protected
51
+
52
+ def add_host(host)
53
+ host.downcase!
54
+ return unless Aquatone::Validation.valid_domain_name?(host)
55
+ @hosts << host unless @hosts.include?(host)
56
+ end
57
+
58
+ def get_request(uri, options={})
59
+ Aquatone::HttpClient.get(uri)
60
+ end
61
+
62
+ def post_request(uri, body=nil, options={})
63
+ options = {
64
+ :body => body
65
+ }.merge(options)
66
+ Aquatone::HttpClient.post(uri, options)
67
+ end
68
+
69
+ def url_escape(string)
70
+ CGI.escape(string)
71
+ end
72
+
73
+ def random_sleep(seconds)
74
+ random_sleep = ((1 - (rand(30) * 0.01)) * seconds.to_i)
75
+ sleep(random_sleep)
76
+ end
77
+
78
+ def get_key(name)
79
+ Aquatone::KeyStore.get(name)
80
+ end
81
+
82
+ def has_key?(name)
83
+ Aquatone::KeyStore.key?(name)
84
+ end
85
+
86
+ def failure(message)
87
+ fail Error, message
88
+ end
89
+
90
+ def check_key_requirements!
91
+ return unless self.class.meta[:require_keys]
92
+ keys = self.class.meta[:require_keys]
93
+ keys.each do |key|
94
+ fail MissingKeyRequirement, "Key '#{key}' has not been set" unless has_key?(key)
95
+ end
96
+ end
97
+
98
+ def self.validate_metadata(meta)
99
+ fail InvalidMetadataError, "Metadata is not a hash" unless meta.is_a?(Hash)
100
+ fail InvalidMetadataError, "Metadata is empty" if meta.empty?
101
+ fail InvalidMetadataError, "Metadata is missing key: name" unless meta.key?(:name)
102
+ fail InvalidMetadataError, "Metadata is missing key: author" unless meta.key?(:author)
103
+ fail InvalidMetadataError, "Metadata is missing key: description" unless meta.key?(:description)
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,20 @@
1
+ module Aquatone
2
+ module Collectors
3
+ class Dictionary < Aquatone::Collector
4
+ self.meta = {
5
+ :name => "Dictionary",
6
+ :author => "Michael Henriksen (@michenriksen)",
7
+ :description => "Uses a dictionary to find hostnames"
8
+ }
9
+
10
+ DICTIONARY = File.join(Aquatone::AQUATONE_ROOT, "subdomains.lst").freeze
11
+
12
+ def run
13
+ dictionary = File.open(DICTIONARY, "r")
14
+ dictionary.each_line do |subdomain|
15
+ add_host("#{subdomain.strip}.#{domain.name}")
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,45 @@
1
+ module Aquatone
2
+ module Collectors
3
+ class Dnsdb < Aquatone::Collector
4
+ self.meta = {
5
+ :name => "DNSDB",
6
+ :author => "Michael Henriksen (@michenriksen)",
7
+ :description => "Uses dnsdb.org to find hostnames"
8
+ }
9
+
10
+ LINK_REGEX = /<a href="(.*?)\">(.*?)<\/a>/.freeze
11
+ INDEX_REGEX = /<a href="([0-9a-z]{1})">[0-9a-z]{1}<\/a>/.freeze
12
+
13
+ def run
14
+ @base_url = "http://www.dnsdb.org/f/#{url_escape(domain.name)}.dnsdb.org/"
15
+ parse_page(@base_url)
16
+ end
17
+
18
+ private
19
+
20
+ def parse_page(url)
21
+ response = get_request(url)
22
+ if response.code != 200
23
+ failure("DNSDB returned unexpected response code: #{response.code}")
24
+ end
25
+
26
+ if response.body.include?("index for")
27
+ response.body.scan(INDEX_REGEX) do |index|
28
+ response = get_request("#{@base_url}#{url_escape(index.first)}")
29
+ extract_hosts(response.body)
30
+ end
31
+ else
32
+ extract_hosts(response.body)
33
+ end
34
+ end
35
+
36
+ def extract_hosts(body)
37
+ body.scan(LINK_REGEX) do |href, hostname|
38
+ if hostname.end_with?(".#{domain.name}")
39
+ add_host(hostname)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,58 @@
1
+ module Aquatone
2
+ module Collectors
3
+ class Gtr < Aquatone::Collector
4
+ self.meta = {
5
+ :name => "Google Transparency Report",
6
+ :author => "Michael Henriksen (@michenriksen)",
7
+ :description => "Uses Google Transparency Report to find hostnames",
8
+ :slug => "gtr"
9
+ }
10
+
11
+ BASE_URI = "https://www.google.com/transparencyreport/jsonp/ct/search"
12
+ PAGES_TO_PROCESS = 30.freeze
13
+
14
+ def run
15
+ token = nil
16
+ PAGES_TO_PROCESS.times do
17
+ response = parse_response(request_page(token))
18
+ response["results"].each do |result|
19
+ host = result["subject"]
20
+ add_host(host) if valid_host?(host)
21
+ end
22
+ break if !response.key?("nextPageToken")
23
+ token = response["nextPageToken"]
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def request_page(token = nil)
30
+ if token.nil?
31
+ uri = "#{BASE_URI}?domain=#{url_escape(domain.name)}&incl_exp=true&incl_sub=true&c=_callbacks_._#{random_jsonp_callback}"
32
+ else
33
+ uri = "#{BASE_URI}?domain=#{url_escape(domain.name)}&incl_exp=true&incl_sub=true&token=#{url_escape(token)}&c=_callbacks_._#{random_jsonp_callback}"
34
+ end
35
+
36
+ get_request(uri,
37
+ { :headers => { "Referer" => "https://www.google.com/transparencyreport/https/ct/?hl=en-US" } }
38
+ )
39
+ end
40
+
41
+ def random_jsonp_callback
42
+ "abcdefghijklmnopqrstuvwxyz0123456789".split("").sample(9).join
43
+ end
44
+
45
+ def parse_response(body)
46
+ body = body.split("(", 2).last
47
+ body.gsub!(");", "")
48
+ JSON.parse(body)
49
+ end
50
+
51
+ def valid_host?(host)
52
+ return false if host.start_with?("*.")
53
+ return false unless host.end_with?(".#{domain.name}")
54
+ true
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,24 @@
1
+ module Aquatone
2
+ module Collectors
3
+ class Hackertarget < Aquatone::Collector
4
+ self.meta = {
5
+ :name => "HackerTarget",
6
+ :author => "Michael Henriksen (@michenriksen)",
7
+ :description => "Uses hackertarget.com to find hostnames"
8
+ }
9
+
10
+ API_BASE_URI = "https://api.hackertarget.com"
11
+
12
+ def run
13
+ response = get_request("#{API_BASE_URI}/hostsearch/?q=#{url_escape(domain.name)}")
14
+ if response.code != 200
15
+ failure("HackerTarget API returned unexpected response code: #{response.code}")
16
+ end
17
+ response.body.each_line do |line|
18
+ host = line.split(",", 2).first.strip
19
+ add_host(host) unless host.empty?
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,48 @@
1
+ module Aquatone
2
+ module Collectors
3
+ class Netcraft < Aquatone::Collector
4
+ self.meta = {
5
+ :name => "Netcraft",
6
+ :author => "Michael Henriksen (@michenriksen)",
7
+ :description => "Uses searchdns.netcraft.com to find hostnames"
8
+ }
9
+
10
+ BASE_URI = "http://searchdns.netcraft.com/".freeze
11
+ HOSTNAME_REGEX = /<a href="http:\/\/(.*?)\/" rel="nofollow">/.freeze
12
+ RESULTS_PER_PAGE = 20.freeze
13
+ PAGES_TO_PROCESS = 10.freeze
14
+
15
+ def run
16
+ last = nil
17
+ count = 0
18
+ PAGES_TO_PROCESS.times do |i|
19
+ page = i + 1
20
+ if page == 1
21
+ uri = "#{BASE_URI}/?restriction=site+contains&host=*.#{url_escape(domain.name)}&lookup=wait..&position=limited"
22
+ else
23
+ uri = "#{BASE_URI}/?host=*.#{url_escape(domain.name)}&last=#{url_escape(last)}&from=#{count + 1}&restriction=site%20contains&position=limited"
24
+ end
25
+ response = get_request(uri,
26
+ { :headers => { "Referer" => "http://searchdns.netcraft.com/" } }
27
+ )
28
+ hosts = extract_hostnames_from_response(response.body)
29
+ last = hosts.last
30
+ count += hosts.count
31
+ hosts.each { |host| add_host(host) }
32
+ break if hosts.count != RESULTS_PER_PAGE
33
+ random_sleep(5)
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def extract_hostnames_from_response(body)
40
+ hosts = []
41
+ body.scan(HOSTNAME_REGEX).each do |match|
42
+ hosts << match.last.to_s.strip.downcase
43
+ end
44
+ hosts
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,45 @@
1
+ module Aquatone
2
+ module Collectors
3
+ class Shodan < Aquatone::Collector
4
+ self.meta = {
5
+ :name => "Shodan",
6
+ :author => "Michael Henriksen (@michenriksen)",
7
+ :description => "Uses the Shodan API to find hostnames",
8
+ :require_keys => ["shodan"]
9
+ }
10
+
11
+ API_BASE_URI = "https://api.shodan.io/shodan".freeze
12
+ API_RESULTS_PER_PAGE = 100.freeze
13
+ PAGES_TO_PROCESS = 10.freeze
14
+
15
+ def run
16
+ request_shodan_page
17
+ end
18
+
19
+ private
20
+
21
+ def request_shodan_page(page=1)
22
+ response = get_request(construct_uri("hostname:#{domain.name}", page))
23
+ if response.code != 200
24
+ failure(response.parsed_response["error"] || "Shodan API returned unexpected response code: #{response.code}")
25
+ end
26
+ return unless response.parsed_response["matches"]
27
+ response.parsed_response["matches"].each do |match|
28
+ next unless match["hostnames"]
29
+ match["hostnames"].each do |hostname|
30
+ add_host(hostname) if hostname.end_with?(".#{domain.name}")
31
+ end
32
+ end
33
+ request_shodan_page(page + 1) if next_page?(page, response.parsed_response)
34
+ end
35
+
36
+ def construct_uri(query, page)
37
+ "#{API_BASE_URI}/host/search?query=#{url_escape(query)}&page=#{page}&key=#{get_key('shodan')}"
38
+ end
39
+
40
+ def next_page?(page, body)
41
+ page <= PAGES_TO_PROCESS && body["total"] && API_RESULTS_PER_PAGE * page < body["total"].to_i
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,25 @@
1
+ module Aquatone
2
+ module Collectors
3
+ class Threatcrowd < Aquatone::Collector
4
+ self.meta = {
5
+ :name => "Threat Crowd",
6
+ :author => "Michael Henriksen (@michenriksen)",
7
+ :description => "Uses threadcrowd.org API to find hostnames",
8
+ :slug => "threatcrowd"
9
+ }
10
+
11
+ API_URI = "https://www.threatcrowd.org/searchApi/v2/domain/report/".freeze
12
+
13
+ def run
14
+ response = get_request("#{API_URI}?domain=#{url_escape(domain.name)}")
15
+ if response.code != 200
16
+ failure("Threat Crowd API returned unexpected status code: #{response.code}")
17
+ end
18
+ body = JSON.parse(response.body)
19
+ if body.key?("subdomains")
20
+ body["subdomains"].each { |host| add_host(host) }
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,24 @@
1
+ module Aquatone
2
+ module Collectors
3
+ class Virustotal < Aquatone::Collector
4
+ self.meta = {
5
+ :name => "VirusTotal",
6
+ :author => "Michael Henriksen (@michenriksen)",
7
+ :description => "Uses virustotal.com domain search to find hostnames",
8
+ :require_keys => ["virustotal"]
9
+ }
10
+
11
+ API_URI = "http://www.virustotal.com/vtapi/v2/domain/report".freeze
12
+
13
+ def run
14
+ response = get_request("#{API_URI}?domain=#{url_escape(domain.name)}&apikey=#{get_key('virustotal')}")
15
+ if response.code != 200
16
+ failure("VirusTotal API returned unexpected status code: #{response.code}")
17
+ end
18
+ if response.parsed_response.key?("subdomains")
19
+ response.parsed_response["subdomains"].each { |host| add_host(host) }
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,152 @@
1
+ module Aquatone
2
+ class Command
3
+ attr_reader :options
4
+
5
+ def initialize(options)
6
+ @options = options
7
+ end
8
+
9
+ def self.run(options)
10
+ self.new(options).execute!
11
+ rescue Interrupt
12
+ output("Caught interrupt; exiting.\n", true)
13
+ rescue => e
14
+ output(red("An unexpected error occurred: #{e.class}: #{e.message}\n"))
15
+ output("#{e.backtrace.join("\n")}\n")
16
+ end
17
+
18
+ def execute!
19
+ fail NotImplementedError, "Commands must overwrite #execute! method"
20
+ end
21
+
22
+ protected
23
+
24
+ def output(text, clear_line = false)
25
+ self.class.output(text, clear_line)
26
+ end
27
+
28
+ def status(message)
29
+ self.class.status(message)
30
+ end
31
+
32
+ def self.output(text, clear_line = false)
33
+ if clear_line
34
+ text = "\r\e[0K#{text}"
35
+ end
36
+ with_output_mutex { print text }
37
+ end
38
+
39
+ def self.status(message)
40
+ output(message, true)
41
+ end
42
+
43
+ def self.colorize(text, color_code)
44
+ "\e[1m\e[#{color_code}m#{text}\e[0m"
45
+ end
46
+
47
+ def uncolorize(text)
48
+ self.class.uncolorize(text)
49
+ end
50
+
51
+ def red(text)
52
+ self.class.red(text)
53
+ end
54
+
55
+ def green(text)
56
+ self.class.green(text)
57
+ end
58
+
59
+ def blue(text)
60
+ self.class.blue(text)
61
+ end
62
+
63
+ def yellow(text)
64
+ self.class.yellow(text)
65
+ end
66
+
67
+ def bold(text)
68
+ self.class.bold(text)
69
+ end
70
+
71
+ def banner(subtitle)
72
+ output(" __\n")
73
+ output(" ____ _____ ___ ______ _/ /_____ ____ ___\n")
74
+ output(" / __ `/ __ `/ / / / __ `/ __/ __ \\/ __ \\/ _ \\\n")
75
+ output("/ /_/ / /_/ / /_/ / /_/ / /_/ /_/ / / / / __/\n")
76
+ output("\\__,_/\\__, /\\__,_/\\__,_/\\__/\\____/_/ /_/\\___/\n")
77
+ output(" /_/ #{subtitle.downcase} v#{Aquatone::VERSION} - by @michenriksen\n\n")
78
+ end
79
+
80
+ def self.uncolorize(text); colorize(text.to_s, 0); end
81
+ def self.red(text); colorize(text.to_s, 31); end
82
+ def self.green(text); colorize(text.to_s, 32); end
83
+ def self.blue(text); colorize(text.to_s, 34); end
84
+ def self.yellow(text); colorize(text.to_s, 33); end
85
+ def self.bold(text); colorize(text.to_s, 1); end
86
+
87
+ def thread_pool
88
+ if options[:sleep]
89
+ Aquatone::ThreadPool.new(1)
90
+ else
91
+ Aquatone::ThreadPool.new(options[:threads])
92
+ end
93
+ end
94
+
95
+ def jitter_sleep
96
+ return unless options[:sleep]
97
+ seconds = options[:sleep].to_i
98
+ if options[:jitter]
99
+ jitter = (options[:jitter].to_f / 100) * seconds
100
+ if rand.round == 0
101
+ seconds = seconds - Random.rand(0..jitter.round)
102
+ else
103
+ seconds = seconds + Random.rand(0..jitter.round)
104
+ end
105
+ seconds = 1 if seconds < 1
106
+ end
107
+ sleep seconds
108
+ end
109
+
110
+ def seconds_to_time(seconds)
111
+ Time.at(seconds).utc.strftime("%H:%M:%S")
112
+ end
113
+
114
+ def asked_for_progress?
115
+ while c = STDIN.read_nonblock(1)
116
+ return true if c == "\n"
117
+ end
118
+ false
119
+ rescue IO::EAGAINWaitReadable
120
+ false
121
+ rescue Errno::EINTR, Errno::EAGAIN, EOFError
122
+ true
123
+ end
124
+
125
+ def has_executable?(name)
126
+ exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
127
+ ENV["PATH"].split(File::PATH_SEPARATOR).each do |path|
128
+ exts.each do |ext|
129
+ exe = File.join(path, "#{name}#{ext}")
130
+ return true if File.executable?(exe) && !File.directory?(exe)
131
+ end
132
+ end
133
+ false
134
+ end
135
+
136
+ def self.jitter
137
+ seconds = @options[:jitter]
138
+ if seconds != 0
139
+ random_sleep = ((1 - (rand(30) * 0.01)) * seconds)
140
+ sleep(random_sleep)
141
+ end
142
+ end
143
+
144
+ def self.with_output_mutex
145
+ output_mutex.synchronize { yield }
146
+ end
147
+
148
+ def self.output_mutex
149
+ @output_mutex ||= Mutex.new
150
+ end
151
+ end
152
+ end