cms_scanner 0.0.2
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 +7 -0
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/.rubocop.yml +6 -0
- data/.travis.yml +14 -0
- data/Gemfile +6 -0
- data/README.md +20 -0
- data/Rakefile +9 -0
- data/app/app.rb +4 -0
- data/app/controllers.rb +2 -0
- data/app/controllers/core.rb +46 -0
- data/app/controllers/core/cli_options.rb +68 -0
- data/app/controllers/interesting_files.rb +12 -0
- data/app/finders.rb +1 -0
- data/app/finders/interesting_files.rb +21 -0
- data/app/finders/interesting_files/fantastico_fileslist.rb +23 -0
- data/app/finders/interesting_files/headers.rb +15 -0
- data/app/finders/interesting_files/robots_txt.rb +22 -0
- data/app/finders/interesting_files/search_replace_db_2.rb +28 -0
- data/app/finders/interesting_files/xml_rpc.rb +62 -0
- data/app/formatters.rb +3 -0
- data/app/formatters/cli.rb +18 -0
- data/app/formatters/cli_no_colour.rb +15 -0
- data/app/formatters/json.rb +12 -0
- data/app/models.rb +5 -0
- data/app/models/fantastico_fileslist.rb +20 -0
- data/app/models/headers.rb +37 -0
- data/app/models/interesting_file.rb +30 -0
- data/app/models/robots_txt.rb +20 -0
- data/app/models/xml_rpc.rb +35 -0
- data/app/views/cli/core/finished.erb +4 -0
- data/app/views/cli/core/started.erb +3 -0
- data/app/views/cli/interesting_files/findings.erb +19 -0
- data/app/views/cli/scan_aborted.erb +4 -0
- data/app/views/json/core/finished.erb +3 -0
- data/app/views/json/core/started.erb +3 -0
- data/app/views/json/interesting_files/findings.erb +1 -0
- data/app/views/json/scan_aborted.erb +4 -0
- data/cms_scanner.gemspec +37 -0
- data/examples/views/cli/wp_custom/test.erb +1 -0
- data/examples/views/json/wp_custom/test.erb +1 -0
- data/examples/wpscan.rb +29 -0
- data/lib/cms_scanner.rb +71 -0
- data/lib/cms_scanner/browser.rb +68 -0
- data/lib/cms_scanner/browser/actions.rb +48 -0
- data/lib/cms_scanner/browser/options.rb +53 -0
- data/lib/cms_scanner/cache/file_store.rb +75 -0
- data/lib/cms_scanner/cache/typhoeus.rb +21 -0
- data/lib/cms_scanner/controller.rb +90 -0
- data/lib/cms_scanner/controllers.rb +34 -0
- data/lib/cms_scanner/errors/auth_errors.rb +15 -0
- data/lib/cms_scanner/finders.rb +5 -0
- data/lib/cms_scanner/finders/finder.rb +27 -0
- data/lib/cms_scanner/finders/finding.rb +32 -0
- data/lib/cms_scanner/finders/findings.rb +25 -0
- data/lib/cms_scanner/finders/independent_finder.rb +30 -0
- data/lib/cms_scanner/finders/independent_finders.rb +41 -0
- data/lib/cms_scanner/formatter.rb +118 -0
- data/lib/cms_scanner/formatter/buffer.rb +15 -0
- data/lib/cms_scanner/target.rb +33 -0
- data/lib/cms_scanner/target/platform.rb +2 -0
- data/lib/cms_scanner/target/platform/php.rb +39 -0
- data/lib/cms_scanner/target/platform/wordpress.rb +35 -0
- data/lib/cms_scanner/target/platform/wordpress/custom_directories.rb +62 -0
- data/lib/cms_scanner/target/server.rb +3 -0
- data/lib/cms_scanner/target/server/apache.rb +43 -0
- data/lib/cms_scanner/target/server/generic.rb +34 -0
- data/lib/cms_scanner/target/server/iis.rb +48 -0
- data/lib/cms_scanner/version.rb +4 -0
- data/lib/cms_scanner/web_site.rb +68 -0
- data/lib/helper.rb +24 -0
- data/spec/app/controllers/core_spec.rb +152 -0
- data/spec/app/controllers/interesting_files_spec.rb +50 -0
- data/spec/app/finders/interesting_files/fantastico_fileslist_spec.rb +68 -0
- data/spec/app/finders/interesting_files/headers_spec.rb +38 -0
- data/spec/app/finders/interesting_files/robots_txt_spec.rb +56 -0
- data/spec/app/finders/interesting_files/search_replace_db_2_spec.rb +55 -0
- data/spec/app/finders/interesting_files/xml_rpc_spec.rb +138 -0
- data/spec/app/finders/interesting_files_spec.rb +13 -0
- data/spec/app/formatters/cli_no_colour_spec.rb +17 -0
- data/spec/app/formatters/cli_spec.rb +21 -0
- data/spec/app/formatters/json_spec.rb +33 -0
- data/spec/app/models/fantastico_fileslist_spec.rb +32 -0
- data/spec/app/models/headers_spec.rb +52 -0
- data/spec/app/models/interesting_file_spec.rb +51 -0
- data/spec/app/models/robots_txt_spec.rb +28 -0
- data/spec/app/models/xml_rpc_spec.rb +47 -0
- data/spec/cache/.gitignore +4 -0
- data/spec/dummy_finders.rb +41 -0
- data/spec/fixtures/interesting_files/fantastico_fileslist/fantastico_fileslist.txt +12 -0
- data/spec/fixtures/interesting_files/file.txt +4 -0
- data/spec/fixtures/interesting_files/headers/interesting.txt +14 -0
- data/spec/fixtures/interesting_files/headers/no_interesting.txt +12 -0
- data/spec/fixtures/interesting_files/robots_txt/robots.txt +10 -0
- data/spec/fixtures/interesting_files/search_replace_db_2/searchreplacedb2.php +188 -0
- data/spec/fixtures/interesting_files/xml_rpc/homepage_in_scope_pingback.html +7 -0
- data/spec/fixtures/interesting_files/xml_rpc/homepage_out_of_scope_pingback.html +7 -0
- data/spec/fixtures/interesting_files/xml_rpc/xmlrpc.php +1 -0
- data/spec/fixtures/output.txt +0 -0
- data/spec/fixtures/target/platform/php/debug_log/debug.log +2 -0
- data/spec/fixtures/target/platform/php/fpd/wp_rss_functions.php +2 -0
- data/spec/fixtures/target/platform/wordpress/custom_directories/custom_w_spaces.html +10 -0
- data/spec/fixtures/target/platform/wordpress/custom_directories/default.html +14 -0
- data/spec/fixtures/target/platform/wordpress/custom_directories/https.html +12 -0
- data/spec/fixtures/target/platform/wordpress/detection/default.html +4 -0
- data/spec/fixtures/target/platform/wordpress/detection/not_wp.html +8 -0
- data/spec/fixtures/target/platform/wordpress/detection/wp_includes.html +3 -0
- data/spec/fixtures/target/server/apache/directory_listing/2.2.16.html +15 -0
- data/spec/fixtures/target/server/generic/server/apache/basic.txt +5 -0
- data/spec/fixtures/target/server/generic/server/iis/basic.txt +6 -0
- data/spec/fixtures/target/server/generic/server/not_detected.txt +3 -0
- data/spec/fixtures/target/server/iis/directory_listing/no_parent.html +3 -0
- data/spec/fixtures/target/server/iis/directory_listing/with_parent.html +3 -0
- data/spec/fixtures/views/base/ctrl/local.erb +1 -0
- data/spec/fixtures/views/base/ctrl/test.erb +3 -0
- data/spec/fixtures/views/base/global.erb +1 -0
- data/spec/fixtures/views/base/test.erb +2 -0
- data/spec/fixtures/views/based_format/test.erb +1 -0
- data/spec/fixtures/views/json/render_me.erb +4 -0
- data/spec/lib/browser_spec.rb +141 -0
- data/spec/lib/cache/file_store_spec.rb +101 -0
- data/spec/lib/cache/typhoeus_spec.rb +30 -0
- data/spec/lib/cms_scanner_spec.rb +45 -0
- data/spec/lib/controller_spec.rb +23 -0
- data/spec/lib/controllers_spec.rb +52 -0
- data/spec/lib/finders/findings_spec.rb +49 -0
- data/spec/lib/finders/independent_finders_spec.rb +98 -0
- data/spec/lib/formatter_spec.rb +136 -0
- data/spec/lib/sub_scanner_spec.rb +27 -0
- data/spec/lib/target/platforms_spec.rb +13 -0
- data/spec/lib/target/servers_spec.rb +13 -0
- data/spec/lib/target_spec.rb +50 -0
- data/spec/lib/web_site_spec.rb +124 -0
- data/spec/shared_examples.rb +11 -0
- data/spec/shared_examples/browser_actions.rb +32 -0
- data/spec/shared_examples/finding.rb +20 -0
- data/spec/shared_examples/formatter_buffer.rb +8 -0
- data/spec/shared_examples/formatter_class_methods.rb +26 -0
- data/spec/shared_examples/independent_finder.rb +33 -0
- data/spec/shared_examples/target/platform/php.rb +58 -0
- data/spec/shared_examples/target/platform/wordpress.rb +41 -0
- data/spec/shared_examples/target/platform/wordpress/custom_directories.rb +50 -0
- data/spec/shared_examples/target/server/apache.rb +33 -0
- data/spec/shared_examples/target/server/generic.rb +34 -0
- data/spec/shared_examples/target/server/iis.rb +38 -0
- data/spec/spec_helper.rb +41 -0
- metadata +432 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module CMSScanner
|
|
2
|
+
module Finders
|
|
3
|
+
# Independent Finders container
|
|
4
|
+
# This class is designed to handle independent results
|
|
5
|
+
# which are not related with others
|
|
6
|
+
# e.g: interesting files
|
|
7
|
+
class IndependentFinders < Array
|
|
8
|
+
# @return [ Findings ]
|
|
9
|
+
def findings
|
|
10
|
+
@findings ||= NS::Finders::Findings.new
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# @param [ Hash ] opts
|
|
14
|
+
# @option opts [ Symbol ] mode :mixed, :passive or :aggressive
|
|
15
|
+
#
|
|
16
|
+
# @return [ Findings ]
|
|
17
|
+
def run(opts = {})
|
|
18
|
+
each do |finder|
|
|
19
|
+
symbols_from_mode(opts[:mode]).each do |symbol|
|
|
20
|
+
r = finder.send(symbol, opts)
|
|
21
|
+
|
|
22
|
+
next unless r
|
|
23
|
+
|
|
24
|
+
findings + [*r]
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
findings
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @param [ Symbol ] mode :mixed, :passive or :aggressive
|
|
32
|
+
# @return [ Array<Symbol> ] The symbols to call for the mode
|
|
33
|
+
def symbols_from_mode(mode)
|
|
34
|
+
symbols = [:passive, :aggressive]
|
|
35
|
+
|
|
36
|
+
return symbols if mode.nil? || mode == :mixed
|
|
37
|
+
symbols.include?(mode) ? [*mode] : []
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
require 'cms_scanner/formatter/buffer'
|
|
2
|
+
|
|
3
|
+
module CMSScanner
|
|
4
|
+
# Formatter
|
|
5
|
+
module Formatter
|
|
6
|
+
# Module to be able to do Formatter.load() & Formatter.availables
|
|
7
|
+
# and do that as well when the Formatter is included in another module
|
|
8
|
+
module ClassMethods
|
|
9
|
+
# @param [ String ] format
|
|
10
|
+
# @param [ Array<String> ] custom_views
|
|
11
|
+
#
|
|
12
|
+
# @return [ Formatter::Base ]
|
|
13
|
+
def load(format = nil, custom_views = nil)
|
|
14
|
+
format ||= 'cli'
|
|
15
|
+
custom_views ||= []
|
|
16
|
+
|
|
17
|
+
f = const_get(format.gsub(/-/, '_').camelize).new
|
|
18
|
+
custom_views.each { |v| f.views_directories << v }
|
|
19
|
+
f
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @return [ Array<String> ] The list of the available formatters (except the Base one)
|
|
23
|
+
# @note: the #load method above should then be used to create the associated formatter
|
|
24
|
+
def availables
|
|
25
|
+
formatters = NS::Formatter.constants.select do |const|
|
|
26
|
+
name = NS::Formatter.const_get(const)
|
|
27
|
+
name.is_a?(Class) && name != NS::Formatter::Base
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
formatters.map { |sym| sym.to_s.underscore.dasherize }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
extend ClassMethods
|
|
35
|
+
|
|
36
|
+
def self.included(base)
|
|
37
|
+
base.extend(ClassMethods)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Base Formatter
|
|
41
|
+
class Base
|
|
42
|
+
attr_reader :controller_name
|
|
43
|
+
|
|
44
|
+
# @return [ String ] The underscored name of the class
|
|
45
|
+
def format
|
|
46
|
+
self.class.name.demodulize.underscore
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @return [ String ] The underscored format to use as a base
|
|
50
|
+
def base_format; end
|
|
51
|
+
|
|
52
|
+
# @return [ Array<String> ]
|
|
53
|
+
def formats
|
|
54
|
+
[format, base_format].compact
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# This is called after the scan
|
|
58
|
+
# and used in some formatters (e.g JSON)
|
|
59
|
+
# to indent results
|
|
60
|
+
def beautify; end
|
|
61
|
+
|
|
62
|
+
# @see #render
|
|
63
|
+
def output(tpl, vars = {}, controller_name = nil)
|
|
64
|
+
puts render(tpl, vars, controller_name)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @param [ String ] tpl
|
|
68
|
+
# @param [ Hash ] vars
|
|
69
|
+
# @param [ String ] controller_name
|
|
70
|
+
def render(tpl, vars = {}, controller_name = nil)
|
|
71
|
+
template_vars(vars)
|
|
72
|
+
@controller_name = controller_name if controller_name
|
|
73
|
+
|
|
74
|
+
# '-' is used to disable new lines when -%> is used
|
|
75
|
+
# See http://www.ruby-doc.org/stdlib-2.1.1/libdoc/erb/rdoc/ERB.html
|
|
76
|
+
ERB.new(File.read(view_path(tpl)), nil, '-').result(binding)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# @param [ Hash ] vars
|
|
80
|
+
#
|
|
81
|
+
# @return [ Void ]
|
|
82
|
+
def template_vars(vars)
|
|
83
|
+
vars.each do |key, value|
|
|
84
|
+
instance_variable_set("@#{key}", value) unless key == :views_directories
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @param [ String ] tpl
|
|
89
|
+
#
|
|
90
|
+
# @return [ String ] The path of the view
|
|
91
|
+
def view_path(tpl)
|
|
92
|
+
if tpl[0, 1] == '@' # Global Template
|
|
93
|
+
tpl = tpl.delete('@')
|
|
94
|
+
else
|
|
95
|
+
fail 'The controller_name can not be nil' unless controller_name
|
|
96
|
+
tpl = "#{controller_name}/#{tpl}"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
fail "Wrong tpl format: '#{tpl}'" unless tpl =~ /\A[\w\/_]+\z/
|
|
100
|
+
|
|
101
|
+
views_directories.reverse.each do |dir|
|
|
102
|
+
formats.each do |format|
|
|
103
|
+
potential_file = File.join(dir, format, "#{tpl}.erb")
|
|
104
|
+
|
|
105
|
+
return potential_file if File.exist?(potential_file)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
fail "View not found for #{format}/#{tpl}"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# @return [ Array<String> ] The directories to look into for views
|
|
113
|
+
def views_directories
|
|
114
|
+
@views_directories ||= [Pathname.new(APP_DIR).join('views').to_s]
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module CMSScanner
|
|
2
|
+
module Formatter
|
|
3
|
+
# Module used to output the rendered views into a buffer
|
|
4
|
+
# and beautify it a the end of the scan
|
|
5
|
+
module Buffer
|
|
6
|
+
def output(tpl, vars = {}, controller_name = nil)
|
|
7
|
+
buffer << render(tpl, vars, controller_name)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def buffer
|
|
11
|
+
@buffer ||= ''
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require 'cms_scanner/web_site'
|
|
2
|
+
require 'cms_scanner/target/platform'
|
|
3
|
+
require 'cms_scanner/target/server'
|
|
4
|
+
|
|
5
|
+
module CMSScanner
|
|
6
|
+
# Target to Scan
|
|
7
|
+
class Target < WebSite
|
|
8
|
+
include Server::Generic
|
|
9
|
+
# @note Subdomains are considered out of scope (maybe consider them in ?)
|
|
10
|
+
# Also, // are handled by Addressable::URI, but worngly :/
|
|
11
|
+
# e.g: Addressable::URI.parse('//file').host => file
|
|
12
|
+
#
|
|
13
|
+
# @param [ String ] url
|
|
14
|
+
#
|
|
15
|
+
# @return [ Boolean ] true if the url given belongs to the target
|
|
16
|
+
def in_scope?(url)
|
|
17
|
+
return true if url[0, 1] == '/' && url[1, 1] != '/'
|
|
18
|
+
|
|
19
|
+
Addressable::URI.parse(url).host == uri.host
|
|
20
|
+
rescue
|
|
21
|
+
false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# TODO: add a force option to re-call the #find rather than return the @interesting_files ?
|
|
25
|
+
#
|
|
26
|
+
# @param [ Hash ] opts
|
|
27
|
+
#
|
|
28
|
+
# @return [ Findings ]
|
|
29
|
+
def interesting_files(opts = {})
|
|
30
|
+
@interesting_files ||= NS::Finders::InterestingFiles.find(self, opts)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module CMSScanner
|
|
2
|
+
class Target < WebSite
|
|
3
|
+
module Platform
|
|
4
|
+
# Some PHP specific implementation
|
|
5
|
+
module PHP
|
|
6
|
+
DEBUG_LOG_PATTERN = /\[[^\]]+\] PHP (?:Warning|Error|Notice):/
|
|
7
|
+
FPD_PATTERN = /Fatal error:.+? in (.+?) on/
|
|
8
|
+
|
|
9
|
+
# @param [ String ] path
|
|
10
|
+
# @param [ Hash ] params The request params
|
|
11
|
+
#
|
|
12
|
+
# @return [ Boolean ] true if url(path) is a debug log, false otherwise
|
|
13
|
+
def debug_log?(path = nil, params = {})
|
|
14
|
+
res = NS::Browser.get(url(path), params.merge(headers: { 'range' => 'bytes=0-700' }))
|
|
15
|
+
|
|
16
|
+
res.body =~ DEBUG_LOG_PATTERN ? true : false
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @param [ String ] path
|
|
20
|
+
# @param [ Hash ] params The request params
|
|
21
|
+
#
|
|
22
|
+
# @return [ Boolean ] true if url(path) contains a FPD, false otherwise
|
|
23
|
+
def full_path_disclosure?(path = nil, params = {})
|
|
24
|
+
!full_path_disclosure_entries(path, params).empty?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @param [ String ] path
|
|
28
|
+
# @param [ Hash ] params The request params
|
|
29
|
+
#
|
|
30
|
+
# @return [ Array<String> ] The FPD found, or an empty array if none
|
|
31
|
+
def full_path_disclosure_entries(path = nil, params = {})
|
|
32
|
+
res = NS::Browser.get(url(path), params)
|
|
33
|
+
|
|
34
|
+
res.body.scan(FPD_PATTERN).flatten
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
%w(custom_directories).each do |required|
|
|
2
|
+
require "cms_scanner/target/platform/wordpress/#{required}"
|
|
3
|
+
end
|
|
4
|
+
|
|
5
|
+
module CMSScanner
|
|
6
|
+
class Target < WebSite
|
|
7
|
+
module Platform
|
|
8
|
+
# Some WordPress specific implementation
|
|
9
|
+
module WordPress
|
|
10
|
+
include PHP
|
|
11
|
+
|
|
12
|
+
WORDPRESS_PATTERN = %r{/(?:(?:wp-content/(?:themes|plugins|plugins))|wp-includes)/}i
|
|
13
|
+
|
|
14
|
+
def wordpress?
|
|
15
|
+
page = Nokogiri::HTML(Browser.get(url).body)
|
|
16
|
+
|
|
17
|
+
page.css('script, link').each do |tag|
|
|
18
|
+
tag_url = tag.attribute('href').to_s
|
|
19
|
+
|
|
20
|
+
next unless in_scope?(tag_url)
|
|
21
|
+
|
|
22
|
+
tag_uri = Addressable::URI.parse(tag_url)
|
|
23
|
+
|
|
24
|
+
return true if tag_uri.path =~ WORDPRESS_PATTERN
|
|
25
|
+
end
|
|
26
|
+
false
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def wordpress_hosted?
|
|
30
|
+
uri.host =~ /wordpress.com$/i ? true : false
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
module CMSScanner
|
|
2
|
+
class Target < WebSite
|
|
3
|
+
module Platform
|
|
4
|
+
# wp-content & plugins directory implementation
|
|
5
|
+
module WordPress
|
|
6
|
+
def content_dir=(dir)
|
|
7
|
+
@content_dir = dir.chomp('/')
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def plugins_dir=(dir)
|
|
11
|
+
@plugins_dir = dir.chomp('/')
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @return [ String ] The wp-content directory
|
|
15
|
+
def content_dir
|
|
16
|
+
unless @content_dir
|
|
17
|
+
page = Nokogiri::HTML(Browser.get(url).body)
|
|
18
|
+
escaped_url = Regexp.escape(url).gsub(/https?/i, 'https?')
|
|
19
|
+
pattern = %r{#{escaped_url}(.+?)\/(?:themes|plugins|uploads)\/}i
|
|
20
|
+
|
|
21
|
+
page.css('link,script,style,img').each do |tag|
|
|
22
|
+
%w(href src).each do |attribute|
|
|
23
|
+
attr_value = tag.attribute(attribute).to_s
|
|
24
|
+
|
|
25
|
+
next if attr_value.nil? || attr_value.empty?
|
|
26
|
+
next unless in_scope?(attr_value) && attr_value.match(pattern)
|
|
27
|
+
|
|
28
|
+
return @content_dir = Regexp.last_match[1]
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
@content_dir
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @return [ Addressable::URI ]
|
|
36
|
+
def content_uri
|
|
37
|
+
uri.join("#{content_dir}/")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @return [ String ]
|
|
41
|
+
def content_url
|
|
42
|
+
content_uri.to_s
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @return [ String ]
|
|
46
|
+
def plugins_dir
|
|
47
|
+
@plugins_dir ||= "#{content_dir}/plugins"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @return [ Addressable::URI ]
|
|
51
|
+
def plugins_uri
|
|
52
|
+
uri.join("#{plugins_dir}/")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @return [ String ]
|
|
56
|
+
def plugins_url
|
|
57
|
+
plugins_uri.to_s
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module CMSScanner
|
|
2
|
+
class Target < WebSite
|
|
3
|
+
module Server
|
|
4
|
+
# Some Apche specific implementation
|
|
5
|
+
module Apache
|
|
6
|
+
# @param [ String ] path
|
|
7
|
+
# @param [ Hash ] params The request params
|
|
8
|
+
#
|
|
9
|
+
# @return [ Symbol ] :apache
|
|
10
|
+
def server(_path = nil, _params = {})
|
|
11
|
+
:Apache
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @param [ String ] path
|
|
15
|
+
# @param [ Hash ] params The request params
|
|
16
|
+
#
|
|
17
|
+
# @return [ Boolean ] true if url(path) has the directory
|
|
18
|
+
# listing enabled, false otherwise
|
|
19
|
+
def directory_listing?(path = nil, params = {})
|
|
20
|
+
res = NS::Browser.get(url(path), params)
|
|
21
|
+
|
|
22
|
+
res.code == 200 && res.body =~ /<h1>Index of/ ? true : false
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @param [ String ] path
|
|
26
|
+
# @param [ Hash ] params The request params
|
|
27
|
+
#
|
|
28
|
+
# @return [ Array<String> ] The first level of directories/files listed,
|
|
29
|
+
# or an empty array if none
|
|
30
|
+
def directory_listing_entries(path = nil, params = {})
|
|
31
|
+
return [] unless directory_listing?(path, params)
|
|
32
|
+
|
|
33
|
+
doc = Nokogiri::HTML(NS::Browser.get(url(path), params).body)
|
|
34
|
+
found = []
|
|
35
|
+
|
|
36
|
+
doc.css('td a').each { |node| found << node.text.to_s }
|
|
37
|
+
|
|
38
|
+
found[1..-1] # returns the array w/o the first element 'Parent Directory'
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module CMSScanner
|
|
2
|
+
class Target < WebSite
|
|
3
|
+
module Server
|
|
4
|
+
# Some Apche specific implementation
|
|
5
|
+
module Generic
|
|
6
|
+
# @param [ String ] path
|
|
7
|
+
# @param [ Hash ] params The request params
|
|
8
|
+
#
|
|
9
|
+
# @return [ Symbol ] The detected remote server (:Apache, :IIS)
|
|
10
|
+
def server(path = nil, params = {})
|
|
11
|
+
headers = headers(path, params)
|
|
12
|
+
|
|
13
|
+
return nil unless headers
|
|
14
|
+
|
|
15
|
+
case headers[:server]
|
|
16
|
+
when /\Aapache/i
|
|
17
|
+
:Apache
|
|
18
|
+
when /\AMicrosoft-IIS/i
|
|
19
|
+
:IIS
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @param [ String ] path
|
|
24
|
+
# @param [ Hash ] params The request params
|
|
25
|
+
#
|
|
26
|
+
# @return [ Hash ] The headers
|
|
27
|
+
def headers(path = nil, params = {})
|
|
28
|
+
# The HEAD method might be rejected by some servers ... maybe switch to GET ?
|
|
29
|
+
NS::Browser.head(url(path), params).headers
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module CMSScanner
|
|
2
|
+
class Target < WebSite
|
|
3
|
+
module Server
|
|
4
|
+
# Some IIS specific implementation
|
|
5
|
+
module IIS
|
|
6
|
+
# @param [ String ] path
|
|
7
|
+
# @param [ Hash ] params The request params
|
|
8
|
+
#
|
|
9
|
+
# @return [ Symbol ] :iis
|
|
10
|
+
def server(_path = nil, _params = {})
|
|
11
|
+
:IIS
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @param [ String ] path
|
|
15
|
+
# @param [ Hash ] params The request params
|
|
16
|
+
#
|
|
17
|
+
# @return [ Boolean ] true if url(path) has the directory
|
|
18
|
+
# listing enabled, false otherwise
|
|
19
|
+
def directory_listing?(path = nil, params = {})
|
|
20
|
+
res = NS::Browser.get(url(path), params)
|
|
21
|
+
|
|
22
|
+
res.code == 200 && res.body =~ /<H1>#{uri.host} - \// ? true : false
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @param [ String ] path
|
|
26
|
+
# @param [ Hash ] params The request params
|
|
27
|
+
#
|
|
28
|
+
# @return [ Array<String> ] The first level of directories/files listed,
|
|
29
|
+
# or an empty array if none
|
|
30
|
+
def directory_listing_entries(path = nil, params = {})
|
|
31
|
+
return [] unless directory_listing?(path, params)
|
|
32
|
+
|
|
33
|
+
doc = Nokogiri::HTML(NS::Browser.get(url(path), params).body)
|
|
34
|
+
found = []
|
|
35
|
+
|
|
36
|
+
doc.css('pre a').each do |node|
|
|
37
|
+
entry = node.text.to_s
|
|
38
|
+
|
|
39
|
+
next if entry == '[To Parent Directory]'
|
|
40
|
+
found << entry
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
found
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|