aquatone 0.1.1 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 78769d828a09c8c64d631762851816956390be05
4
- data.tar.gz: fed735f12ef3d389c87033f55b7ccf2570afc985
3
+ metadata.gz: 1572ba4c07838fd7294dfc001c8ff23b2f7c19fc
4
+ data.tar.gz: 2c7fc94f9cef66e71fc8adeed1840a3170cf6367
5
5
  SHA512:
6
- metadata.gz: d1a29f270be14944375aa6b9d0231a81a37484d8b4ad5a9561c354cc4e7e40fe0385e1998dd4fee7cc6985741d04d867520d8be03f0e8c32ce8c5bba79e50980
7
- data.tar.gz: 2738fb5ced2b632399b26d42ed25d9c2e7cbbc84d2ee398cf1c111264fda511e4d696c725e64e21b2dbc8256058afc9f7486f05fa5499f8cca8bbf9362f0d047
6
+ metadata.gz: d5086248f07db25eeead317a570ee56cf3d622eb72e471d85c1c33611f3cfc4bba0be721a9f65658e6e078b264bbbb2327157574070e235d1542c4a85ba4ec6e
7
+ data.tar.gz: f81b9ab7409889e524c59fda6e53e7ce4e891a515f7492885f5fcd5e558ae8bdb187c2e6295ad5ee40324f51b12994dfbc39759a272a03fbcbd08b75e5f90fc6
@@ -9,6 +9,22 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
9
9
 
10
10
  ### Changed
11
11
 
