aquatone 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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