aquatone 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -1
- data/README.md +37 -0
- data/exe/aquatone-discover +5 -4
- data/exe/aquatone-takeover +113 -0
- data/lib/aquatone.rb +6 -0
- data/lib/aquatone/commands/takeover.rb +147 -0
- data/lib/aquatone/detector.rb +94 -0
- data/lib/aquatone/detectors/campaign_monitor.rb +23 -0
- data/lib/aquatone/detectors/cargo.rb +25 -0
- data/lib/aquatone/detectors/cloudfront.rb +23 -0
- data/lib/aquatone/detectors/desk.rb +23 -0
- data/lib/aquatone/detectors/fastly.rb +23 -0
- data/lib/aquatone/detectors/feedpress.rb +23 -0
- data/lib/aquatone/detectors/freshdesk.rb +23 -0
- data/lib/aquatone/detectors/ghost.rb +29 -0
- data/lib/aquatone/detectors/github_pages.rb +25 -0
- data/lib/aquatone/detectors/helpjuice.rb +23 -0
- data/lib/aquatone/detectors/helpscout.rb +23 -0
- data/lib/aquatone/detectors/heroku.rb +25 -0
- data/lib/aquatone/detectors/instapage.rb +21 -0
- data/lib/aquatone/detectors/pingdom.rb +21 -0
- data/lib/aquatone/detectors/s3.rb +23 -0
- data/lib/aquatone/detectors/shopify.rb +25 -0
- data/lib/aquatone/detectors/statuspage.rb +23 -0
- data/lib/aquatone/detectors/surveygizmo.rb +25 -0
- data/lib/aquatone/detectors/teamwork.rb +23 -0
- data/lib/aquatone/detectors/tictail.rb +25 -0
- data/lib/aquatone/detectors/tumblr.rb +25 -0
- data/lib/aquatone/detectors/unbounce.rb +25 -0
- data/lib/aquatone/detectors/uservoice.rb +29 -0
- data/lib/aquatone/detectors/wpengine.rb +25 -0
- data/lib/aquatone/detectors/zendesk.rb +23 -0
- data/lib/aquatone/port_lists.rb +1 -1
- data/lib/aquatone/resolver.rb +34 -0
- data/lib/aquatone/version.rb +1 -1
- metadata +31 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 89f0cbe4f00ec77bae2d323be2978261680a75d8
|
4
|
+
data.tar.gz: 32948fb248e6614400c878ea2ac4681a5da9558b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 941e0e136a451eb295184a6b35dde232cbcae72624a4e421a5c304f89f70c281744cc85c05eafe951f210fd744ec6b3406362b554a4cab9a46b666e58c7be254
|
7
|
+
data.tar.gz: 48ccd44ff3e7fa4cfd05bd7221e6287479f9b80b362342915789ffe673c12a9f8a25fc8ef63524286eee401f4fccddf0fb8395810ca1e352aa6b81c0445325df
|
data/CHANGELOG.md
CHANGED
@@ -10,6 +10,15 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
|
10
10
|
### Changed
|
11
11
|
|
12
12
|
|
13
|
+
## [0.3.0]
|
14
|
+
### Added
|
15
|
+
- New Tool: aquatone-takeover: Check discovered hosts for subdomain takeover vulnerabilities
|
16
|
+
|
17
|
+
### Changed
|
18
|
+
- Show key requirements in for collectors when issuing `aquatone-discover --list-collectors`
|
19
|
+
- Add alias `xlarge` to `huge` port list.
|
20
|
+
|
21
|
+
|
13
22
|
## [0.2.0]
|
14
23
|
### Added
|
15
24
|
- New Collector: riddler.io (Thanks, [@jolle](https://github.com/jolle)!)
|
@@ -38,6 +47,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
|
38
47
|
|
39
48
|
### Changed
|
40
49
|
|
41
|
-
[Unreleased]: https://github.com/michenriksen/aquatone/compare/v0.
|
50
|
+
[Unreleased]: https://github.com/michenriksen/aquatone/compare/v0.3.0...HEAD
|
51
|
+
[0.3.0]: https://github.com/michenriksen/aquatone/compare/v0.2.0...v0.3.0
|
42
52
|
[0.2.0]: https://github.com/michenriksen/aquatone/compare/v0.1.1...v0.2.0
|
43
53
|
[0.1.1]: https://github.com/michenriksen/aquatone/compare/v0.1.0...v0.1.1
|
data/README.md
CHANGED
@@ -152,6 +152,43 @@ When aquatone-gather is finished, it will have created several directories in th
|
|
152
152
|
* `screenshots/`: Contains PNG images of how each web page looks like in a browser
|
153
153
|
* `report/` Contains report files in HTML displaying the gathered information for easy analysis
|
154
154
|
|
155
|
+
### Subdomain Takeover
|
156
|
+
|
157
|
+
Subdomain takeover is a very prevalent and potentially critical security issue which commonly occurs when an organization assigns a subdomain to a third-party service provider and then later discontinues use, but forgets to remove the DNS configuration. This leaves the subdomain vulnerable to complete takover by attackers by signing up to the same service provider and claiming the dangling subdomain.
|
158
|
+
|
159
|
+
aquatone-takeover can be used to check hosts uncovered by aquatone-discover for potential domain takeover vulnerabilities:
|
160
|
+
|
161
|
+
$ aquatone-takeover --domain example.com
|
162
|
+
|
163
|
+
aquatone-takeover can detect potential subdomain takeover situations from 25 different service providers, including GitHub Pages, Heroku, Amazon S3, Desk and WPEngine.
|
164
|
+
|
165
|
+
#### Results
|
166
|
+
|
167
|
+
aquatone-takeover will create a `takeovers.json` file in the domain's assessment directory which will contain information in JSON format about any potential subdomain takeover vulnerabilities:
|
168
|
+
|
169
|
+
```
|
170
|
+
{
|
171
|
+
"shop.example.com": {
|
172
|
+
"service": "Shopify",
|
173
|
+
"service_website": "https://www.shopify.com/",
|
174
|
+
"description": "Ecommerce platform",
|
175
|
+
"resource": {
|
176
|
+
"type": "CNAME",
|
177
|
+
"value": "shops.myshopify.com"
|
178
|
+
}
|
179
|
+
},
|
180
|
+
"help.example.com": {
|
181
|
+
"service": "Desk",
|
182
|
+
"service_website": "https://www.desk.com/",
|
183
|
+
"description": "Customer service and helpdesk ticket software",
|
184
|
+
"resource": {
|
185
|
+
"type": "CNAME",
|
186
|
+
"value": "example.desk.com"
|
187
|
+
}
|
188
|
+
},
|
189
|
+
...
|
190
|
+
}
|
191
|
+
```
|
155
192
|
|
156
193
|
## Contributing
|
157
194
|
|
data/exe/aquatone-discover
CHANGED
@@ -67,10 +67,11 @@ OptionParser.new do |opts|
|
|
67
67
|
|
68
68
|
opts.on("--list-collectors", "See information on collectors") do
|
69
69
|
Aquatone::Collector.descendants.each do |collector|
|
70
|
-
puts "Name
|
71
|
-
puts "Description
|
72
|
-
puts "Author
|
73
|
-
puts "Key
|
70
|
+
puts "Name............: #{collector.meta[:name]}"
|
71
|
+
puts "Description.....: #{collector.meta[:description]}" if collector.meta[:description]
|
72
|
+
puts "Author..........: #{collector.meta[:author]}"
|
73
|
+
puts "Key Requirements: #{collector.meta.key?(:require_keys) ? collector.meta[:require_keys].join(', ') : 'None'}"
|
74
|
+
puts "Key.............: #{collector.sluggified_name}\n\n"
|
74
75
|
puts "--------------------------------------------------\n\n"
|
75
76
|
end
|
76
77
|
exit
|
@@ -0,0 +1,113 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "aquatone"
|
4
|
+
|
5
|
+
options = {
|
6
|
+
:fallback_nameservers => %w(8.8.8.8 8.8.4.4),
|
7
|
+
:threads => 5,
|
8
|
+
}
|
9
|
+
|
10
|
+
OptionParser.new do |opts|
|
11
|
+
opts.banner = "Usage: aquatone-takeover OPTIONS"
|
12
|
+
|
13
|
+
opts.on("-d", "--domain DOMAIN", "Domain name to assess") do |v|
|
14
|
+
if !Aquatone::Validation.valid_domain_name?(v)
|
15
|
+
puts "#{v} doesn't look like a valid domain name."
|
16
|
+
exit 1
|
17
|
+
end
|
18
|
+
options[:domain] = v
|
19
|
+
end
|
20
|
+
|
21
|
+
opts.on("--nameservers NAMESERVERS", "Nameservers to use") do |v|
|
22
|
+
ips = v.split(",").map(&:strip).uniq
|
23
|
+
if ips.empty?
|
24
|
+
puts "Nameservers can't be empty."
|
25
|
+
exit 1
|
26
|
+
end
|
27
|
+
ips.each do |ip|
|
28
|
+
if !Aquatone::Validation.valid_ip?(ip)
|
29
|
+
puts "#{ip} is not a valid IP address."
|
30
|
+
exit 1
|
31
|
+
end
|
32
|
+
end
|
33
|
+
options[:nameservers] = ips
|
34
|
+
end
|
35
|
+
|
36
|
+
opts.on("--fallback-nameservers NAMESERVERS", "Nameservers to fall back to") do |v|
|
37
|
+
ips = v.split(",").map(&:strip).uniq
|
38
|
+
if ips.empty?
|
39
|
+
puts "Fallback nameservers can't be empty."
|
40
|
+
exit 1
|
41
|
+
end
|
42
|
+
ips.each do |ip|
|
43
|
+
if !Aquatone::Validation.valid_ip?(ip)
|
44
|
+
puts "#{ip} is not a valid IP address."
|
45
|
+
exit 1
|
46
|
+
end
|
47
|
+
end
|
48
|
+
options[:fallback_nameservers] = ips
|
49
|
+
end
|
50
|
+
|
51
|
+
opts.on("--list-detectors", "See information on subdomain takeover detectors") do
|
52
|
+
Aquatone::Detector.descendants.each do |detector|
|
53
|
+
puts "Service........: #{detector.meta[:service]}"
|
54
|
+
puts "Service Website: #{detector.meta[:service_website]}"
|
55
|
+
puts "Description....: #{detector.meta[:description]}" if detector.meta[:description]
|
56
|
+
puts "Author.........: #{detector.meta[:author]}"
|
57
|
+
puts "Key............: #{detector.sluggified_name}\n\n"
|
58
|
+
puts "--------------------------------------------------\n\n"
|
59
|
+
end
|
60
|
+
exit
|
61
|
+
end
|
62
|
+
|
63
|
+
opts.on("--only-detectors DETECTORS", "Only run specified takeover detectors") do |v|
|
64
|
+
known_detectors = Aquatone::Detector.descendants.map(&:sluggified_name)
|
65
|
+
detectors = v.split(",").map(&:strip).uniq
|
66
|
+
detectors.each do |detector|
|
67
|
+
if !known_detectors.include?(detector)
|
68
|
+
puts "Unknown takeover detector key: #{detector}"
|
69
|
+
exit 1
|
70
|
+
end
|
71
|
+
end
|
72
|
+
options[:only_detectors] = detectors
|
73
|
+
end
|
74
|
+
|
75
|
+
opts.on("--disable-detectors DETECTORS", "Disable specified takeover detectors") do |v|
|
76
|
+
known_detectors = Aquatone::Detector.descendants.map(&:sluggified_name)
|
77
|
+
detectors = v.split(",").map(&:strip).uniq
|
78
|
+
detectors.each do |detector|
|
79
|
+
if !known_detectors.include?(detector)
|
80
|
+
puts "Unknown detector key: #{detector}"
|
81
|
+
exit 1
|
82
|
+
end
|
83
|
+
end
|
84
|
+
options[:disable_detectors] = detectors
|
85
|
+
end
|
86
|
+
|
87
|
+
opts.on("-t", "--threads THREADS", "Number of concurrent threads to use") do |v|
|
88
|
+
options[:threads] = v.to_i
|
89
|
+
end
|
90
|
+
|
91
|
+
opts.on("-s", "--sleep SECONDS", "Seconds to sleep between checks") do |v|
|
92
|
+
if v.to_i < 1
|
93
|
+
puts "Sleep can't be less than 1"
|
94
|
+
exit 1
|
95
|
+
end
|
96
|
+
options[:sleep] = v.to_i
|
97
|
+
end
|
98
|
+
|
99
|
+
opts.on("-j", "--jitter PERCENTAGE", "Jitter factor for sleep intervals") do |v|
|
100
|
+
if !v.to_i.between?(1, 100)
|
101
|
+
puts "Jitter factor must be between 1 and 100"
|
102
|
+
exit 1
|
103
|
+
end
|
104
|
+
options[:jitter] = v.to_f
|
105
|
+
end
|
106
|
+
|
107
|
+
opts.on("-h", "--help", "Show help") do
|
108
|
+
puts opts
|
109
|
+
exit 0
|
110
|
+
end
|
111
|
+
end.parse!
|
112
|
+
|
113
|
+
Aquatone::Commands::Takeover.run(options)
|
data/lib/aquatone.rb
CHANGED
@@ -22,6 +22,7 @@ require "aquatone/assessment"
|
|
22
22
|
require "aquatone/report"
|
23
23
|
require "aquatone/command"
|
24
24
|
require "aquatone/collector"
|
25
|
+
require "aquatone/detector"
|
25
26
|
|
26
27
|
module Aquatone
|
27
28
|
AQUATONE_ROOT = File.expand_path(File.join(File.dirname(__FILE__), "..")).freeze
|
@@ -38,6 +39,11 @@ Dir[File.join(File.dirname(__FILE__), "aquatone", "collectors", "*.rb")].each do
|
|
38
39
|
require collector
|
39
40
|
end
|
40
41
|
|
42
|
+
Dir[File.join(File.dirname(__FILE__), "aquatone", "detectors", "*.rb")].each do |detector|
|
43
|
+
require detector
|
44
|
+
end
|
45
|
+
|
41
46
|
require "aquatone/commands/discover"
|
42
47
|
require "aquatone/commands/scan"
|
43
48
|
require "aquatone/commands/gather"
|
49
|
+
require "aquatone/commands/takeover"
|
@@ -0,0 +1,147 @@
|
|
1
|
+
module Aquatone
|
2
|
+
module Commands
|
3
|
+
class Takeover < Aquatone::Command
|
4
|
+
def execute!
|
5
|
+
if !options[:domain]
|
6
|
+
output("Please specify a domain to assess\n")
|
7
|
+
exit 1
|
8
|
+
end
|
9
|
+
|
10
|
+
@domain = Aquatone::Domain.new(options[:domain])
|
11
|
+
@assessment = Aquatone::Assessment.new(options[:domain])
|
12
|
+
|
13
|
+
banner("Takeover")
|
14
|
+
prepare_host_dictionary
|
15
|
+
prepare_detectors
|
16
|
+
setup_resolver
|
17
|
+
check_hosts
|
18
|
+
write_to_takeovers_file
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def prepare_host_dictionary
|
24
|
+
if !@assessment.has_file?("hosts.json")
|
25
|
+
output(red("#{@assessment.path} does not contain hosts.json file\n\n"))
|
26
|
+
output("Did you run aquatone-discover first?\n")
|
27
|
+
exit 1
|
28
|
+
end
|
29
|
+
hosts = JSON.parse(@assessment.read_file("hosts.json"))
|
30
|
+
@hosts = hosts.keys
|
31
|
+
output("Loaded #{bold(@hosts.count)} hosts from #{bold(File.join(@assessment.path, 'hosts.json'))}\n")
|
32
|
+
end
|
33
|
+
|
34
|
+
def prepare_detectors
|
35
|
+
@detectors = Aquatone::Detector.descendants
|
36
|
+
output("Loaded #{bold(@detectors.count)} domain takeover detectors\n\n")
|
37
|
+
end
|
38
|
+
|
39
|
+
def setup_resolver
|
40
|
+
if options[:nameservers]
|
41
|
+
nameservers = options[:nameservers]
|
42
|
+
else
|
43
|
+
output("Identifying nameservers for #{@domain.name}... ")
|
44
|
+
nameservers = @domain.nameservers
|
45
|
+
output("Done\n")
|
46
|
+
if nameservers.empty?
|
47
|
+
output(yellow("#{@domain.name} has no nameservers. Using fallback nameservers.\n\n"))
|
48
|
+
nameservers = []
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
if !nameservers.empty?
|
53
|
+
output("Using nameservers:\n\n")
|
54
|
+
nameservers.each do |ns|
|
55
|
+
output(" - #{ns}\n")
|
56
|
+
end
|
57
|
+
output("\n")
|
58
|
+
end
|
59
|
+
@resolver = Aquatone::Resolver.new(
|
60
|
+
:nameservers => nameservers,
|
61
|
+
:fallback_nameservers => options[:fallback_nameservers]
|
62
|
+
)
|
63
|
+
end
|
64
|
+
|
65
|
+
def check_hosts
|
66
|
+
pool = thread_pool
|
67
|
+
@task_count = 1
|
68
|
+
@takeovers = {}
|
69
|
+
@takeovers_detected = 0
|
70
|
+
@start_time = Time.now.to_i
|
71
|
+
output("Checking hosts for domain takeover vulnerabilities...\n\n")
|
72
|
+
@hosts.each do |host|
|
73
|
+
resource = @resolver.resource(host)
|
74
|
+
next unless valid_resource?(resource)
|
75
|
+
pool.schedule do
|
76
|
+
output_progress if asked_for_progress?
|
77
|
+
@task_count += 1
|
78
|
+
@detectors.each do |detector|
|
79
|
+
next if skip_detector?(detector)
|
80
|
+
detector_instance = detector.new(host, resource)
|
81
|
+
if detector_instance.positive?
|
82
|
+
resource_type = resource.class.to_s.split("::").last
|
83
|
+
resource_value = resource.is_a?(Resolv::DNS::Resource::IN::CNAME) ? resource.name.to_s : resource.address.to_s
|
84
|
+
output(red("Potential domain takeover detected!\n"))
|
85
|
+
output("#{bold('Host...........:')} #{host}\n")
|
86
|
+
output("#{bold('Service........:')} #{detector.meta[:service]}\n")
|
87
|
+
output("#{bold('Service website:')} #{detector.meta[:service_website]}\n")
|
88
|
+
output("#{bold('Resource.......:')} #{resource_type} #{resource_value}\n")
|
89
|
+
output("\n")
|
90
|
+
@takeovers[host] = {
|
91
|
+
"service" => detector.meta[:service],
|
92
|
+
"service_website" => detector.meta[:service_website],
|
93
|
+
"description" => detector.meta[:description],
|
94
|
+
"resource" => {
|
95
|
+
"type" => resource_type,
|
96
|
+
"value" => resource_value
|
97
|
+
}
|
98
|
+
}
|
99
|
+
@takeovers_detected += 1
|
100
|
+
break
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
jitter_sleep
|
105
|
+
end
|
106
|
+
pool.shutdown
|
107
|
+
output("Finished checking hosts:\n\n")
|
108
|
+
output(" - Vulnerable : #{bold(red(@takeovers_detected))}\n")
|
109
|
+
output(" - Not Vulnerable : #{bold(green(@hosts.count - @takeovers_detected))}\n\n")
|
110
|
+
end
|
111
|
+
|
112
|
+
def write_to_takeovers_file
|
113
|
+
@assessment.write_file("takeovers.json", @takeovers.to_json)
|
114
|
+
output("Wrote #{bold(@takeovers.keys.count)} potential subdomain takeovers to:\n\n")
|
115
|
+
output(" - #{bold('file://' + File.join(@assessment.path, 'takeovers.json'))}\n\n")
|
116
|
+
end
|
117
|
+
|
118
|
+
def output_progress
|
119
|
+
output("Stats: #{seconds_to_time(Time.now.to_i - @start_time)} elapsed; " \
|
120
|
+
"#{@task_count} out of #{@hosts.count} hosts checked (#{@takeovers_detected} takeovers detected); " \
|
121
|
+
"#{(@task_count.to_f / @hosts.count.to_f * 100.00).round(1)}% done\n")
|
122
|
+
end
|
123
|
+
|
124
|
+
def valid_resource?(resource)
|
125
|
+
[Resolv::DNS::Resource::IN::CNAME, Resolv::DNS::Resource::IN::A].include?(resource.class)
|
126
|
+
end
|
127
|
+
|
128
|
+
def skip_detector?(detector)
|
129
|
+
if options[:only_detectors]
|
130
|
+
if options[:only_detectors].include?(detector.sluggified_name)
|
131
|
+
false
|
132
|
+
else
|
133
|
+
true
|
134
|
+
end
|
135
|
+
elsif options[:disable_detectors]
|
136
|
+
if options[:disable_detectors].include?(detector.sluggified_name)
|
137
|
+
true
|
138
|
+
else
|
139
|
+
false
|
140
|
+
end
|
141
|
+
else
|
142
|
+
false
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module Aquatone
|
2
|
+
class Detector
|
3
|
+
class Error < StandardError; end
|
4
|
+
class InvalidMetadataError < Error; end
|
5
|
+
class MetadataNotSetError < Error; end
|
6
|
+
|
7
|
+
attr_reader :host, :resource
|
8
|
+
|
9
|
+
def self.meta
|
10
|
+
@meta || fail(MetadataNotSetError, "Metadata has not been set")
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.meta=(meta)
|
14
|
+
validate_metadata(meta)
|
15
|
+
@meta = meta
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.descendants
|
19
|
+
detectors = ObjectSpace.each_object(Class).select { |klass| klass < self }
|
20
|
+
detectors.sort { |x, y| x.meta[:service] <=> y.meta[:service] }
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.sluggified_name
|
24
|
+
return meta[:slug].downcase if meta[:slug]
|
25
|
+
meta[:service].strip.downcase.gsub(/[^a-z0-9]+/, '-').gsub("--", "-")
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize(host, resource)
|
29
|
+
@host = host
|
30
|
+
@resource = resource
|
31
|
+
end
|
32
|
+
|
33
|
+
def run
|
34
|
+
fail NotImplementedError
|
35
|
+
end
|
36
|
+
|
37
|
+
def positive?
|
38
|
+
run
|
39
|
+
rescue
|
40
|
+
false
|
41
|
+
end
|
42
|
+
|
43
|
+
protected
|
44
|
+
|
45
|
+
def cname_resource?
|
46
|
+
resource.is_a?(Resolv::DNS::Resource::IN::CNAME)
|
47
|
+
end
|
48
|
+
|
49
|
+
def apex_resource?
|
50
|
+
resource.is_a?(Resolv::DNS::Resource::IN::A)
|
51
|
+
end
|
52
|
+
|
53
|
+
def resource_value
|
54
|
+
cname_resource? ? resource.name.to_s : resource.address.to_s
|
55
|
+
end
|
56
|
+
|
57
|
+
def get_request(uri, options={})
|
58
|
+
options = {
|
59
|
+
:timeout => 10
|
60
|
+
}.merge(options)
|
61
|
+
Aquatone::HttpClient.get(uri, options)
|
62
|
+
end
|
63
|
+
|
64
|
+
def post_request(uri, body=nil, options={})
|
65
|
+
options = {
|
66
|
+
:body => body,
|
67
|
+
:timeout => 10
|
68
|
+
}.merge(options)
|
69
|
+
Aquatone::HttpClient.post(uri, options)
|
70
|
+
end
|
71
|
+
|
72
|
+
def url_escape(string)
|
73
|
+
CGI.escape(string)
|
74
|
+
end
|
75
|
+
|
76
|
+
def random_sleep(seconds)
|
77
|
+
random_sleep = ((1 - (rand(30) * 0.01)) * seconds.to_i)
|
78
|
+
sleep(random_sleep)
|
79
|
+
end
|
80
|
+
|
81
|
+
def failure(message)
|
82
|
+
fail Error, message
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.validate_metadata(meta)
|
86
|
+
fail InvalidMetadataError, "Metadata is not a hash" unless meta.is_a?(Hash)
|
87
|
+
fail InvalidMetadataError, "Metadata is empty" if meta.empty?
|
88
|
+
fail InvalidMetadataError, "Metadata is missing key: service" unless meta.key?(:service)
|
89
|
+
fail InvalidMetadataError, "Metadata is missing key: service_website" unless meta.key?(:service_website)
|
90
|
+
fail InvalidMetadataError, "Metadata is missing key: author" unless meta.key?(:author)
|
91
|
+
fail InvalidMetadataError, "Metadata is missing key: description" unless meta.key?(:description)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Aquatone
|
2
|
+
module Detectors
|
3
|
+
class CampaignMonitor < Aquatone::Detector
|
4
|
+
self.meta = {
|
5
|
+
:service => "Campaign Monitor",
|
6
|
+
:service_website => "https://www.zendesk.com/",
|
7
|
+
:author => "Michael Henriksen (@michenriksen)",
|
8
|
+
:description => "Email marketing"
|
9
|
+
}
|
10
|
+
|
11
|
+
CNAME_VALUE = "name.createsend.com".freeze
|
12
|
+
RESPONSE_FINGERPRINT = "<strong>Trying to access your account?</strong>".freeze
|
13
|
+
|
14
|
+
def run
|
15
|
+
return false unless cname_resource?
|
16
|
+
if resource_value == CNAME_VALUE
|
17
|
+
return get_request("http://#{host}/").body.include?(RESPONSE_FINGERPRINT)
|
18
|
+
end
|
19
|
+
false
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Aquatone
|
2
|
+
module Detectors
|
3
|
+
class Cargo < Aquatone::Detector
|
4
|
+
self.meta = {
|
5
|
+
:service => "Cargo",
|
6
|
+
:service_website => "https://cargocollective.com/",
|
7
|
+
:author => "Michael Henriksen (@michenriksen)",
|
8
|
+
:description => "Web publishing platform"
|
9
|
+
}
|
10
|
+
|
11
|
+
APEX_VALUE = "173.203.204.123".freeze
|
12
|
+
CNAME_VALUE = "cargocollective.com".freeze
|
13
|
+
RESPONSE_FINGERPRINT = "Use a personal domain name".freeze
|
14
|
+
|
15
|
+
def run
|
16
|
+
if apex_resource?
|
17
|
+
return false unless resource_value == APEX_VALUE
|
18
|
+
elsif cname_resource?
|
19
|
+
return false unless resource_value == CNAME_VALUE
|
20
|
+
end
|
21
|
+
get_request("http://#{host}/").body.include?(RESPONSE_FINGERPRINT)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Aquatone
|
2
|
+
module Detectors
|
3
|
+
class Cloudfront < Aquatone::Detector
|
4
|
+
self.meta = {
|
5
|
+
:service => "Cloudfront",
|
6
|
+
:service_website => "https://aws.amazon.com/cloudfront/",
|
7
|
+
:author => "Michael Henriksen (@michenriksen)",
|
8
|
+
:description => "Content delivery network"
|
9
|
+
}
|
10
|
+
|
11
|
+
CNAME_VALUE = ".cloudfront.net".freeze
|
12
|
+
RESPONSE_FINGERPRINT = "The request could not be satisfied".freeze
|
13
|
+
|
14
|
+
def run
|
15
|
+
return false unless cname_resource?
|
16
|
+
if resource_value.end_with?(CNAME_VALUE)
|
17
|
+
return get_request("http://#{host}/").body.include?(RESPONSE_FINGERPRINT)
|
18
|
+
end
|
19
|
+
false
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Aquatone
|
2
|
+
module Detectors
|
3
|
+
class Desk < Aquatone::Detector
|
4
|
+
self.meta = {
|
5
|
+
:service => "Desk",
|
6
|
+
:service_website => "https://www.desk.com/",
|
7
|
+
:author => "Michael Henriksen (@michenriksen)",
|
8
|
+
:description => "Customer service and helpdesk ticket software"
|
9
|
+
}
|
10
|
+
|
11
|
+
CNAME_VALUE = ".desk.com".freeze
|
12
|
+
RESPONSE_FINGERPRINT = "Sorry, We Couldn't Find That Page".freeze
|
13
|
+
|
14
|
+
def run
|
15
|
+
return false unless cname_resource?
|
16
|
+
if resource_value.end_with?(CNAME_VALUE)
|
17
|
+
return get_request("http://#{host}/").body.include?(RESPONSE_FINGERPRINT)
|
18
|
+
end
|
19
|
+
false
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Aquatone
|
2
|
+
module Detectors
|
3
|
+
class Fastly < Aquatone::Detector
|
4
|
+
self.meta = {
|
5
|
+
:service => "Fastly",
|
6
|
+
:service_website => "https://www.fastly.com/",
|
7
|
+
:author => "Michael Henriksen (@michenriksen)",
|
8
|
+
:description => "Content delivery network"
|
9
|
+
}
|
10
|
+
|
11
|
+
CNAME_VALUE = ".fastly.net".freeze
|
12
|
+
RESPONSE_FINGERPRINT = "Fastly error: unknown domain".freeze
|
13
|
+
|
14
|
+
def run
|
15
|
+
return false unless cname_resource?
|
16
|
+
if resource_value.end_with?(CNAME_VALUE)
|
17
|
+
return get_request("http://#{host}/").body.include?(RESPONSE_FINGERPRINT)
|
18
|
+
end
|
19
|
+
false
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Aquatone
|
2
|
+
module Detectors
|
3
|
+
class Feedpress < Aquatone::Detector
|
4
|
+
self.meta = {
|
5
|
+
:service => "FeedPress",
|
6
|
+
:service_website => "https://feed.press/",
|
7
|
+
:author => "Michael Henriksen (@michenriksen)",
|
8
|
+
:description => "Feed analytics and Podcast hosting"
|
9
|
+
}
|
10
|
+
|
11
|
+
CNAME_VALUE = "redirect.feedpress.me".freeze
|
12
|
+
RESPONSE_FINGERPRINT = "The feed has not been found.".freeze
|
13
|
+
|
14
|
+
def run
|
15
|
+
return false unless cname_resource?
|
16
|
+
if resource_value == CNAME_VALUE
|
17
|
+
return get_request("http://#{host}/").body.include?(RESPONSE_FINGERPRINT)
|
18
|
+
end
|
19
|
+
false
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Aquatone
|
2
|
+
module Detectors
|
3
|
+
class Freshdesk < Aquatone::Detector
|
4
|
+
self.meta = {
|
5
|
+
:service => "Freshdesk",
|
6
|
+
:service_website => "https://freshdesk.com/",
|
7
|
+
:author => "Michael Henriksen (@michenriksen)",
|
8
|
+
:description => "Customer support software and ticketing system"
|
9
|
+
}
|
10
|
+
|
11
|
+
CNAME_VALUE = ".freshdesk.com".freeze
|
12
|
+
RESPONSE_FINGERPRINT = "You can claim it now at".freeze
|
13
|
+
|
14
|
+
def run
|
15
|
+
return false unless cname_resource?
|
16
|
+
if resource_value.end_with?(CNAME_VALUE)
|
17
|
+
return get_request("http://#{host}/").body.include?(RESPONSE_FINGERPRINT)
|
18
|
+
end
|
19
|
+
false
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Aquatone
|
2
|
+
module Detectors
|
3
|
+
class Ghost < Aquatone::Detector
|
4
|
+
self.meta = {
|
5
|
+
:service => "Ghost",
|
6
|
+
:service_website => "https://ghost.org/",
|
7
|
+
:author => "Michael Henriksen (@michenriksen)",
|
8
|
+
:description => "Publishing platform"
|
9
|
+
}
|
10
|
+
|
11
|
+
CNAME_VALUE = ".ghost.io".freeze
|
12
|
+
RESPONSE_FINGERPRINT = "The thing you were looking for is no longer here, or never was".freeze
|
13
|
+
|
14
|
+
def run
|
15
|
+
return false unless cname_resource?
|
16
|
+
if resource_value.end_with?(CNAME_VALUE)
|
17
|
+
response = get_request("http://#{host}/",
|
18
|
+
# Set a proper User-Agent to avoid potential CloudFlare CAPTCHA wall
|
19
|
+
:headers => {
|
20
|
+
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36"
|
21
|
+
}
|
22
|
+
)
|
23
|
+
return response.body.include?(RESPONSE_FINGERPRINT)
|
24
|
+
end
|
25
|
+
false
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Aquatone
|
2
|
+
module Detectors
|
3
|
+
class GithubPages < Aquatone::Detector
|
4
|
+
self.meta = {
|
5
|
+
:service => "GitHub Pages",
|
6
|
+
:service_website => "https://pages.github.com/",
|
7
|
+
:author => "Michael Henriksen (@michenriksen)",
|
8
|
+
:description => "GitHub static website hosting"
|
9
|
+
}
|
10
|
+
|
11
|
+
APEX_VALUES = %w(192.30.252.153 192.30.252.154).freeze
|
12
|
+
CNAME_VALUE = ".github.io".freeze
|
13
|
+
RESPONSE_FINGERPRINT = "There isn't a GitHub Pages site here.".freeze
|
14
|
+
|
15
|
+
def run
|
16
|
+
if apex_resource?
|
17
|
+
return false unless APEX_VALUES.include?(resource_value)
|
18
|
+
elsif cname_resource?
|
19
|
+
return false unless resource_value.end_with?(CNAME_VALUE)
|
20
|
+
end
|
21
|
+
get_request("http://#{host}/").body.include?(RESPONSE_FINGERPRINT)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Aquatone
|
2
|
+
module Detectors
|
3
|
+
class Helpjuice < Aquatone::Detector
|
4
|
+
self.meta = {
|
5
|
+
:service => "Helpjuice",
|
6
|
+
:service_website => "https://helpjuice.com/",
|
7
|
+
:author => "Michael Henriksen (@michenriksen)",
|
8
|
+
:description => "Knowledge base software"
|
9
|
+
}
|
10
|
+
|
11
|
+
CNAME_VALUE = ".helpjuice.com".freeze
|
12
|
+
RESPONSE_FINGERPRINT = "<title>No such app</title>".freeze
|
13
|
+
|
14
|
+
def run
|
15
|
+
return false unless cname_resource?
|
16
|
+
if resource_value.end_with?(CNAME_VALUE)
|
17
|
+
return get_request("http://#{host}/").body.include?(RESPONSE_FINGERPRINT)
|
18
|
+
end
|
19
|
+
false
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Aquatone
|
2
|
+
module Detectors
|
3
|
+
class Helpscout < Aquatone::Detector
|
4
|
+
self.meta = {
|
5
|
+
:service => "Help Scout",
|
6
|
+
:service_website => "https://www.helpscout.net/",
|
7
|
+
:author => "Michael Henriksen (@michenriksen)",
|
8
|
+
:description => "Customer service software and education platform"
|
9
|
+
}
|
10
|
+
|
11
|
+
CNAME_VALUE = ".helpscoutdocs.com".freeze
|
12
|
+
RESPONSE_FINGERPRINT = "No settings were found for this company".freeze
|
13
|
+
|
14
|
+
def run
|
15
|
+
return false unless cname_resource?
|
16
|
+
if resource_value.end_with?(CNAME_VALUE)
|
17
|
+
return get_request("http://#{host}/").body.include?(RESPONSE_FINGERPRINT)
|
18
|
+
end
|
19
|
+
false
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Aquatone
|
2
|
+
module Detectors
|
3
|
+
class Heroku < Aquatone::Detector
|
4
|
+
self.meta = {
|
5
|
+
:service => "Heroku",
|
6
|
+
:service_website => "https://www.heroku.com/",
|
7
|
+
:author => "Michael Henriksen (@michenriksen)",
|
8
|
+
:description => "Cloud application platform"
|
9
|
+
}
|
10
|
+
|
11
|
+
CNAME_VALUES = %w(.herokudns.com .herokussl.com .herokuapp.com).freeze
|
12
|
+
RESPONSE_FINGERPRINT = "<title>No such app</title>".freeze
|
13
|
+
|
14
|
+
def run
|
15
|
+
return false unless cname_resource?
|
16
|
+
CNAME_VALUES.each do |cname_value|
|
17
|
+
if resource_value.end_with?(cname_value)
|
18
|
+
return get_request("http://#{host}/").body.include?(RESPONSE_FINGERPRINT)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
false
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Aquatone
|
2
|
+
module Detectors
|
3
|
+
class Instapage < Aquatone::Detector
|
4
|
+
self.meta = {
|
5
|
+
:service => "Instapage",
|
6
|
+
:service_website => "https://instapage.com/",
|
7
|
+
:author => "Michael Henriksen (@michenriksen)",
|
8
|
+
:description => "Landing page platform"
|
9
|
+
}
|
10
|
+
|
11
|
+
CNAME_VALUES = %w(pageserve.co secure.pageserve.co).freeze
|
12
|
+
RESPONSE_FINGERPRINT = "You've Discovered A Missing Link. Our Apologies!".freeze
|
13
|
+
|
14
|
+
def run
|
15
|
+
return false unless cname_resource?
|
16
|
+
return false unless CNAME_VALUES.include?(resource_value)
|
17
|
+
get_request("http://#{host}/").body.include?(RESPONSE_FINGERPRINT)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Aquatone
|
2
|
+
module Detectors
|
3
|
+
class Pingdom < Aquatone::Detector
|
4
|
+
self.meta = {
|
5
|
+
:service => "Pingdom",
|
6
|
+
:service_website => "https://www.pingdom.com/",
|
7
|
+
:author => "Michael Henriksen (@michenriksen)",
|
8
|
+
:description => "Website and performance monitoring"
|
9
|
+
}
|
10
|
+
|
11
|
+
CNAME_VALUE = "stats.pingdom.com".freeze
|
12
|
+
RESPONSE_FINGERPRINT = "Sorry, couldn’t find the status page".freeze
|
13
|
+
|
14
|
+
def run
|
15
|
+
return false unless cname_resource?
|
16
|
+
return false unless resource_value == CNAME_VALUE
|
17
|
+
get_request("http://#{host}/").body.include?(RESPONSE_FINGERPRINT)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Aquatone
|
2
|
+
module Detectors
|
3
|
+
class S3 < Aquatone::Detector
|
4
|
+
self.meta = {
|
5
|
+
:service => "Amazon S3",
|
6
|
+
:service_website => "https://aws.amazon.com/s3/",
|
7
|
+
:author => "Michael Henriksen (@michenriksen)",
|
8
|
+
:description => "Cloud storage"
|
9
|
+
}
|
10
|
+
|
11
|
+
CNAME_VALUE = ".amazonaws.com".freeze
|
12
|
+
RESPONSE_FINGERPRINT = "NoSuchBucket".freeze
|
13
|
+
|
14
|
+
def run
|
15
|
+
return false unless cname_resource?
|
16
|
+
if resource_value.end_with?(CNAME_VALUE)
|
17
|
+
return get_request("http://#{host}/").body.include?(RESPONSE_FINGERPRINT)
|
18
|
+
end
|
19
|
+
false
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Aquatone
|
2
|
+
module Detectors
|
3
|
+
class Shopify < Aquatone::Detector
|
4
|
+
self.meta = {
|
5
|
+
:service => "Shopify",
|
6
|
+
:service_website => "https://www.shopify.com/",
|
7
|
+
:author => "Michael Henriksen (@michenriksen)",
|
8
|
+
:description => "Ecommerce platform"
|
9
|
+
}
|
10
|
+
|
11
|
+
APEX_VALUE = "23.227.38.32".freeze
|
12
|
+
CNAME_VALUE = "shops.myshopify.com".freeze
|
13
|
+
RESPONSE_FINGERPRINT = "Sorry, this shop is currently unavailable.".freeze
|
14
|
+
|
15
|
+
def run
|
16
|
+
if apex_resource?
|
17
|
+
return false unless resource_value == APEX_VALUE
|
18
|
+
elsif cname_resource?
|
19
|
+
return false unless resource_value == CNAME_VALUE
|
20
|
+
end
|
21
|
+
get_request("http://#{host}/").body.include?(RESPONSE_FINGERPRINT)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Aquatone
|
2
|
+
module Detectors
|
3
|
+
class Statuspage < Aquatone::Detector
|
4
|
+
self.meta = {
|
5
|
+
:service => "StatusPage",
|
6
|
+
:service_website => "https://www.statuspage.io/",
|
7
|
+
:author => "Michael Henriksen (@michenriksen)",
|
8
|
+
:description => "Status page hosting"
|
9
|
+
}
|
10
|
+
|
11
|
+
CNAME_VALUE = ".stspg-customer.com".freeze
|
12
|
+
RESPONSE_FINGERPRINT = "<title>Hosted Status Pages for Your Company</title>".freeze
|
13
|
+
|
14
|
+
def run
|
15
|
+
return false unless cname_resource?
|
16
|
+
if resource_value.end_with?(CNAME_VALUE)
|
17
|
+
return get_request("http://#{host}/").body.include?(RESPONSE_FINGERPRINT)
|
18
|
+
end
|
19
|
+
false
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Aquatone
|
2
|
+
module Detectors
|
3
|
+
class Surveygizmo < Aquatone::Detector
|
4
|
+
self.meta = {
|
5
|
+
:service => "SurveyGizmo",
|
6
|
+
:service_website => "https://www.surveygizmo.com/",
|
7
|
+
:author => "Michael Henriksen (@michenriksen)",
|
8
|
+
:description => "Online survey software"
|
9
|
+
}
|
10
|
+
|
11
|
+
CNAME_VALUES = %w(privatedomain.sgizmo.com privatedomain.surveygizmo.eu privatedomain.sgizmoca.com).freeze
|
12
|
+
RESPONSE_FINGERPRINT = 'data-html-name="Header Logo Link"'.freeze
|
13
|
+
|
14
|
+
def run
|
15
|
+
return false unless cname_resource?
|
16
|
+
CNAME_VALUES.each do |cname_value|
|
17
|
+
if resource_value == cname_value
|
18
|
+
return get_request("http://#{host}/").body.include?(RESPONSE_FINGERPRINT)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
false
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Aquatone
|
2
|
+
module Detectors
|
3
|
+
class Teamwork < Aquatone::Detector
|
4
|
+
self.meta = {
|
5
|
+
:service => "Teamwork",
|
6
|
+
:service_website => "https://www.teamwork.com/",
|
7
|
+
:author => "Michael Henriksen (@michenriksen)",
|
8
|
+
:description => "Project management, help desk and chat software"
|
9
|
+
}
|
10
|
+
|
11
|
+
CNAME_VALUE = ".teamwork.com".freeze
|
12
|
+
RESPONSE_FINGERPRINT = "<title>Oops - We didn't find your site.</title>".freeze
|
13
|
+
|
14
|
+
def run
|
15
|
+
return false unless cname_resource?
|
16
|
+
if resource_value.end_with?(CNAME_VALUE)
|
17
|
+
return get_request("http://#{host}/").body.include?(RESPONSE_FINGERPRINT)
|
18
|
+
end
|
19
|
+
false
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Aquatone
|
2
|
+
module Detectors
|
3
|
+
class Tictail < Aquatone::Detector
|
4
|
+
self.meta = {
|
5
|
+
:service => "Tictail",
|
6
|
+
:service_website => "https://tictail.com/",
|
7
|
+
:author => "Michael Henriksen (@michenriksen)",
|
8
|
+
:description => "Social shopping platform"
|
9
|
+
}
|
10
|
+
|
11
|
+
APEX_VALUE = "46.137.181.142".freeze
|
12
|
+
CNAME_VALUE = "domains.tictail.com".freeze
|
13
|
+
RESPONSE_FINGERPRINT = 'class="MarketplaceHeader__tictailLogo"'.freeze
|
14
|
+
|
15
|
+
def run
|
16
|
+
if apex_resource?
|
17
|
+
return false unless resource_value == APEX_VALUE
|
18
|
+
elsif cname_resource?
|
19
|
+
return false unless resource_value == CNAME_VALUE
|
20
|
+
end
|
21
|
+
get_request("http://#{host}/").body.include?(RESPONSE_FINGERPRINT)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Aquatone
|
2
|
+
module Detectors
|
3
|
+
class Tumblr < Aquatone::Detector
|
4
|
+
self.meta = {
|
5
|
+
:service => "Tumblr",
|
6
|
+
:service_website => "https://www.tumblr.com/",
|
7
|
+
:author => "Michael Henriksen (@michenriksen)",
|
8
|
+
:description => "Microblogging and social networking platform"
|
9
|
+
}
|
10
|
+
|
11
|
+
APEX_VALUE = "66.6.44.4".freeze
|
12
|
+
CNAME_VALUE = "domains.tumblr.com".freeze
|
13
|
+
RESPONSE_FINGERPRINT = "Whatever you were looking for doesn't currently exist at this address.".freeze
|
14
|
+
|
15
|
+
def run
|
16
|
+
if apex_resource?
|
17
|
+
return false unless resource_value == APEX_VALUE
|
18
|
+
elsif cname_resource?
|
19
|
+
return false unless resource_value == CNAME_VALUE
|
20
|
+
end
|
21
|
+
get_request("http://#{host}/").body.include?(RESPONSE_FINGERPRINT)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Aquatone
|
2
|
+
module Detectors
|
3
|
+
class Unbounce < Aquatone::Detector
|
4
|
+
self.meta = {
|
5
|
+
:service => "Unbounce",
|
6
|
+
:service_website => "https://unbounce.com/",
|
7
|
+
:author => "Michael Henriksen (@michenriksen)",
|
8
|
+
:description => "Landing page builder and conversion marketing platform"
|
9
|
+
}
|
10
|
+
|
11
|
+
APEX_VALUE = "54.84.104.245".freeze
|
12
|
+
CNAME_VALUE = "unbouncepages.com".freeze
|
13
|
+
RESPONSE_FINGERPRINT = "The requested URL was not found on this server.".freeze
|
14
|
+
|
15
|
+
def run
|
16
|
+
if apex_resource?
|
17
|
+
return false unless resource_value == APEX_VALUE
|
18
|
+
elsif cname_resource?
|
19
|
+
return false unless resource_value == CNAME_VALUE
|
20
|
+
end
|
21
|
+
get_request("http://#{host}/").body.include?(RESPONSE_FINGERPRINT)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Aquatone
|
2
|
+
module Detectors
|
3
|
+
class Uservoice < Aquatone::Detector
|
4
|
+
self.meta = {
|
5
|
+
:service => "UserVoice",
|
6
|
+
:service_website => "https://www.uservoice.com/",
|
7
|
+
:author => "Michael Henriksen (@michenriksen)",
|
8
|
+
:description => "Product management software"
|
9
|
+
}
|
10
|
+
|
11
|
+
CNAME_VALUE = ".uservoice.com".freeze
|
12
|
+
RESPONSE_FINGERPRINTS = [
|
13
|
+
"The page you have requested does not exist.",
|
14
|
+
"This UserVoice subdomain is currently available!"
|
15
|
+
].freeze
|
16
|
+
|
17
|
+
def run
|
18
|
+
return false unless cname_resource?
|
19
|
+
if resource_value.end_with?(CNAME_VALUE)
|
20
|
+
response = get_request("http://#{host}/")
|
21
|
+
RESPONSE_FINGERPRINTS.each do |fingerprint|
|
22
|
+
return true if response.body.include?(fingerprint)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
false
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Aquatone
|
2
|
+
module Detectors
|
3
|
+
class Wpengine < Aquatone::Detector
|
4
|
+
self.meta = {
|
5
|
+
:service => "WPEngine",
|
6
|
+
:service_website => "https://wpengine.com/",
|
7
|
+
:author => "Michael Henriksen (@michenriksen)",
|
8
|
+
:description => "WordPress blog hosting"
|
9
|
+
}
|
10
|
+
|
11
|
+
APEX_VALUE = "130.211.160.56".freeze
|
12
|
+
CNAME_VALUE = ".wpengine.com".freeze
|
13
|
+
RESPONSE_FINGERPRINT = "but is not configured for an account on our platform.".freeze
|
14
|
+
|
15
|
+
def run
|
16
|
+
if apex_resource?
|
17
|
+
return false unless resource_value == APEX_VALUE
|
18
|
+
elsif cname_resource?
|
19
|
+
return false unless resource_value.end_with?(CNAME_VALUE)
|
20
|
+
end
|
21
|
+
get_request("http://#{host}/").body.include?(RESPONSE_FINGERPRINT)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Aquatone
|
2
|
+
module Detectors
|
3
|
+
class Zendesk < Aquatone::Detector
|
4
|
+
self.meta = {
|
5
|
+
:service => "Zendesk",
|
6
|
+
:service_website => "https://www.zendesk.com/",
|
7
|
+
:author => "Michael Henriksen (@michenriksen)",
|
8
|
+
:description => "Customer service software and support ticket system"
|
9
|
+
}
|
10
|
+
|
11
|
+
CNAME_VALUE = ".zendesk.com".freeze
|
12
|
+
RESPONSE_FINGERPRINT = "<title>Help Center Closed | Zendesk</title>".freeze
|
13
|
+
|
14
|
+
def run
|
15
|
+
return false unless cname_resource?
|
16
|
+
if resource_value.end_with?(CNAME_VALUE)
|
17
|
+
return get_request("http://#{host}/").body.include?(RESPONSE_FINGERPRINT)
|
18
|
+
end
|
19
|
+
false
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/aquatone/port_lists.rb
CHANGED
data/lib/aquatone/resolver.rb
CHANGED
@@ -22,6 +22,20 @@ module Aquatone
|
|
22
22
|
nil
|
23
23
|
end
|
24
24
|
|
25
|
+
def resource(host)
|
26
|
+
resource = resource_with_nameserver(host,
|
27
|
+
Resolv::DNS::Resource::IN::CNAME) ||
|
28
|
+
resource_with_fallback_nameserver(host,
|
29
|
+
Resolv::DNS::Resource::IN::CNAME)
|
30
|
+
if !resource
|
31
|
+
resource = resource_with_nameserver(host,
|
32
|
+
Resolv::DNS::Resource::IN::A) ||
|
33
|
+
resource_with_fallback_nameserver(host,
|
34
|
+
Resolv::DNS::Resource::IN::A)
|
35
|
+
end
|
36
|
+
resource
|
37
|
+
end
|
38
|
+
|
25
39
|
private
|
26
40
|
|
27
41
|
def resolve_with_nameserver(host)
|
@@ -32,6 +46,14 @@ module Aquatone
|
|
32
46
|
_resolve(host, options[:fallback_nameservers].sample)
|
33
47
|
end
|
34
48
|
|
49
|
+
def resource_with_nameserver(host, typeclass)
|
50
|
+
_resource(host, typeclass, options[:nameservers].sample)
|
51
|
+
end
|
52
|
+
|
53
|
+
def resource_with_fallback_nameserver(host, typeclass)
|
54
|
+
_resource(host, typeclass, options[:fallback_nameservers].sample)
|
55
|
+
end
|
56
|
+
|
35
57
|
def _resolve(host, nameserver_ip)
|
36
58
|
nameserver = Resolv::DNS.new(:nameserver => nameserver_ip).tap do |ns|
|
37
59
|
ns.timeouts = options[:timeout]
|
@@ -43,5 +65,17 @@ module Aquatone
|
|
43
65
|
nameserver.close
|
44
66
|
nil
|
45
67
|
end
|
68
|
+
|
69
|
+
def _resource(host, typeclass, nameserver_ip)
|
70
|
+
nameserver = Resolv::DNS.new(:nameserver => nameserver_ip).tap do |ns|
|
71
|
+
ns.timeouts = options[:timeout]
|
72
|
+
end
|
73
|
+
resource = nameserver.getresource(host, typeclass)
|
74
|
+
nameserver.close
|
75
|
+
resource
|
76
|
+
rescue Resolv::ResolvError
|
77
|
+
nameserver.close
|
78
|
+
nil
|
79
|
+
end
|
46
80
|
end
|
47
81
|
end
|
data/lib/aquatone/version.rb
CHANGED
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.
|
4
|
+
version: 0.3.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-
|
11
|
+
date: 2017-07-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: httparty
|
@@ -87,6 +87,7 @@ executables:
|
|
87
87
|
- aquatone-discover
|
88
88
|
- aquatone-gather
|
89
89
|
- aquatone-scan
|
90
|
+
- aquatone-takeover
|
90
91
|
extensions: []
|
91
92
|
extra_rdoc_files: []
|
92
93
|
files:
|
@@ -103,6 +104,7 @@ files:
|
|
103
104
|
- exe/aquatone-discover
|
104
105
|
- exe/aquatone-gather
|
105
106
|
- exe/aquatone-scan
|
107
|
+
- exe/aquatone-takeover
|
106
108
|
- lib/aquatone.rb
|
107
109
|
- lib/aquatone/assessment.rb
|
108
110
|
- lib/aquatone/browser.rb
|
@@ -124,6 +126,33 @@ files:
|
|
124
126
|
- lib/aquatone/commands/discover.rb
|
125
127
|
- lib/aquatone/commands/gather.rb
|
126
128
|
- lib/aquatone/commands/scan.rb
|
129
|
+
- lib/aquatone/commands/takeover.rb
|
130
|
+
- lib/aquatone/detector.rb
|
131
|
+
- lib/aquatone/detectors/campaign_monitor.rb
|
132
|
+
- lib/aquatone/detectors/cargo.rb
|
133
|
+
- lib/aquatone/detectors/cloudfront.rb
|
134
|
+
- lib/aquatone/detectors/desk.rb
|
135
|
+
- lib/aquatone/detectors/fastly.rb
|
136
|
+
- lib/aquatone/detectors/feedpress.rb
|
137
|
+
- lib/aquatone/detectors/freshdesk.rb
|
138
|
+
- lib/aquatone/detectors/ghost.rb
|
139
|
+
- lib/aquatone/detectors/github_pages.rb
|
140
|
+
- lib/aquatone/detectors/helpjuice.rb
|
141
|
+
- lib/aquatone/detectors/helpscout.rb
|
142
|
+
- lib/aquatone/detectors/heroku.rb
|
143
|
+
- lib/aquatone/detectors/instapage.rb
|
144
|
+
- lib/aquatone/detectors/pingdom.rb
|
145
|
+
- lib/aquatone/detectors/s3.rb
|
146
|
+
- lib/aquatone/detectors/shopify.rb
|
147
|
+
- lib/aquatone/detectors/statuspage.rb
|
148
|
+
- lib/aquatone/detectors/surveygizmo.rb
|
149
|
+
- lib/aquatone/detectors/teamwork.rb
|
150
|
+
- lib/aquatone/detectors/tictail.rb
|
151
|
+
- lib/aquatone/detectors/tumblr.rb
|
152
|
+
- lib/aquatone/detectors/unbounce.rb
|
153
|
+
- lib/aquatone/detectors/uservoice.rb
|
154
|
+
- lib/aquatone/detectors/wpengine.rb
|
155
|
+
- lib/aquatone/detectors/zendesk.rb
|
127
156
|
- lib/aquatone/domain.rb
|
128
157
|
- lib/aquatone/http_client.rb
|
129
158
|
- lib/aquatone/key_store.rb
|