12
+
13
+ ## [0.2.0]
14
+ ### Added
15
+ - New Collector: riddler.io (Thanks, [@jolle](https://github.com/jolle)!)
16
+ - New Collector: crt.sh (Thanks, [@jolle](https://github.com/jolle)!)
17
+ - New Collector: censys.io (Thanks, [@vortexau](https://github.com/vortexau)!)
18
+ - New Collector: passivetotal.org
19
+
20
+ ### Changed
21
+ - Capture potential `NameError` exception in `asked_for_progress?` method,
22
+ related to known JRuby bug (issue #4)
23
+ - Capture potential `Errno::EBADF` exception in `asked_for_progress?` method (issue #15)
24
+ - Improve handling of error when aquatone-gather is run on a system without a graphical desktop session (X11)
25
+ - Exclude hosts resolving to broadcast addresses in aquatone-discover (issue #11)
26
+
27
+
12
28
  ## [0.1.1]
13
29
  ### Added
14
30
 
@@ -22,5 +38,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
22
38
 
23
39
  ### Changed
24
40
 
25
- [Unreleased]: https://github.com/michenriksen/aquatone/compare/v0.1.0...HEAD
26
- [0.1.1]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.1.0...v0.1.1
41
+ [Unreleased]: https://github.com/michenriksen/aquatone/compare/v0.2.0...HEAD
42
+ [0.2.0]: https://github.com/michenriksen/aquatone/compare/v0.1.1...v0.2.0
43
+ [0.1.1]: https://github.com/michenriksen/aquatone/compare/v0.1.0...v0.1.1
data/README.md CHANGED
@@ -19,6 +19,9 @@ Finally, the tool itself can be installed with the following command in a termin
19
19
 
20
20
  $ gem install aquatone
21
21
 
22
+ **IMPORTANT:** AQUATONE's screenshotting capabilities depend on being run on a system with a graphical desktop environment. It is strongly recommended to install and run AQUATONE in a [Kali linux](https://www.kali.org/) virtual machine.
23
+ **I will not provide support or bug fixing for other systems than Kali Linux.**
24
+
22
25
  ## Usage
23
26
 
24
27
  ### Discovery
@@ -1,6 +1,9 @@
1
1
  module Aquatone
2
2
  class Browser
3
3
  module Drivers
4
+
5
+ class IncompatabilityError < StandardError; end
6
+
4
7
  class Nightmare
5
8
  attr_reader :url, :vhost, :html_destination, :headers_destination, :screenshot_destination, :options
6
9
 
@@ -23,6 +26,8 @@ module Aquatone
23
26
  wout.close
24
27
  command_output = rout.readlines.join("\n").strip
25
28
  JSON.parse(command_output)
29
+ rescue JSON::ParserError
30
+ fail IncompatabilityError, "Nightmarejs must be run on a system with a graphical desktop session (X11)"
26
31
  rescue => e
27
32
  process.stop if process
28
33
  return {
@@ -56,7 +56,7 @@ module Aquatone
56
56
  end
57
57
 
58
58
  def get_request(uri, options={})
59
- Aquatone::HttpClient.get(uri)
59
+ Aquatone::HttpClient.get(uri, options)
60
60
  end
61
61
 
62
62
  def post_request(uri, body=nil, options={})
@@ -0,0 +1,83 @@
1
+ module Aquatone
2
+ module Collectors
3
+ class Censys < Aquatone::Collector
4
+ self.meta = {
5
+ :name => "Censys",
6
+ :author => "James McLean (@vortexau)",
7
+ :description => "Uses the Censys API to find hostnames in TLS certificates",
8
+ :require_keys => ["censys_secret","censys_id"],
9
+ }
10
+
11
+ API_BASE_URI = "https://www.censys.io/api/v1".freeze
12
+ API_RESULTS_PER_PAGE = 100.freeze
13
+ PAGES_TO_PROCESS = 10.freeze
14
+
15
+ def run
16
+ request_censys_page
17
+ end
18
+
19
+ def request_censys_page(page=1)
20
+ # Initial version only supporting Censys Certificates API
21
+
22
+ # Censys expects Basic Auth for requests.
23
+ auth = {
24
+ :username => get_key('censys_id'),
25
+ :password => get_key('censys_secret')
26
+ }
27
+
28
+ # Define this is JSON content
29
+ headers = {
30
+ 'Content-Type' => 'application/json',
31
+ 'Accept' => 'application/json'
32
+ }
33
+
34
+ # The post body itself, as JSON
35
+ query = {
36
+ 'query' => url_escape("#{domain.name}"),
37
+ 'page' => page,
38
+ 'fields' => [ "parsed.names", "parsed.extensions.subject_alt_name.dns_names" ],
39
+ 'flatten' => true
40
+ }
41
+
42
+ # Search API documented at https://censys.io/api/v1/docs/search
43
+ response = post_request(
44
+ "#{API_BASE_URI}/search/certificates",
45
+ query.to_json,
46
+ {
47
+ :basic_auth => auth,
48
+ :headers => headers
49
+ }
50
+ )
51
+
52
+ if response.code != 200
53
+ failure(response.parsed_response["error"] || "Censys API encountered error: #{response.code}")
54
+ end
55
+
56
+ # If nothing returned from Censys, return:
57
+ return unless response.parsed_response["results"]
58
+
59
+ response.parsed_response["results"].each do |result|
60
+
61
+ next unless result["parsed.extensions.subject_alt_name.dns_names"]
62
+ result["parsed.extensions.subject_alt_name.dns_names"].each do |altdns|
63
+ add_host(altdns) if altdns.end_with?(".#{domain.name}")
64
+ end
65
+
66
+ next unless result["parsed.names"]
67
+ result["parsed.names"].each do |parsedname|
68
+ add_host(parsedname) if parsedname.end_with?(".#{domain.name}")
69
+ end
70
+ end
71
+
72
+ # Get the next page of results
73
+ request_censys_page(page + 1) if next_page?(page, response.parsed_response)
74
+
75
+ end
76
+
77
+ def next_page?(page, body)
78
+ page <= PAGES_TO_PROCESS && body["metadata"]["pages"] && API_RESULTS_PER_PAGE * page < body["metadata"]["count"].to_i
79
+ end
80
+
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,19 @@
1
+ module Aquatone
2
+ module Collectors
3
+ class Crtsh < Aquatone::Collector
4
+ self.meta = {
5
+ :name => "Certificate Search",
6
+ :author => "Joel (@jolle)",
7
+ :description => "Uses crt.sh by COMODO CA to find hostnames"
8
+ }
9
+
10
+ def run
11
+ response = get_request("https://crt.sh/?dNSName=%25.#{url_escape(domain.name)}")
12
+
13
+ response.body.to_enum(:scan, /<TD>([a-zA-Z0-9\*_.-]+\.#{Regexp.escape(domain.name)})<\/TD>/).map do |column|
14
+ add_host(column[0].gsub("*.", ""))
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,31 @@
1
+ module Aquatone
2
+ module Collectors
3
+ class Passivetotal < Aquatone::Collector
4
+ self.meta = {
5
+ :name => "PassiveTotal",
6
+ :author => "Michael Henriksen (@michenriksen)",
7
+ :description => "Uses RiskIQ's PassiveTotal API to find hostnames",
8
+ :require_keys => ["passivetotal_key", "passivetotal_secret"]
9
+ }
10
+
11
+ API_BASE_URI = "https://api.passivetotal.org".freeze
12
+
13
+ def run
14
+ response = get_request(
15
+ "#{API_BASE_URI}/v2/enrichment/subdomains?query=.#{url_escape(domain.name)}",
16
+ :basic_auth => {:username => get_key("passivetotal_key"), :password => get_key("passivetotal_secret")}
17
+ )
18
+ body = response.parsed_response
19
+ if response.code != 200
20
+ failure(failure(body["message"] || "PassiveTotal API returned unexpected response code: #{response.code}"))
21
+ end
22
+ if !(body.key?("success") && body["success"])
23
+ failure("Request failed")
24
+ end
25
+ body["subdomains"].each do |subdomain|
26
+ add_host("#{subdomain}.#{domain.name}")
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,43 @@
1
+ module Aquatone
2
+ module Collectors
3
+ class Riddler < Aquatone::Collector
4
+ self.meta = {
5
+ :name => "Riddler",
6
+ :author => "Joel (@jolle)",
7
+ :description => "Uses Riddler by F-Secure to find hostnames",
8
+ :require_keys => ["riddler_username", "riddler_password"]
9
+ }
10
+
11
+ API_BASE_URI = "https://riddler.io"
12
+
13
+ def run
14
+ auth_response = post_request("#{API_BASE_URI}/auth/login", {
15
+ :email => get_key("riddler_username"),
16
+ :password => get_key("riddler_password")
17
+ }.to_json, {
18
+ :headers => { "Content-Type" => "application/json" }
19
+ })
20
+
21
+ if auth_response.code == 400 or auth_response.parsed_response["meta"]["code"] == 400
22
+ failure("Invalid credentials to Riddler.io")
23
+ elsif auth_response.code != 200 or auth_response.parsed_response["meta"]["code"] != 200
24
+ failure("Riddler.io auth API returned unexpected response code: #{response.code}")
25
+ end
26
+
27
+ token = auth_response.parsed_response["response"]["user"]["authentication_token"]
28
+
29
+ response = post_request("#{API_BASE_URI}/api/search", {
30
+ :query => "pld:#{url_escape(domain.name)}",
31
+ :output => "host",
32
+ :limit => 500
33
+ }.to_json, {
34
+ :headers => { "Content-Type" => "application/json", "Authentication-Token" => token }
35
+ })
36
+
37
+ response.parsed_response.each do |record|
38
+ add_host(record["host"]) unless record["host"].empty?
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -112,14 +112,18 @@ module Aquatone
112
112
  end
113
113
 
114
114
  def asked_for_progress?
115
- while c = STDIN.read_nonblock(1)
116
- return true if c == "\n"
115
+ begin
116
+ while c = STDIN.read_nonblock(1)
117
+ return true if c == "\n"
118
+ end
119
+ false
120
+ rescue IO::EAGAINWaitReadable, Errno::EBADF
121
+ false
122
+ rescue Errno::EINTR, Errno::EAGAIN, EOFError
123
+ true
117
124
  end
125
+ rescue NameError
118
126
  false
119
- rescue IO::EAGAINWaitReadable
120
- false
121
- rescue Errno::EINTR, Errno::EAGAIN, EOFError
122
- true
123
127
  end
124
128
 
125
129
  def has_executable?(name)
@@ -103,8 +103,7 @@ module Aquatone
103
103
  "#{(task_count.to_f / @hosts.count.to_f * 100.00).round(1)}% done\n")
104
104
  end
105
105
  if ip = @resolver.resolve(host)
106
- next if wildcard_ip?(ip)
107
- next if (options[:ignore_private] && private_ip?(ip))
106
+ next if exclude_ip?(ip)
108
107
  @host_dictionary[host] = ip
109
108
  output("#{ip.ljust(15)} #{bold(host)}\n")
110
109
  end
@@ -157,6 +156,10 @@ module Aquatone
157
156
  0 1 2 3 4 5 6 7 8 9).shuffle.take(10).join
158
157
  end
159
158
 
159
+ def exclude_ip?(ip)
160
+ wildcard_ip?(ip) || (options[:ignore_private] && private_ip?(ip)) || broadcast_ip?(ip)
161
+ end
162
+
160
163
  def wildcard_ip?(ip)
161
164
  @wildcard_ips.include?(ip)
162
165
  end
@@ -165,6 +168,10 @@ module Aquatone
165
168
  ip =~ /(\A127\.)|(\A10\.)|(\A172\.1[6-9]\.)|(\A172\.2[0-9]\.)|(\A172\.3[0-1]\.)|(\A192\.168\.)/
166
169
  end
167
170
 
171
+ def broadcast_ip?(ip)
172
+ ip == "255.255.255.255"
173
+ end
174
+
168
175
  def skip_collector?(collector)
169
176
  if options[:only_collectors]
170
177
  if options[:only_collectors].include?(collector.sluggified_name)
@@ -78,17 +78,23 @@ module Aquatone
78
78
  @tasks.shuffle.each do |task|
79
79
  host, port, domain = task
80
80
  pool.schedule do
81
- output_progress if asked_for_progress?
82
- visit = visit_page(host, port, domain)
83
- if visit['success']
84
- output("#{green('Processed:')} #{Aquatone::UrlMaker.make(host, port)} (#{domain}) - #{visit['status']}\n")
85
- @successful += 1
86
- else
87
- output(" #{red('Failed:')} #{Aquatone::UrlMaker.make(host, port)} (#{domain}) - #{visit['error']} #{visit['details']}\n")
88
- @failed += 1
81
+ begin
82
+ output_progress if asked_for_progress?
83
+ visit = visit_page(host, port, domain)
84
+ if visit['success']
85
+ output("#{green('Processed:')} #{Aquatone::UrlMaker.make(host, port)} (#{domain}) - #{visit['status']}\n")
86
+ @successful += 1
87
+ else
88
+ output(" #{red('Failed:')} #{Aquatone::UrlMaker.make(host, port)} (#{domain}) - #{visit['error']} #{visit['details']}\n")
89
+ @failed += 1
90
+ end
91
+ jitter_sleep
92
+ @task_count += 1
93
+ rescue Aquatone::Browser::Drivers::IncompatabilityError => e
94
+ output("\n")
95
+ output(red("Incompatability Error:") + " #{e.message}\n")
96
+ exit 1
89
97
  end
90
- jitter_sleep
91
- @task_count += 1
92
98
  end
93
99
  end
94
100
  pool.shutdown
@@ -1,3 +1,3 @@
1
1
  module Aquatone
2
- VERSION = "0.1.1".freeze
2
+ VERSION = "0.2.0".freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aquatone
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Henriksen
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-06-18 00:00:00.000000000 Z
11
+ date: 2017-06-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: httparty
@@ -108,11 +108,15 @@ files:
108
108
  - lib/aquatone/browser.rb
109
109
  - lib/aquatone/browser/drivers/nightmare.rb
110
110
  - lib/aquatone/collector.rb
111
+ - lib/aquatone/collectors/censys.rb
112
+ - lib/aquatone/collectors/crtsh.rb
111
113
  - lib/aquatone/collectors/dictionary.rb
112
114
  - lib/aquatone/collectors/dnsdb.rb
113
115
  - lib/aquatone/collectors/gtr.rb
114
116
  - lib/aquatone/collectors/hackertarget.rb
115
117
  - lib/aquatone/collectors/netcraft.rb
118
+ - lib/aquatone/collectors/passivetotal.rb
119
+ - lib/aquatone/collectors/riddler.rb
116
120
  - lib/aquatone/collectors/shodan.rb
117
121
  - lib/aquatone/collectors/threatcrowd.rb
118
122
  - lib/aquatone/collectors/virustotal.rb