Codebase list ruby-cms-scanner / 81215ed
New upstream version 0.0.40 Sophie Brun 5 years ago
234 changed file(s) with 8751 addition(s) and 0 deletion(s). Raw diff Collapse all Expand all
0 *.gem
1 *.rbc
2 .bundle
3 .config
4 coverage
5 pkg
6 Gemfile.lock
0 --color
1 --fail-fast
0 AllCops:
1 TargetRubyVersion: 2.3
2 Exclude:
3 - '*.gemspec'
4 - 'vendor/**/*'
5 - 'example/**/*'
6 ClassVars:
7 Enabled: false
8 LineLength:
9 Max: 120
10 MethodLength:
11 Max: 18
12 Lint/UriEscapeUnescape:
13 Enabled: false
14 Metrics/AbcSize:
15 Max: 25
16 Metrics/BlockLength:
17 Exclude:
18 - 'spec/**/*'
19 Metrics/CyclomaticComplexity:
20 Max: 10
21 Metrics/PerceivedComplexity:
22 Max: 9
23 Style/FrozenStringLiteralComment:
24 Enabled: false
25 Style/FormatStringToken:
26 Exclude:
27 - lib/cms_scanner/finders/finder.rb
28 Style/MixinUsage:
29 Exclude:
30 - lib/cms_scanner/formatter.rb
0 language: ruby
1 sudo: false
2 cache: bundler
3 rvm:
4 - 2.3.0
5 - 2.3.1
6 - 2.3.2
7 - 2.3.3
8 - 2.3.4
9 - 2.4.0
10 - 2.4.1
11 - 2.5.0
12 - ruby-head
13 before_install:
14 - "echo 'gem: --no-ri --no-rdoc' > ~/.gemrc"
15 - "gem update --system"
16 matrix:
17 allow_failures:
18 - rvm: ruby-head
19 script:
20 - bundle exec rubocop
21 - bundle exec rspec
22 notifications:
23 email:
24 - [email protected]
0 source 'https://rubygems.org'
1 gemspec
0 Copyright (C) 2014-2015 - WPScanTeam
1
2 Permission is hereby granted, free of charge, to any person obtaining a copy
3 of this software and associated documentation files (the "Software"), to deal
4 in the Software without restriction, including without limitation the rights
5 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
6 copies of the Software, and to permit persons to whom the Software is
7 furnished to do so, subject to the following conditions:
8
9 The above copyright notice and this permission notice shall be included in all
10 copies or substantial portions of the Software.
11
12 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
13 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
14 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
15 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
16 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
17 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
18 SOFTWARE.
0 # CMSScanner
1
2 [![Gem Version](https://badge.fury.io/rb/cms_scanner.svg)](https://badge.fury.io/rb/cms_scanner)
3 [![Build Status](https://img.shields.io/travis/wpscanteam/CMSScanner.svg)](https://travis-ci.org/wpscanteam/CMSScanner)
4 [![Coverage Status](https://img.shields.io/coveralls/wpscanteam/CMSScanner.svg)](https://coveralls.io/r/wpscanteam/CMSScanner)
5 [![Code Climate](https://api.codeclimate.com/v1/badges/b90b7f9f6982792ef8d6/maintainability)](https://codeclimate.com/github/wpscanteam/CMSScanner/maintainability)
6 [![Dependency Status](https://img.shields.io/gemnasium/wpscanteam/CMSScanner.svg)](https://gemnasium.com/wpscanteam/CMSScanner)
7
8 The goal of this gem is to provide a quick and easy way to create a CMS/WebSite Scanner by acting like a Framework and providing classes, formatters etc.
9
10 ## /!\ This gem is currently Experimental /!\
11
12 ## A basic implementation example is available in the example folder.
13
14 To start to play with it, copy all its files and folders into a new git repository and run `bundle install && rake install` inside it.
15 It will create a `cmsscan` command that you can run against a target, ie `cmsscan --url https://www.google.com`
16
17
18 Install Dependencies: `bundle install`
19
20 ## Contributing
21
22 1. Fork it ( https://github.com/wpscanteam/CMSScanner/fork )
23 2. Create your feature branch (`git checkout -b my-new-feature`)
24 3. Commit your changes (`git commit -am 'Add some feature'`)
25 4. Push to the branch (`git push origin my-new-feature`)
26 5. Create new Pull Request
0 require 'bundler/gem_tasks'
1 require 'rspec/core/rake_task'
2 require 'rubocop/rake_task'
3
4 RuboCop::RakeTask.new
5 RSpec::Core::RakeTask.new(:spec)
6
7 # Run rubocop & rspec before the build
8 task build: %i[rubocop spec]
0 require_relative 'formatters'
1 require_relative 'controllers'
2 require_relative 'models'
3 require_relative 'finders'
0 module CMSScanner
1 module Controller
2 # CLI Options for the Core Controller
3 class Core < Base
4 def cli_options
5 formats = NS::Formatter.availables
6
7 [
8 OptURL.new(['-u', '--url URL', 'The URL to scan'],
9 required_unless: %i[help version],
10 default_protocol: 'http')
11 ] + mixed_cli_options + [
12 OptFilePath.new(['-o', '--output FILE', 'Output to FILE'], writable: true, exists: false),
13 OptChoice.new(['-f', '--format FORMAT',
14 'Output results in the format supplied'], choices: formats),
15 OptChoice.new(['--detection-mode MODE'],
16 choices: %w[mixed passive aggressive],
17 normalize: :to_sym,
18 default: :mixed),
19 OptArray.new(['--scope DOMAINS',
20 'Comma separated (sub-)domains to consider in scope. ',
21 'Wildcard(s) allowed in the trd of valid domains, e.g: *.target.tld'])
22 ] + cli_browser_options
23 end
24
25 def mixed_cli_options
26 [
27 OptBoolean.new(['-h', '--help', 'Display the help and exit']),
28 OptBoolean.new(['--version', 'Display the version and exit']),
29 OptBoolean.new(['--ignore-main-redirect', 'Ignore the main redirect (if any) and scan the target url']),
30 OptBoolean.new(['-v', '--verbose', 'Verbose mode']),
31 OptBoolean.new(['--[no-]banner', 'Whether or not to display the banner'], default: true)
32 ]
33 end
34
35 # @return [ Array<OptParseValidator::OptBase> ]
36 def cli_browser_options
37 cli_browser_headers_options + [
38 OptBoolean.new(['--random-user-agent', '--rua',
39 'Use a random user-agent for each scan']),
40 OptFilePath.new(['--user-agents-list FILE-PATH',
41 'List of agents to use with --random-user-agent'], exists: true),
42 OptCredentials.new(['--http-auth login:password']),
43 OptPositiveInteger.new(['--max-threads VALUE', '-t', 'The max threads to use'],
44 default: 5),
45 OptPositiveInteger.new(['--throttle MilliSeconds', 'Milliseconds to wait before doing another web request. ' \
46 'If used, the max threads will be set to 1.']),
47 OptPositiveInteger.new(['--request-timeout SECONDS', 'The request timeout in seconds'],
48 default: 60),
49 OptPositiveInteger.new(['--connect-timeout SECONDS', 'The connection timeout in seconds'],
50 default: 30),
51 OptBoolean.new(['--disable-tls-checks', 'Disables SSL/TLS certificate verification'])
52 ] + cli_browser_proxy_options + cli_browser_cookies_options + cli_browser_cache_options
53 end
54
55 # @return [ Array<OptParseValidator::OptBase> ]
56 def cli_browser_headers_options
57 [
58 OptString.new(['--user-agent VALUE', '--ua']),
59 OptHeaders.new(['--headers HEADERS', 'Additional headers to append in requests']),
60 OptString.new(['--vhost VALUE', 'The virtual host (Host header) to use in requests'])
61 ]
62 end
63
64 # @return [ Array<OptParseValidator::OptBase> ]
65 def cli_browser_proxy_options
66 [
67 OptProxy.new(['--proxy protocol://IP:port',
68 'Supported protocols depend on the cURL installed']),
69 OptCredentials.new(['--proxy-auth login:password'])
70 ]
71 end
72
73 # @return [ Array<OptParseValidator::OptBase> ]
74 def cli_browser_cookies_options
75 [
76 OptString.new(['--cookie-string COOKIE',
77 'Cookie string to use in requests, ' \
78 'format: cookie1=value1[; cookie2=value2]']),
79 OptFilePath.new(['--cookie-jar FILE-PATH', 'File to read and write cookies'],
80 writable: true,
81 readable: true,
82 create: true,
83 default: File.join(tmp_directory, 'cookie_jar.txt'))
84 ]
85 end
86
87 # @return [ Array<OptParseValidator::OptBase> ]
88 def cli_browser_cache_options
89 [
90 OptInteger.new(['--cache-ttl TIME_TO_LIVE', 'The cache time to live in seconds'], default: 600),
91 OptBoolean.new(['--clear-cache', 'Clear the cache before the scan']),
92 OptDirectoryPath.new(['--cache-dir PATH'],
93 readable: true,
94 writable: true,
95 create: true,
96 default: File.join(tmp_directory, 'cache'))
97 ]
98 end
99 end
100 end
101 end
0 require_relative 'core/cli_options'
1
2 module CMSScanner
3 module Controller
4 # Core Controller
5 class Core < Base
6 def setup_cache
7 return unless parsed_options[:cache_dir]
8
9 storage_path = File.join(parsed_options[:cache_dir], Digest::MD5.hexdigest(target.url))
10
11 Typhoeus::Config.cache = Cache::Typhoeus.new(storage_path)
12 Typhoeus::Config.cache.clean if parsed_options[:clear_cache]
13 end
14
15 def before_scan
16 maybe_output_banner_help_and_version
17
18 setup_cache
19 check_target_availability
20 end
21
22 def maybe_output_banner_help_and_version
23 output('banner') if parsed_options[:banner]
24 output('help', help: option_parser.to_s) if parsed_options[:help]
25 output('version') if parsed_options[:version]
26
27 exit(NS::ExitCode::OK) if parsed_options[:help] || parsed_options[:version]
28 end
29
30 # Checks that the target is accessible, raises related errors otherwise
31 #
32 # @return [ Void ]
33 def check_target_availability
34 res = NS::Browser.get(target.url)
35
36 case res.code
37 when 0
38 raise TargetDownError, res
39 when 401
40 raise HTTPAuthRequiredError
41 when 403
42 raise AccessForbiddenError
43 when 407
44 raise ProxyAuthRequiredError
45 end
46
47 # Checks for redirects
48 # An out of scope redirect will raise an HTTPRedirectError
49 effective_url = target.homepage_res.effective_url
50
51 return if target.in_scope?(effective_url)
52
53 raise HTTPRedirectError, effective_url unless parsed_options[:ignore_main_redirect]
54
55 target.homepage_res = res
56 end
57
58 def run
59 @start_time = Time.now
60 @start_memory = memory_usage
61
62 output('started', url: target.url, effective_url: target.homepage_url)
63 end
64
65 def after_scan
66 @stop_time = Time.now
67 @elapsed = @stop_time - @start_time
68 @used_memory = memory_usage - @start_memory
69 @requests_done = CMSScanner.total_requests
70
71 output('finished')
72 end
73 end
74 end
75 end
0 module CMSScanner
1 module Controller
2 # InterestingFindings Controller
3 class InterestingFindings < Base
4 def cli_options
5 [
6 OptChoice.new(
7 ['--interesting-findings-detection MODE',
8 'Use the supplied mode for the interesting findings detection. '],
9 choices: %w[mixed passive aggressive],
10 normalize: :to_sym
11 )
12 ]
13 end
14
15 def run
16 mode = parsed_options[:interesting_findings_detection] || parsed_options[:detection_mode]
17 findings = target.interesting_findings(mode: mode)
18
19 output('findings', findings: findings) unless findings.empty?
20 end
21 end
22 end
23 end
0 require_relative 'controllers/core'
1 require_relative 'controllers/interesting_findings'
0 module CMSScanner
1 module Finders
2 module InterestingFindings
3 # FantasticoFileslist finder
4 class FantasticoFileslist < Finder
5 # @return [ String ] The url of the fantastico_fileslist.txt file
6 def url
7 target.url('fantastico_fileslist.txt')
8 end
9
10 # @return [ InterestingFinding ]
11 def aggressive(_opts = {})
12 res = NS::Browser.get(url)
13
14 return unless res&.code == 200 && !res.body.empty?
15 return unless res.headers && res.headers['Content-Type'] =~ %r{\Atext/plain}
16
17 NS::FantasticoFileslist.new(url, confidence: 70, found_by: found_by)
18 end
19 end
20 end
21 end
22 end
0 module CMSScanner
1 module Finders
2 module InterestingFindings
3 # Interesting Headers finder
4 class Headers < Finder
5 # @return [ InterestingFinding ]
6 def passive(_opts = {})
7 r = NS::Headers.new(target.homepage_url, confidence: 100, found_by: found_by)
8
9 r.interesting_entries.empty? ? nil : r
10 end
11 end
12 end
13 end
14 end
0 module CMSScanner
1 module Finders
2 module InterestingFindings
3 # Robots.txt finder
4 class RobotsTxt < Finder
5 # @return [ String ] The url of the robots.txt file
6 def url
7 target.url('robots.txt')
8 end
9
10 # @return [ InterestingFinding ]
11 def aggressive(_opts = {})
12 res = NS::Browser.get(url)
13
14 return unless res&.code == 200 && res.body =~ /(?:user-agent|(?:dis)?allow):/i
15
16 NS::RobotsTxt.new(url, confidence: 100, found_by: found_by)
17 end
18 end
19 end
20 end
21 end
0 module CMSScanner
1 module Finders
2 module InterestingFindings
3 # SearchReplaceDB2 finder
4 class SearchReplaceDB2 < Finder
5 # @return [ String ] The url to the searchreplacedb2 PHP file
6 def url
7 target.url('searchreplacedb2.php')
8 end
9
10 # @return [ InterestingFinding ]
11 def aggressive(_opts = {})
12 res = NS::Browser.get(url)
13
14 return unless res&.code == 200 && res.body =~ /by interconnect/i
15
16 NS::InterestingFinding.new(url, confidence: 100,
17 found_by: found_by,
18 references: references)
19 end
20
21 def references
22 { url: 'https://interconnectit.com/products/search-and-replace-for-wordpress-databases/' }
23 end
24 end
25 end
26 end
27 end
0 module CMSScanner
1 module Finders
2 module InterestingFindings
3 # XML RPC finder
4 class XMLRPC < Finder
5 # @return [ Array<String> ] The potential urls to the XMl RPC file
6 def potential_urls
7 @potential_urls ||= []
8 end
9
10 # @return [ Array<XMLRPC> ]
11 def passive(opts = {})
12 [passive_headers(opts), passive_body(opts)].compact
13 end
14
15 # @return [ XMLRPC ]
16 def passive_headers(_opts = {})
17 url = target.homepage_res.headers['X-Pingback']
18
19 return unless target.in_scope?(url)
20
21 potential_urls << url
22
23 NS::XMLRPC.new(url, confidence: 30, found_by: 'Headers (Passive Detection)')
24 end
25
26 # @return [ XMLRPC ]
27 def passive_body(_opts = {})
28 target.homepage_res.html.css('link[rel="pingback"]').each do |tag|
29 url = tag.attribute('href').to_s
30
31 next unless target.in_scope?(url)
32
33 potential_urls << url
34
35 return NS::XMLRPC.new(url, confidence: 30,
36 found_by: 'Link Tag (Passive Detection)')
37 end
38 nil
39 end
40
41 # @return [ XMLRPC ]
42 def aggressive(_opts = {})
43 potential_urls << target.url('xmlrpc.php')
44
45 potential_urls.uniq.each do |potential_url|
46 next unless target.in_scope?(potential_url)
47
48 res = NS::Browser.get(potential_url)
49
50 next unless res&.body =~ /XML-RPC server accepts POST requests only/i
51
52 return NS::XMLRPC.new(potential_url,
53 confidence: 100,
54 found_by: DIRECT_ACCESS)
55 end
56 nil
57 end
58 end
59 end
60 end
61 end
0 require_relative 'interesting_findings/headers'
1 require_relative 'interesting_findings/robots_txt'
2 require_relative 'interesting_findings/fantastico_fileslist'
3 require_relative 'interesting_findings/search_replace_db_2'
4 require_relative 'interesting_findings/xml_rpc'
5
6 module CMSScanner
7 module Finders
8 module InterestingFindings
9 # Interesting Files Finder
10 class Base
11 include IndependentFinder
12
13 # @param [ CMSScanner::Target ] target
14 def initialize(target)
15 %w[Headers RobotsTxt FantasticoFileslist SearchReplaceDB2 XMLRPC].each do |f|
16 finders << NS::Finders::InterestingFindings.const_get(f).new(target)
17 end
18 end
19 end
20 end
21 end
22 end
0 require_relative 'finders/interesting_findings'
0 module CMSScanner
1 module Formatter
2 # CLI Formatter
3 class Cli < Base
4 # @return [ String ]
5 def info_icon
6 green('[+]')
7 end
8
9 # @return [ String ]
10 def notice_icon
11 blue('[i]')
12 end
13
14 # @return [ String ]
15 def warning_icon
16 amber('[!]')
17 end
18
19 # @return [ String ]
20 def critical_icon
21 red('[!]')
22 end
23
24 # @param [ String ] text
25 # @return [ String ]
26 def bold(text)
27 colorize(text, 1)
28 end
29
30 # @param [ String ] text
31 # @return [ String ]
32 def red(text)
33 colorize(text, 31)
34 end
35
36 # @param [ String ] text
37 # @return [ String ]
38 def green(text)
39 colorize(text, 32)
40 end
41
42 # @param [ String ] text
43 # @return [ String ]
44 def amber(text)
45 colorize(text, 33)
46 end
47
48 # @param [ String ] text
49 # @return [ String ]
50 def blue(text)
51 colorize(text, 34)
52 end
53
54 # @param [ String ] text
55 # @param [ Integer ] color_code
56 # @return [ String ]
57 def colorize(text, color_code)
58 "\e[#{color_code}m#{text}\e[0m"
59 end
60 end
61 end
62 end
0 module CMSScanner
1 module Formatter
2 # Because Reason https://github.com/wpscanteam/CMSScanner/issues/56
3 class CliNoColor < CliNoColour
4 end
5 end
6 end
0 module CMSScanner
1 module Formatter
2 # CLI No Colour Formatter
3 class CliNoColour < Cli
4 # Override to get the cli views
5 def format
6 'cli'
7 end
8
9 def colorize(text, _color_code)
10 text
11 end
12 end
13 end
14 end
0 module CMSScanner
1 module Formatter
2 # JSON Formatter
3 class Json < Base
4 include Buffer
5
6 def beautify
7 puts JSON.pretty_generate(JSON.parse("{#{buffer.chomp.chomp(',')}}"))
8 end
9 end
10 end
11 end
0 require_relative 'formatters/cli'
1 require_relative 'formatters/cli_no_colour'
2 require_relative 'formatters/cli_no_color'
3 require_relative 'formatters/json'
0 module CMSScanner
1 # FantasticoFileslist
2 class FantasticoFileslist < InterestingFinding
3 # @return [ Array<String> ] The interesting files/dirs detected
4 def interesting_entries
5 results = []
6
7 entries.each do |entry|
8 next unless entry =~ /(?:admin|\.log|\.sql|\.db)/i
9
10 results << entry
11 end
12 results
13 end
14
15 def references
16 { url: ['http://www.acunetix.com/vulnerabilities/fantastico-fileslist/'] }
17 end
18 end
19 end
0 module CMSScanner
1 # Interesting Headers
2 class Headers < InterestingFinding
3 # @return [ Hash ] The headers
4 def entries
5 res = NS::Browser.get(url)
6 return [] unless res&.headers
7
8 res.headers
9 end
10
11 # @return [ Array<String> ] The interesting headers detected
12 def interesting_entries
13 results = []
14
15 entries.each do |header, value|
16 next if known_headers.include?(header.downcase)
17
18 results << "#{header}: #{[*value].join(', ')}"
19 end
20 results
21 end
22
23 # @return [ Array<String> ] Downcased known headers
24 def known_headers
25 %w[
26 age accept-ranges cache-control content-encoding content-length content-type connection date
27 etag expires keep-alive location last-modified link pragma set-cookie strict-transport-security
28 transfer-encoding vary x-cache x-content-security-policy x-content-type-options
29 x-frame-options x-language x-permitted-cross-domain-policies x-pingback x-varnish
30 x-webkit-csp x-xss-protection
31 ]
32 end
33 end
34 end
0 module CMSScanner
1 # Interesting Finding
2 class InterestingFinding
3 include Finders::Finding
4
5 attr_reader :url
6 attr_writer :to_s
7
8 # @param [ String ] url
9 # @param [ Hash ] opts
10 # :to_s (override the to_s method)
11 # See Finders::Finding for other available options
12 def initialize(url, opts = {})
13 @url = url
14 @to_s = opts[:to_s]
15
16 parse_finding_options(opts)
17 end
18
19 # @return [ Array<String> ]
20 def entries
21 res = NS::Browser.get(url)
22
23 return [] unless res && res.headers['Content-Type'] =~ %r{\Atext/plain;}i
24
25 res.body.split("\n").reject { |s| s.strip.empty? }
26 end
27
28 # @return [ String ]
29 def to_s
30 @to_s || url
31 end
32
33 # @return [ Boolean ]
34 def ==(other)
35 self.class == other.class && to_s == other.to_s
36 end
37 end
38 end
0 module CMSScanner
1 # Robots.txt
2 class RobotsTxt < InterestingFinding
3 # @todo Better detection, currently everything not empty or / is returned
4 #
5 # @return [ Array<String> ] The interesting Allow/Disallow rules detected
6 def interesting_entries
7 results = []
8
9 entries.each do |entry|
10 next unless entry =~ /\A(?:dis)?allow:\s*(.+)\z/i
11
12 match = Regexp.last_match(1)
13 next if match == '/'
14
15 results << match
16 end
17
18 results.uniq
19 end
20 end
21 end
0 module CMSScanner
1 # User
2 class User
3 include Finders::Finding
4
5 attr_accessor :password
6 attr_reader :id, :username
7
8 # @param [ String ] username
9 # @param [ Hash ] opts
10 # @option opts [ Integer ] :id
11 # @option opts [ String ] :password
12 def initialize(username, opts = {})
13 @username = username
14 @password = opts[:password]
15 @id = opts[:id]
16
17 parse_finding_options(opts)
18 end
19
20 def ==(other)
21 return false unless self.class == other.class
22
23 username == other.username
24 end
25
26 def to_s
27 username
28 end
29 end
30 end
0 module CMSScanner
1 # Version
2 class Version
3 include Finders::Finding
4
5 attr_reader :number
6
7 def initialize(number, opts = {})
8 @number = number.to_s
9 @number = "0#{number}" if @number[0, 1] == '.'
10
11 parse_finding_options(opts)
12 end
13
14 # @param [ Version, String ] other
15 # rubocop:disable Style/NumericPredicate
16 def ==(other)
17 (self <=> other) == 0
18 end
19 # rubocop:enable all
20
21 # @param [ Version, String ] other
22 def <(other)
23 (self <=> other) == -1
24 end
25
26 # @param [ Version, String ] other
27 def >(other)
28 (self <=> other) == 1
29 end
30
31 # @param [ Version, String ] other
32 def <=>(other)
33 other = self.class.new(other) unless other.is_a?(self.class) # handle potential '.1' version
34
35 Gem::Version.new(number) <=> Gem::Version.new(other.number)
36 rescue ArgumentError
37 false
38 end
39
40 def to_s
41 number
42 end
43 end
44 end
0 module CMSScanner
1 # XML RPC
2 class XMLRPC < InterestingFinding
3 # @return [ Browser ]
4 def browser
5 @browser ||= NS::Browser.instance
6 end
7
8 # @return [ Array<String> ]
9 def available_methods
10 return @available_methods if @available_methods
11
12 @available_methods = []
13
14 res = method_call('system.listMethods').run
15 doc = Nokogiri::XML.parse(res.body)
16
17 doc.search('methodResponse params param value array data value string').each do |s|
18 @available_methods << s.text
19 end
20
21 @available_methods
22 end
23
24 # @return [ Boolean ] Whether or not the XMLRPC is enabled
25 def enabled?
26 !available_methods.empty?
27 end
28
29 # @param [ String ] method_name
30 # @param [ Array ] method_params
31 # @param [ Hash ] request_params
32 #
33 # @return [ Typhoeus::Request ]
34 def method_call(method_name, method_params = [], request_params = {})
35 browser.forge_request(
36 url,
37 request_params.merge(
38 method: :post,
39 body: ::XMLRPC::Create.new.methodCall(method_name, *method_params)
40 )
41 )
42 end
43
44 # @param [ Array<Array> ] methods_and_params
45 # @param [ Hash ] request_params
46 #
47 # Example of methods_and_params:
48 # [
49 # [method1, param1, param2],
50 # [method2, param1],
51 # [method3]
52 # ]
53 #
54 # @return [ Typhoeus::Request ]
55 def multi_call(methods_and_params = [], request_params = {})
56 browser.forge_request(
57 url,
58 request_params.merge(
59 method: :post,
60 body: ::XMLRPC::Create.new.methodCall(
61 'system.multicall',
62 methods_and_params.collect { |m| { methodName: m[0], params: m[1..-1] } }
63 )
64 )
65 )
66 end
67 end
68 end
0 require_relative 'models/interesting_finding'
1 require_relative 'models/robots_txt'
2 require_relative 'models/fantastico_fileslist'
3 require_relative 'models/headers'
4 require_relative 'models/xml_rpc'
5 require_relative 'models/version'
6 require_relative 'models/user'
0 # Windows
1 Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.5 (KHTML, like Gecko) Chrome/4.0.249.0 Safari/532.5
2 Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/534.14 (KHTML, like Gecko) Chrome/9.0.601.0 Safari/534.14
3 Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/534.27 (KHTML, like Gecko) Chrome/12.0.712.0 Safari/534.27
4 Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.24 Safari/535.1
5 Mozilla/5.0 (Windows; U; Windows NT 5.1; tr; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8 ( .NET CLR 3.5.30729; .NET4.0E)
6 Mozilla/5.0 (Windows NT 6.1; rv:2.0.1) Gecko/20100101 Firefox/4.0.1
7 Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.0.1) Gecko/20100101 Firefox/4.0.1
8 Mozilla/5.0 (Windows NT 6.1; WOW64; rv:7.0.1) Gecko/20100101 Firefox/7.0.1
9 Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6
10 Mozilla/5.0 (Windows NT 6.1; WOW64; rv:10.0.1) Gecko/20100101 Firefox/10.0.1
11 Mozilla/5.0 (Windows NT 6.1; rv:12.0) Gecko/20120403211507 Firefox/12.0
12 Mozilla/5.0 (Windows NT 6.1; WOW64; rv:15.0) Gecko/20120427 Firefox/15.0a1
13 Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0)
14 Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)
15 Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; Trident/6.0)
16 Opera/9.80 (Windows NT 6.1; U; es-ES) Presto/2.9.181 Version/12.00
17 Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/533.19.4 (KHTML, like Gecko) Version/5.0.2 Safari/533.18.5
18
19 # MAC
20 Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_5; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.15 Safari/534.13
21 Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10.5; en-US; rv:1.9.2.15) Gecko/20110303 Firefox/3.6.15
22 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:2.0.1) Gecko/20100101 Firefox/4.0.1
23 Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/418.8 (KHTML, like Gecko) Safari/419.3
24 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3
25 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_2; rv:10.0.1) Gecko/20100101 Firefox/10.0.1
26 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/534.55.3 (KHTML, like Gecko) Version/5.1.3 Safari/534.53.10
27
28 # Linux
29 Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.20 Safari/535.1
30 Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.24 (KHTML, like Gecko) Ubuntu/10.10 Chromium/12.0.703.0 Chrome/12.0.703.0 Safari/534.24
31 Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.9) Gecko/20100915 Gentoo Firefox/3.6.9
32 Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.1.16) Gecko/20120421 Gecko Firefox/11.0
33 Mozilla/5.0 (X11; Linux i686; rv:12.0) Gecko/20100101 Firefox/12.0
34 Opera/9.80 (X11; Linux x86_64; U; pl) Presto/2.7.62 Version/11.00
35 Mozilla/5.0 (X11; U; Linux x86_64; us; rv:1.9.1.19) Gecko/20110430 shadowfox/7.0 (like Firefox/7.0
36
37 # iPad
38 Mozilla/5.0 (iPad; CPU OS 7_1_1 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D201 Safari/9537.53
39 Mozilla/5.0 (iPad; CPU OS 7_0_4 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11B554a Safari/9537.53
40 Mozilla/5.0 (iPad; CPU OS 6_1_3 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10B329 Safari/8536.25
41
42 # iPhone
43 Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53
44 Mozilla/5.0 (iPhone; CPU iPhone OS 7_0_3 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11B511 Safari/9537.53
45 Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_1 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D201 Safari/9537.53
0 <% # Empty file, the banner should be implemented in each scanner %>
0 <%= info_icon %> Finished: <%= @stop_time.asctime %>
1 <%= info_icon %> Requests Done: <%= @requests_done %>
2 <%= info_icon %> Memory used: <%= @used_memory.bytes_to_human %>
3 <%= info_icon %> Elapsed time: <%= Time.at(@elapsed).utc.strftime('%H:%M:%S') %>
0 <%= info_icon %> URL: <%= @url %>
1 <% if @url != @effective_url -%>
2 <%= info_icon %> Effective URL: <%= @effective_url %>
3 <% end -%>
4 <%= info_icon %> Started: <%= @start_time.asctime %>
5
0 Version: <%= NS::VERSION %>
0 <% unless @a.empty? -%>
1 <% if @a.size == 1 -%>
2 | <%= @s %>: <%= @a.first %>
3 <% else -%>
4 | <%= @p %>:
5 <% @a.each do |line| -%>
6 | - <%= line %>
7 <% end -%>
8 <% end -%>
9 <% end -%>
0 <% unless @findings.empty? -%>
1 Interesting Finding(s):
2 <% @findings.each do |finding| -%>
3
4 <%= info_icon %> <%= finding %>
5 <%= render('_array', a: finding.interesting_entries, s: 'Interesting Entry', p: 'Interesting Entries') -%>
6 | Found By: <%= finding.found_by %>
7 <% if finding.confidence > 0 -%>
8 | Confidence: <%= finding.confidence %>%
9 <% end -%>
10 <% unless (confirmed = finding.confirmed_by).empty? -%>
11 <% if confirmed.size == 1 -%>
12 | Confirmed By: <%= confirmed.first.found_by %><% if confirmed.first.confidence > 0 %>, <%= confirmed.first.confidence %>% confidence<% end %>
13 <% else -%>
14 | Confirmed By:
15 <% confirmed.each do |c| -%>
16 | - <%= c.found_by %><% if c.confidence > 0 %>, <%= c.confidence %>% confidence<% end %>
17 <% end -%>
18 <% end -%>
19 <% end -%>
20 <%= render('_array', a: finding.references_urls, s: 'Reference', p: 'References') -%>
21 <% end -%>
22 <% end %>
0 Scan Aborted: <%= @reason %>
1 <% if @verbose -%>
2 Trace: <%= @trace.join("\n") %>
3 <% end %>
0 <%= @msg %>
1
2 Please use --help/-h for the list of available options.
0 <% # Empty file, the banner should be implemented in each scanner %>
0 "stop_time": <%= @stop_time.to_i %>,
1 "elapsed": <%= @elapsed.to_i %>,
2 "requests_done": <%= @requests_done.to_i %>,
3 "used_memory": <%= @used_memory.to_i %>,
0 "help": <%= @help.to_s.to_json %>,
0 "start_time": <%= @start_time.to_i %>,
1 "start_memory": <%= @start_memory.to_i %>,
2 "target_url": <%= @url.to_s.to_json %>,
3 "effective_url": <%= @effective_url.to_s.to_json %>,
0 "version": <%= NS::VERSION.to_s.to_json %>,
0 "interesting_findings": [
1 <% unless @findings.empty? -%>
2 <% last_index = @findings.size - 1 %>
3 <% @findings.each.with_index do |finding, index| -%>
4 {
5 "url": <%= finding.url.to_s.to_json %>,
6 "to_s": <%= finding.to_s.to_json %>,
7 "found_by": <%= finding.found_by.to_s.to_json %>,
8 "confidence": <%= finding.confidence.to_json %>,
9 "confirmed_by": {
10 <% unless (confirmed = finding.confirmed_by).empty? -%>
11 <% c_last_index = confirmed.size - 1 %>
12 <% confirmed.each.with_index do |c, i| -%>
13 <%= c.found_by.to_s.to_json %>: { "confidence": <%= c.confidence.to_json %> }<% unless i == c_last_index %>,<% end %>
14 <% end -%>
15 <% end -%>
16 },
17 "references": <%= finding.references.to_json %>,
18 "interesting_entries": <%= finding.interesting_entries.to_json %>
19 }<% unless index == last_index %>,<% end %>
20 <% end -%>
21 <% end -%>
22 ],
0 "scan_aborted": <%= @reason.to_json %>,
1 <% if @verbose -%>
2 "trace": <%= @trace.to_json %>,
3 <% end %>
0 lib = File.expand_path('../lib', __FILE__)
1 $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
2
3 require 'cms_scanner/version'
4
5 Gem::Specification.new do |s|
6 s.name = 'cms_scanner'
7 s.version = CMSScanner::VERSION
8 s.platform = Gem::Platform::RUBY
9 s.required_ruby_version = '>= 2.3'
10 s.authors = ['WPScanTeam']
11 s.email = ['[email protected]']
12 s.summary = 'CMS Scanner Framework (experimental)'
13 s.description = 'Framework to provide an easy way to implement CMS Scanners'
14 s.homepage = 'https://github.com/wpscanteam/CMSScanner'
15 s.license = 'MIT'
16
17 s.files = `git ls-files -z`.split("\x0").reject do |file|
18 file =~ %r{^(?:
19 spec\/.*
20 |Gemfile
21 |Rakefile
22 |\.rspec
23 |\.gitignore
24 |\.rubocop.yml
25 |\.travis.yml
26 )$}x
27 end
28
29 s.test_files = []
30 s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
31 s.require_path = 'lib'
32
33 s.add_dependency 'nokogiri', '~> 1.8.0'
34 s.add_dependency 'opt_parse_validator', '~> 0.0.16.2'
35 s.add_dependency 'public_suffix', '~> 3.0.0'
36 s.add_dependency 'ruby-progressbar', '~> 1.10.0'
37 s.add_dependency 'typhoeus', '~> 1.3.0'
38 s.add_dependency 'xmlrpc', '~> 0.3'
39 s.add_dependency 'yajl-ruby', '~> 1.4.1' # Better JSON parser regarding memory usage
40
41 # Already required by opt_parse_validator
42 # so version restriction loosen to avoid potential future conflicts
43 s.add_dependency 'activesupport', '~> 5.2'
44 s.add_dependency 'addressable', '~> 2.5'
45
46 s.add_development_dependency 'bundler', '~> 1.6'
47 s.add_development_dependency 'coveralls', '~> 0.8.0'
48 s.add_development_dependency 'rake', '~> 12.3'
49 s.add_development_dependency 'rspec', '~> 3.8.0'
50 s.add_development_dependency 'rspec-its', '~> 1.2.0'
51 s.add_development_dependency 'rubocop', '~> 0.59.1'
52 s.add_development_dependency 'simplecov', '~> 0.16.1'
53 s.add_development_dependency 'webmock', '~> 3.4.2'
54 end
0 *.gem
1 *.rbc
2 .bundle
3 .config
4 coverage
5 pkg
6 rdoc
7 Gemfile.lock
8
9 # YARD artifacts
10 .yardoc
11 _yardoc
12 doc/
0 --color
1 --fail-fast
0 AllCops:
1 Exclude:
2 - '*.gemspec'
3 - 'vendor/**/*'
4 LineLength:
5 Max: 120
6 ClassVars:
7 Enabled: false
8 Style/RescueModifier:
9 Enabled: false
10 Style/SignalException:
11 EnforcedStyle: semantic
12 MethodLength:
13 Max: 17
14 Metrics/AbcSize:
15 Max: 25
16 Metrics/CyclomaticComplexity:
17 Max: 10
18 Metrics/PerceivedComplexity:
19 Max: 9
0 language: ruby
1 sudo: false
2 cache: bundler
3 rvm:
4 - 2.3.0
5 - 2.3.1
6 - 2.3.2
7 - 2.3.3
8 - 2.3.4
9 - 2.4.0
10 - 2.4.1
11 - ruby-head
12 before_install:
13 - "echo 'gem: --no-ri --no-rdoc' > ~/.gemrc"
14 - "gem update --system"
15 matrix:
16 allow_failures:
17 - rvm: ruby-head
18 script:
19 - bundle exec rspec
20 - bundle exec rubocop
0 source 'https://rubygems.org'
1 gemspec
0 require 'bundler/gem_tasks'
1 require 'rspec/core/rake_task'
2 require 'rubocop/rake_task'
3
4 RuboCop::RakeTask.new
5 RSpec::Core::RakeTask.new(:spec)
6
7 # Run rubocop & rspec before the build
8 task build: %i[rubocop spec]
0 require_relative 'controllers'
0 module CMSScan
1 module Controller
2 # Example Controller
3 class Example < CMSScanner::Controller::Core
4 # @return [ Array<OptParseValidator::Opt> ]
5 def cli_options
6 [
7 OptString.new(['--dummy VALUE', 'Dummy CLI Option'])
8 ]
9 end
10
11 def before_scan
12 # Anything to do before ?
13 end
14
15 def run
16 # Let's check and display whether or not the word 'scan' is present in the homepage of the target
17
18 is_present = target.homepage_res.body =~ /scan/ ? true : false
19
20 output('scan_word', is_present: is_present)
21 end
22
23 # Alternative way of doing it
24 def run2
25 @is_present = Browser.get(target.homepage_url).body =~ /scan/ ? true : false
26
27 output('scan_word')
28 end
29
30 def after_scan
31 # Anything after ?
32 end
33 end
34 end
35 end
0 require_relative 'controllers/example'
0
1 CMS Scanner Example <%= CMSScan::VERSION %>
2
0 <% if @is_present -%>
1 <%= warning_icon %> The word 'scan' is present in the homepage
2 <% else -%>
3 <%= notice_icon %> The word 'scan' was not detected in the homepage
4 <% end %>
0 "banner": {
1 "version": <%= CMSScan::VERSION.to_json %>
2 },
0 "scan_word_present": <%= @is_present.to_json %>,
0 #!/usr/bin/env ruby
1
2 require 'cmsscan'
3
4 CMSScan::Scan.new do |s|
5 s.controllers << CMSScan::Controller::Example.new
6
7 s.run
8 end
0 # coding: utf-8
1
2 lib = File.expand_path('../lib', __FILE__)
3 $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
5 require 'cmsscan/version'
6
7 Gem::Specification.new do |s|
8 s.name = 'cmsscan'
9 s.version = CMSScan::VERSION
10 s.platform = Gem::Platform::RUBY
11 s.required_ruby_version = '>= 2.1.0'
12 s.authors = ['WPScanTeam']
13 s.date = Time.now.utc.strftime('%Y-%m-%d')
14 s.email = ['[email protected]']
15 s.summary = 'CMSScan Gem Example'
16 s.description = 'CMSScanner Implementation Example'
17 s.homepage = 'https://github.com/wpscanteam/CMSScanner'
18 s.license = 'MIT'
19
20 s.files = `git ls-files -z`.split("\x0").reject do |file|
21 file =~ %r{^(?:
22 spec\/.*
23 |Gemfile
24 |Rakefile
25 |\.rspec
26 |\.gitignore
27 |\.rubocop.yml
28 |\.travis.yml
29 )$}x
30 end
31
32 s.test_files = []
33 s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
34 s.require_path = 'lib'
35
36 s.add_dependency 'cms_scanner', '~> 0.0.39.0'
37
38 # Already required by CMSScanner, so version restrictions loosen
39 s.add_dependency 'activesupport', '~> 5.1'
40 s.add_dependency 'yajl-ruby', '~> 1.3'
41
42 s.add_development_dependency 'bundler', '~> 1.6'
43 s.add_development_dependency 'coveralls', '~> 0.8.0'
44 s.add_development_dependency 'rake', '~> 12.0'
45 s.add_development_dependency 'rspec', '~> 3.7.0'
46 s.add_development_dependency 'rspec-its', '~> 1.2.0'
47 s.add_development_dependency 'rubocop', '~> 0.52.0'
48 s.add_development_dependency 'simplecov', '~> 0.14.0' # Can't update to 0.15 as it breaks coveralls dep
49 s.add_development_dependency 'webmock', '~> 3.3.0'
50 end
0 module CMSScan
1 # Needed to load at least the Core controller
2 # Otherwise, the following error will be raised:
3 # `initialize': uninitialized constant CMSScan::Controller::Core (NameError)
4 module Controller
5 include CMSScanner::Controller
6 end
7 end
0 module CMSScan
1 # Custom Target Class
2 class Target < CMSScanner::Target
3 # Put your own methods there
4 end
5 end
0 # Version
1 module CMSScan
2 VERSION = '1.0'.freeze
3 end
0 # Gems
1 require 'cms_scanner'
2 require 'yajl/json_gem'
3 require 'addressable/uri'
4 require 'active_support/all'
5 # Standard Lib
6 require 'uri'
7 require 'time'
8 require 'readline'
9 require 'securerandom'
10 # Custom Libs
11 require 'cmsscan/target'
12 require 'cmsscan/version'
13 require 'cmsscan/controller'
14
15 Encoding.default_external = Encoding::UTF_8
16
17 # CMSScan
18 module CMSScan
19 include CMSScanner
20
21 APP_DIR = Pathname.new(__FILE__).dirname.join('..', 'app').expand_path
22 # Not needed in this example
23 # DB_DIR = File.join(Dir.home, '.cmsscan', 'db')
24
25 # Override, otherwise it would be returned as 'cms_scan'
26 # doesn't really matter in this example.
27 #
28 # @return [ String ]
29 def self.app_name
30 'cmsscan'
31 end
32 end
33
34 require "#{CMSScan::APP_DIR}/app"
0 module CMSScanner
1 class Browser
2 # Browser Actions (get, post etc)
3 module Actions
4 # @param [ String ] url
5 # @param [ Hash ] params
6 #
7 # @return [ Typhoeus::Response ]
8 def get(url, params = {})
9 process(url, params.merge(method: :get))
10 end
11
12 # @param [ String ] url
13 # @param [ Hash ] params
14 #
15 # @return [ Typhoeus::Response ]
16 def post(url, params = {})
17 process(url, params.merge(method: :post))
18 end
19
20 # @param [ String ] url
21 # @param [ Hash ] params
22 #
23 # @return [ Typhoeus::Response ]
24 def head(url, params = {})
25 process(url, params.merge(method: :head))
26 end
27
28 # @param [ String ] url
29 # @param [ Hash ] params
30 #
31 # @return [ Typhoeus::Response ]
32 def get_and_follow_location(url, params = {})
33 get(url, params.merge(followlocation: true))
34 end
35
36 protected
37
38 # @param [ String ] url
39 # @param [ Hash ] params
40 #
41 # @return [ Typhoeus::Response ]
42 def process(url, params)
43 Typhoeus::Request.new(url, NS::Browser.instance.request_params(params)).run
44 end
45 end
46 end
47 end
0 module CMSScanner
1 # Options available in the Browser
2 class Browser
3 OPTIONS = %i[
4 cache_ttl
5 cookie_jar
6 cookie_string
7 connect_timeout
8 disable_tls_checks
9 headers
10 http_auth
11 max_threads
12 proxy
13 proxy_auth
14 random_user_agent
15 request_timeout
16 throttle
17 user_agent
18 user_agents_list
19 vhost
20 ].freeze
21
22 attr_accessor(*OPTIONS)
23
24 # @return [ String ]
25 def default_user_agent
26 "#{NS} v#{NS::VERSION}"
27 end
28
29 def hydra
30 @hydra ||= Typhoeus::Hydra.new(max_concurrency: max_threads || 1)
31 end
32
33 # @param [ Hash ] options
34 def load_options(options = {})
35 OPTIONS.each do |sym|
36 send("#{sym}=", options[sym]) if options.key?(sym)
37 end
38 end
39
40 # Set the threads attribute and update
41 # the max_concurrency of Typhoeus::Hydra
42 #
43 # If the throttle attribute is > 0, max_threads will be forced to 1
44 #
45 # @param [ Integer ] number
46 def max_threads=(number)
47 @max_threads = number.to_i.positive? && throttle.zero? ? number.to_i : 1
48
49 hydra.max_concurrency = @max_threads
50 end
51
52 # @return [ String ] The user agent
53 def user_agent
54 @user_agent ||= random_user_agent ? user_agents.sample : default_user_agent
55 end
56
57 # @return [ Array<String> ]
58 def user_agents
59 return @user_agents if @user_agents
60
61 @user_agents = []
62
63 File.open(user_agents_list).each do |line|
64 next if line == "\n" || line[0, 1] == '#'
65
66 @user_agents << line.chomp
67 end
68
69 @user_agents
70 end
71
72 # @return [ String ] The path to the user agents list
73 def user_agents_list
74 @user_agents_list ||= File.join(APP_DIR, 'user_agents.txt')
75 end
76
77 # @param [ value ] The throttle time in milliseconds
78 #
79 # if value > 0, the max_threads will be set to 1
80 def throttle=(value)
81 @throttle = value.to_i.abs / 1000.0
82
83 self.max_threads = 1 if @throttle.positive?
84 end
85
86 def trottle!
87 sleep(throttle) if throttle.positive?
88 end
89 end
90 end
0 require 'cms_scanner/browser/actions'
1 require 'cms_scanner/browser/options'
2
3 module CMSScanner
4 # Singleton used to perform HTTP/HTTPS request to the target
5 class Browser
6 extend Actions
7
8 # @param [ Hash ] parsed_options
9 #
10 # @return [ Void ]
11 def initialize(parsed_options = {})
12 self.throttle = 0
13
14 load_options(parsed_options)
15 end
16
17 private_class_method :new
18
19 # @param [ Hash ] parsed_options
20 #
21 # @return [ Browser ] The instance
22 def self.instance(parsed_options = {})
23 @@instance ||= new(parsed_options)
24 end
25
26 def self.reset
27 @@instance = nil
28 end
29
30 # @param [ String ] url
31 # @param [ Hash ] params
32 #
33 # @return [ Typhoeus::Request ]
34 def forge_request(url, params = {})
35 Typhoeus::Request.new(url, request_params(params))
36 end
37
38 # @return [ Hash ]
39 def typhoeus_to_browser_opts
40 { connecttimeout: :connect_timeout, cache_ttl: :cache_ttl,
41 proxy: :proxy, timeout: :request_timeout, cookiejar: :cookie_jar,
42 cookiefile: :cookie_jar, cookie: :cookie_string }
43 end
44
45 # @return [ Hash ]
46 def default_request_params
47 params = {
48 headers: { 'User-Agent' => user_agent }.merge(headers || {}),
49 accept_encoding: 'gzip, deflate',
50 method: :get
51 }
52
53 if disable_tls_checks
54 # See http://curl.haxx.se/libcurl/c/CURLOPT_SSL_VERIFYHOST.html
55 params[:ssl_verifypeer] = false
56 params[:ssl_verifyhost] = 0
57 end
58
59 typhoeus_to_browser_opts.each do |typhoeus_opt, browser_opt|
60 attr_value = public_send(browser_opt)
61 params[typhoeus_opt] = attr_value unless attr_value.nil?
62 end
63
64 params[:proxyuserpwd] = "#{proxy_auth[:username]}:#{proxy_auth[:password]}" if proxy_auth
65 params[:userpwd] = "#{http_auth[:username]}:#{http_auth[:password]}" if http_auth
66
67 params[:headers]['Host'] = vhost if vhost
68
69 params
70 end
71
72 # @param [ Hash ] params
73 #
74 # @return [ Hash ]
75 def request_params(params = {})
76 default_request_params.merge(params) do |key, oldval, newval|
77 key == :headers ? oldval.merge(newval) : newval
78 end
79 end
80 end
81 end
0 module CMSScanner
1 module Cache
2 # Cache Implementation using files
3 class FileStore
4 attr_reader :storage_path, :serializer
5
6 # The serializer must have the 2 methods #load and #dump
7 # (Marshal and YAML have them)
8 # YAML is Human Readable, contrary to Marshal which store in a binary format
9 # Marshal does not need any "require"
10 #
11 # @param [ String ] storage_path
12 # @param [ Constant ] serializer
13 def initialize(storage_path, serializer = Marshal)
14 @storage_path = File.expand_path(storage_path)
15 @serializer = serializer
16
17 FileUtils.mkdir_p(@storage_path) unless Dir.exist?(@storage_path)
18 end
19
20 # TODO: rename this to clear ?
21 def clean
22 Dir[File.join(storage_path, '*')].each do |f|
23 File.delete(f) unless File.symlink?(f)
24 end
25 end
26
27 # @param [ String ] key
28 #
29 # @return [ Mixed ]
30 def read_entry(key)
31 return if expired_entry?(key)
32
33 serializer.load(File.read(entry_path(key)))
34 rescue StandardError
35 nil
36 end
37
38 # @param [ String ] key
39 # @param [ Mixed ] data_to_store
40 # @param [ Integer ] cache_ttl
41 def write_entry(key, data_to_store, cache_ttl)
42 return unless cache_ttl.to_i.positive?
43
44 File.write(entry_path(key), serializer.dump(data_to_store))
45 File.write(entry_expiration_path(key), Time.now.to_i + cache_ttl)
46 end
47
48 # @param [ String ] key
49 #
50 # @return [ String ] The file path associated to the key
51 def entry_path(key)
52 File.join(storage_path, key)
53 end
54
55 # @param [ String ] key
56 #
57 # @return [ String ] The expiration file path associated to the key
58 def entry_expiration_path(key)
59 entry_path(key) + '.expiration'
60 end
61
62 private
63
64 # @param [ String ] key
65 #
66 # @return [ Boolean ]
67 def expired_entry?(key)
68 File.read(entry_expiration_path(key)).to_i <= Time.now.to_i
69 rescue StandardError
70 true
71 end
72 end
73 end
74 end
0 require 'cms_scanner/cache/file_store'
1
2 module CMSScanner
3 module Cache
4 # Cache implementation for Typhoeus
5 class Typhoeus < FileStore
6 # @param [ Typhoeus::Request ] request
7 #
8 # @return [ Typhoeus::Response ]
9 def get(request)
10 read_entry(request.hash.to_s)
11 end
12
13 # @param [ Typhoeus::Request ] request
14 # @param [ Typhoeus::Response ] response
15 def set(request, response)
16 write_entry(request.hash.to_s, response, request.cache_ttl)
17 end
18 end
19 end
20 end
0 module CMSScanner
1 module Controller
2 # Base Controller
3 class Base
4 include OptParseValidator
5
6 # @return [ Array<OptParseValidator::OptBase> ]
7 def cli_options; end
8
9 def before_scan; end
10
11 def run; end
12
13 def after_scan; end
14
15 def ==(other)
16 self.class == other.class
17 end
18
19 # Reset all the class attibutes
20 # Currently only used in specs
21 def self.reset
22 @@target = nil
23 @@parsed_options = nil
24 @@datastore = nil
25 @@formatter = nil
26 end
27
28 # @return [ Target ]
29 def target
30 @@target ||= NS::Target.new(parsed_options[:url], parsed_options)
31 end
32
33 # @param [ OptParsevalidator::OptParser ] parser
34 def self.option_parser=(parser)
35 @@option_parser = parser
36 end
37
38 # @return [ OptParsevalidator::OptParser ]
39 def option_parser
40 @@option_parser
41 end
42
43 # Set the parsed options and initialize the browser
44 # with them
45 #
46 # @param [ Hash ] options
47 def self.parsed_options=(options)
48 @@parsed_options = options
49
50 NS::Browser.instance(options)
51 end
52
53 # @return [ Hash ]
54 def parsed_options
55 @@parsed_options ||= {}
56 end
57
58 # @return [ Hash ]
59 def datastore
60 @@datastore ||= {}
61 end
62
63 # @return [ Formatter::Base ]
64 def formatter
65 @@formatter ||= NS::Formatter.load(parsed_options[:format], datastore[:views])
66 end
67
68 # @see Formatter#output
69 #
70 # @return [ Void ]
71 def output(tpl, vars = {})
72 formatter.output(*tpl_params(tpl, vars))
73 end
74
75 # @see Formatter#render
76 #
77 # @return [ String ]
78 def render(tpl, vars = {})
79 formatter.render(*tpl_params(tpl, vars))
80 end
81
82 # @return [ Boolean ]
83 def user_interaction?
84 formatter.user_interaction? && !parsed_options[:output]
85 end
86
87 # @return [ String ]
88 def tmp_directory
89 File.join('/tmp', NS.app_name)
90 end
91
92 protected
93
94 # @param [ String ] tpl
95 # @param [ Hash ] vars
96 #
97 # @return [ Array<String> ]
98 def tpl_params(tpl, vars)
99 [
100 tpl,
101 instance_variable_values.merge(vars),
102 self.class.name.demodulize.underscore
103 ]
104 end
105
106 # @return [ Hash ] All the instance variable keys (and their values) and the verbose value
107 def instance_variable_values
108 h = { verbose: parsed_options[:verbose] }
109 instance_variables.each do |a|
110 s = a.to_s
111 n = s[1..s.size]
112 h[n.to_sym] = instance_variable_get(a)
113 end
114 h
115 end
116 end
117 end
118 end
0 module CMSScanner
1 # Controllers Container
2 class Controllers < Array
3 attr_reader :option_parser
4
5 # @param [ OptParsevalidator::OptParser ] options_parser
6 def initialize(option_parser = OptParseValidator::OptParser.new(nil, 40))
7 @option_parser = option_parser
8
9 register_options_files
10 end
11
12 # Adds the potential option file paths to the option_parser
13 def register_options_files
14 [Dir.home, Dir.pwd].each do |dir|
15 option_parser.options_files.supported_extensions.each do |ext|
16 @option_parser.options_files << Pathname.new(dir).join(".#{NS.app_name}", "cli_options.#{ext}").to_s
17 end
18 end
19 end
20
21 # @param [ Controller::Base ] controller
22 #
23 # @retun [ Controllers ] self
24 def <<(controller)
25 options = controller.cli_options
26
27 unless include?(controller)
28 option_parser.add(*options) if options
29 super(controller)
30 end
31 self
32 end
33
34 def run
35 parsed_options = option_parser.results
36 first.class.option_parser = option_parser
37 first.class.parsed_options = parsed_options
38
39 redirect_output_to_file(parsed_options[:output]) if parsed_options[:output]
40
41 each(&:before_scan)
42 each(&:run)
43 # Reverse is used here as the app/controllers/core#after_scan finishes the output
44 # and must be the last one to be executed
45 reverse_each(&:after_scan)
46 end
47 end
48 end
0 module CMSScanner
1 class Error < StandardError
2 end
3
4 # Target Down Error
5 class TargetDownError < Error
6 attr_reader :response
7
8 def initialize(response)
9 @response = response
10 end
11
12 def to_s
13 "The url supplied '#{response.request.url}' seems to be down (#{response.return_message})"
14 end
15 end
16
17 # HTTP Authentication Required Error
18 class HTTPAuthRequiredError < Error
19 # :nocov:
20 def to_s
21 'HTTP authentication required (or was invalid), please provide it with --http-auth'
22 end
23 # :nocov:
24 end
25
26 # Proxy Authentication Required Error
27 class ProxyAuthRequiredError < Error
28 # :nocov:
29 def to_s
30 'Proxy authentication required (or was invalid), please provide it with --proxy-auth'
31 end
32 # :nocov:
33 end
34
35 # Access Forbidden Error
36 class AccessForbiddenError < Error
37 # :nocov:
38 def to_s
39 'The target is responding with a 403, this might be due to a WAF. ' \
40 'Please re-try with --random-user-agent'
41 end
42 # :nocov:
43 end
44
45 # HTTP Redirect Error
46 class HTTPRedirectError < Error
47 attr_reader :redirect_uri
48
49 # @param [ String ] url
50 def initialize(url)
51 @redirect_uri = Addressable::URI.parse(url).normalize
52 end
53
54 def to_s
55 "The URL supplied redirects to #{redirect_uri}. Use the --ignore-main-redirect "\
56 'option to ignore the redirection and scan the target.'
57 end
58 end
59 end
0 module CMSScanner
1 # Exit Code Values
2 module ExitCode
3 # No error, scan finished w/o any vulnerabilies found
4 OK = 0
5
6 # All exceptions raised by OptParseValidator and OptionParser
7 CLI_OPTION_ERROR = 1
8
9 # Interrupt received
10 INTERRUPTED = 2
11
12 # Exceptions
13 ERROR = 3
14
15 # The target has at least one vulnerability.
16 # Currently, the interesting findings do not count as vulnerable things
17 VULNERABLE = 4
18 end
19 end
0 module CMSScanner
1 module Finders
2 # Base class container for the Finders (i.e IndependentFinders etc)
3 class BaseFinders < Array
4 # @return [ Findings ]
5 def findings
6 @findings ||= NS::Finders::Findings.new
7 end
8
9 # Should be implemented in child classes
10 def run; end
11
12 protected
13
14 # @param [ Symbol ] mode :mixed, :passive or :aggressive
15 # @return [ Array<Symbol> ] The symbols to call for the mode
16 def symbols_from_mode(mode)
17 symbols = %i[passive aggressive]
18
19 return symbols if mode.nil? || mode == :mixed
20
21 symbols.include?(mode) ? [*mode] : []
22 end
23
24 # @param [ CMSScanner::Finders::Finder ] finder
25 # @param [ Symbol ] symbol See return values of #symbols_from_mode
26 # @param [ Hash ] opts
27 def run_finder(finder, symbol, opts)
28 [*finder.send(symbol, opts.merge(found: findings))].compact.each do |found|
29 findings << found
30 end
31 end
32
33 # Allow child classes to filter the findings, such as return the best one
34 # or remove the low confidence ones.
35 #
36 # @return [ Findings ]
37 def filter_findings
38 findings
39 end
40 end
41 end
42 end
0 module CMSScanner
1 module Finders
2 class Finder
3 # Module to provide an easy way to perform password attacks
4 module BreadthFirstDictionaryAttack
5 # @param [ Array<CMSScanner::User> ] users
6 # @param [ Array<String> ] passwords
7 # @param [ Hash ] opts
8 # @option opts [ Boolean ] :show_progression
9 #
10 # @yield [ CMSScanner::User ] When a valid combination is found
11 #
12 # TODO: Make rubocop happy about metrics etc
13 #
14 # rubocop:disable all
15 def attack(users, passwords, opts = {})
16 create_progress_bar(total: users.size * passwords.size, show_progression: opts[:show_progression])
17 queue_count = 0
18
19 passwords.each_with_index do |password, password_index|
20 remaining_users = users.select { |u| u.password.nil? }
21
22 break if remaining_users.empty?
23
24 remaining_users.each do |user|
25 request = login_request(user.username, password)
26
27 request.on_complete do |res|
28 progress_bar.title = "Trying #{user.username} / #{password}"
29 progress_bar.increment
30
31 if valid_credentials?(res)
32 user.password = password
33
34 yield user
35
36 offset = progress_bar.total - progress_bar.progress < hydra.max_concurrency ? 2 : 1
37
38 progress_bar.total -= passwords.size - password_index - offset
39 elsif errored_response?(res)
40 output_error(res)
41 end
42 end
43
44 hydra.queue(request)
45 queue_count += 1
46
47 if queue_count >= hydra.max_concurrency
48 hydra.run
49 queue_count = 0
50 end
51 end
52 end
53
54 hydra.run
55 progress_bar.stop
56 end
57 # rubocop:enable all
58
59 # @param [ String ] username
60 # param [ String ] password
61 #
62 # @return [ Typhoeus::Request ]
63 def login_request(username, password)
64 # To Implement in the finder related to the attack
65 end
66
67 # @param [ Typhoeus::Response ] response
68 #
69 # @return [ Boolean ] Whether or not credentials related to the request are valid
70 def valid_credentials?(response)
71 # To Implement in the finder related to the attack
72 end
73
74 # @param [ Typhoeus::Response ] response
75 #
76 # @return [ Boolean ] Whether or not something wrong happened
77 # other than wrong credentials
78 def errored_response?(response)
79 # To Implement in the finder related to the attack
80 end
81
82 protected
83
84 # @param [ Typhoeus::Response ] response
85 def output_error(response)
86 error = if response.timed_out?
87 'Request timed out.'
88 elsif response.code.zero?
89 "No response from remote server. WAF/IPS? (#{response.return_message})"
90 elsif response.code.to_s =~ /^50/
91 'Server error, try reducing the number of threads.'
92 else
93 "Unknown response received Code: #{response.code}\nBody: #{response.body}"
94 end
95
96 progress_bar.log("Error: #{error}")
97 end
98 end
99 end
100 end
101 end
0 module CMSScanner
1 module Finders
2 class Finder
3 # Module to provide an easy way to enumerate items such as plugins, themes etc
4 module Enumerator
5 # @param [ Hash ] The target urls
6 # @param [ Hash ] opts
7 # @option opts [ Boolean ] :show_progression Wether or not to display the progress bar
8 # @option opts [ Regexp ] :exclude_content
9 #
10 # @yield [ Typhoeus::Response, String ]
11 def enumerate(target_urls, opts = {})
12 create_progress_bar(opts.merge(total: target_urls.size))
13
14 target_urls.each do |url, id|
15 request = browser.forge_request(url, request_params)
16
17 request.on_complete do |res|
18 progress_bar.increment
19
20 next if target.homepage_or_404?(res)
21
22 if opts[:exclude_content]
23 next if res.response_headers&.match(opts[:exclude_content]) || res.body.match(opts[:exclude_content])
24 end
25
26 yield res, id
27 end
28
29 hydra.queue(request)
30 end
31
32 hydra.run
33 end
34
35 # @return [ Hash ]
36 def request_params
37 # disabling the cache, as it causes a 'stack level too deep' exception
38 # with a large number of requests :/
39 # See https://github.com/typhoeus/typhoeus/issues/408
40 { cache_ttl: 0 }
41 end
42 end
43 end
44 end
45 end
0 module CMSScanner
1 module Finders
2 class Finder
3 # Module to provide an easy way to fingerprint things such as versions
4 module Fingerprinter
5 # @param [ Hash ] fingerprints The fingerprints
6 # Format should be like the following:
7 # {
8 # file_path_1: {
9 # md5_hash_1: version_1,
10 # md5_hash_2: [version_2]
11 # },
12 # file_path_2: {
13 # md5_hash_3: [version_1, version_2],
14 # md5_hash_4: version_3
15 # }
16 # }
17 # Note that the version can either be an array or a string
18 #
19 # @param [ Hash ] opts
20 # @option opts [ Boolean ] :show_progression Wether or not to display the progress bar
21 #
22 # @yield [ Mixed, String, String ] version/s, url, hash The version associated to the
23 # fingerprint of the url
24 def fingerprint(fingerprints, opts = {})
25 create_progress_bar(opts.merge(total: fingerprints.size))
26
27 fingerprints.each do |path, f|
28 url = target.url(path.dup)
29 request = browser.forge_request(url, request_params)
30
31 request.on_complete do |res|
32 progress_bar.increment
33
34 md5sum = hexdigest(res.body)
35
36 next unless f.key?(md5sum)
37
38 yield f[md5sum], url, md5sum
39 end
40
41 hydra.queue(request)
42 end
43
44 hydra.run
45 end
46
47 # @return [ Hash ]
48 def request_params
49 {}
50 end
51
52 # @return [ String ] The hashed value for the given body
53 def hexdigest(body)
54 Digest::MD5.hexdigest(body)
55 end
56 end
57 end
58 end
59 end
0 module CMSScanner
1 module Finders
2 class Finder
3 module SmartURLChecker
4 # Findings
5 class Findings < Array
6 def <<(finding)
7 return self unless finding
8
9 each do |f|
10 next unless f == finding && f.found_by == finding.found_by
11
12 # This makes sure entries added are unique
13 # and prevent pages redirecting to the same one to be added twice
14 entries_to_add = finding.interesting_entries - f.interesting_entries
15 return self if entries_to_add.empty?
16
17 entries_to_add.each { |entry| f.interesting_entries << entry }
18
19 f.confidence += finding.confidence
20
21 return self
22 end
23
24 super(finding)
25 end
26 end
27 end
28 end
29 end
30 end
0 require 'cms_scanner/finders/finder/smart_url_checker/findings'
1
2 module CMSScanner
3 module Finders
4 class Finder
5 # Smart URL Checker
6 # Typically used when some URLs are potentially in the homepage. If they are found
7 # in it, they will be checked in the #passive (like a browser/client would do when loading the page).
8 # Otherwise they will be checked in the #aggressive
9 module SmartURLChecker
10 # @param [ Array<String> ] urls
11 # @param [ Hash ] opts
12 #
13 # @return []
14 def process_urls(_urls, _opts = {})
15 raise NotImplementedError
16 end
17
18 # @param [ Hash ] opts
19 #
20 # @return [ Array<Finding> ]
21 def passive(opts = {})
22 process_urls(passive_urls(opts), opts)
23 end
24
25 # @param [ Hash ] opts
26 #
27 # @return [ Array<String> ]
28 def passive_urls(_opts = {})
29 target.in_scope_urls(target.homepage_res, passive_urls_xpath)
30 end
31
32 # @return [ String ]
33 def passive_urls_xpath
34 raise NotImplementedError
35 end
36
37 # @param [ Hash ] opts
38 #
39 # @return [ Array<Finding> ]
40 def aggressive(opts = {})
41 # To avoid scanning the same twice
42 urls = aggressive_urls(opts)
43 urls -= passive_urls(opts) if opts[:mode] == :mixed
44
45 process_urls(urls, opts)
46 end
47
48 # @param [ Hash ] opts
49 #
50 # @return [ Array<String> ]
51 def aggressive_urls(_opts = {})
52 raise NotImplementedError
53 end
54 end
55 end
56 end
57 end
0 require 'cms_scanner/finders/finder/smart_url_checker'
1 require 'cms_scanner/finders/finder/enumerator'
2 require 'cms_scanner/finders/finder/fingerprinter'
3 require 'cms_scanner/finders/finder/breadth_first_dictionary_attack'
4
5 module CMSScanner
6 module Finders
7 # Finder
8 class Finder
9 # Constants for common found_by
10 DIRECT_ACCESS = 'Direct Access (Aggressive Detection)'.freeze
11
12 attr_accessor :target, :progress_bar
13
14 def initialize(target)
15 @target = target
16 end
17
18 # @return [ String ] The titleized name of the finder
19 def titleize
20 self.class.to_s.demodulize.underscore.titleize
21 end
22
23 # @param [ Hash ] _opts
24 def passive(_opts = {}); end
25
26 # @param [ Hash ] _opts
27 def aggressive(_opts = {}); end
28
29 # @param [ Hash ] opts See https://github.com/jfelchner/ruby-progressbar/wiki/Options
30 # @option opts [ Boolean ] :show_progression
31 #
32 # @return [ ProgressBar::Base ]
33 def create_progress_bar(opts = {})
34 bar_opts = { format: '%t %a <%B> (%c / %C) %P%% %e' }
35 bar_opts[:output] = ProgressBarNullOutput unless opts[:show_progression]
36
37 @progress_bar = ::ProgressBar.create(bar_opts.merge(opts))
38 end
39
40 # @return [ Browser ]
41 def browser
42 @browser ||= NS::Browser.instance
43 end
44
45 # @return [ Typhoeus::Hydra ]
46 def hydra
47 @hydra ||= browser.hydra
48 end
49
50 # @param [ String, Symbol ] klass
51 # @return [ String ]
52 def found_by(klass = self)
53 caller_locations.each do |call|
54 label = call.label
55
56 next unless %w[aggressive passive].include? label
57
58 return "#{klass.titleize} (#{label.capitalize} Detection)"
59 end
60 nil
61 end
62 end
63 end
64 end
0 module CMSScanner
1 module Finders
2 # Finding
3 module Finding
4 # Fix for "Double/Dynamic Inclusion Problem"
5 def self.included(base)
6 base.include References
7 super(base)
8 end
9
10 FINDING_OPTS = %i[confidence confirmed_by references found_by interesting_entries].freeze
11
12 attr_accessor(*FINDING_OPTS)
13
14 # @return [ Array ]
15 def confirmed_by
16 @confirmed_by ||= []
17 end
18
19 # Should be overriden in child classes
20 # @return [ Array ]
21 def interesting_entries
22 @interesting_entries ||= []
23 end
24
25 # @return [ Integer ]
26 def confidence
27 @confidence ||= 0
28 end
29
30 # @param [ Integer ] value
31 def confidence=(value)
32 @confidence = value >= 100 ? 100 : value
33 end
34
35 # @param [ Hash ] opts
36 def parse_finding_options(opts = {})
37 FINDING_OPTS.each { |opt| send("#{opt}=", opts[opt]) if opts.key?(opt) }
38 end
39
40 # TODO: maybe also check for interesting_entries and confirmed_by ?
41 # So far this is used in specs only
42 def eql?(other)
43 self == other && confidence == other.confidence && found_by == other.found_by
44 end
45
46 def <=>(other)
47 to_s.downcase <=> other.to_s.downcase
48 end
49 end
50 end
51 end
0 module CMSScanner
1 module Finders
2 # Findings container
3 class Findings < Array
4 # Override to include the confirmed_by logic
5 #
6 # @param [ Finding ] finding
7 def <<(finding)
8 return self unless finding
9
10 each do |found|
11 next unless found == finding
12
13 found.confirmed_by << finding
14 found.confidence += finding.confidence
15
16 return self
17 end
18
19 super(finding)
20 end
21 end
22 end
23 end
0 module CMSScanner
1 module Finders
2 # Independent Finder
3 module IndependentFinder
4 extend ActiveSupport::Concern
5
6 # See ActiveSupport::Concern
7 module ClassMethods
8 def find(target, opts = {})
9 new(target).find(opts)
10 end
11 end
12
13 # @param [ Hash ] opts
14 # @option opts [ Symbol ] mode (:mixed, :passive, :aggressive)
15 #
16 # @return [ Findings ]
17 def find(opts = {})
18 finders.run(opts)
19 end
20
21 # @return [ Array ]
22 def finders
23 @finders ||= NS::Finders::IndependentFinders.new
24 end
25 end
26 end
27 end
0 module CMSScanner
1 module Finders
2 # This class is designed to handle independent results
3 # which are not related with each others
4 # e.g: interesting files
5 class IndependentFinders < BaseFinders
6 # @param [ Hash ] opts
7 # @option opts [ Symbol ] mode :mixed, :passive or :aggressive
8 #
9 # @return [ Findings ]
10 def run(opts = {})
11 methods = symbols_from_mode(opts[:mode])
12
13 each do |finder|
14 methods.each do |symbol|
15 run_finder(finder, symbol, opts)
16 end
17 end
18
19 filter_findings
20 end
21 end
22 end
23 end
0 module CMSScanner
1 module Finders
2 # Same Type Finder
3 module SameTypeFinder
4 def self.included(klass)
5 klass.class_eval do
6 include IndependentFinder
7
8 # @return [ Array ]
9 def finders
10 @finders ||= NS::Finders::SameTypeFinders.new
11 end
12 end
13 end
14 end
15 end
16 end
0 module CMSScanner
1 module Finders
2 # This class is designed to handle same type results, such as enumeration of plugins,
3 # themes etc.
4 class SameTypeFinders < BaseFinders
5 # @param [ Hash ] opts
6 # @option opts [ Symbol ] :mode :mixed, :passive or :aggressive
7 # @option opts [ Boolean ] :sort Wether or not to sort the findings
8 #
9 # @return [ Findings ]
10 def run(opts = {})
11 symbols_from_mode(opts[:mode]).each do |symbol|
12 each do |finder|
13 run_finder(finder, symbol, opts)
14 end
15 end
16
17 findings.sort! if opts[:sort]
18
19 filter_findings
20 end
21 end
22 end
23 end
0 module CMSScanner
1 module Finders
2 # Unique Finder
3 module UniqueFinder
4 def self.included(klass)
5 klass.class_eval do
6 include IndependentFinder
7
8 # @return [ Array ]
9 def finders
10 @finders ||= NS::Finders::UniqueFinders.new
11 end
12 end
13 end
14 end
15 end
16 end
0 module CMSScanner
1 module Finders
2 # This class is designed to return a unique result such as a version
3 # Note: Finders contained can return multiple results but the #run will only
4 # returned the best finding
5 class UniqueFinders < BaseFinders
6 # @param [ Hash ] opts
7 # @option opts [ Symbol ] :mode :mixed, :passive or :aggressive
8 # @option opts [ Int ] :confidence_threshold If a finding's confidence reaches this value,
9 # it will be returned as the best finding.
10 # Default is 100.
11 # If <= 0, all finders will be ran.
12 #
13 # @return [ Object, false ] The best finding or false if none
14 def run(opts = {})
15 opts[:confidence_threshold] ||= 100
16
17 symbols_from_mode(opts[:mode]).each do |symbol|
18 each do |finder|
19 run_finder(finder, symbol, opts)
20
21 next if opts[:confidence_threshold] <= 0
22
23 findings.each { |f| return f if f.confidence >= opts[:confidence_threshold] }
24 end
25 end
26
27 filter_findings
28 end
29
30 protected
31
32 # @return [ Object, false ] The best finding or false if none
33 def filter_findings
34 # results are sorted by confidence ASC
35 findings.sort_by!(&:confidence)
36
37 # If all findings have the same confidence, false is returned
38 return false if findings.size > 1 && findings.first.confidence == findings.last.confidence
39
40 findings.last || false
41 end
42 end
43 end
44 end
0 require 'cms_scanner/finders/finder'
1 require 'cms_scanner/finders/finding'
2 require 'cms_scanner/finders/findings'
3 require 'cms_scanner/finders/base_finders'
4 require 'cms_scanner/finders/independent_finders'
5 require 'cms_scanner/finders/independent_finder'
6 require 'cms_scanner/finders/unique_finders'
7 require 'cms_scanner/finders/unique_finder'
8 require 'cms_scanner/finders/same_type_finders'
9 require 'cms_scanner/finders/same_type_finder'
0 module CMSScanner
1 module Formatter
2 # Module used to output the rendered views into a buffer
3 # and beautify it a the end of the scan
4 module Buffer
5 def output(tpl, vars = {}, controller_name = nil)
6 buffer << render(tpl, vars, controller_name).encode('UTF-8', invalid: :replace, undef: :replace)
7 end
8
9 def buffer
10 @buffer ||= ''
11 end
12 end
13 end
14 end
0 require 'cms_scanner/formatter/buffer'
1
2 module CMSScanner
3 # Formatter
4 module Formatter
5 # Module to be able to do Formatter.load() & Formatter.availables
6 # and do that as well when the Formatter is included in another module
7 module ClassMethods
8 # @param [ String ] format
9 # @param [ Array<String> ] custom_views
10 #
11 # @return [ Formatter::Base ]
12 def load(format = nil, custom_views = nil)
13 format ||= 'cli'
14 custom_views ||= []
15
16 f = const_get(format.tr('-', '_').camelize).new
17 custom_views.each { |v| f.views_directories << v }
18 f
19 end
20
21 # @return [ Array<String> ] The list of the available formatters (except the Base one)
22 # @note: the #load method above should then be used to create the associated formatter
23 def availables
24 formatters = NS::Formatter.constants.select do |const|
25 name = NS::Formatter.const_get(const)
26 name.is_a?(Class) && name != NS::Formatter::Base
27 end
28
29 formatters.map { |sym| sym.to_s.underscore.dasherize }
30 end
31 end
32
33 extend ClassMethods
34
35 def self.included(base)
36 base.extend(ClassMethods)
37 end
38
39 # This module should be implemented in the code which uses this Framework to
40 # be able to override/implements instance methods for all the Formatters
41 # w/o having to include/write the methods in each formatters.
42 #
43 # Example: to override the #views_directories (see the wpscan-v3/lib/wpscan/formatter.rb)
44 module InstanceMethods
45 end
46
47 # Base Formatter
48 class Base
49 attr_reader :controller_name
50
51 def initialize
52 # Can't put this at the top level of the class, due to the NS::
53 extend NS::Formatter::InstanceMethods
54 end
55
56 # @return [ String ] The underscored name of the class
57 def format
58 self.class.name.demodulize.underscore
59 end
60
61 # @return [ Boolean ]
62 def user_interaction?
63 format == 'cli'
64 end
65
66 # @return [ String ] The underscored format to use as a base
67 def base_format; end
68
69 # @return [ Array<String> ]
70 def formats
71 [format, base_format].compact
72 end
73
74 # This is called after the scan
75 # and used in some formatters (e.g JSON)
76 # to indent results
77 def beautify; end
78
79 # @see #render
80 def output(tpl, vars = {}, controller_name = nil)
81 puts render(tpl, vars, controller_name)
82 end
83
84 # @param [ String ] tpl
85 # @param [ Hash ] vars
86 # @param [ String ] controller_name
87 def render(tpl, vars = {}, controller_name = nil)
88 template_vars(vars)
89 @controller_name = controller_name if controller_name
90
91 # '-' is used to disable new lines when -%> is used
92 # See http://www.ruby-doc.org/stdlib-2.1.1/libdoc/erb/rdoc/ERB.html
93 ERB.new(File.read(view_path(tpl)), nil, '-').result(binding)
94 end
95
96 # @param [ Hash ] vars
97 #
98 # @return [ Void ]
99 def template_vars(vars)
100 vars.each do |key, value|
101 instance_variable_set("@#{key}", value) unless key == :views_directories
102 end
103 end
104
105 # @param [ String ] tpl
106 #
107 # @return [ String ] The path of the view
108 def view_path(tpl)
109 if tpl[0, 1] == '@' # Global Template
110 tpl = tpl.delete('@')
111 else
112 raise 'The controller_name can not be nil' unless controller_name
113
114 tpl = "#{controller_name}/#{tpl}"
115 end
116
117 raise "Wrong tpl format: '#{tpl}'" unless tpl =~ %r{\A[\w/_]+\z}
118
119 views_directories.reverse_each do |dir|
120 formats.each do |format|
121 potential_file = File.join(dir, format, "#{tpl}.erb")
122
123 return potential_file if File.exist?(potential_file)
124 end
125 end
126
127 raise "View not found for #{format}/#{tpl}"
128 end
129
130 # @return [ Array<String> ] The directories to look into for views
131 def views_directories
132 @views_directories ||= [
133 APP_DIR, NS::APP_DIR,
134 File.join(Dir.home, ".#{NS.app_name}"), File.join(Dir.pwd, ".#{NS.app_name}")
135 ].uniq.reduce([]) { |acc, elem| acc << Pathname.new(elem).join('views').to_s }
136 end
137 end
138 end
139 end
0 # @param [ String ] file The file path
1 def redirect_output_to_file(file)
2 $stdout.reopen(file, 'w')
3 $stdout.sync = true
4 $stderr.reopen($stdout) # Not sure if this is needed
5 end
6
7 # @return [ Integer ] The memory of the current process in Bytes
8 def memory_usage
9 `ps -o rss= -p #{Process.pid}`.to_i * 1024 # ps returns the value in KB
10 end
0 # Hack of the Numeric class
1 class Numeric
2 # @return [ String ] A human readable string of the value
3 def bytes_to_human
4 units = %w[B KB MB GB TB]
5 e = abs.zero? ? abs : (Math.log(abs) / Math.log(1024)).floor
6 s = format('%.3f', (abs.to_f / 1024**e))
7
8 s.sub(/\.?0*$/, ' ' + units[e])
9 end
10 end
0 require 'ruby-progressbar/outputs/null'
1
2 module CMSScanner
3 # Adds the feature to log message sent to #log
4 # So they can be retrieved at some point, like after a password attack with a JSON output
5 # which won't display the progressbar but still call #log for errors etc
6 class ProgressBarNullOutput < ::ProgressBar::Outputs::Null
7 # @retutn [ Array<String> ]
8 def logs
9 @logs ||= []
10 end
11
12 # Override of parent method
13 # @return [ Array<String> ] return the logs when no string provided
14 def log(string = nil)
15 return logs if string.nil?
16
17 logs << string
18 end
19 end
20 end
0 module PublicSuffix
1 # Monkey Patch to include the match logic
2 class Domain
3 # For Sanity
4 def ==(other)
5 name == other.name
6 end
7
8 # @return [ Boolean ]
9 #
10 def match(pattern)
11 pattern = PublicSuffix.parse(pattern) unless pattern.is_a?(PublicSuffix::Domain)
12
13 return name == pattern.name unless pattern.trd
14 return false unless tld == pattern.tld && sld == pattern.sld
15
16 matching_pattern?(pattern)
17 end
18
19 protected
20
21 # @rturn [ Boolean ]
22 def matching_pattern?(pattern)
23 pattern_trds = pattern.trd.split('.')
24 domain_trds = trd.split('.')
25
26 case pattern_trds.first
27 when '*'
28 pattern_trds[1..-1] == domain_trds[1..-1]
29 when '**'
30 pa = pattern_trds[1..-1]
31 pa_size = pa.size
32
33 domain_trds[domain_trds.size - pa_size, pa_size] == pa
34 else
35 name == pattern.name
36 end
37 end
38 end
39 end
0 module CMSScanner
1 # References related to the issue
2 module References
3 extend ActiveSupport::Concern
4
5 # See ActiveSupport::Concern
6 module ClassMethods
7 # @return [ Array<Symbol> ]
8 def references_keys
9 @references_keys ||= %i[cve secunia osvdb exploitdb url metasploit packetstorm securityfocus]
10 end
11 end
12
13 # @param [ Hash ] refs
14 def references=(refs)
15 @references = {}
16
17 self.class.references_keys.each do |key|
18 @references[key] = [*refs[key]].map(&:to_s) if refs.key?(key)
19 end
20 end
21
22 # @return [ Hash ]
23 def references
24 @references ||= {}
25 end
26
27 # @return [ Array<String> ] All the references URLs
28 def references_urls
29 cve_urls + secunia_urls + osvdb_urls + exploitdb_urls + urls + msf_urls +
30 packetstorm_urls + securityfocus_urls
31 end
32
33 # @return [ Array<String> ] The CVEs
34 def cves
35 references[:cve] || []
36 end
37
38 # @return [ Array<String> ]
39 def cve_urls
40 cves.reduce([]) { |acc, elem| acc << cve_url(elem) }
41 end
42
43 # @return [ String ] The URL to the CVE
44 def cve_url(cve)
45 "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-#{cve}"
46 end
47
48 # @return [ Array<String> ] The Secunia IDs
49 def secunia_ids
50 references[:secunia] || []
51 end
52
53 # @return [ Array<String> ]
54 def secunia_urls
55 secunia_ids.reduce([]) { |acc, elem| acc << secunia_url(elem) }
56 end
57
58 # @return [ String ] The URL to the Secunia advisory
59 def secunia_url(id)
60 "https://secunia.com/advisories/#{id}/"
61 end
62
63 # @return [ Array<String> ] The OSVDB IDs
64 def osvdb_ids
65 references[:osvdb] || []
66 end
67
68 # @return [ Array<String> ]
69 def osvdb_urls
70 osvdb_ids.reduce([]) { |acc, elem| acc << osvdb_url(elem) }
71 end
72
73 # @return [ String ] The URL to the ExploitDB advisory
74 def osvdb_url(id)
75 "http://osvdb.org/show/osvdb/#{id}"
76 end
77
78 # @return [ Array<String> ] The ExploitDB ID
79 def exploitdb_ids
80 references[:exploitdb] || []
81 end
82
83 # @return [ Array<String> ]
84 def exploitdb_urls
85 exploitdb_ids.reduce([]) { |acc, elem| acc << exploitdb_url(elem) }
86 end
87
88 # @return [ String ]
89 def exploitdb_url(id)
90 "https://www.exploit-db.com/exploits/#{id}/"
91 end
92
93 # @return [ String<Array> ]
94 def urls
95 references[:url] || []
96 end
97
98 # @return [ Array<String> ] The metasploit modules
99 def msf_modules
100 references[:metasploit] || []
101 end
102
103 # @return [ Array<String> ]
104 def msf_urls
105 msf_modules.reduce([]) { |acc, elem| acc << msf_url(elem) }
106 end
107
108 # @return [ String ] The URL to the metasploit module page
109 def msf_url(mod)
110 "https://www.rapid7.com/db/modules/#{mod.sub(%r{^/}, '')}"
111 end
112
113 # @return [ Array<String> ] The Packetstormsecurity IDs
114 def packetstorm_ids
115 @packetstorm_ids ||= references[:packetstorm] || []
116 end
117
118 # @return [ Array<String> ]
119 def packetstorm_urls
120 packetstorm_ids.reduce([]) { |acc, elem| acc << packetstorm_url(elem) }
121 end
122
123 # @return [ String ]
124 def packetstorm_url(id)
125 "http://packetstormsecurity.com/files/#{id}/"
126 end
127
128 # @return [ Array<String> ] The Security Focus IDs
129 def securityfocus_ids
130 references[:securityfocus] || []
131 end
132
133 # @return [ Array<String> ]
134 def securityfocus_urls
135 securityfocus_ids.reduce([]) { |acc, elem| acc << securityfocus_url(elem) }
136 end
137
138 # @return [ String ]
139 def securityfocus_url(id)
140 "http://www.securityfocus.com/bid/#{id}/"
141 end
142 end
143 end
0 module CMSScanner
1 # Scope system logic
2 class Target < WebSite
3 # @note Comments are deleted to avoid cache generation details
4 #
5 # @param [ Typhoeus::Response, String ] page
6 #
7 # @return [ String ] The md5sum of the page
8 def self.page_hash(page)
9 page = NS::Browser.get(page, followlocation: true) unless page.is_a?(Typhoeus::Response)
10
11 # Removes comments and script tags before computing the hash
12 # to remove any potential cached stuff
13 html = Nokogiri::HTML(page.body)
14 html.xpath('//script|//comment()').each(&:remove)
15
16 Digest::MD5.hexdigest(html)
17 end
18
19 # @return [ String ] The hash of the homepage
20 def homepage_hash
21 @homepage_hash ||= self.class.page_hash(url)
22 end
23
24 # @note This is used to detect potential custom 404 responding with a 200
25 # @return [ String ] The hash of a 404
26 def error_404_hash
27 @error_404_hash ||= self.class.page_hash(non_existant_page_url)
28 end
29
30 # @return [ String ] The URL of an unlikely existant page
31 def non_existant_page_url
32 uri.join(Digest::MD5.hexdigest(rand(999_999_999).to_s) + '.html').to_s
33 end
34
35 # @param [ Typhoeus::Response, String ] page
36 # @return [ Boolean ] Wether or not the page is a the homepage or a 404 based on its md5sum
37 def homepage_or_404?(page)
38 md5sum = self.class.page_hash(page)
39
40 [homepage_hash, error_404_hash].include?(md5sum)
41 end
42 end
43 end
0 module CMSScanner
1 class Target < WebSite
2 module Platform
3 # Some PHP specific implementation
4 module PHP
5 DEBUG_LOG_PATTERN = /\[[^\]]+\] PHP (?:Warning|Error|Notice):/
6 FPD_PATTERN = /Fatal error:.+? in (.+?) on/
7 ERROR_LOG_PATTERN = /PHP Fatal error/i
8
9 # @param [ String ] path
10 # @param [ Regexp ] pattern
11 # @param [ Hash ] params The request params
12 #
13 # @return [ Boolean ]
14 def log_file?(path, pattern, params = {})
15 # Only the first 700 bytes of the file are retrieved to avoid getting enture log file
16 # which can be huge (~ 2Go)
17 res = NS::Browser.get(url(path), params.merge(headers: { 'range' => 'bytes=0-700' }))
18
19 res.body =~ pattern ? true : false
20 end
21
22 # @param [ String ] path
23 # @param [ Hash ] params The request params
24 #
25 # @return [ Boolean ] true if url(path) is a debug log, false otherwise
26 def debug_log?(path, params = {})
27 log_file?(path, DEBUG_LOG_PATTERN, params)
28 end
29
30 # @param [ String ] path
31 # @param [ Hash ] params The request params
32 #
33 # @return [ Boolean ] Wether or not url(path) is an error log file
34 def error_log?(path, params = {})
35 log_file?(path, ERROR_LOG_PATTERN, params)
36 end
37
38 # @param [ String ] path
39 # @param [ Hash ] params The request params
40 #
41 # @return [ Boolean ] true if url(path) contains a FPD, false otherwise
42 def full_path_disclosure?(path = nil, params = {})
43 !full_path_disclosure_entries(path, params).empty?
44 end
45
46 # @param [ String ] path
47 # @param [ Hash ] params The request params
48 #
49 # @return [ Array<String> ] The FPD found, or an empty array if none
50 def full_path_disclosure_entries(path = nil, params = {})
51 res = NS::Browser.get(url(path), params)
52
53 res.body.scan(FPD_PATTERN).flatten
54 end
55 end
56 end
57 end
58 end
0 require 'cms_scanner/target/platform/php'
0 module CMSScanner
1 # Scope system logic
2 class Target < WebSite
3 # @return [ Array<PublicSuffix::Domain, String> ]
4 def scope
5 @scope ||= Scope.new
6 end
7
8 # @param [ String ] url An absolute URL
9 #
10 # @return [ Boolean ] true if the url given is in scope
11 def in_scope?(url)
12 scope.include?(Addressable::URI.parse(url.strip).host)
13 rescue StandardError
14 false
15 end
16
17 # @param [ Typhoeus::Response ] res
18 # @param [ String ] xpath
19 #
20 # @yield [ String, Nokogiri::XML::Element ] The in scope url and its associated tag
21 #
22 # @return [ Array<String> ] The in scope absolute URLs detected in the response's body
23 def in_scope_urls(res, xpath = '//@href|//@src|//@data-src')
24 found = []
25
26 urls_from_page(res, xpath) do |url, tag|
27 next unless in_scope?(url)
28
29 yield url, tag if block_given?
30
31 found << url
32 end
33
34 found
35 end
36
37 # Scope Implementation
38 class Scope
39 # @return [ Array<PublicSuffix::Domain ] The valid domains in scope
40 def domains
41 @domains ||= []
42 end
43
44 # @return [ Array<String> ] The invalid domains in scope (such as IP addresses etc)
45 def invalid_domains
46 @invalid_domains ||= []
47 end
48
49 def <<(element)
50 if PublicSuffix.valid?(element)
51 domains << PublicSuffix.parse(element)
52 else
53 invalid_domains << element
54 end
55 end
56
57 # @return [ Boolean ] Wether or not the host is in the scope
58 def include?(host)
59 if PublicSuffix.valid?(host)
60 domain = PublicSuffix.parse(host)
61
62 domains.each { |d| return true if domain.match(d) }
63 else
64 invalid_domains.each { |d| return true if host == d }
65 end
66
67 false
68 end
69 end
70 end
71 end
0 module CMSScanner
1 class Target < WebSite
2 module Server
3 # Some Apche specific implementation
4 module Apache
5 # @param [ String ] path
6 # @param [ Hash ] params The request params
7 #
8 # @return [ Symbol ] :Apache
9 def server(_path = nil, _params = {})
10 :Apache
11 end
12
13 # @param [ String ] path
14 # @param [ Hash ] params The request params
15 #
16 # @return [ Array<String> ] The first level of directories/files listed,
17 # or an empty array if none
18 def directory_listing_entries(path = nil, params = {})
19 super(path, params, 'td a')
20 end
21 end
22 end
23 end
24 end
0 module CMSScanner
1 class Target < WebSite
2 module Server
3 # Generic Server methods
4 module Generic
5 # @param [ String ] path
6 # @param [ Hash ] params The request params
7 #
8 # @return [ Symbol ] The detected remote server (:Apache, :IIS, :Nginx)
9 def server(path = nil, params = {})
10 headers = headers(path, params)
11
12 return unless headers
13
14 case headers[:server]
15 when /\Aapache/i
16 :Apache
17 when /\AMicrosoft-IIS/i
18 :IIS
19 when /\Anginx/
20 :Nginx
21 end
22 end
23
24 # @param [ String ] path
25 # @param [ Hash ] params The request params
26 #
27 # @return [ Hash ] The headers
28 def headers(path = nil, params = {})
29 # The HEAD method might be rejected by some servers ... maybe switch to GET ?
30 NS::Browser.head(url(path), params).headers
31 end
32
33 # @param [ String ] path
34 # @param [ Hash ] params The request params
35 #
36 # @return [ Boolean ] true if url(path) has the directory
37 # listing enabled, false otherwise
38 def directory_listing?(path = nil, params = {})
39 res = NS::Browser.get(url(path), params)
40
41 res.code == 200 && res.body =~ /<h1>Index of/ ? true : false
42 end
43
44 # @param [ String ] path
45 # @param [ Hash ] params The request params
46 # @param [ String ] selector
47 # @param [ Regexp ] ignore
48 #
49 # @return [ Array<String> ] The first level of directories/files listed,
50 # or an empty array if none
51 def directory_listing_entries(
52 path = nil, params = {},
53 selector = 'pre a', ignore = /parent directory/i
54 )
55 return [] unless directory_listing?(path, params)
56
57 found = []
58
59 NS::Browser.get(url(path), params).html.css(selector).each do |node|
60 entry = node.text.to_s
61
62 next if entry =~ ignore
63
64 found << entry
65 end
66
67 found
68 end
69 end
70 end
71 end
72 end
0 module CMSScanner
1 class Target < WebSite
2 module Server
3 # Some IIS specific implementation
4 module IIS
5 # @param [ String ] path
6 # @param [ Hash ] params The request params
7 #
8 # @return [ Symbol ] :IIS
9 def server(_path = nil, _params = {})
10 :IIS
11 end
12
13 # @param [ String ] path
14 # @param [ Hash ] params The request params
15 #
16 # @return [ Boolean ] true if url(path) has the directory
17 # listing enabled, false otherwise
18 def directory_listing?(path = nil, params = {})
19 res = NS::Browser.get(url(path), params)
20
21 res.code == 200 && res.body =~ %r{<H1>#{uri.host} - /} ? true : false
22 end
23 end
24 end
25 end
26 end
0 module CMSScanner
1 class Target < WebSite
2 module Server
3 # Some Nginx specific implementation
4 module Nginx
5 # @param [ String ] path
6 # @param [ Hash ] params The request params
7 #
8 # @return [ Symbol ] :Nginx
9 def server(_path = nil, _params = {})
10 :Nginx
11 end
12
13 # @param [ String ] path
14 # @param [ Hash ] params The request params
15 #
16 # @return [ Array<String> ] The first level of directories/files listed,
17 # or an empty array if none
18 def directory_listing_entries(path = nil, params = {})
19 super(path, params, 'pre a', /\A\.\./i)
20 end
21 end
22 end
23 end
24 end
0 require 'cms_scanner/target/server/generic'
1 require 'cms_scanner/target/server/apache'
2 require 'cms_scanner/target/server/iis'
3 require 'cms_scanner/target/server/nginx'
0 require 'cms_scanner/web_site'
1 require 'cms_scanner/target/platform'
2 require 'cms_scanner/target/server'
3 require 'cms_scanner/target/scope'
4 require 'cms_scanner/target/hashes'
5
6 module CMSScanner
7 # Target to Scan
8 class Target < WebSite
9 include Server::Generic
10
11 # @param [ String ] url
12 # @param [ Hash ] opts
13 # @option opts [ Array<PublicSuffix::Domain, String> ] :scope
14 def initialize(url, opts = {})
15 super(url, opts)
16
17 scope << uri.host
18 [*opts[:scope]].each { |s| scope << s }
19 end
20
21 # @param [ Hash ] opts
22 #
23 # @return [ Findings ]
24 def interesting_findings(opts = {})
25 @interesting_findings ||= NS::Finders::InterestingFindings::Base.find(self, opts)
26 end
27
28 # Weteher or not vulnerabilities have been found.
29 # Used to set the exit code of the script
30 # and it should be overriden in the implementation
31 #
32 # @return [ Boolean ]
33 def vulnerable?
34 false
35 end
36
37 # @param [ String ] xpath
38 # @param [ Regexp ] pattern
39 # @param [ Typhoeus::Response, String ] page
40 #
41 # @return [ Array<Array<MatchData, Nokogiri::XML::Element>> ]
42 # @yield [ MatchData, Nokogiri::XML::Element ]
43 def xpath_pattern_from_page(xpath, pattern, page = nil)
44 page = NS::Browser.get(url(page)) unless page.is_a?(Typhoeus::Response)
45 matches = []
46
47 page.html.xpath(xpath).each do |node|
48 next unless node.text.strip =~ pattern
49
50 yield Regexp.last_match, node if block_given?
51
52 matches << [Regexp.last_match, node]
53 end
54
55 matches
56 end
57
58 # @param [ Regexp ] pattern
59 # @param [ Typhoeus::Response, String ] page
60 #
61 # @return [ Array<Array<MatchData, Nokogiri::XML::Comment>> ]
62 # @yield [ MatchData, Nokogiri::XML::Comment ]
63 def comments_from_page(pattern, page = nil)
64 xpath_pattern_from_page('//comment()', pattern, page) do |match, node|
65 yield match, node if block_given?
66 end
67 end
68
69 # @param [ Regexp ] pattern
70 # @param [ Typhoeus::Response, String ] page
71 #
72 # @return [ Array<Array<MatchData, Nokogiri::XML::Element>> ]
73 # @yield [ MatchData, Nokogiri::XML::Element ]
74 def javascripts_from_page(pattern, page = nil)
75 xpath_pattern_from_page('//script', pattern, page) do |match, node|
76 yield match, node if block_given?
77 end
78 end
79
80 # @param [ Typhoeus::Response, String ] page
81 # @param [ String ] xpath
82 #
83 # @yield [ String, Nokogiri::XML::Element ] The url and its associated tag
84 #
85 # @return [ Array<String> ] The absolute URLs detected in the response's body from the HTML tags
86 def urls_from_page(page = nil, xpath = '//@href|//@src|//@data-src')
87 page = NS::Browser.get(url(page)) unless page.is_a?(Typhoeus::Response)
88 found = []
89
90 page.html.xpath(xpath).each do |node|
91 attr_value = node.text.to_s
92
93 next unless attr_value && !attr_value.empty?
94
95 node_uri = begin
96 uri.join(attr_value.strip)
97 rescue StandardError
98 # Skip potential malformed URLs etc.
99 next
100 end
101
102 node_uri_string = node_uri.to_s
103
104 next unless node_uri.host
105
106 yield node_uri_string, node.parent if block_given? && !found.include?(node_uri_string)
107
108 found << node_uri_string
109 end
110
111 found.uniq
112 end
113 end
114 end
0 module Typhoeus
1 # Ensure a clean abort of hydra
2 # See https://github.com/typhoeus/typhoeus/issues/439
3 class Hydra
4 def abort
5 super
6 run
7 end
8 end
9 end
0 module Typhoeus
1 # Custom Response class
2 class Response
3 # @return [ Nokogiri::HTML ] The response's body parsed by Nokogiri::HTML
4 def html
5 @html ||= Nokogiri::HTML(body.encode('UTF-8', invalid: :replace, undef: :replace))
6 end
7
8 # @return [ Nokogiri::XML ] The response's body parsed by Nokogiri::XML
9 def xml
10 @xml ||= Nokogiri::XML(body.encode('UTF-8', invalid: :replace, undef: :replace))
11 end
12 end
13 end
0 # Version
1 module CMSScanner
2 VERSION = '0.0.40'.freeze
3 end
0 module CMSScanner
1 # Generic Vulnerability
2 class Vulnerability
3 include References
4
5 attr_reader :title, :type, :fixed_in
6
7 # @param [ String ] title
8 # @param [ Hash ] references
9 # @option references [ Array<String>, String ] cve
10 # @option references [ Array<String>, String ] secunia
11 # @option references [ Array<String>, String ] osvdb
12 # @option references [ Array<String>, String ] exploitdb
13 # @option references [ Array<String> ] url URL(s) to related advisories etc
14 # @option references [ Array<String>, String ] metasploit The related metasploit module(s)
15 # @param [ String ] type
16 # @param [ String ] fixed_in
17 def initialize(title, references = {}, type = nil, fixed_in = nil)
18 @title = title
19 @type = type
20 @fixed_in = fixed_in
21
22 self.references = references
23 end
24
25 # param [ Vulnerability ] other
26 #
27 # @return [ Boolean ]
28 def ==(other)
29 title == other.title &&
30 type == other.type &&
31 references == other.references &&
32 fixed_in == other.fixed_in
33 end
34 end
35 end
0 module CMSScanner
1 # WebSite Implementation
2 class WebSite
3 attr_reader :uri, :opts
4
5 # @param [ String ] site_url
6 # @param [ Hash ] opts
7 def initialize(site_url, opts = {})
8 self.url = site_url.dup
9 @opts = opts
10 end
11
12 def url=(site_url)
13 # Add a trailing slash to the site url
14 site_url << '/' if site_url[-1, 1] != '/'
15
16 # Use the validator to ensure the site_url has a correct format
17 OptParseValidator::OptURL.new([]).validate(site_url)
18
19 @uri = Addressable::URI.parse(site_url).normalize
20 end
21
22 # Used for convenience
23 #
24 # URI.encode is preferered over Addressable::URI.encode as it will encode
25 # leading # character:
26 # URI.encode('#t#') => %23t%23
27 # Addressable::URI.encode('#t#') => #t%23
28 #
29 # @param [ String ] path Optional path to merge with the uri
30 #
31 # @return [ String ]
32 def url(path = nil)
33 return @uri.to_s unless path
34
35 @uri.join(URI.encode(path)).to_s
36 end
37
38 attr_writer :homepage_res
39
40 # @return [ Typhoeus::Response ]
41 #
42 # As webmock does not support redirects mocking, coverage is ignored
43 # :nocov:
44 def homepage_res
45 @homepage_res ||= NS::Browser.get_and_follow_location(url)
46 end
47 # :nocov:
48
49 # @return [ String ]
50 def homepage_url
51 @homepage_url ||= homepage_res.effective_url
52 end
53
54 # Checks if the remote website is up.
55 #
56 # @param [ String ] path
57 #
58 # @return [ Boolean ]
59 def online?(path = nil)
60 NS::Browser.get(url(path)).code.nonzero? ? true : false
61 end
62
63 # @param [ String ] path
64 #
65 # @return [ Boolean ]
66 def http_auth?(path = nil)
67 NS::Browser.get(url(path)).code == 401
68 end
69
70 # @param [ String ] path
71 #
72 # @return [ Boolean ]
73 def access_forbidden?(path = nil)
74 NS::Browser.get(url(path)).code == 403
75 end
76
77 # @param [ String ] path
78 #
79 # @return [ Boolean ]
80 def proxy_auth?(path = nil)
81 NS::Browser.get(url(path)).code == 407
82 end
83
84 # @param [ String ] url
85 #
86 # @return [ String ] The redirection url or nil
87 #
88 # As webmock does not support redirects mocking, coverage is ignored
89 # :nocov:
90 def redirection(url = nil)
91 url ||= @uri.to_s
92
93 return unless [301, 302].include?(NS::Browser.get(url).code)
94
95 res = NS::Browser.get(url, followlocation: true)
96
97 res.effective_url == url ? nil : res.effective_url
98 end
99 # :nocov:
100 end
101 end
0 # Gems
1 require 'typhoeus'
2 require 'nokogiri'
3 require 'yajl/json_gem'
4 require 'public_suffix'
5 require 'addressable/uri'
6 require 'ruby-progressbar'
7 require 'opt_parse_validator'
8 require 'active_support/concern'
9 require 'active_support/inflector'
10 # Standard Libs
11 require 'erb'
12 require 'uri'
13 require 'fileutils'
14 require 'pathname'
15 require 'xmlrpc/client'
16 # Monkey Patches
17 require 'cms_scanner/typhoeus/response' # Adds a Response#html using Nokogiri to parse the body
18 require 'cms_scanner/typhoeus/hydra' # https://github.com/typhoeus/typhoeus/issues/439
19 require 'cms_scanner/public_suffix/domain' # Adds a Domain#match method and logic, used in scope stuff
20 require 'cms_scanner/numeric' # Adds a Numeric#bytes_to_human
21 require 'cms_scanner/progressbar_null_output'
22 # Custom Libs
23 require 'cms_scanner/helper'
24 require 'cms_scanner/exit_code'
25 require 'cms_scanner/errors/http'
26 require 'cms_scanner/cache/typhoeus'
27 require 'cms_scanner/target'
28 require 'cms_scanner/browser'
29 require 'cms_scanner/version'
30 require 'cms_scanner/controller'
31 require 'cms_scanner/controllers'
32 require 'cms_scanner/formatter'
33 require 'cms_scanner/references'
34 require 'cms_scanner/finders'
35 require 'cms_scanner/vulnerability'
36
37 # Module
38 module CMSScanner
39 APP_DIR = Pathname.new(__FILE__).dirname.join('..', 'app').expand_path
40 NS = self
41
42 # Number of requests performed to display at the end of the scan
43 Typhoeus.on_complete do |response|
44 next if response.cached?
45
46 self.total_requests += 1
47
48 NS::Browser.instance.trottle!
49 end
50
51 # Module to be able to use these class methods when the CMSScanner
52 # is included in another module
53 module ClassMethods
54 # @return [ Integer ]
55 def total_requests
56 @@total_requests ||= 0
57 end
58
59 # @param [ Integer ]
60 def total_requests=(value)
61 @@total_requests = value
62 end
63
64 # The lowercase name of the scanner
65 # Mainly used in directory paths like the default cookie-jar file and
66 # path to load the cli options from files
67 #
68 # @return [ String ]
69 def app_name
70 to_s.underscore
71 end
72 end
73
74 extend ClassMethods
75
76 def self.included(base)
77 remove_const(:NS)
78 const_set(:NS, base)
79
80 base.extend(ClassMethods)
81 super(base)
82 end
83
84 # Scan
85 class Scan
86 attr_reader :run_error
87
88 def initialize
89 controllers << NS::Controller::Core.new
90
91 exit_hook
92
93 yield self if block_given?
94 end
95
96 # @return [ Controllers ]
97 def controllers
98 @controllers ||= NS::Controllers.new
99 end
100
101 def run
102 controllers.run
103 rescue OptParseValidator::NoRequiredOption => e
104 @run_error = e
105
106 formatter.output('@usage', msg: e.message)
107 rescue StandardError, SignalException => e
108 @run_error = e
109
110 formatter.output('@scan_aborted',
111 reason: e.is_a?(Interrupt) ? 'Canceled by User' : e.message,
112 trace: e.backtrace,
113 verbose: controllers.first.parsed_options[:verbose])
114 ensure
115 Browser.instance.hydra.abort
116
117 formatter.beautify
118 end
119
120 # Used for convenience
121 # @See Formatter
122 def formatter
123 controllers.first.formatter
124 end
125
126 # @return [ Hash ]
127 def datastore
128 controllers.first.datastore
129 end
130
131 # Hook to be able to have an exit code returned
132 # depending on the findings / errors
133 def exit_hook
134 at_exit do
135 exit(run_error_exit_code) if run_error
136
137 controller = controllers.first
138
139 # The parsed_option[:url] must be checked to avoid raising erros when only -h/-v are given
140 exit(NS::ExitCode::VULNERABLE) if controller.parsed_options[:url] && controller.target.vulnerable?
141 exit(NS::ExitCode::OK)
142 end
143 end
144
145 # @return [ Integer ] The exit code related to the run_error
146 def run_error_exit_code
147 return NS::ExitCode::CLI_OPTION_ERROR if run_error.is_a?(OptParseValidator::Error) ||
148 run_error.is_a?(OptionParser::ParseError)
149
150 return NS::ExitCode::INTERRUPTED if run_error.is_a?(Interrupt)
151
152 NS::ExitCode::ERROR
153 end
154 end
155 end
156
157 require "#{CMSScanner::APP_DIR}/app"
0 require 'spec_helper'
1
2 describe CMSScanner::Controller::Core do
3 subject(:core) { described_class.new }
4 let(:target_url) { 'http://example.com/' }
5 let(:cli_args) { "--url #{target_url}" }
6 let(:parsed_options) { rspec_parsed_options(cli_args) }
7
8 before do
9 CMSScanner::Browser.reset
10 described_class.parsed_options = parsed_options
11 end
12
13 describe '#cli_options' do
14 its(:cli_options) { should_not be_empty }
15 its(:cli_options) { should be_a Array }
16
17 it 'contaisn the expected options' do
18 expect(core.cli_options.map(&:to_sym)).to match_array(
19 %i[
20 banner cache_dir cache_ttl clear_cache connect_timeout cookie_jar cookie_string
21 detection_mode disable_tls_checks format headers help http_auth ignore_main_redirect
22 max_threads output proxy proxy_auth random_user_agent request_timeout scope
23 throttle url user_agent user_agents_list verbose version vhost
24 ]
25 )
26 end
27 end
28
29 describe '#setup_cache' do
30 context 'when no cache_dir supplied (or default)' do
31 it 'returns nil' do
32 expect(core.setup_cache).to eq nil
33 end
34 end
35
36 context 'when cache_dir' do
37 let(:cli_args) { "#{super()} --cache-dir #{CACHE}" }
38 let(:cache) { Typhoeus::Config.cache }
39 let(:storage) { File.join(parsed_options[:cache_dir], Digest::MD5.hexdigest(target_url)) }
40
41 before { core.setup_cache }
42 after { Typhoeus::Config.cache = nil }
43
44 it 'sets up the cache' do
45 expect(cache).to be_a CMSScanner::Cache::Typhoeus
46 expect(cache.storage_path).to eq storage
47 end
48 end
49 end
50
51 describe 'maybe_output_banner_help_and_version' do
52 before { described_class.option_parser = 'spec' }
53
54 context 'when --no-banner' do
55 let(:cli_args) { "#{super()} --no-banner" }
56
57 it 'calls output' do
58 expect(core.formatter).to_not receive(:output)
59
60 expect { core.maybe_output_banner_help_and_version }.to_not raise_error
61 end
62 end
63
64 context 'when --help' do
65 let(:cli_args) { '--help' }
66
67 it 'calls the output' do
68 expect(core.formatter).to receive(:output).with('banner', { verbose: nil }, 'core')
69 expect(core.formatter).to receive(:output).with('help', hash_including(help: 'spec'), 'core')
70
71 expect { core.maybe_output_banner_help_and_version }.to raise_error(SystemExit)
72 end
73 end
74
75 context 'when --version' do
76 let(:cli_args) { "#{super()} --version" }
77
78 it 'calls the output' do
79 expect(core.formatter).to receive(:output).with('banner', { verbose: nil }, 'core')
80 expect(core.formatter).to receive(:output).with('version', { verbose: nil }, 'core')
81
82 expect { core.maybe_output_banner_help_and_version }.to raise_error(SystemExit)
83 end
84 end
85 end
86
87 describe '#before_scan' do
88 context 'when --no-banner' do
89 let(:cli_args) { "#{super()} --no-banner" }
90
91 before { expect(core.formatter).to_not receive(:output) }
92
93 it 'does not raise an error when everything is fine' do
94 stub_request(:get, target_url).to_return(status: 200)
95
96 expect { core.before_scan }.to_not raise_error
97 end
98 end
99
100 context 'when --banner (default)' do
101 before { expect(core.formatter).to receive(:output) }
102
103 it 'does not raise an error when everything is fine' do
104 stub_request(:get, target_url).to_return(status: 200)
105
106 expect { core.before_scan }.to_not raise_error
107 end
108
109 it 'raise an error when the site is down' do
110 stub_request(:get, target_url).to_return(status: 0)
111
112 expect { core.before_scan }
113 .to raise_error(
114 CMSScanner::TargetDownError,
115 "The url supplied '#{target_url}' seems to be down ()"
116 )
117 end
118
119 context 'when it redirects' do
120 before do
121 stub_request(:get, target_url).to_return(status: 301, headers: { location: redirection })
122
123 expect(core.target).to receive(:homepage_res).and_return(Typhoeus::Response.new(effective_url: redirection))
124 end
125
126 context 'when out of scope' do
127 let(:redirection) { 'http://somewhere.com/' }
128
129 context 'when the --ignore-main-redirect is not supplied' do
130 it 'raises an error' do
131 expect { core.before_scan }.to raise_error(
132 CMSScanner::HTTPRedirectError,
133 "The URL supplied redirects to #{redirection}." \
134 ' Use the --ignore-main-redirect option to ignore the redirection and scan the target.'
135 )
136 end
137 end
138
139 context 'when the --ignore-main-redirect is supplied' do
140 let(:cli_args) { "#{super()} --ignore-main-redirect" }
141
142 it 'does not raise any error' do
143 expect { core.before_scan }.to_not raise_error
144 expect(core.target.url).to eql target_url
145
146 expect(core.target).to receive(:homepage_res).and_call_original
147 expect(core.target.homepage_url).to eql target_url
148 end
149 end
150 end
151
152 context 'when in scope' do
153 let(:redirection) { "#{target_url}home" }
154
155 it 'does not raise any error' do
156 expect { core.before_scan }.to_not raise_error
157 expect(core.target.url).to eql target_url
158
159 # expect(core.target).to receive(:homepage_res).and_call_original
160 # expect(core.target.homepage_url).to eql redirection # Doesn't work, no idea why :x
161 end
162 end
163 end
164
165 context 'when access is forbidden' do
166 before { stub_request(:get, target_url).to_return(status: 403) }
167
168 it 'raises an error' do
169 expect { core.before_scan }.to raise_error(CMSScanner::AccessForbiddenError)
170 end
171 end
172
173 # This is quite a mess (as Webmock doesn't issue itself another 401
174 # when credential are incorrect :/)
175 context 'when http authentication' do
176 context 'when no credentials' do
177 before { stub_request(:get, target_url).to_return(status: 401) }
178
179 it 'raises an error' do
180 expect { core.before_scan }.to raise_error(CMSScanner::HTTPAuthRequiredError)
181 end
182 end
183
184 context 'when credentials' do
185 context 'when valid' do
186 before { stub_request(:get, 'http://example.com').with(basic_auth: %w[user pass]) }
187
188 let(:cli_args) { "#{super()} --http-auth user:pass" }
189
190 it 'does not raise any error' do
191 expect { core.before_scan }.to_not raise_error
192 end
193 end
194
195 context 'when invalid' do
196 before do
197 stub_request(:get, 'http://example.com')
198 .with(basic_auth: %w[user p@ss]).to_return(status: 401)
199 end
200
201 let(:cli_args) { "#{super()} --http-auth user:p@ss" }
202
203 it 'raises an error' do
204 expect { core.before_scan }.to raise_error(CMSScanner::HTTPAuthRequiredError)
205 end
206 end
207 end
208 end
209
210 context 'when proxy authentication' do
211 before { stub_request(:get, target_url).to_return(status: 407) }
212
213 context 'when no credentials' do
214 it 'raises an error' do
215 expect { core.before_scan }.to raise_error(CMSScanner::ProxyAuthRequiredError)
216 end
217 end
218
219 context 'when invalid credentials' do
220 let(:cli_args) { "#{super()} --proxy-auth user:p@ss" }
221
222 it 'raises an error' do
223 expect(CMSScanner::Browser.instance.proxy_auth).to eq(parsed_options[:proxy_auth])
224
225 expect { core.before_scan }.to raise_error(CMSScanner::ProxyAuthRequiredError)
226 end
227 end
228
229 context 'when valid credentials' do
230 before { stub_request(:get, target_url) }
231
232 let(:cli_args) { "#{super()} --proxy-auth user:pass" }
233
234 it 'raises an error' do
235 expect(CMSScanner::Browser.instance.proxy_auth).to eq(parsed_options[:proxy_auth])
236
237 expect { core.before_scan }.to_not raise_error
238 end
239 end
240 end
241 end
242 end
243
244 describe '#run' do
245 it 'calls the formatter with the correct parameters' do
246 expect(core.formatter).to receive(:output)
247 .with('started',
248 hash_including(:start_memory, :start_time, :verbose, url: target_url),
249 'core')
250
251 core.run
252 end
253 end
254
255 describe '#after_scan' do
256 let(:keys) { %i[verbose start_time stop_time start_memory elapsed used_memory] }
257
258 it 'calls the formatter with the correct parameters' do
259 # Call the #run once to ensure that @start_time & @start_memory are set
260 expect(core).to receive(:output).with('started', hash_including(url: target_url))
261 core.run
262
263 RSpec::Mocks.space.proxy_for(core).reset # Must reset due to the above statements
264
265 expect(core.formatter).to receive(:output)
266 .with('finished', hash_including(*keys), 'core')
267
268 core.after_scan
269 end
270 end
271 end
0 require 'spec_helper'
1
2 describe CMSScanner::Controller::InterestingFindings do
3 subject(:controller) { described_class.new }
4 let(:target_url) { 'http://example.com/' }
5 let(:cli_args) { "--url #{target_url}" }
6 let(:parsed_options) { rspec_parsed_options(cli_args) }
7
8 before do
9 CMSScanner::Browser.reset
10 described_class.parsed_options = parsed_options
11 end
12
13 its(:before_scan) { should be_nil }
14 its(:after_scan) { should be_nil }
15
16 describe '#cli_options' do
17 its(:cli_options) { should_not be_empty }
18 its(:cli_options) { should be_a Array }
19
20 it 'contains to correct options' do
21 expect(controller.cli_options.map(&:to_sym)).to eq [:interesting_findings_detection]
22 end
23 end
24
25 describe '#run' do
26 before do
27 expect(controller.target).to receive(:interesting_findings)
28 .with(
29 mode: parsed_options[:interesting_findings_detection] || parsed_options[:detection_mode]
30 ).and_return(stubbed)
31 end
32
33 after { controller.run }
34
35 %i[mixed passive aggressive].each do |mode|
36 context "when --detection-mode #{mode}" do
37 let(:cli_args) { "#{super()} --detection-mode #{mode}" }
38
39 context 'when no findings' do
40 let(:stubbed) { [] }
41
42 before { expect(controller.formatter).to_not receive(:output) }
43
44 it 'does not call the formatter' do
45 # Handled by the before statements above
46 end
47
48 context 'when --interesting-files-detection mode supplied' do
49 let(:cli_args) { "#{super()} --interesting-findings-detection passive" }
50
51 it 'gives the correct detection paramter' do
52 # Handled by before/after statements
53 end
54 end
55 end
56
57 context 'when findings' do
58 let(:stubbed) { ['yolo'] }
59
60 it 'calls the formatter with the correct parameter' do
61 expect(controller.formatter).to receive(:output)
62 .with('findings', hash_including(findings: stubbed), 'interesting_findings')
63 end
64 end
65 end
66 end
67 end
68 end
0 require 'spec_helper'
1
2 describe CMSScanner::Finders::InterestingFindings::FantasticoFileslist do
3 subject(:finder) { described_class.new(target) }
4 let(:target) { CMSScanner::Target.new(url) }
5 let(:url) { 'http://example.com/' }
6 let(:file) { url + 'fantastico_fileslist.txt' }
7 let(:fixtures) { FIXTURES_FINDERS.join('interesting_findings', 'fantastico_fileslist') }
8
9 describe '#url' do
10 its(:url) { should eq file }
11 end
12
13 describe '#aggressive' do
14 after do
15 stub_request(:get, file).to_return(status: status, body: body, headers: headers)
16
17 result = finder.aggressive
18
19 expect(result).to be_a CMSScanner::FantasticoFileslist if @expected
20 expect(result).to eql @expected
21 end
22
23 let(:body) { '' }
24 let(:headers) { { 'Content-Type' => 'text/html ' } }
25
26 context 'when 404' do
27 let(:status) { 404 }
28
29 it 'returns nil' do
30 @expected = nil
31 end
32 end
33
34 context 'when 200' do
35 let(:status) { 200 }
36
37 context 'when the body is empty' do
38 it 'returns nil' do
39 @expected = nil
40 end
41 end
42
43 context 'when not a text/plain Content-Type' do
44 let(:body) { 'not an empty body' }
45
46 it 'returns nil' do
47 @expected = nil
48 end
49 end
50
51 context 'when the body matches and Content-Type = text/plain' do
52 let(:body) { File.read(fixtures.join('fantastico_fileslist.txt')) }
53 let(:headers) { { 'Content-Type' => 'text/plain' } }
54
55 it 'returns the InterestingFinding result' do
56 @expected = CMSScanner::FantasticoFileslist.new(
57 file,
58 confidence: 70,
59 found_by: 'Fantastico Fileslist (Aggressive Detection)'
60 )
61 end
62 end
63 end
64 end
65 end
0 require 'spec_helper'
1
2 describe CMSScanner::Finders::InterestingFindings::Headers do
3 subject(:finder) { described_class.new(target) }
4 let(:target) { CMSScanner::Target.new(url) }
5 let(:url) { 'http://example.com/' }
6 let(:fixtures) { FIXTURES_FINDERS.join('interesting_findings', 'headers') }
7 let(:fixture) { fixtures.join('interesting.txt') }
8 let(:headers) { parse_headers_file(fixture) }
9
10 describe '#passive' do
11 before { stub_request(:get, url).to_return(headers: headers) }
12
13 after do
14 if @expected
15 result = finder.passive
16
17 expect(result).to be_a CMSScanner::Headers
18 expect(result).to eql @expected
19 end
20 end
21
22 context 'when no headers' do
23 let(:headers) { {} }
24
25 its(:passive) { should be nil }
26 end
27
28 context 'when headers' do
29 it 'returns the result' do
30 opts = { confidence: 100, found_by: 'Headers (Passive Detection)' }
31 @expected = CMSScanner::Headers.new(url, opts)
32 end
33 end
34 end
35 end
0 require 'spec_helper'
1
2 describe CMSScanner::Finders::InterestingFindings::RobotsTxt do
3 subject(:finder) { described_class.new(target) }
4 let(:target) { CMSScanner::Target.new(url) }
5 let(:url) { 'http://example.com/' }
6 let(:robots_txt) { url + 'robots.txt' }
7 let(:fixtures) { FIXTURES_FINDERS.join('interesting_findings', 'robots_txt') }
8
9 describe '#url' do
10 its(:url) { should eq robots_txt }
11 end
12
13 describe '#aggressive' do
14 after do
15 stub_request(:get, robots_txt).to_return(status: status, body: body)
16
17 result = finder.aggressive
18
19 expect(result).to be_a CMSScanner::RobotsTxt if @expected
20 expect(finder.aggressive).to eql @expected
21 end
22
23 let(:body) { '' }
24
25 context 'when 404' do
26 let(:status) { 404 }
27
28 it 'returns nil' do
29 @expected = nil
30 end
31 end
32
33 context 'when 200' do
34 let(:status) { 200 }
35
36 context 'when the body is empty' do
37 it 'returns nil' do
38 @expected = nil
39 end
40 end
41
42 context 'when the body matches a robots.txt' do
43 let(:body) { File.read(fixtures.join('robots.txt')) }
44
45 it 'returns the InterestingFinding result' do
46 @expected = CMSScanner::RobotsTxt.new(robots_txt,
47 confidence: 100,
48 found_by: 'Robots Txt (Aggressive Detection)')
49 end
50 end
51 end
52 end
53 end
0 require 'spec_helper'
1
2 describe CMSScanner::Finders::InterestingFindings::SearchReplaceDB2 do
3 subject(:finder) { described_class.new(target) }
4 let(:target) { CMSScanner::Target.new(url) }
5 let(:url) { 'http://example.com/' }
6 let(:file) { url + 'searchreplacedb2.php' }
7 let(:fixtures) { FIXTURES_FINDERS.join('interesting_findings', 'search_replace_db_2') }
8
9 describe '#url' do
10 its(:url) { should eq file }
11 end
12
13 describe '#aggressive' do
14 after do
15 stub_request(:get, file).to_return(status: status, body: body)
16
17 expect(finder.aggressive).to eql @expected
18 end
19
20 let(:body) { '' }
21
22 context 'when 404' do
23 let(:status) { 404 }
24
25 it 'returns nil' do
26 @expected = nil
27 end
28 end
29
30 context 'when 200' do
31 let(:status) { 200 }
32
33 context 'when the body is empty' do
34 it 'returns nil' do
35 @expected = nil
36 end
37 end
38
39 context 'when the body matches' do
40 let(:body) { File.read(fixtures.join('searchreplacedb2.php')) }
41
42 it 'returns the InterestingFinding result' do
43 @expected = CMSScanner::InterestingFinding.new(
44 file,
45 confidence: 100,
46 found_by: 'Search Replace Db2 (Aggressive Detection)'
47 )
48 end
49 end
50 end
51 end
52 end
0 require 'spec_helper'
1
2 describe CMSScanner::Finders::InterestingFindings::XMLRPC do
3 subject(:finder) { described_class.new(target) }
4 let(:target) { CMSScanner::Target.new(url) }
5 let(:url) { 'http://e.org/' }
6 let(:xml_rpc_url) { url + 'xmlrpc.php' }
7 let(:fixtures) { FIXTURES_FINDERS.join('interesting_findings', 'xml_rpc') }
8
9 describe '#potential_urls' do
10 its(:potential_urls) { should be_empty }
11 end
12
13 describe '#passive' do
14 before do
15 expect(finder).to receive(:passive_headers).and_return(headers_stub)
16 expect(finder).to receive(:passive_body).and_return(body_stub)
17 end
18
19 context 'when both passives return nil' do
20 let(:headers_stub) { nil }
21 let(:body_stub) { nil }
22
23 its(:passive) { should be_empty }
24 end
25
26 context 'when one passive is not nil' do
27 let(:headers_stub) { nil }
28 let(:body_stub) { 'test' }
29
30 its(:passive) { should eq %w[test] }
31 end
32 end
33
34 describe '#passive_headers' do
35 before { stub_request(:get, url).to_return(headers: headers) }
36
37 let(:headers) { {} }
38
39 context 'when no headers' do
40 its(:passive_headers) { should be_nil }
41 end
42
43 context 'when headers' do
44 context 'when URL is out of scope' do
45 let(:headers) { { 'X-Pingback' => 'http://ex.org/yolo' } }
46
47 its(:passive_headers) { should be_nil }
48 end
49
50 context 'when URL is in scope' do
51 let(:headers) { { 'X-Pingback' => xml_rpc_url } }
52
53 it 'adds the url to #potential_urls and returns the XMLRPC' do
54 result = finder.passive_headers
55
56 expect(finder.potential_urls).to eq [xml_rpc_url]
57
58 expect(result).to be_a CMSScanner::XMLRPC
59 expect(result).to eql CMSScanner::XMLRPC.new(
60 xml_rpc_url,
61 confidence: 30,
62 found_by: 'Headers (Passive Detection)'
63 )
64 end
65 end
66 end
67 end
68
69 describe '#passive_body' do
70 before { stub_request(:get, url).to_return(body: body) }
71
72 context 'when no link rel="pingback" tag' do
73 let(:body) { '' }
74
75 its(:passive_body) { should be_nil }
76 end
77
78 context 'when the tag is present' do
79 context 'when the URL is out of scope' do
80 let(:body) { File.read(fixtures.join('homepage_out_of_scope_pingback.html')) }
81
82 its(:passive_body) { should be_nil }
83 end
84
85 context 'when URL is in scope' do
86 let(:body) { File.read(fixtures.join('homepage_in_scope_pingback.html')) }
87 let(:expected_url) { 'http://e.org/wp/xmlrpc.php' }
88
89 it 'adds the URL to the #potential_urls and returns the XMLRPC' do
90 result = finder.passive_body
91
92 expect(finder.potential_urls).to eq [expected_url]
93
94 expect(result).to be_a CMSScanner::XMLRPC
95 expect(result).to eql CMSScanner::XMLRPC.new(
96 expected_url,
97 confidence: 30,
98 found_by: 'Link Tag (Passive Detection)'
99 )
100 end
101 end
102 end
103 end
104
105 describe '#aggressive' do
106 # Adds an out of scope URL which should be ignored
107 before { finder.potential_urls << 'htpp://ex.org' }
108
109 after do
110 stub_request(:get, xml_rpc_url).to_return(body: body)
111
112 expect(finder.aggressive).to eql @expected
113 end
114
115 context 'when the body does not match' do
116 let(:body) { '' }
117
118 it 'returns nil' do
119 @expected = nil
120 end
121 end
122
123 context 'when the body matches' do
124 let(:body) { File.read(fixtures.join('xmlrpc.php')) }
125
126 it 'returns the InterestingFinding result' do
127 @expected = CMSScanner::XMLRPC.new(
128 xml_rpc_url,
129 confidence: 100,
130 found_by: described_class::DIRECT_ACCESS
131 )
132 end
133 end
134 end
135 end
0 require 'spec_helper'
1
2 describe CMSScanner::Finders::InterestingFindings::Base do
3 it_behaves_like CMSScanner::Finders::IndependentFinder do
4 let(:expected_finders) { %w[Headers RobotsTxt FantasticoFileslist SearchReplaceDB2 XMLRPC] }
5 let(:expected_finders_class) { CMSScanner::Finders::IndependentFinders }
6 end
7
8 subject(:files) { described_class.new(target) }
9 let(:target) { CMSScanner::Target.new(url) }
10 let(:url) { 'http://example.com/' }
11 end
0 require 'spec_helper'
1
2 describe CMSScanner::Formatter::CliNoColour do
3 subject(:formatter) { described_class.new }
4
5 its(:format) { should eq 'cli' }
6 its(:user_interaction?) { should be true }
7
8 describe '#colorize' do
9 it 'returns the text w/o any colour' do
10 expect(formatter.red('Text')).to eq 'Text'
11 end
12 end
13 end
0 require 'spec_helper'
1
2 describe CMSScanner::Formatter::Cli do
3 subject(:formatter) { described_class.new }
4
5 its(:format) { should eq 'cli' }
6 its(:user_interaction?) { should be true }
7
8 describe '#bold, #red, #green, #amber, #blue, #colorize' do
9 it 'returns the correct bold string' do
10 expect(formatter.bold('Text')).to eq "\e[1mText\e[0m"
11 end
12
13 it 'returns the correct red string' do
14 expect(formatter.red('Text')).to eq "\e[31mText\e[0m"
15 end
16
17 it 'returns the correct green string' do
18 expect(formatter.green('Another Text')).to eq "\e[32mAnother Text\e[0m"
19 end
20
21 it 'returns the correct amber string' do
22 expect(formatter.amber('Text')).to eq "\e[33mText\e[0m"
23 end
24
25 it 'returns the correct blue string' do
26 expect(formatter.blue('Text')).to eq "\e[34mText\e[0m"
27 end
28 end
29
30 describe '#*_icon' do
31 {
32 info: "\e[32m[+]\e[0m",
33 notice: "\e[34m[i]\e[0m",
34 warning: "\e[33m[!]\e[0m",
35 critical: "\e[31m[!]\e[0m"
36 }.each do |icon_type, expected|
37 it "returns the correct #{icon_type} icon" do
38 expect(formatter.send("#{icon_type}_icon")).to eql expected
39 end
40 end
41 end
42 end
0 require 'spec_helper'
1
2 describe CMSScanner::Formatter::Json do
3 it_behaves_like CMSScanner::Formatter::Buffer
4
5 subject(:formatter) { described_class.new }
6 let(:output_file) { FIXTURES.join('output.txt') }
7
8 before { formatter.views_directories << FIXTURES_VIEWS }
9
10 its(:format) { should eq 'json' }
11 its(:user_interaction?) { should be false }
12
13 describe '#output' do
14 it 'puts the rendered text in the buffer' do
15 2.times { formatter.output('@render_me', test: 'Working') }
16
17 expect(formatter.buffer).to eq "\"test\": \"Working\",\n" * 2
18 end
19 end
20
21 describe '#beautify' do
22 it 'writes the buffer in the $stdout' do
23 2.times { formatter.output('@render_me', test: 'yolo') }
24
25 expect($stdout).to receive(:puts).with(JSON.pretty_generate(JSON.parse('{"test": "yolo"}')))
26 formatter.beautify
27 end
28
29 context 'when invalid UTF-8 chars' do
30 it 'tries to convert/replace them' do
31 formatter.output('@render_me', test: 'it’s'.encode('CP1252'))
32
33 expect($stdout).to receive(:puts).with(JSON.pretty_generate(JSON.parse('{"test": "it�s"}')))
34 formatter.beautify
35 end
36 end
37 end
38 end
0 require 'spec_helper'
1
2 describe CMSScanner::FantasticoFileslist do
3 subject(:file) { described_class.new(url) }
4 let(:url) { 'http://example.com/robots.txt' }
5 let(:fixtures) { FIXTURES_FINDERS.join('interesting_findings', 'fantastico_fileslist') }
6
7 describe '#interesting_entries' do
8 let(:headers) { { 'Content-Type' => 'text/plain; charset=utf-8' } }
9
10 after do
11 body = File.read(fixtures.join(fixture))
12
13 stub_request(:get, file.url).to_return(headers: headers, body: body)
14
15 expect(file.interesting_entries).to eq @expected
16 end
17
18 context 'when empty or / entries' do
19 let(:fixture) { 'fantastico_fileslist.txt' }
20
21 it 'ignores them and only returns the others' do
22 @expected = %w[data.sql admin.txt]
23 end
24 end
25 end
26
27 describe '#references' do
28 its(:references) { should_not be_nil }
29 end
30 end
0 require 'spec_helper'
1
2 describe CMSScanner::Headers do
3 subject(:file) { described_class.new(url) }
4 let(:url) { 'http://example.com/' }
5 let(:fixtures) { FIXTURES_FINDERS.join('interesting_findings', 'headers') }
6 let(:fixture) { fixtures.join('interesting.txt') }
7 let(:headers) { {} }
8
9 before { stub_request(:get, file.url).to_return(headers: headers) }
10
11 describe '#known_headers' do
12 it 'does not contains dupliactes' do
13 expect(file.known_headers).to eql file.known_headers.uniq
14 end
15 end
16
17 describe '#entries' do
18 after { expect(file.entries).to eq @expected if @expected }
19
20 context 'when no headers' do
21 its(:entries) { should eq({}) }
22 end
23
24 context 'when headers' do
25 let(:headers) { parse_headers_file(fixture) }
26
27 it 'returns the headers' do
28 @expected = headers
29 end
30 end
31 end
32
33 describe '#interesting_entries' do
34 after { expect(file.interesting_entries).to eq @expected if @expected }
35
36 context 'when interesting headers' do
37 let(:headers) { parse_headers_file(fixture) }
38
39 it 'returns an array with the headers' do
40 @expected = ['Server: nginx/1.1.19', 'X-Powered-By: ASP.NET, PHP', 'X-Article-Id: 12']
41 end
42 end
43
44 context 'when no interesting headers' do
45 let(:headers) { parse_headers_file(fixtures.join('no_interesting.txt')) }
46
47 its(:interesting_entries) { should eq [] }
48 end
49 end
50 end
0 require 'spec_helper'
1
2 describe CMSScanner::InterestingFinding do
3 it_behaves_like CMSScanner::Finders::Finding
4
5 subject(:finding) { described_class.new(url, opts) }
6 let(:opts) { {} }
7 let(:url) { 'http://example.com/' }
8 let(:fixtures) { FIXTURES_FINDERS.join('interesting_findings') }
9
10 describe '#to_s' do
11 context 'when no opts[:to_s]' do
12 its(:to_s) { should eql url }
13
14 context 'when setter used' do
15 it 'returns the value from the setter' do
16 finding.to_s = 'also works'
17
18 expect(finding.to_s).to eql 'also works'
19 end
20 end
21 end
22
23 context 'when opts[:to_s]' do
24 let(:opts) { super().merge(to_s: 'works') }
25
26 its(:to_s) { should eql 'works' }
27
28 context 'when setter used' do
29 it 'returns the value from the setter' do
30 finding.to_s = 'also works'
31
32 expect(finding.to_s).to eql 'also works'
33 end
34 end
35 end
36 end
37
38 describe '#entries' do
39 after do
40 stub_request(:get, finding.url).to_return(headers: headers, body: @body)
41
42 expect(finding.entries).to eq @expected
43 end
44
45 context 'when content-type matches text/plain' do
46 let(:headers) { { 'Content-Type' => 'text/plain; charset=utf-8' } }
47
48 it 'returns the finding content as an array w/o empty strings' do
49 @body = File.read(fixtures.join('file.txt'))
50 @expected = ['This is', 'a test file', 'with some content']
51 end
52 end
53
54 context 'when other content-type' do
55 let(:headers) { { 'Content-Type' => 'text.html; charset=utf-8' } }
56
57 it 'returns an empty array' do
58 @expected = []
59 end
60 end
61 end
62
63 describe '#==' do
64 context 'when same URL' do
65 context 'when the same #to_s' do
66 it 'returns true' do
67 expect(finding == described_class.new(url)).to be true
68 end
69 end
70
71 context 'when different #to_s' do
72 it 'returns false' do
73 expect(finding == described_class.new(url, to_s: 'another')).to be false
74 end
75 end
76 end
77
78 context 'when not the same URL' do
79 it 'returns false' do
80 expect(finding == described_class.new('http://e.org')).to be false
81 end
82 end
83 end
84
85 describe '#<=>' do
86 context 'when same URL' do
87 it 'returns 0' do
88 expect(finding <=> described_class.new(url)).to eql 0
89 end
90 end
91
92 context 'when the other URL <= current one' do
93 it 'returns 1' do
94 expect(finding <=> described_class.new('http://e.org')).to eql 1
95 end
96 end
97
98 context 'when the other URL >= current one' do
99 it 'returns -1' do
100 expect(finding <=> described_class.new('http://exi.org/')).to eql(-1)
101 end
102 end
103
104 context 'when using capitals' do
105 it 'returns ' do
106 expect(finding <=> described_class.new('Sftp://a.org')).to eql(-1)
107 end
108 end
109 end
110 end
0 require 'spec_helper'
1
2 describe CMSScanner::RobotsTxt do
3 subject(:file) { described_class.new(url) }
4 let(:url) { 'http://example.com/robots.txt' }
5 let(:fixtures) { FIXTURES_FINDERS.join('interesting_findings', 'robots_txt') }
6
7 describe '#interesting_entries' do
8 let(:headers) { { 'Content-Type' => 'text/plain; charset=utf-8' } }
9
10 after do
11 body = File.read(fixtures.join(fixture))
12
13 stub_request(:get, file.url).to_return(headers: headers, body: body)
14
15 expect(file.interesting_entries).to eq @expected
16 end
17
18 context 'when empty or / entries' do
19 let(:fixture) { 'robots.txt' }
20
21 it 'ignores them and only returns the others w/o duplicate' do
22 @expected = %w[/admin /public/home]
23 end
24 end
25 end
26 end
0 require 'spec_helper'
1
2 describe CMSScanner::User do
3 subject(:user) { described_class.new(username, opts) }
4 let(:username) { 'john' }
5 let(:opts) { {} }
6
7 describe '#new' do
8 its(:username) { should eql username }
9
10 context 'when opts' do
11 let(:opts) { super().merge(id: 12, password: 'passwd') }
12
13 its(:id) { should eql 12 }
14 its(:password) { should eql 'passwd' }
15 end
16 end
17
18 describe '#==' do
19 context 'when another object' do
20 it 'returns false' do
21 expect(user == username).to be false
22 end
23 end
24
25 context 'when the same username' do
26 it 'return true' do
27 expect(user == user.dup).to be true
28 end
29 end
30
31 context 'when not the same username' do
32 it 'returns false' do
33 expect(user == CMSScanner::User.new('test')).to be false
34 end
35 end
36 end
37
38 describe '#to_s' do
39 its(:to_s) { should eql username }
40 end
41 end
0 require 'spec_helper'
1
2 describe CMSScanner::Version do
3 it_behaves_like CMSScanner::Finders::Finding
4
5 subject(:version) { described_class.new(number, opts) }
6 let(:opts) { {} }
7 let(:number) { '1.0' }
8
9 its(:to_s) { should eql '1.0' }
10
11 describe '#number' do
12 its(:number) { should eql '1.0' }
13
14 context 'when float number supplied' do
15 let(:number) { 2.0 }
16
17 its(:number) { should eql '2.0' }
18 its(:to_s) { should eql '2.0' }
19 end
20
21 context 'when starting with a dot' do
22 let(:number) { '.2' }
23
24 its(:number) { should eql '0.2' }
25 end
26 end
27
28 describe '#<=>, #==, #>, #<' do
29 it 'returns true' do
30 expect(version == '1.0').to be true
31 expect(version == 1.0).to be true
32 expect(version == described_class.new('1.0')).to be true
33 expect(version > '0.9').to be true
34 expect(version < '2').to be true
35
36 expect(described_class.new('0.1') == '.1').to be true
37 expect(described_class.new('.1') == '0.1').to be true
38 end
39
40 it 'returns false' do
41 expect(version == '2.0').to be false
42 expect(version == described_class.new('2')).to be false
43 expect(version > '2.0').to be false
44 expect(version < '1.0').to be false
45
46 expect(version < 'gg').to be false
47 expect(version == '').to be false
48 expect(version == true).to be false
49 end
50 end
51 end
0 require 'spec_helper'
1
2 describe CMSScanner::XMLRPC do
3 subject(:xml_rpc) { described_class.new(url) }
4 let(:url) { 'http://example.com/xmlrpc' }
5 let(:fixtures) { FIXTURES_MODELS.join('xml_rpc') }
6
7 describe '#method_call' do
8 after do
9 request = xml_rpc.method_call(method, method_params, request_params)
10
11 expect(request).to be_a Typhoeus::Request
12 expect(request.options[:body]).to eql @expected_body
13
14 expect(request.options).to include(request_params) unless request_params.empty?
15 end
16
17 let(:method) { 'rpc-test' }
18 let(:method_params) { [] }
19 let(:request_params) { {} }
20
21 context 'when no params' do
22 it 'sets the correct body in the request' do
23 @expected_body = '<?xml version="1.0" ?><methodCall>'
24 @expected_body << "<methodName>#{method}</methodName><params/>"
25 @expected_body << "</methodCall>\n"
26 end
27 end
28
29 context 'when method_params and request_params' do
30 let(:method_params) { %w[p1 p2] }
31 let(:request_params) { { spec_key: 'yolo' } }
32
33 it 'set the correct body in the request' do
34 @expected_body = '<?xml version="1.0" ?><methodCall>'
35 @expected_body << "<methodName>#{method}</methodName><params>"
36 @expected_body << '<param><value><string>p1</string></value></param>'
37 @expected_body << '<param><value><string>p2</string></value></param>'
38 @expected_body << "</params></methodCall>\n"
39 end
40 end
41 end
42
43 describe '#multi_call' do
44 after do
45 request = xml_rpc.multi_call(methods_and_params, request_params)
46
47 expect(request).to be_a Typhoeus::Request
48 expect(request.options[:body]).to eql @expected_body
49
50 expect(request.options).to include(request_params) unless request_params.empty?
51 end
52
53 let(:methods_and_params) { [%w[m1 p1 p2], %w[m2 p1], %w[m3]] }
54 let(:request_params) { {} }
55
56 it 'sets the correct body in the request' do
57 @expected_body = File.read(fixtures.join('multi_call.xml'))
58 end
59 end
60
61 describe '#available_methods' do
62 after do
63 expect(xml_rpc.available_methods).to eql @expected
64
65 # When calling a second time, should not redo a request
66 expect(xml_rpc).to_not receive(:method_call)
67 expect(xml_rpc.available_methods).to eql @expected
68 end
69
70 context 'when an empty response' do
71 it 'returns an empty array' do
72 stub_request(:post, xml_rpc.url).and_return(body: '')
73
74 @expected = []
75 end
76 end
77
78 context 'when a correct response' do
79 it 'returns the expected array' do
80 stub_request(:post, xml_rpc.url).and_return(
81 body: '<?xml version="1.0" ?><methodResponse><params><param><value><array><data>'\
82 '<value><string>system.listMethods</string></value>'\
83 '<value><string>m1</string></value>'\
84 '</data></array></value></param></params></methodResponse>'
85 )
86
87 @expected = %w[system.listMethods m1]
88 end
89 end
90 end
91
92 describe '#enabled?' do
93 context 'when no methods available' do
94 it 'returns false' do
95 expect(xml_rpc).to receive(:available_methods).and_return([])
96 expect(xml_rpc.enabled?).to be false
97 end
98 end
99
100 context 'when methods available' do
101 it 'returns true' do
102 expect(xml_rpc).to receive(:available_methods).and_return(%w[m1 m2])
103 expect(xml_rpc.enabled?).to be true
104 end
105 end
106 end
107 end
0 require 'spec_helper'
1
2 describe 'App::Views' do
3 let(:target_url) { 'http://e.org/' }
4 let(:fixtures) { SPECS.join('output') }
5
6 # CliNoColour is used to test the CLI output to avoid the painful colours
7 # in the expected output.
8 %i[JSON CliNoColour].each do |formatter|
9 context "when #{formatter}" do
10 it_behaves_like 'App::Views::Core'
11 it_behaves_like 'App::Views::InterestingFindings'
12
13 let(:parsed_options) { { url: target_url, format: formatter.to_s.underscore.dasherize } }
14
15 before do
16 controller.class.parsed_options = parsed_options
17 # Resets the formatter to ensure the correct one is loaded
18 controller.class.class_variable_set(:@@formatter, nil)
19 end
20
21 after do
22 view_filename = defined?(expected_view) ? expected_view : view
23 view_filename = "#{view_filename}.#{formatter.to_s.underscore.downcase}"
24 controller_dir = controller.class.to_s.demodulize.underscore.downcase
25 expected_output = @expected_output || File.read(fixtures.join(controller_dir, view_filename))
26
27 expect($stdout).to receive(:puts).with(expected_output)
28
29 controller.output(view, @tpl_vars)
30 controller.formatter.beautify # Mandatory to be able to test formatter such as JSON
31 end
32 end
33 end
34 end
0 # Ignore everything in this directory
1 *
2 # Except this file
3 !.gitignore
0 module CMSScanner
1 # Dummy Finding
2 class DummyFinding
3 include Finders::Finding
4
5 attr_reader :r
6
7 def initialize(finding, opts = {})
8 @r = finding
9 parse_finding_options(opts)
10 end
11
12 def ==(other)
13 r == other.r
14 end
15
16 def eql?(other)
17 r == other.r && confidence == other.confidence && found_by == other.found_by
18 end
19
20 def to_s
21 r
22 end
23 end
24 end
0 require 'dummy_finding'
1
2 module CMSScanner
3 module Finders
4 module Independent
5 # Dummy Test Finder
6 class DummyFinder < Finder
7 def passive(_opts = {})
8 # the nil is there to ensure such value is ignored
9 [DummyFinding.new('test', found_by: found_by), nil]
10 end
11
12 def aggressive(_opts = {})
13 DummyFinding.new('test', confidence: 100, found_by: 'override')
14 end
15 end
16
17 # No aggressive result finder
18 class NoAggressiveResult < Finder
19 def passive(_opts = {})
20 DummyFinding.new('spotted', confidence: 10, found_by: found_by)
21 end
22 end
23 end
24 end
25 end
0 require 'dummy_finding'
1
2 module CMSScanner
3 module Finders
4 module Unique
5 # Dummy Test Finder
6 class Dummy < Finder
7 def passive(_opts = {})
8 # the nil is there to ensure such value is ignored
9 [DummyFinding.new('v1', found_by: found_by), nil]
10 end
11
12 def aggressive(_opts = {})
13 DummyFinding.new('v1', confidence: 100, found_by: 'override')
14 end
15 end
16
17 # No aggressive result
18 class NoAggressive < Finder
19 def passive(_opts = {})
20 DummyFinding.new('v2', confidence: 10, found_by: found_by)
21 end
22 end
23
24 # Dummy2
25 class Dummy2 < Finder
26 def aggressive(_opts = {})
27 DummyFinding.new('v1', confidence: 90)
28 end
29 end
30 end
31 end
32 end
0 includes
1 misc
2 modules
3 .htaccess
4 CHANGELOG.txt
5 cron.php
6 data.sql
7 admin.txt
8 robots.txt
9 update.php
10 UPGRADE.txt
11 xmlrpc.php
0 This is
1 a test file
2
3 with some content
0 HTTP/1.1 200 OK
1 Server: nginx/1.1.19
2 X-Powered-By: ASP.NET
3 X-Powered-By: PHP
4 Date: Thu
5 Content-Type: text/plain; charset=utf-8
6 Connection: keep-alive
7 X-Content-Type-Options: nosniff
8 Cache-Control: s-maxage=3600, must-revalidate, max-age=0
9 X-Article-Id: 12
10 X-Language: en
11 Last-Modified: Tue, 26 Nov 2013 17:39:43 GMT
12 Vary: X-Subdomain,X-Use-HHVM
13 X-Varnish: 11545
14 Age: 206
15 Set-Cookie: GeoIP=; Path=/; Domain=.test.lo
0 HTTP/1.1 200 OK
1 Date: Thu
2 Content-Type: text/plain; charset=utf-8
3 Connection: keep-alive
4 X-Content-Type-Options: nosniff
5 Cache-Control: s-maxage=3600, must-revalidate, max-age=0
6 X-Language: en
7 Last-Modified: Tue, 26 Nov 2013 17:39:43 GMT
8 Vary: X-Subdomain,X-Use-HHVM
9 X-Varnish: 15154
10 Age: 206
11 Set-Cookie: GeoIP=; Path=/; Domain=.test.lo
0 # advertising-related bots:
1 User-agent: Mediapartners-Google*
2 Disallow: /
3
4 # Wikipedia work bots:
5 User-agent: IsraBot
6 Disallow:
7
8 Disallow: /admin
9 Allow: /public/home
10
11 # Because why not :o
12 Allow: /admin
0 <!DOCTYPE html>
1 <html xmlns="http://www.w3.org/1999/xhtml" xmlns:dc="http://purl.org/dc/terms/" dir="ltr" lang="en-US">
2 <head profile="http://gmpg.org/xfn/11">
3 <title>Search and replace DB.</title>
4 <style type="text/css">
5 body {
6 background-color: #E5E5E5;
7 color: #353231;
8 font: 14px/18px "Gill Sans MT","Gill Sans",Calibri,sans-serif;
9 }
10
11 p {
12 line-height: 18px;
13 margin: 18px 0;
14 max-width: 520px;
15 }
16
17 p.byline {
18 margin: 0 0 18px 0;
19 padding-bottom: 9px;
20 border-bottom: 1px dashed #999999;
21 max-width: 100%;
22 }
23
24 h1,h2,h3 {
25 font-weight: normal;
26 line-height: 36px;
27 font-size: 24px;
28 margin: 9px 0;
29 text-shadow: 1px 1px 0 rgba(255, 255, 255, 0.8);
30 }
31
32 h2 {
33 font-weight: normal;
34 line-height: 24px;
35 font-size: 21px;
36 margin: 9px 0;
37 text-shadow: 1px 1px 0 rgba(255, 255, 255, 0.8);
38 }
39
40 h3 {
41 font-weight: normal;
42 line-height: 18px;
43 margin: 9px 0;
44 text-shadow: 1px 1px 0 rgba(255, 255, 255, 0.8);
45 }
46
47 a {
48 -moz-transition: color 0.2s linear 0s;
49 color: #DE1301;
50 text-decoration: none;
51 font-weight: normal;
52 }
53
54 a:visited {
55 -moz-transition: color 0.2s linear 0s;
56 color: #AE1301;
57 }
58
59 a:hover, a:visited:hover {
60 -moz-transition: color 0.2s linear 0s;
61 color: #FE1301;
62 text-decoration: underline;
63 }
64
65 #container {
66 display:block;
67 width: 768px;
68 padding: 10px;
69 margin: 0px auto;
70 border:solid 10px 0px 0px 0px #ccc;
71 border-top: 18px solid #DE1301;
72 background-color: #F5F5F5;
73 }
74
75 fieldset {
76 border: 0 none;
77 }
78
79 .error {
80 border: solid 1px #c00;
81 padding: 5px;
82 background-color: #FFEBE8;
83 text-align: center;
84 margin-bottom: 10px;
85 }
86
87 label {
88 display:block;
89 line-height: 18px;
90 cursor: pointer;
91 }
92
93 select.multi,
94 input.text {
95 margin-bottom: 1em;
96 display:block;
97 width: 90%;
98 }
99
100 select.multi {
101 height: 144px;
102 }
103
104
105 input.button {
106 }
107
108 div.help {
109 border-top: 1px dashed #999999;
110 margin-top: 9px;
111 }
112
113 </style>
114 </head>
115 <body>
116 <div id="container">
117
118 <h1>Safe Search Replace</h1>
119 <p class="byline">by interconnect/<strong>it</strong></p>
120 <h2>Database details</h2>
121 <form action="searchreplacedb2.php?step=3" method="post">
122 <fieldset>
123 <p>
124 <label for="host">Server Name:</label>
125 <input class="text" type="text" name="host" id="host" value="localhost" />
126 </p>
127
128 <p>
129 <label for="data">Database Name:</label>
130 <input class="text" type="text" name="data" id="data" value="" />
131 </p>
132
133 <p>
134 <label for="user">Username:</label>
135 <input class="text" type="text" name="user" id="user" value="" />
136 </p>
137
138 <p>
139 <label for="pass">Password:</label>
140 <input class="text" type="password" name="pass" id="pass" value="" />
141 </p>
142
143 <p>
144 <label for="pass">Charset:</label>
145 <input class="text" type="text" name="char" id="char" value="" />
146 </p>
147 <input type="submit" class="button" value="Submit DB details" /> </fieldset>
148 </form> <div class="help">
149 <h4><a href="http://interconnectit.com/">interconnect/it</a> <a href="http://interconnectit.com/124/search-and-replace-for-wordpress-databases/">Safe Search and Replace on Database with Serialized Data v2.0.0</a></h4>
150 <p>This developer/sysadmin tool helps solve the problem of doing a search and replace on a
151 WordPress site when doing a migration to a domain name with a different length.</p>
152
153 <p><style="color:red">WARNING!</strong> Take a backup first, and carefully test the results of this code.
154 If you don't, and you vape your data then you only have yourself to blame.
155 Seriously. And if you're English is bad and you don't fully understand the
156 instructions then STOP. Right there. Yes. Before you do any damage.
157
158 <h2>Don't Forget to Remove Me!</h3>
159
160 <p style="color:red">Delete this utility from your
161 server after use. It represents a major security threat to your database if
162 maliciously used.</p>
163
164 <h2>Use Of This Script Is Entirely At Your Own Risk</h2>
165
166 <p> We accept no liability from the use of this tool.</p>
167
168 <p>If you're not comfortable with this kind of stuff, get an expert, like us, to do
169 this work for you. You do this ENTIRELY AT YOUR OWN RISK! We accept no responsibility
170 if you mess up your data. There is NO UNDO here!</p>
171
172 <p>The easiest way to use it is to copy your site's files and DB to the new location.
173 You then, if required, fix up your .htaccess and wp-config.php appropriately. Once
174 done, run this script, select your tables (in most cases all of them) and then
175 enter the search replace strings. You can press back in your browser to do
176 this several times, as may be required in some cases.</p>
177
178 <p>Of course, you can use the script in many other ways - for example, finding
179 all references to a company name and changing it when a rebrand comes along. Or
180 perhaps you changed your name. Whatever you want to search and replace the code will help.</p>
181
182 <p><a href="http://interconnectit.com/124/search-and-replace-for-wordpress-databases/">Got feedback on this script? Come tell us!</a>
183
184 </div>
185 </div>
186 </body>
187 </html>
0 <head>
1 <meta charset="UTF-8">
2 <meta name="viewport" content="width=device-width">
3 <title>WordPress 4.0 | Just another WordPress site</title>
4 <link rel="profile" href="http://gmpg.org/xfn/11">
5 <link rel="pingback" href="http://e.org/wp/xmlrpc.php">
6 </head>
0 <head>
1 <meta charset="UTF-8">
2 <meta name="viewport" content="width=device-width">
3 <title>WordPress 4.0 | Just another WordPress site</title>
4 <link rel="profile" href="http://gmpg.org/xfn/11">
5 <link rel="pingback" href="http://wp.lab/wordpress-4.0/xmlrpc.php">
6 </head>
0 XML-RPC server accepts POST requests only.
0 <?xml version="1.0" ?><methodCall><methodName>system.multicall</methodName><params><param><value><array><data><value><struct><member><name>methodName</name><value><string>m1</string></value></member><member><name>params</name><value><array><data><value><string>p1</string></value><value><string>p2</string></value></data></array></value></member></struct></value><value><struct><member><name>methodName</name><value><string>m2</string></value></member><member><name>params</name><value><array><data><value><string>p1</string></value></data></array></value></member></struct></value><value><struct><member><name>methodName</name><value><string>m3</string></value></member><member><name>params</name><value><array><data/></array></value></member></struct></value></data></array></value></param></params></methodCall>
(New empty file)
0 <!DOCTYPE html>
1 <html lang="en-US" class="no-js">
2 <head>
3 <meta charset="UTF-8">
4 <meta name="viewport" content="width=device-width">
5 <link rel="profile" href="http://gmpg.org/xfn/11">
6 <link rel="pingback" href="http://wp.lab/wordpress-4.1.1/xmlrpc.php">
7 <!--[if lt IE 9]>
8 <script src="http://wp.lab/wordpress-4.1.1/wp-content/themes/twentyfifteen/js/html5.js"></script>
9 <![endif]-->
10 <script>(function(){document.documentElement.className='js'})();</script>
11 <title>WP 4.1.1 | Just another WordPress site</title>
12 <meta name='robots' content='noindex,follow' />
13
14 <!-- All in One SEO Pack 2.2.5.1 by Michael Torbert of Semper Fi Web Design -->
15 <link rel="canonical" href="http://wp.lab/wordpress-4.1.1/" />
16 <!-- /all in one seo pack -->
17 <!--[if lt IE 9]>
18 <link rel='stylesheet' id='twentyfifteen-ie-css' href='http://wp.lab/wordpress-4.1.1/wp-content/themes/twentyfifteen/css/ie.css?ver=20141010' type='text/css' media='all' />
19 <![endif]-->
20 <!--[if lt IE 8]>
21 <link rel='stylesheet' id='twentyfifteen-ie7-css' href='http://wp.lab/wordpress-4.1.1/wp-content/themes/twentyfifteen/css/ie7.css?ver=20141010' type='text/css' media='all' />
22 <![endif]-->
23
24 <!-- .site-branding -->
25 <!-- .site-header -->
26
27 </body>
28 </html>
0 <!DOCTYPE html>
1 <html lang="en-US" class="no-js">
2 <head>
3 <meta charset="UTF-8">
4 <meta name="viewport" content="width=device-width">
5 <link rel="profile" href="http://gmpg.org/xfn/11">
6 <link rel="pingback" href="http://wp.lab/wordpress-4.1.1/xmlrpc.php">
7
8 <script type="text/javascript">var _version = '1.2.4';</script>
9
0 [11-Oct-2012 00:00:00] PHP Notice: Undefined index: ec_email in /var/www/wp/wp-content/plugins/easy-contact/econtact.php on line 33
1 [11-Oct-2012 00:00:00] PHP Notice: Undefined index: ec_url in /var/www/wp/wp-content/plugins/easy-contact/econtact.php on line 34
0 [13-Jan-2009 01:53:25] PHP Fatal error: Class 'Log' not found in /home/****/public_html/lab/wp-content/plugins/fbconnect/Log/null.php on line 19
1 [13-Jan-2009 01:55:58] PHP Fatal error: Class 'Log' not found in /home/****/public_html/lab/wp-content/plugins/fbconnect/Log/file.php on line 20
2 [13-Jan-2009 02:13:34] PHP Fatal error: Class 'Log' not found in /home/****/public_html/lab/wp-content/plugins/fbconnect/Log/error_log.php on line 19
3 [15-Feb-2009 10:47:54] PHP Fatal error: Class 'Log' not found in /home/****/public_html/lab/wp-content/plugins/fbconnect/Log/error_log.php on line 19
4 [15-Feb-2009 11:36:15] PHP Fatal error: Class 'Log' not found in /home/****/public_html/lab/wp-content/plugins/fbconnect/Log/null.php on line 19
0
1 Fatal error: Call to undefined function _deprecated_file() in /short-path/rss-f.php on line 8
0 <a href="http://e.org/f.txt">Link</a>
1 <a href="http://e.org/f.txt">Link</a> <!-- Duplicates should be ignored -->
2
3 <a href="mailto:[email protected]">eMail me!</a>
4 <a href="jaVaScript:alert(2)">Click me Fool !</a>
5
6 <script src=" https://cdn.e.org/f2.js "></script> <!-- head & tail spaces should be removed -->
7
8 <script src="/script/s.js"></script>
9
10 <link rel="alternate" type="application/rss+xml" title="Spec" href="http://wp-lamp/robots.txt" />
11
12 <link rel="canonical" href="https://duckduckgo.com/">
13
14 <img src="http://out.of.scope.com/img.jpg" width="1000" height="288" alt="" />
15
16 <a href="">Empty Link</a>
17
18 <link rel="alternate" type="application/rss+xml" title="WordPress 4.1 &raquo; Feed" href="http://e.org/feed" />
19
20 <img src="//g.com/img.jpg" width="" height="" alt="" />
21
22 <img src="//out.of.scope.com/img.jpg" width="" height="" alt="" />
23
0 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
1 <html>
2 <head>
3 <title>Index of /wordpress-4.0/wp-content/plugins/wp-dbmanager</title>
4 </head>
5 <body>
6 <h1>Index of /wordpress-4.0/wp-content/plugins/wp-dbmanager</h1>
7 <table><tr><th><img src="/icons/blank.gif" alt="[ICO]"></th><th><a href="?C=N;O=D">Name</a></th><th><a href="?C=M;O=A">Last modified</a></th><th><a href="?C=S;O=A">Size</a></th><th><a href="?C=D;O=A">Description</a></th></tr><tr><th colspan="5"><hr></th></tr>
8 <tr><td valign="top"><img src="/icons/back.gif" alt="[DIR]"></td><td><a href="/wordpress-4.0/wp-content/plugins/">Parent Directory</a></td><td>&nbsp;</td><td align="right"> - </td><td>&nbsp;</td></tr>
9 <tr><td valign="top"><img src="/icons/unknown.gif" alt="[ ]"></td><td><a href="backup.php">backup.php</a></td><td align="right">07-Oct-2014 18:43 </td><td align="right"> 10K</td><td>&nbsp;</td></tr>
10 <tr><td valign="top"><img src="/icons/unknown.gif" alt="[ ]"></td><td><a href="database-empty.php">database-empty.php</a></td><td align="right">07-Oct-2014 18:43 </td><td align="right">3.9K</td><td>&nbsp;</td></tr>
11 <tr><th colspan="5"><hr></th></tr>
12 </table>
13 <address>Apache/2.2.16 (Debian) Server at wp.lab Port 80</address>
14 </body></html>
0 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
1 <html>
2 <head>
3 <title>Index of /wordpress-4.0/wp-content/plugins/wp-dbmanager</title>
4 </head>
5 <body>
6 <h1>Index of /wordpress-4.0/wp-content/plugins/wp-dbmanager</h1>
7 <table><tr><th><img src="/icons/blank.gif" alt="[ICO]"></th><th><a href="?C=N;O=D">Name</a></th><th><a href="?C=M;O=A">Last modified</a></th><th><a href="?C=S;O=A">Size</a></th><th><a href="?C=D;O=A">Description</a></th></tr></tr>
8 </table>
9 <address>Apache/2.2.16 (Debian) Server at wp.lab Port 80</address>
10 </body></html>
0 Date: Sun, 12 Oct 2014 19:44:42 GMT
1 Server: Apache/2.2.16 (Debian)
2 X-Powered-By: PHP/5.3.3-7+squeeze19
3 Vary: Accept-Encoding
4 Content-Type: text/html
0 Content-Length: 1027
1 Content-Type: text/html; charset=UTF-8
2 Server: Microsoft-IIS/7.5
3 X-Powered-By: ASP.NET
4 X-UA-Compatible: IE=EmulateIE7
5 Date: Sun, 12 Oct 2014 20:15:14 GMT
0 HTTP/1.1 200 OK
1 Server: nginx
2 Date: Sun, 15 Mar 2015 19:43:52 GMT
3 Content-Type: text/html
4 Connection: keep-alive
0 Date: Sun, 12 Oct 2014 19:44:42 GMT
1 Vary: Accept-Encoding
2 Content-Type: text/html
0 <html><head><title>e.org - /dir/</title></head><body><H1>e.org - /dir/</H1><hr>
1
2 <pre>10/8/2014 11:00 PM &lt;dir&gt; <A HREF="/sub-dir/">sub-dir</A>10/10/2014 10:00 PM 168 <A HREF="/web.config">web.config</A><br></pre><hr></body></html>
0 <html><head><title>e.org - /dir/</title></head><body><H1>e.org - /dir/</H1><hr>
1
2 <pre><A HREF="/">[To Parent Directory]</A><br><br> 10/8/2014 11:00 PM &lt;dir&gt; <A HREF="/sub-dir/">sub-dir</A>10/10/2014 10:00 PM 168 <A HREF="/web.config">web.config</A><br></pre><hr></body></html>
0 <html>
1 <head><title>Index of /fanart/</title></head>
2 <body bgcolor="white">
3 <h1>Index of /fanart/</h1><hr>
4 <hr></body>
5 </html>
0 <html>
1 <head><title>Index of /fanart/</title></head>
2 <body bgcolor="white">
3 <h1>Index of /fanart/</h1><hr><pre><a href="../">../</a>
4 <a href="1931/">1931/</a> 16-Jun-2013 05:46 -
5 <a href="720/">720/</a> 16-Jun-2013 05:46 -
6 <a href="down">down</a> 16-Jun-2013 05:46 53
7 </pre><hr></body>
8 </html>
0 <a href="http://e.org/f.txt">Link</a>
1 Duplicates should be ignored
2 <a href="http://e.org/f.txt">Link</a>
3
4 <a href="mailto:[email protected]">eMail me!</a>
5 <a href="jaVaScript:alert(2)">Click me Fool !</a>
6
7 Head and tail spaces should be removed
8 <script src=" https://cdn.e.org/f2.js "></script>
9
10 <script src="/script/s.js"></script>
11
12 <link rel="alternate" type="application/rss+xml" title="Spec" href="http://wp-lamp/feed.xml" />
13
14 <a href="">Empty Link should be ignored</a>
15
16 <img src="//g.com/img.jpg" width="" height="" alt="" />
17
18 <a href="http://">no host, should be ignored</a>
19
20 Don't parse that either
21 <img src="data:image/jpeg;base64,/9j/4AAQ/" />
22
23 <img class="fl-photo-img wp-image-608 size-full" src="data:image/png;base64,SNIPPED" alt="XXX" itemprop="image" height="10" width="100" data-src="//g.org/logo.png"
0 # Coments should be ignored
1 UA-1
2
3 UA-2
0 Test: <%= @var %>
1 <%= render('local') %>
2 <%= render('@global') %>
0 It <%= @test %>
1 Views Dirs: <%= @views_directories %>
0 Override the base/test.erb
0 "test": <%= @test.to_json %>,
1 <% if @var %>
2 "var": <%= @var.to_json %>
3 <% end %>
0 require 'spec_helper'
1
2 describe CMSScanner::Browser do
3 it_behaves_like described_class::Actions
4
5 subject(:browser) { described_class.instance(options) }
6 before { described_class.reset }
7 let(:options) { {} }
8 let(:default) do
9 {
10 headers: { 'User-Agent' => "CMSScanner v#{CMSScanner::VERSION}" },
11 accept_encoding: 'gzip, deflate',
12 method: :get
13 }
14 end
15
16 describe '#forge_request' do
17 it 'returns a Typhoeus::Request' do
18 expect(browser.forge_request('http://example.com')).to be_a Typhoeus::Request
19 end
20 end
21
22 describe '#default_request_params' do
23 its(:default_request_params) { should eq default }
24
25 context 'when some attributes are set' do
26 let(:options) do
27 {
28 cache_ttl: 200, connect_timeout: 10,
29 http_auth: { username: 'log', password: 'pwd' },
30 cookie_jar: '/tmp/cookie_jar.txt',
31 vhost: 'testing',
32 headers: { 'Test' => 'aa' },
33 proxy_auth: { username: 'u', password: 'pwd' },
34 disable_tls_checks: true
35 }
36 end
37
38 let(:expected) do
39 default.merge(
40 cache_ttl: 200, connecttimeout: 10,
41 userpwd: 'log:pwd', proxyuserpwd: 'u:pwd',
42 cookiejar: options[:cookie_jar], cookiefile: options[:cookie_jar],
43 ssl_verifypeer: false, ssl_verifyhost: 0
44 ).merge(headers: default[:headers].merge('Host' => 'testing', 'Test' => 'aa'))
45 end
46
47 its(:default_request_params) { should eq expected }
48 end
49 end
50
51 describe '#request_params' do
52 context 'when no param is given' do
53 its(:request_params) { should eq default }
54 end
55
56 context 'when params are supplied' do
57 let(:params) { { another_param: true, headers: { 'Accept' => 'None' } } }
58
59 it 'merges them (headers should be correctly merged)' do
60 expect(browser.request_params(params)).to eq default
61 .merge(params) { |key, oldval, newval| key == :headers ? oldval.merge(newval) : newval }
62 end
63
64 context 'when browser options' do
65 let(:options) { { proxy: 'http://127.0.0.1:8080', headers: { 'T' => 'a' } } }
66
67 it 'returns the correct hash' do
68 expect(browser.request_params(params)).to eq default
69 .merge(options) { |key, oldval, newval| key == :headers ? oldval.merge(newval) : newval }
70 .merge(params) { |key, oldval, newval| key == :headers ? oldval.merge(newval) : newval }
71 end
72 end
73 end
74 end
75
76 describe '#load_options' do
77 context 'when no options' do
78 it 'does not load anything' do
79 described_class::OPTIONS.each do |sym|
80 expected = case sym
81 when :user_agent
82 browser.default_user_agent
83 when :user_agents_list
84 File.join(CMSScanner::APP_DIR, 'user_agents.txt')
85 when :throttle
86 0.0
87 end
88
89 expect(browser.send(sym)).to eq expected
90 end
91 end
92 end
93
94 context 'when options are supplied' do
95 module CMSScanner
96 # Test accessor
97 class Browser
98 attr_accessor :test
99 end
100 end
101
102 let(:options) do
103 {
104 cache_ttl: 200, max_threads: 10, test: 'should not be set', throttle: 0,
105 user_agent: 'UA', proxy: false, user_agents_list: 'test.txt'
106 }
107 end
108
109 it 'merges the browser options only' do
110 described_class::OPTIONS.each do |sym|
111 expected = options.key?(sym) ? options[sym] : nil
112
113 expect(browser.send(sym)).to eq expected
114 end
115
116 expect(browser.test).to be nil
117 end
118 end
119 end
120
121 describe '#hydra' do
122 context 'when #max_threads is nil' do
123 its('hydra.max_concurrency') { should eq 1 }
124 end
125
126 context 'when #max_threads' do
127 let(:options) { { max_threads: 20 } }
128
129 its('hydra.max_concurrency') { should eq options[:max_threads] }
130 end
131 end
132
133 describe '#max_threads=' do
134 after do
135 browser.max_threads = @threads
136
137 expect(browser.max_threads).to eq @expected
138 expect(browser.hydra.max_concurrency).to eq @expected
139 end
140
141 context 'when <= 0' do
142 it 'sets max_threads to 1' do
143 @threads = -2
144 @expected = 1
145 end
146 end
147
148 context 'when > 0' do
149 it 'sets max_threads to 20' do
150 @threads = 20
151 @expected = @threads
152 end
153 end
154
155 context 'when throttle is used' do
156 let(:options) { { throttle: 2000 } }
157
158 it 'sets max_threads to 1' do
159 @threads = 20
160 @expected = 1
161 end
162 end
163 end
164
165 describe '#throttle=, #throttle' do
166 context 'when not used' do
167 let(:options) { { max_threads: 20 } }
168
169 its(:throttle) { should eql 0.0 }
170 its(:max_threads) { should eql 20 }
171 end
172
173 context 'when max_threads and throttle supplied as options' do
174 let(:options) { { max_threads: 20, throttle: 200 } }
175
176 its(:throttle) { should eql 0.2 }
177 its(:max_threads) { should eql 1 }
178 end
179
180 context 'when used' do
181 let(:options) { { max_threads: 10 } }
182
183 after do
184 browser.throttle = @throttle
185
186 expect(browser.throttle).to eql @throttle.to_i.abs / 1000.0 # This one is in seconds
187 expect(browser.max_threads).to eql 1
188 expect(browser.hydra.max_concurrency).to eql 1
189 end
190
191 context 'when a negative value is supplied' do
192 it 'uses the absolute value and set the max_threads to 1' do
193 @throttle = -100
194 end
195 end
196
197 context 'when a positive value is supplied' do
198 it 'sets and set the max_threads to 1' do
199 @throttle = 1000
200 end
201 end
202 end
203 end
204
205 describe '#user_agent' do
206 context 'when no --random-user-agent' do
207 context 'when no --user-agent' do
208 its(:user_agent) { should eql browser.default_user_agent }
209 end
210
211 context 'when --user-agent' do
212 let(:options) { super().merge(user_agent: 'Test UA') }
213
214 its(:user_agent) { should eql 'Test UA' }
215 end
216 end
217
218 context 'when --random-user-agent' do
219 let(:options) { super().merge(random_user_agent: true) }
220
221 it 'select a random UA in the user_agents' do
222 expect(browser.user_agent).to_not eql browser.default_user_agent
223 # Should not pick up a random one each time
224 expect(browser.user_agent).to eql browser.user_agent
225 end
226 end
227 end
228
229 describe '#user_agents' do
230 let(:options) { { user_agents_list: FIXTURES.join('user_agents.txt') } }
231
232 its(:user_agents) { should eql %w[UA-1 UA-2] }
233 end
234 end
0 require 'spec_helper'
1
2 describe CMSScanner::Cache::FileStore do
3 let(:cache_dir) { CACHE.join('cache_file_store').to_s }
4 subject(:cache) { described_class.new(cache_dir) }
5
6 before { FileUtils.rm_r(cache_dir, secure: true) if Dir.exist?(cache_dir) }
7 after { cache.clean }
8
9 describe '#new, #storage_path, #serializer' do
10 its(:serializer) { should be Marshal }
11 its(:storage_path) { should eq cache_dir }
12 end
13
14 describe '#clean' do
15 it 'removes all files from the cache dir' do
16 # let's create some files into the directory first
17 (0..5).each do |i|
18 File.new(File.join(cache.storage_path, "file_#{i}.txt"), File::CREAT)
19 end
20
21 expect(count_files_in_dir(cache.storage_path, 'file_*.txt')).to eq 6
22 cache.clean
23 expect(count_files_in_dir(cache.storage_path)).to eq 0
24 end
25 end
26
27 describe '#read_entry?' do
28 let(:key) { 'key1' }
29
30 after do
31 File.write(cache.entry_expiration_path(key), @expiration) if @expiration
32
33 expect(cache.read_entry(key)).to eq @expected
34 end
35
36 context 'when the entry does not exists' do
37 it 'returns nil' do
38 @expected = nil
39 end
40 end
41
42 context 'when the file is empty (marshal data too short error)' do
43 it 'returns nil' do
44 File.new(cache.entry_path(key), File::CREAT)
45
46 @expiration = Time.now.to_i + 200
47 @expected = nil
48 end
49 end
50
51 context 'when the entry has expired' do
52 it 'returns nil' do
53 @expiration = Time.now.to_i - 200
54 @expected = nil
55 end
56 end
57
58 context 'when the entry has not expired' do
59 it 'returns the entry' do
60 File.write(cache.entry_path(key), cache.serializer.dump('testing data'))
61
62 @expiration = Time.now.to_i + 600
63 @expected = 'testing data'
64 end
65 end
66 end
67
68 describe '#write_entry' do
69 after do
70 cache.write_entry(@key, @data, @ttl)
71 expect(cache.read_entry(@key)).to eq @expected
72 end
73
74 it 'should get the correct entry (string)' do
75 @ttl = 10
76 @key = 'some_key'
77 @data = 'Hello World !'
78 @expected = @data
79 end
80
81 context 'when cache_ttl <= 0' do
82 it 'does not write the entry' do
83 @ttl = 0
84 @key = 'another_key'
85 @data = 'Another Hello World !'
86 @expected = nil
87 end
88 end
89
90 context 'when cache_ttl is nil' do
91 it 'does not write the entry' do
92 @ttl = nil
93 @key = 'test'
94 @data = 'test'
95 @expected = nil
96 end
97 end
98 end
99 end
0 require 'spec_helper'
1
2 describe CMSScanner::Cache::Typhoeus do
3 subject(:cache) { described_class.new(cache_dir) }
4
5 let(:cache_dir) { CACHE.join('typhoeus_cache') }
6 let(:url) { 'http://example.com' }
7 let(:request) { Typhoeus::Request.new(url, cache_ttl: 20) }
8 let(:key) { request.hash.to_s }
9
10 describe '#get' do
11 it 'calls #read_entry' do
12 expect(cache).to receive(:read_entry).with(key)
13
14 cache.get(request)
15 end
16 end
17
18 describe '#set' do
19 let(:response) { Typhoeus::Response.new }
20
21 it 'calls #write_entry' do
22 expect(cache).to receive(:write_entry).with(key, response, request.cache_ttl)
23
24 cache.set(request, response)
25 end
26 end
27 end
0 require 'spec_helper'
1
2 describe CMSScanner do
3 let(:target_url) { 'http://wp.lab/' }
4
5 before do
6 scanner = CMSScanner::Scan.new
7 scanner.controllers.first.class.parsed_options = rspec_parsed_options("--url #{target_url}")
8 end
9
10 describe 'typhoeus_on_complete' do
11 before { CMSScanner.total_requests = 0 }
12
13 # TODO: find a way to test the cached requests which should not be counted
14 it 'returns the expected number of requests' do
15 stub_request(:get, /.*/)
16
17 CMSScanner::Browser.get(target_url)
18
19 expect(CMSScanner.total_requests).to eql 1
20 end
21 end
22
23 describe '#app_name' do
24 it 'returns the excpected string' do
25 expect(CMSScanner.app_name).to eql 'cms_scanner'
26 end
27 end
28 end
29
30 describe CMSScanner::Scan do
31 subject(:scanner) { described_class.new }
32 let(:controller) { CMSScanner::Controller }
33
34 before do
35 Object.send(:remove_const, :ARGV)
36 Object.const_set(:ARGV, [])
37 end
38
39 describe '#new, #controllers' do
40 its(:controllers) { should eq([controller::Core.new]) }
41 end
42
43 describe '#run' do
44 after do
45 scanner.run
46
47 if defined?(run_error)
48 expect(scanner.run_error).to be_a run_error.class
49 expect(scanner.run_error.message).to eql run_error.message
50 end
51 end
52
53 it 'runs the controlllers and calls the formatter#beautify' do
54 hydra = CMSScanner::Browser.instance.hydra
55
56 expect(scanner.controllers).to receive(:run).ordered
57 expect(hydra).to receive(:abort).ordered
58 expect(scanner.formatter).to receive(:beautify).ordered
59 end
60
61 context 'when no required option supplied' do
62 it 'calls the formatter to display the usage view' do
63 expect(scanner.formatter).to receive(:output)
64 .with('@usage', msg: 'One of the following options is required: url, help, version')
65 end
66 end
67
68 context 'when an Interrupt is raised during the scan' do
69 it 'aborts the scan with the correct output' do
70 expect(scanner.controllers.option_parser).to receive(:results).and_return({})
71
72 expect(scanner.controllers.first)
73 .to receive(:before_scan)
74 .and_raise(Interrupt)
75
76 expect(scanner.formatter).to receive(:output)
77 .with('@scan_aborted', hash_including(reason: 'Canceled by User', trace: anything, verbose: nil))
78 end
79 end
80
81 [RuntimeError.new('error spotted'), SignalException.new('SIGTERM')].each do |error|
82 context "when an/a #{error.class} is raised during the scan" do
83 let(:run_error) { error }
84
85 it 'aborts the scan with the associated output' do
86 expect(scanner.controllers.option_parser).to receive(:results).and_return({})
87
88 expect(scanner.controllers.first)
89 .to receive(:before_scan)
90 .and_raise(run_error.class, run_error.message)
91
92 expect(scanner.formatter).to receive(:output)
93 .with('@scan_aborted', hash_including(reason: run_error.message, trace: anything, verbose: nil))
94 end
95 end
96 end
97 end
98
99 describe '#datastore' do
100 its(:datastore) { should eq({}) }
101 end
102
103 describe '#exit_hook' do
104 # No idea how to test that, maybe with another at_exit hook ? oO
105 xit
106 end
107 end
0 require 'spec_helper'
1
2 describe CMSScanner::Controller do
3 subject(:controller) { described_class::Base.new }
4
5 context 'when parsed_options' do
6 before do
7 described_class::Base.option_parser = nil
8 described_class::Base.parsed_options = parsed_options
9 end
10
11 let(:parsed_options) { { url: 'http://example.com/' } }
12
13 its(:option_parser) { should be nil }
14 its(:parsed_options) { should eq(parsed_options) }
15 its(:formatter) { should be_a CMSScanner::Formatter::Cli }
16 its(:user_interaction?) { should be true }
17 its(:tmp_directory) { should eql '/tmp/cms_scanner' }
18 its(:target) { should be_a CMSScanner::Target }
19 its('target.scope.domains') { should eq [PublicSuffix.parse('example.com')] }
20
21 context 'when output option' do
22 let(:parsed_options) { super().merge(output: '/tmp/spec.txt') }
23
24 its(:user_interaction?) { should be false }
25 end
26
27 describe '#render' do
28 it 'calls the formatter#render' do
29 expect(controller.formatter).to receive(:render).with('test', { verbose: nil }, 'base')
30 controller.render('test')
31 end
32 end
33 end
34 end
0 require 'spec_helper'
1
2 module CMSScanner
3 module Controller
4 class Spec < Base
5 end
6 end
7 end
8
9 describe CMSScanner::Controllers do
10 subject(:controllers) { described_class.new }
11 let(:controller_mod) { CMSScanner::Controller }
12
13 describe '#<<' do
14 its(:size) { should be 0 }
15
16 context 'when controllers are added' do
17 before { controllers << controller_mod::Spec.new << controller_mod::Base.new }
18
19 its(:size) { should be 2 }
20 end
21
22 context 'when a controller is added twice' do
23 before { 2.times { controllers << controller_mod::Spec.new } }
24
25 its(:size) { should be 1 }
26 end
27
28 it 'returns self' do
29 expect(controllers << controller_mod::Spec.new).to be_a described_class
30 end
31 end
32
33 describe '#run' do
34 it 'runs the before_scan, run and after_scan methods of each controller' do
35 spec = controller_mod::Spec.new
36 base = controller_mod::Base.new
37
38 controllers << base << spec
39
40 # Needed otherwise the default_argv is taken from rspec
41 # (@default_argv=["--pattern", "spec/**{,/*/**}/*_spec.rb"]>)
42 expect(controllers.option_parser).to receive(:results).and_return({})
43
44 [base, spec].each { |c| expect(c).to receive(:before_scan).ordered }
45 [base, spec].each { |c| expect(c).to receive(:run).ordered }
46 [spec, base].each { |c| expect(c).to receive(:after_scan).ordered }
47
48 controllers.run
49 end
50 end
51
52 describe '#register_options_files' do
53 it 'register the correct files' do
54 expect(File).to receive(:exist?).exactly(4).times.and_return(true)
55
56 expected = []
57 option_parser = controllers.option_parser
58
59 [Dir.home, Dir.pwd].each do |dir|
60 option_parser.options_files.supported_extensions.each do |ext|
61 expected << File.join(dir, '.cms_scanner', "cli_options.#{ext}")
62 end
63 end
64
65 expect(option_parser.options_files.map(&:path)).to eql expected
66 end
67 end
68 end
0 require 'spec_helper'
1 require 'dummy_independent_finders'
2
3 describe CMSScanner::Finders::BaseFinders do
4 subject(:finders) { described_class.new }
5
6 describe '#symbols_from_mode' do
7 after { expect(finders.send(:symbols_from_mode, @mode)).to eq @expected }
8
9 context 'when :mixed' do
10 it 'returns [:passive, :aggressive]' do
11 @mode = :mixed
12 @expected = %i[passive aggressive]
13 end
14 end
15
16 context 'when :passive or :aggresssive' do
17 %i[passive aggressive].each do |symbol|
18 it 'returns it in an array' do
19 @mode = symbol
20 @expected = [*symbol]
21 end
22 end
23 end
24
25 context 'otherwise' do
26 it 'returns []' do
27 @mode = :unallowed
28 @expected = []
29 end
30 end
31 end
32
33 describe '#run_finder' do
34 # currently handled in independent_finders_spec
35 end
36
37 describe '#findings' do
38 it 'returns a Findings object' do
39 expect(finders.findings).to be_a CMSScanner::Finders::Findings
40 end
41 end
42 end
0 require 'spec_helper'
1
2 describe CMSScanner::Finders::Finder::BreadthFirstDictionaryAttack do
3 # Dummy class to test the module
4 class DummyBreadthFirstDictionaryAttack < CMSScanner::Finders::Finder
5 include CMSScanner::Finders::Finder::BreadthFirstDictionaryAttack
6
7 def login_request(username, password)
8 Typhoeus::Request.new('http://e.org/login.php',
9 method: :post,
10 body: { username: username, pwd: password })
11 end
12
13 def valid_credentials?(response)
14 response.code == 302
15 end
16
17 def errored_response?(response)
18 response.timed_out? || response.body =~ /Error:/
19 end
20 end
21
22 subject(:finder) { DummyBreadthFirstDictionaryAttack.new(target) }
23 let(:target) { CMSScanner::Target.new('http://e.org') }
24 let(:login_url) { target.url('login.php') }
25
26 describe '#attack' do
27 let(:users) { %w[admin root user].map { |u| CMSScanner::User.new(u) } }
28 let(:passwords) { %w[pwd admin P@ssw0rd] }
29
30 before do
31 # Mock all login requests to 401
32 passwords.each do |password|
33 users.each do |user|
34 stub_request(:post, login_url)
35 .with(body: { username: user.username, pwd: password })
36 .to_return(status: 401)
37 end
38 end
39 end
40
41 context 'when no valid credentials' do
42 it 'does not yield anything' do
43 expect { |block| finder.attack(users, passwords, &block) }.not_to yield_control
44 end
45 end
46
47 context 'when valid credentials' do
48 before do
49 stub_request(:post, login_url)
50 .with(body: { username: 'admin', pwd: 'admin' })
51 .to_return(status: 302)
52 end
53
54 it 'yields the matching user' do
55 expect { |block| finder.attack(users, passwords, &block) }
56 .to yield_with_args(CMSScanner::User.new('admin', password: 'admin'))
57 end
58 end
59
60 context 'when an error is present in a response' do
61 before do
62 if defined?(stub_params)
63 stub_request(:post, login_url)
64 .with(body: { username: 'admin', pwd: 'pwd' })
65 .to_return(stub_params)
66 else
67 stub_request(:post, login_url)
68 .with(body: { username: 'admin', pwd: 'pwd' })
69 .to_timeout
70 end
71
72 finder.attack(users, passwords)
73 end
74
75 context 'when request timeout' do
76 it 'logs to correct message' do
77 expect(finder.progress_bar.log).to eql [
78 'Error: Request timed out.'
79 ]
80 end
81 end
82
83 context 'when status/code = 0' do
84 let(:stub_params) { { status: 0, body: 'Error: Down' } }
85
86 it 'logs to correct message' do
87 expect(finder.progress_bar.log).to eql [
88 'Error: No response from remote server. WAF/IPS? ()'
89 ]
90 end
91 end
92
93 context 'when error 500' do
94 let(:stub_params) { { status: 500, body: 'Error: 500' } }
95
96 it 'logs to correct message' do
97 expect(finder.progress_bar.log).to eql [
98 'Error: Server error, try reducing the number of threads.'
99 ]
100 end
101 end
102
103 context 'when unknown error' do
104 let(:stub_params) { { status: 200, body: 'Error: Something went wrong' } }
105
106 it 'logs to correct message' do
107 expect(finder.progress_bar.log).to eql [
108 "Error: Unknown response received Code: 200\nBody: Error: Something went wrong"
109 ]
110 end
111 end
112 end
113 end
114 end
0 require 'spec_helper'
1
2 describe CMSScanner::Finders::Finder::Enumerator do
3 # Dummy class to test the module
4 class DummyEnumeratorFinder < CMSScanner::Finders::Finder
5 include CMSScanner::Finders::Finder::Enumerator
6 end
7
8 subject(:finder) { DummyEnumeratorFinder.new(target) }
9 let(:target) { CMSScanner::Target.new('http://e.org') }
10
11 its(:request_params) { should eql(cache_ttl: 0) }
12
13 describe '#enumerate' do
14 before do
15 target_urls.each_key { |url| stub_request(:get, url).to_return(status: 200, body: 'rspec') }
16 end
17
18 let(:target_urls) do
19 {
20 target.url('1') => 1,
21 target.url('2') => 2
22 }
23 end
24
25 context 'when no opts' do
26 let(:opts) { {} }
27
28 context 'when response are the homepage or custom 404' do
29 before { expect(finder.target).to receive(:homepage_or_404?).twice.and_return(true) }
30
31 it 'does not yield anything' do
32 expect { |b| finder.enumerate(target_urls, opts, &b) }.to_not yield_control
33 end
34 end
35
36 context 'when not the hompage or 404' do
37 before { expect(finder.target).to receive(:homepage_or_404?).twice }
38
39 it 'yield the expected items' do
40 expect { |b| finder.enumerate(target_urls, opts, &b) }.to yield_successive_args(
41 [Typhoeus::Response, 1], [Typhoeus::Response, 2]
42 )
43 end
44 end
45 end
46
47 context 'when opts' do
48 context 'when :exclude_content' do
49 before { expect(finder.target).to receive(:homepage_or_404?).twice }
50
51 context 'when body matches' do
52 let(:opts) { { exclude_content: /spec/i } }
53
54 it 'does not yield anything' do
55 expect { |b| finder.enumerate(target_urls, opts, &b) }.to_not yield_control
56 end
57 end
58
59 context 'when body does not match' do
60 let(:opts) { { exclude_content: /not/i } }
61
62 it 'yield the expected items' do
63 expect { |b| finder.enumerate(target_urls, opts, &b) }.to yield_successive_args(
64 [Typhoeus::Response, 1], [Typhoeus::Response, 2]
65 )
66 end
67 end
68
69 context 'when header matches' do
70 let(:opts) { { exclude_content: %r{Location: /aa}i } }
71
72 before do
73 target_urls.each_key do |url|
74 stub_request(:get, url).to_return(status: 301,
75 headers: { 'Location' => '/aa' })
76 end
77 end
78
79 it 'does not yield anything' do
80 expect { |b| finder.enumerate(target_urls, opts, &b) }.to_not yield_control
81 end
82 end
83
84 context 'when header does not match' do
85 let(:opts) { { exclude_content: /not 301/i } }
86
87 it 'yield the expected items' do
88 expect { |b| finder.enumerate(target_urls, opts, &b) }.to yield_successive_args(
89 [Typhoeus::Response, 1], [Typhoeus::Response, 2]
90 )
91 end
92 end
93 end
94 end
95 end
96 end
0 require 'spec_helper'
1
2 describe CMSScanner::Finders::Finder::Fingerprinter do
3 # Dummy class to test the module
4 class DummyFingerprinterFinder < CMSScanner::Finders::Finder
5 include CMSScanner::Finders::Finder::Fingerprinter
6 end
7
8 subject(:finder) { DummyFingerprinterFinder.new(target) }
9 let(:target) { CMSScanner::Target.new('http://e.org/') }
10
11 its(:request_params) { should eql({}) }
12
13 describe '#fingerprint' do
14 let(:fingerprints) do
15 {
16 target.url('f1.css') => {
17 finder.hexdigest('f1_body') => 'v1'
18 },
19 target.url('f2.js') => {
20 finder.hexdigest('f2_body') => %w[v1 v2],
21 finder.hexdigest('f2_2_body') => %w[v3]
22 }
23 }
24 end
25
26 context 'when no matches' do
27 before { stub_request(:get, /.*/).to_return(body: '404') }
28
29 it 'does not yield anything' do
30 expect { |b| finder.fingerprint(fingerprints, &b) }.not_to yield_control
31 end
32 end
33
34 context 'when matches' do
35 before do
36 stub_request(:get, target.url('f1.css')).to_return(body: 'f1_body')
37 stub_request(:get, target.url('f2.js')).to_return(body: 'f2_body')
38 end
39
40 it 'yields the expected arguments' do
41 expect { |b| finder.fingerprint(fingerprints, &b) }.to yield_successive_args(
42 ['v1', target.url('f1.css'), finder.hexdigest('f1_body')],
43 [%w[v1 v2], target.url('f2.js'), finder.hexdigest('f2_body')]
44 )
45 end
46 end
47 end
48 end
0 require 'spec_helper'
1 require 'dummy_finding'
2
3 describe CMSScanner::Finders::Finder::SmartURLChecker::Findings do
4 subject(:findings) { described_class.new }
5 let(:finding) { CMSScanner::DummyFinding }
6
7 describe '#<<' do
8 after { expect(findings).to match_array(@expected.map { |f| eql(f) }) }
9
10 context 'when no findings already in' do
11 it 'adds it' do
12 findings << finding.new('empty-test')
13 @expected = [finding.new('empty-test')]
14 end
15 end
16
17 context 'when findings already in' do
18 let(:confirmed) { finding.new('confirmed', interesting_entries: entries) }
19 let(:entries) { %w[e1 e2] }
20
21 before { findings << nil << nil << finding.new('test') << confirmed }
22
23 it 'adds a confirmed result correctly and ignore nil values' do
24 confirmed_dup = confirmed.dup
25 confirmed_dup.confidence = 100
26 confirmed_dup.interesting_entries = %w[e2 e3]
27
28 findings << confirmed_dup
29
30 confirmed.confirmed_by = confirmed_dup
31
32 @expected = [] << finding.new('test') << confirmed
33
34 expect(findings[1].interesting_entries).to eql(%w[e1 e2 e3])
35 end
36 end
37 end
38 end
0 require 'spec_helper'
1
2 describe CMSScanner::Finders::Finder::SmartURLChecker do
3 # Dummy class to test the module
4 class DummyFinder < CMSScanner::Finders::Finder
5 include CMSScanner::Finders::Finder::SmartURLChecker
6 end
7
8 subject(:finder) { DummyFinder.new(target) }
9 let(:target) { CMSScanner::Target.new('http://e.org') }
10
11 before { stub_request(:get, target.url) }
12
13 context 'when methods are not implemented' do
14 it 'raises errors' do
15 expect { finder.process_urls([]) }.to raise_error NotImplementedError
16 expect { finder.passive }.to raise_error NotImplementedError
17 expect { finder.aggressive_urls }.to raise_error NotImplementedError
18 end
19 end
20
21 describe '#aggressive' do
22 before { expect(finder).to receive(:aggressive_urls).and_return(%w[u1 u2 u3]) }
23
24 after do
25 expect(finder).to receive(:process_urls).with(@expected_urls, mode: mode)
26 finder.aggressive(mode: mode)
27 end
28
29 context 'when :mode = :mixed' do
30 before { expect(finder).to receive(:passive_urls).and_return(%w[u2]) }
31
32 let(:mode) { :mixed }
33
34 it 'calls #process_urls with the correct argument' do
35 @expected_urls = %w[u1 u3]
36 end
37 end
38
39 %i[passive aggressive].each do |m|
40 context "when :mode = #{m}" do
41 let(:mode) { m }
42
43 it 'calls #process_urls with the correct argument' do
44 @expected_urls = %w[u1 u2 u3]
45 end
46 end
47 end
48 end
49 end
0 require 'spec_helper'
1
2 describe CMSScanner::Finders::Finder do
3 subject(:finder) { described_class.new('target') }
4
5 describe '#create_progress_bar' do
6 before { finder.create_progress_bar(opts) }
7
8 context 'when opts[:show_progression] is true' do
9 let(:opts) { { show_progression: true } }
10
11 it 'uses the default progress-bar output' do
12 expect(finder.progress_bar.send(:output)).to be_a ProgressBar::Outputs::Tty
13 end
14 end
15
16 context 'when opts[:show_progression] is false' do
17 let(:opts) { { show_progression: false } }
18
19 it 'uses the null progress_bar outout' do
20 expect(finder.progress_bar.send(:output)).to be_a ProgressBar::Outputs::Null
21 end
22
23 context 'when logging data' do
24 context 'when no logs' do
25 it 'returns an empty array' do
26 expect(finder.progress_bar.log).to eql([])
27 end
28 end
29
30 context 'when adding messages' do
31 it 'returns the messages' do
32 finder.progress_bar.log 'Hello'
33 finder.progress_bar.log 'World'
34
35 expect(finder.progress_bar.log).to eql(%w[Hello World])
36 end
37 end
38 end
39 end
40 end
41
42 its(:browser) { should be_a CMSScanner::Browser }
43
44 its(:hydra) { should be_a Typhoeus::Hydra }
45
46 describe '#found_by' do
47 context 'when no klass supplied' do
48 context 'when no passive or aggresive match' do
49 it 'returns nil' do
50 expect(finder).to receive(:caller_locations).and_return([])
51
52 expect(finder.found_by).to be_nil
53 end
54 end
55
56 # TODO: make the below work
57 # context 'when aggressive match' do
58 # it 'returns the expected string' do
59 # expect(finder).to receive(:caller_locations)
60 # .and_return([Thread::Backtrace::Location.new("/aaaaa/file.rb:xx:in `aggressive'")])
61 #
62 # expect(finder.found_by).to eql 'Finder (Aggressive Detection)'
63 # end
64 # end
65 end
66
67 # context 'when class supplied' do
68 # it 'returns the expected string' do
69 # expect(finder).to receive(:caller_locations)
70 # .and_return(["/aaaaa/file.rb:xx:in `passive'"])
71 #
72 # expect(finder.found_by('Rspec')).to eql 'Rspec (Passive Detection)'
73 # end
74 # end
75 end
76 end
0 require 'spec_helper'
1 require 'dummy_finding'
2
3 describe CMSScanner::Finders::Findings do
4 subject(:findings) { described_class.new }
5 let(:finding) { CMSScanner::DummyFinding }
6
7 describe '#<<' do
8 after { expect(findings).to match_array(@expected.map { |f| eql(f) }) }
9
10 context 'when no findings already in' do
11 it 'adds it' do
12 findings << finding.new('empty-test', found_by: 'rspec', confidence: 20)
13 @expected = [finding.new('empty-test', found_by: 'rspec', confidence: 20)]
14 end
15 end
16
17 context 'when findings already in' do
18 let(:confirmed) { finding.new('confirmed') }
19
20 before { findings << nil << nil << finding.new('test') << confirmed }
21
22 it 'adds a confirmed result correctly and ignore the nil values' do
23 confirmed_dup = confirmed.dup
24 confirmed_dup.confidence = 100
25
26 findings << finding.new('test2')
27 findings << confirmed_dup
28
29 confirmed.confirmed_by = confirmed_dup
30
31 @expected = [] << finding.new('test') << confirmed << finding.new('test2')
32 end
33 end
34 end
35 end
0 require 'spec_helper'
1 require 'dummy_independent_finders'
2
3 describe CMSScanner::Finders::IndependentFinders do
4 subject(:finders) { described_class.new }
5
6 describe '#run' do
7 let(:target) { 'target' }
8 let(:finding) { CMSScanner::DummyFinding }
9 let(:expected_aggressive) { [finding.new('test', found_by: 'override', confidence: 100)] }
10 let(:expected_passive) do
11 [
12 finding.new('test', found_by: 'Dummy Finder (Passive Detection)'),
13 finding.new('spotted', found_by: 'No Aggressive Result (Passive Detection)', confidence: 10)
14 ]
15 end
16
17 before do
18 finders <<
19 CMSScanner::Finders::Independent::DummyFinder.new(target) <<
20 CMSScanner::Finders::Independent::NoAggressiveResult.new(target)
21 end
22
23 describe 'method calls order' do
24 after { finders.run(mode: mode) }
25
26 %i[passive aggressive].each do |current_mode|
27 context "when #{current_mode} mode" do
28 let(:mode) { current_mode }
29
30 it "calls the #{current_mode} method on each finder" do
31 finders.each do |f|
32 expect(f).to receive(current_mode).with(hash_including(found: [])).ordered
33 end
34 end
35 end
36 end
37
38 context 'when :mixed mode' do
39 let(:mode) { :mixed }
40
41 it 'calls :passive then :aggressive on each finder' do
42 finders.each do |finder|
43 %i[passive aggressive].each do |method|
44 expect(finder).to receive(method).with(hash_including(found: [])).ordered
45 end
46 end
47 end
48 end
49 end
50
51 describe 'returned results' do
52 before do
53 @found = finders.run(mode: mode)
54
55 expect(@found).to be_a(CMSScanner::Finders::Findings)
56
57 @found.each { |f| expect(f).to be_a finding }
58 end
59
60 context 'when :passive mode' do
61 let(:mode) { :passive }
62
63 it 'returns 2 results' do
64 expect(@found).to match_array(expected_passive.map { |f| eql(f) })
65 end
66 end
67
68 context 'when :aggressive mode' do
69 let(:mode) { :aggressive }
70
71 it 'returns 1 result' do
72 expect(@found).to match_array(expected_aggressive.map { |f| eql(f) })
73 end
74 end
75
76 context 'when :mixed mode' do
77 let(:mode) { :mixed }
78
79 it 'returns 2 results' do
80 # As the first passive is confirmed by the expected_aggressive, the confidence
81 # increases and should be 100% due to the expected_aggressive.confidence
82 first_passive = expected_passive.first.dup
83 first_passive.confidence = 100
84
85 expect(@found.size).to eq 2
86 expect(@found.first).to eql first_passive
87 expect(@found.first.confirmed_by).to eql expected_aggressive
88 expect(@found.last).to eql expected_passive.last
89 end
90 end
91
92 context 'when multiple results returned' do
93 xit
94 end
95 end
96 end
97 end
0 require 'spec_helper'
1
2 module CMSScanner
3 module Finders
4 # Dummy Class to test the module
5 class PluginsFinderSpec
6 include SameTypeFinder
7
8 def initialize(_target); end
9 end
10 end
11 end
12
13 describe CMSScanner::Finders::PluginsFinderSpec do
14 it_behaves_like CMSScanner::Finders::IndependentFinder do
15 let(:expected_finders) { [] }
16 let(:expected_finders_class) { CMSScanner::Finders::SameTypeFinders }
17 end
18
19 subject(:plugins) { described_class.new(target) }
20 let(:target) { CMSScanner::Target.new(url) }
21 let(:url) { 'http://example.com/' }
22 end
0 require 'spec_helper'
1 require 'dummy_independent_finders' # will use those for convenience
2
3 describe CMSScanner::Finders::SameTypeFinders do
4 subject(:finders) { described_class.new }
5 let(:independent_finders) { CMSScanner::Finders::Independent }
6
7 describe '#run' do
8 let(:target) { 'target' }
9 let(:finding) { CMSScanner::DummyFinding }
10 let(:opts) { {} }
11
12 before do
13 finders <<
14 independent_finders::DummyFinder.new(target) <<
15 independent_finders::NoAggressiveResult.new(target)
16 end
17
18 after do
19 result = finders.run(opts)
20
21 expect(result).to be_a CMSScanner::Finders::Findings
22 expect(result).to match_array(@expected.map { |f| eql(f) })
23 end
24
25 let(:dummy_passive) { independent_finders::DummyFinder.new(target).passive(opts) }
26 let(:dummy_aggresssive) { independent_finders::DummyFinder.new(target).aggressive(opts) }
27 let(:noaggressive) { independent_finders::NoAggressiveResult.new(target).passive(opts) }
28
29 context 'when :mixed mode' do
30 let(:opts) { super().merge(mode: :mixed) }
31
32 it 'calls all #passive then #aggressive on finders and returns the results' do
33 expect(finders[0]).to receive(:passive)
34 .with(hash_including(found: [])).ordered.and_call_original
35
36 expect(finders[1]).to receive(:passive)
37 .with(hash_including(found: [dummy_passive.first])).ordered.and_call_original
38
39 expect(finders[0]).to receive(:aggressive)
40 .with(hash_including(found: [dummy_passive.first, noaggressive]))
41 .ordered.and_call_original
42
43 expect(finders[1]).to receive(:aggressive)
44 .with(hash_including(:found))
45 .ordered
46
47 @expected = []
48
49 @expected << finding.new('test', confidence: 100,
50 found_by: 'Dummy Finder (Passive Detection)')
51
52 @expected.first.confirmed_by << finding.new('test', confidence: 100, found_by: 'override')
53
54 @expected << finding.new('spotted', confidence: 10,
55 found_by: 'No Aggressive Result (Passive Detection)')
56 end
57 end
58
59 context 'when :passive mode' do
60 let(:opts) { super().merge(mode: :passive) }
61
62 before do
63 expect(finders[0]).to receive(:passive)
64 .with(hash_including(found: [])).ordered.and_call_original
65
66 expect(finders[1]).to receive(:passive)
67 .with(hash_including(found: [dummy_passive.first])).ordered.and_call_original
68
69 finders.each { |f| expect(f).to_not receive(:aggressive) }
70 end
71
72 it 'calls #passive on all finders and returns the results' do
73 @expected = []
74 @expected << finding.new('test', found_by: 'Dummy Finder (Passive Detection)')
75 @expected << finding.new('spotted', confidence: 10,
76 found_by: 'No Aggressive Result (Passive Detection)')
77 end
78
79 context 'when :sort used' do
80 let(:opts) { super().merge(sort: true) }
81
82 it 'returns the sorted results' do
83 @expected = []
84 @expected << finding.new('spotted', confidence: 10,
85 found_by: 'No Aggressive Result (Passive Detection)')
86 @expected << finding.new('test', found_by: 'Dummy Finder (Passive Detection)')
87 end
88 end
89
90 # TODO: make this work
91 # context 'when :vulnerable used' do
92 # let(:opts) { super().merge(vulnerable: true) }
93
94 # it 'returns the vulnerable results' do
95 # expect(dummy_passive).to receive(:vulnerable?).and_return(true)
96 # expect(noaggressive).to receive(:vulnerable?)
97
98 # @expected = [finding.new('test', found_by: 'Dummy Finder (Passive Detection)')]
99 # end
100 # end
101 end
102
103 context 'when :aggressive mode' do
104 let(:opts) { super().merge(mode: :aggressive) }
105
106 it 'calls #aggressive on all finders and returns the results' do
107 finders.each { |f| expect(f).to_not receive(:passive) }
108
109 expect(finders[0]).to receive(:aggressive)
110 .with(hash_including(found: [])).ordered.and_call_original
111
112 expect(finders[1]).to receive(:aggressive)
113 .with(hash_including(found: [dummy_aggresssive])).ordered
114
115 @expected = [finding.new('test', confidence: 100, found_by: 'override')]
116 end
117 end
118 end
119 end
0 require 'spec_helper'
1
2 module CMSScanner
3 module Finders
4 # Dummy Class to test the module
5 class VersionFinderSpec
6 include UniqueFinder
7
8 def initialize(_target); end
9 end
10 end
11 end
12
13 describe CMSScanner::Finders::VersionFinderSpec do
14 it_behaves_like CMSScanner::Finders::IndependentFinder do
15 let(:expected_finders) { [] }
16 let(:expected_finders_class) { CMSScanner::Finders::UniqueFinders }
17 end
18
19 subject(:version) { described_class.new(target) }
20 let(:target) { CMSScanner::Target.new(url) }
21 let(:url) { 'http://example.com/' }
22 end
0 require 'spec_helper'
1 require 'dummy_unique_finders'
2
3 describe CMSScanner::Finders::UniqueFinders do
4 subject(:finders) { described_class.new }
5 let(:unique_finders) { CMSScanner::Finders::Unique }
6
7 describe '#filter_finding' do
8 let(:findings) { [] }
9
10 after do
11 finders.instance_variable_set(:@findings, findings)
12
13 expect(finders.send(:filter_findings)).to eql @expected
14 end
15
16 context 'when no findings' do
17 it 'returns false' do
18 @expected = false
19 end
20 end
21
22 context 'when one finding' do
23 let(:findings) { [CMSScanner::DummyFinding.new('one', confidence: 40)] }
24
25 it 'returns it' do
26 @expected = findings[0]
27 end
28 end
29
30 context 'when multiple findings' do
31 let(:findings) do
32 (1..5).reduce([]) { |acc, elem| acc << CMSScanner::DummyFinding.new(elem, confidence: 20) }
33 end
34
35 context 'when they have the same confidence' do
36 it 'returns false' do
37 @expected = false
38 end
39 end
40
41 context 'when there is a best confidence' do
42 (0..4).each do |position|
43 context "when at [#{position}]" do
44 it 'returns it' do
45 findings[position].confidence = 100
46
47 @expected = findings[position]
48 end
49 end
50 end
51 end
52 end
53 end
54
55 describe '#run' do
56 let(:target) { 'target' }
57 let(:finding) { CMSScanner::DummyFinding }
58 let(:opts) { {} }
59
60 before do
61 finders <<
62 unique_finders::Dummy.new(target) <<
63 unique_finders::NoAggressive.new(target) <<
64 unique_finders::Dummy2.new(target)
65 end
66
67 after do
68 result = finders.run(opts)
69
70 expect(result).to be_a finding if @expected
71 expect(result).to eql @expected
72 end
73
74 let(:dummy_passive) { unique_finders::Dummy.new(target).passive(opts) }
75 let(:dummy_aggresssive) { unique_finders::Dummy.new(target).aggressive(opts) }
76 let(:noaggressive) { unique_finders::NoAggressive.new(target).passive(opts) }
77 let(:dummy2_aggressive) { unique_finders::Dummy2.new(target).aggressive }
78
79 context 'when :confidence_threshold <= 0' do
80 let(:opts) { super().merge(confidence_threshold: 0) }
81
82 context 'when :mixed mode' do
83 let(:opts) { super().merge(mode: :mixed) }
84
85 it 'calls all #passive then #aggressive on finders and returns the best result' do
86 # Maybe there is a way to factorise this
87 expect(finders[0]).to receive(:passive)
88 .with(hash_including(found: [])).ordered.and_call_original
89
90 expect(finders[1]).to receive(:passive)
91 .with(hash_including(found: [dummy_passive.first])).ordered.and_call_original
92
93 expect(finders[2]).to receive(:passive)
94 .with(hash_including(found: [dummy_passive.first, noaggressive])).ordered
95
96 expect(finders[0]).to receive(:aggressive).with(hash_including(:found)).ordered
97 .and_call_original
98
99 expect(finders[1]).to receive(:aggressive).with(hash_including(:found)).ordered
100 expect(finders[2]).to receive(:aggressive).with(hash_including(:found)).ordered
101 .and_call_original
102
103 @expected = finding.new('v1', confidence: 100, found_by: 'Dummy (Passive Detection)')
104 @expected.confirmed_by << finding.new('v1', confidence: 100, found_by: 'override')
105 @expected.confirmed_by << finding.new('v1', confidence: 90)
106 end
107 end
108
109 context 'when :passive mode' do
110 let(:opts) { super().merge(mode: :passive) }
111
112 it 'calls #passive on all finders and returns the best result' do
113 expect(finders[0]).to receive(:passive)
114 .with(hash_including(found: [])).ordered.and_call_original
115
116 expect(finders[1]).to receive(:passive)
117 .with(hash_including(found: [dummy_passive.first])).ordered.and_call_original
118
119 expect(finders[2]).to receive(:passive)
120 .with(hash_including(found: [dummy_passive.first, noaggressive])).ordered
121
122 finders.each { |f| expect(f).to_not receive(:aggressive) }
123
124 @expected = finding.new('v2', confidence: 10,
125 found_by: 'No Aggressive (Passive Detection)')
126 end
127 end
128
129 context 'when :aggressive mode' do
130 let(:opts) { super().merge(mode: :aggressive) }
131
132 it 'calls #aggressive on all finders and returns the best result' do
133 finders.each { |f| expect(f).to_not receive(:passive) }
134
135 expect(finders[0]).to receive(:aggressive)
136 .with(hash_including(found: [])).ordered.and_call_original
137
138 expect(finders[1]).to receive(:aggressive)
139 .with(hash_including(found: [dummy_aggresssive])).ordered
140
141 expect(finders[2]).to receive(:aggressive)
142 .with(hash_including(:found)).ordered.and_call_original
143
144 @expected = finding.new('v1', confidence: 100, found_by: 'override')
145 @expected.confirmed_by << finding.new('v1', confidence: 90)
146 end
147 end
148 end
149
150 context 'when :confidence_threshold = 100 (default)' do
151 context 'when :mixed mode' do
152 let(:opts) { super().merge(mode: :mixed) }
153
154 it 'calls all #passive then #aggressive methods on finders and returns the '\
155 'result which reaches 100% confidence during the process' do
156 expect(finders[0]).to receive(:passive)
157 .with(hash_including(found: [])).ordered.and_call_original
158
159 expect(finders[1]).to receive(:passive)
160 .with(hash_including(found: [dummy_passive.first])).ordered.and_call_original
161
162 expect(finders[2]).to receive(:passive)
163 .with(hash_including(found: [dummy_passive.first, noaggressive])).ordered
164
165 expect(finders[0]).to receive(:aggressive).with(hash_including(:found)).ordered
166 .and_call_original
167
168 expect(finders[1]).to_not receive(:aggressive)
169 expect(finders[2]).to_not receive(:aggressive)
170
171 @expected = finding.new('v1', confidence: 100, found_by: 'Dummy (Passive Detection)')
172 @expected.confirmed_by << finding.new('v1', confidence: 100, found_by: 'override')
173 end
174 end
175
176 context 'when :passive mode' do
177 let(:opts) { super().merge(mode: :passive) }
178
179 it 'calls all #passive and returns the best result' do
180 expect(finders[0]).to receive(:passive)
181 .with(hash_including(found: [])).ordered.and_call_original
182
183 expect(finders[1]).to receive(:passive)
184 .with(hash_including(found: [dummy_passive.first])).ordered.and_call_original
185
186 expect(finders[2]).to receive(:passive)
187 .with(hash_including(found: [dummy_passive.first, noaggressive])).ordered
188
189 finders.each { |f| expect(f).to_not receive(:aggressive) }
190
191 @expected = finding.new('v2', confidence: 10,
192 found_by: 'No Aggressive (Passive Detection)')
193 end
194 end
195
196 context 'when :aggressive mode' do
197 let(:opts) { super().merge(mode: :aggressive) }
198
199 it 'calls all #aggressive and returns the result which reaches 100% confidence' do
200 finders.each { |f| expect(f).to_not receive(:passive) }
201
202 expect(finders[0]).to receive(:aggressive)
203 .with(hash_including(found: [])).ordered.and_call_original
204
205 expect(finders[1]).to_not receive(:aggressive)
206 expect(finders[2]).to_not receive(:aggressive)
207
208 @expected = finding.new('v1', confidence: 100, found_by: 'override')
209 end
210 end
211 end
212 end
213 end
0 require 'spec_helper'
1
2 module CMSScanner
3 module Formatter
4 module Spec
5 # Base Format Test Class
6 class BasedFormat < Base
7 def base_format
8 'base'
9 end
10 end
11 end
12 end
13 end
14
15 describe CMSScanner::Formatter::Base do
16 subject(:formatter) { described_class.new }
17
18 describe '#format' do
19 its(:format) { should eq 'base' }
20 end
21
22 describe '#user_interaction?' do
23 context 'when not a cli format' do
24 its(:user_interaction?) { should be false }
25 end
26
27 context 'when a cli format' do
28 before { expect(formatter).to receive(:format).and_return('cli') }
29
30 its(:user_interaction?) { should be true }
31 end
32 end
33
34 describe '#render, output' do
35 before { formatter.views_directories << FIXTURES_VIEWS }
36
37 it 'renders the global template and does not override the @views_directories' do
38 expect($stdout).to receive(:puts)
39 .with("It Works!\nViews Dirs: #{formatter.views_directories}")
40
41 formatter.output('@test', test: 'Works!', views_directories: 'owned')
42 end
43
44 context 'when global and local rendering are used inside a template' do
45 it 'renders them correcly' do
46 rendered = formatter.render('test', { var: 'Works' }, 'ctrl')
47
48 expect(rendered).to eq "Test: Works\nLocal View\nGlobal View"
49 end
50 end
51
52 it 'raises an error if the controller_name is nil and tpl is not a global one' do
53 expect { formatter.output('test') }.to raise_error('The controller_name can not be nil')
54 end
55 end
56
57 describe '#view_path' do
58 before do
59 formatter.views_directories << FIXTURES_VIEWS
60 formatter.render('local', {}, 'ctrl') # Used to set the @controller_name
61 end
62
63 context 'when the tpl format is invalid' do
64 let(:tpl) { '../try-this' }
65
66 it 'raises an error' do
67 expect { formatter.view_path(tpl) }.to raise_error("Wrong tpl format: 'ctrl/#{tpl}'")
68 end
69 end
70
71 context 'when the tpl is not found' do
72 let(:tpl) { 'not_there' }
73
74 it 'raises an error' do
75 expect { formatter.view_path(tpl) }.to raise_error("View not found for base/ctrl/#{tpl}")
76 end
77 end
78
79 context 'when the tpl is found' do
80 after { expect(formatter.view_path(@tpl)).to eq @expected.to_s }
81
82 context 'if it\'s a global tpl' do
83 it 'returns its path' do
84 @expected = FIXTURES_VIEWS.join('base', 'test.erb')
85 @tpl = '@test'
86 end
87 end
88
89 context 'if it\s a local tpl' do
90 it 'retuns its path' do
91 @expected = FIXTURES_VIEWS.join('base', 'ctrl', 'local.erb')
92 @tpl = 'local'
93 end
94 end
95 end
96
97 context 'when base_format' do
98 subject(:formatter) { CMSScanner::Formatter::Spec::BasedFormat.new }
99
100 after { expect(formatter.view_path(@tpl)).to eq @expected.to_s }
101
102 context 'when the ovverided view exists' do
103 it 'returns it' do
104 @expected = FIXTURES_VIEWS.join('based_format', 'test.erb')
105 @tpl = '@test'
106 end
107 end
108
109 it 'returns the base views otherwise' do
110 @expected = FIXTURES_VIEWS.join('base', 'ctrl', 'local.erb')
111 @tpl = 'local'
112 end
113 end
114 end
115
116 describe '#views_directories' do
117 let(:default_directories) do
118 [Dir.home, Dir.pwd].reduce([APP_VIEWS]) do |a, e|
119 a << Pathname.new(e).join(".#{CMSScanner.app_name}", 'views').to_s
120 end
121 end
122
123 context 'when default directories' do
124 its(:views_directories) { should eq(default_directories) }
125 end
126
127 context 'when adding directories' do
128 it 'adds them' do
129 formatter.views_directories << 'testing'
130
131 expect(formatter.views_directories).to eq(default_directories << 'testing')
132 end
133 end
134 end
135 end
0 require 'spec_helper'
1
2 describe Numeric do
3 describe '#bytes_to_human' do
4 context 'when positive' do
5 it 'returns the expected value' do
6 expect(100.bytes_to_human).to eql '100 B'
7 expect(11_497_472.bytes_to_human).to eql '10.965 MB'
8 end
9 end
10
11 context 'when negative' do
12 it 'uses the absolute value' do
13 expect(-11_497_472.bytes_to_human).to eql '10.965 MB'
14 end
15 end
16
17 context 'when zero' do
18 it 'returns zero' do
19 expect(0.bytes_to_human).to eql '0 B'
20 end
21 end
22 end
23 end
0 require 'spec_helper'
1
2 describe CMSScanner::ProgressBarNullOutput do
3 subject(:output) { described_class.new }
4
5 describe '#log, #logs' do
6 context 'when no log added' do
7 its(:logs) { should eql([]) }
8 end
9
10 context 'when adding log' do
11 it 'contains the added logs' do
12 output.log 'M1'
13 expect(output.logs).to eql(%w[M1])
14
15 output.log 'M2'
16 expect(output.logs).to eql(%w[M1 M2])
17
18 expect(output.log).to eql(%w[M1 M2])
19 end
20 end
21 end
22 end
0 require 'spec_helper'
1
2 describe PublicSuffix::Domain do
3 describe '#match' do
4 it 'returns true' do
5 expect(PublicSuffix.parse('g.com').match('g.com')).to eql true
6 end
7
8 it 'returns true' do
9 expect(PublicSuffix.parse('s.g.com').match('*.g.com')).to eql true
10 end
11
12 it 'returns false' do
13 expect(PublicSuffix.parse('a.b.g.com').match('*.g.com')).to eql false
14 end
15
16 it 'returns true' do
17 expect(PublicSuffix.parse('a.b.g.com').match('*.b.g.com')).to eql true
18 end
19
20 it 'returns true' do
21 expect(PublicSuffix.parse('a.b.g.com').match('**.g.com')).to eql true
22 end
23
24 it 'returns false' do
25 expect(PublicSuffix.parse('a.b.y.g.com').match('**.b.g.com')).to eql false
26 end
27
28 it 'returns false' do
29 expect(PublicSuffix.parse('w.g.com').match('*.g2.com')).to eql false
30 end
31
32 it 'returns true' do
33 expect(PublicSuffix.parse('a.b.g.com').match('a.b.g.com')).to eql true
34 end
35
36 it 'returns false' do
37 expect(PublicSuffix.parse('a.b.g.com').match('a.y.g.com')).to eql false
38 end
39
40 it 'returns true' do
41 expect(PublicSuffix.parse('a.b.c.d.g.com').match('**.c.d.g.com')).to eql true
42 end
43
44 it 'returns true' do
45 expect(PublicSuffix.parse('a.b.c.d.g.com').match('*.b.c.d.g.com')).to eql true
46 end
47 end
48 end
0 require 'spec_helper'
1
2 describe 'SubScanner' do
3 before :all do
4 # Module including the CMSScanner to test its correct inclusion
5 module SubScanner
6 include CMSScanner
7
8 # Override to make sure it can be overriden
9 def self.app_name
10 'subscanner'
11 end
12
13 VERSION = '1.0-Spec'.freeze
14 APP_DIR = '/tmp/sub_scanner/spec'.freeze
15
16 # This Target class should be called in the CMSScanner::Controller::Base
17 # instead of the CMSScanner::Target
18 class Target < CMSScanner::Target
19 def new_method
20 'working'
21 end
22 end
23
24 # Testing the override of the register_options_files
25 class Controllers < CMSScanner::Controllers
26 def register_options_files
27 option_parser.options_files << File.join(".#{SubScanner.app_name}", 'rspec.yml')
28 end
29 end
30
31 # Custom method for all formatters
32 module Formatter
33 include CMSScanner::Formatter
34
35 # Implements a #custom method which should be available in all formatters
36 module InstanceMethods
37 def custom
38 'It Works!'
39 end
40 end
41 end
42 end
43
44 CMSScanner::Controller::Base.reset
45 CMSScanner::Browser.reset
46 end
47
48 after :all do
49 CMSScanner.send(:remove_const, :NS)
50 CMSScanner.const_set(:NS, CMSScanner)
51 CMSScanner::Controller::Base.reset
52 end
53
54 subject(:scanner) { SubScanner::Scan.new }
55 let(:formatter_class) { SubScanner::Formatter }
56 let(:target_url) { 'http://ex.lo/' }
57
58 before do
59 scanner.controllers.first.class.parsed_options = { url: target_url }
60 end
61
62 describe '#app_name' do
63 it 'returns the correct app_name' do
64 expect(SubScanner.app_name).to eql 'subscanner'
65 end
66 end
67
68 describe 'Browser#default_user_agent' do
69 it 'returns the correct user_agent' do
70 expect(SubScanner::Browser.instance.default_user_agent).to eql 'SubScanner v1.0-Spec'
71 end
72 end
73
74 describe 'Controllers' do
75 describe '#target' do
76 it 'loads the overrided Target class' do
77 target = scanner.controllers.first.target
78
79 expect(target).to be_a SubScanner::Target
80 expect(target).to respond_to(:new_method)
81 expect(target.new_method).to eq 'working'
82 expect(target.url).to eql target_url
83 end
84 end
85
86 describe '#register_options_files' do
87 let(:options_file_path) { '.subscanner/rspec.yml' }
88
89 it 'register the correct file' do
90 allow(File).to receive(:exist?).and_call_original
91 allow(File).to receive(:exist?).with(options_file_path).and_return(true)
92
93 option_parser = SubScanner::Scan.new.controllers.option_parser
94
95 expect(option_parser.options_files.map(&:path)).to eql [options_file_path]
96 end
97 end
98 end
99
100 describe 'Controller::Base#tmp_directory' do
101 it 'returns the expected value' do
102 expect(scanner.controllers.first.tmp_directory).to eql '/tmp/subscanner'
103 end
104 end
105
106 describe 'Formatter' do
107 it_behaves_like CMSScanner::Formatter::ClassMethods do
108 subject(:formatter) { formatter_class }
109 end
110
111 describe '.load' do
112 it 'adds the #custom method for all formatters' do
113 formatter_class.availables.each do |format|
114 expect(formatter_class.load(format).custom).to eql 'It Works!'
115 end
116 end
117 end
118
119 describe '#views_directories' do
120 it 'returns the expected paths' do
121 expect(scanner.formatter.views_directories).to eql(
122 [
123 CMSScanner::APP_DIR, SubScanner::APP_DIR,
124 File.join(Dir.home, '.subscanner'), File.join(Dir.pwd, '.subscanner')
125 ].reduce([]) do |a, e|
126 a << File.join(e, 'views')
127 end
128 )
129 end
130 end
131 end
132 end
0 require 'spec_helper'
1
2 describe CMSScanner::Target do
3 subject(:target) { described_class.new(url) }
4 let(:url) { 'http://e.org' }
5
6 def md5sum(body)
7 Digest::MD5.hexdigest(body)
8 end
9
10 describe '#page_hash' do
11 after { expect(described_class.page_hash(page)).to eql @expected }
12
13 context 'when the page is an url' do
14 let(:page) { 'http://e.org/somepage.php' }
15
16 it 'returns the MD5 hash of the page' do
17 body = 'Hello World !'
18
19 stub_request(:get, page).to_return(body: body)
20
21 @expected = md5sum(body)
22 end
23 end
24
25 context 'when the page is a Typhoeus::Response' do
26 let(:page) { Typhoeus::Response.new(body: 'Hello Example!') }
27
28 it 'returns the correct hash' do
29 @expected = md5sum('Hello Example!')
30 end
31 end
32
33 context 'when there are comments' do
34 let(:page) do
35 body = "yolo\n\n<!--I should <script>no longer be</script> there -->\nworld!"
36 Typhoeus::Response.new(body: body)
37 end
38
39 it 'removes them' do
40 @expected = md5sum("yolo\n\n\nworld!")
41 end
42 end
43
44 context 'when there are script tags' do
45 let(:page) do
46 body = "aa<script>var t = 'test';</script>bbb"
47
48 Typhoeus::Response.new(body: body)
49 end
50
51 it 'removes them' do
52 @expected = md5sum('aabbb')
53 end
54 end
55 end
56
57 describe '#homepage_hash' do
58 it 'returns the MD5 hash of the homepage' do
59 body = 'Hello World'
60
61 stub_request(:get, target.url).to_return(body: body)
62
63 expect(target.homepage_hash).to eql md5sum(body)
64 end
65 end
66
67 describe '#error_404_hash' do
68 it 'returns the md5sum of the 404 page' do
69 stub_request(:any, /.*/).to_return(status: 404, body: '404 page !')
70
71 expect(target.error_404_hash).to eql md5sum('404 page !')
72 end
73 end
74
75 describe '#homepage_or_404?' do
76 let(:page_url) { target.url('page') }
77
78 before do
79 expect(target).to receive(:homepage_hash).and_return(md5sum('Home'))
80 expect(target).to receive(:error_404_hash).and_return(md5sum('Custom 404'))
81
82 stub_request(:get, page_url).to_return(body: body)
83 end
84
85 context 'when hashes do not match' do
86 let(:body) { 'Page!' }
87
88 it 'returns false' do
89 expect(target.homepage_or_404?(page_url)).to eql false
90 end
91 end
92
93 context 'when hashes match' do
94 let(:body) { 'Custom 404' }
95
96 it 'returns true' do
97 expect(target.homepage_or_404?(page_url)).to eql true
98 end
99 end
100 end
101 end
0 require 'spec_helper'
1
2 [:PHP].each do |platform|
3 describe CMSScanner::Target do
4 subject(:target) do
5 described_class.new(url).extend(described_class::Platform.const_get(platform))
6 end
7
8 let(:url) { 'http://e.org' }
9 let(:fixtures) { FIXTURES.join('target', 'platform', platform.to_s.downcase) }
10
11 it_behaves_like described_class::Platform.const_get(platform)
12 end
13 end
0 require 'spec_helper'
1
2 describe CMSScanner::Target do
3 subject(:target) { described_class.new(url, opts) }
4 let(:url) { 'http://e.org' }
5 let(:fixtures) { FIXTURES.join('target', 'scope') }
6 let(:opts) { { scope: nil } }
7
8 describe '#scope' do
9 let(:default_domains) { [PublicSuffix.parse('e.org')] }
10
11 context 'when none supplied' do
12 its('scope.domains') { should eq default_domains }
13 end
14
15 context 'when scope provided' do
16 let(:opts) { super().merge(scope: ['*.e.org']) }
17
18 its('scope.domains') { should eq default_domains << PublicSuffix.parse(opts[:scope].first) }
19
20 context 'when invalid domains provided' do
21 let(:opts) { super().merge(scope: ['wp-lamp']) }
22
23 it 'adds them in the invalid_domains attribute' do
24 expect(target.scope.domains).to eq default_domains
25 expect(target.scope.invalid_domains).to eq opts[:scope]
26 end
27 end
28 end
29 end
30
31 describe '#in_scope?' do
32 context 'when default scope (target domain)' do
33 [nil, '', 'http://out-of-scope.com', '//jquery.com/j.js',
34 'javascript:alert(3)', 'mailto:[email protected]'].each do |url|
35 it "returns false for #{url}" do
36 expect(target.in_scope?(url)).to eql false
37 end
38 end
39
40 %w[https://e.org/file.txt http://e.org/ //e.org].each do |url|
41 it "returns true for #{url}" do
42 expect(target.in_scope?(url)).to eql true
43 end
44 end
45 end
46
47 context 'when custom scope' do
48 let(:opts) { { scope: ['*.e.org', '192.168.1.12'] } }
49
50 [nil, '', 'http://out-of-scope.com', '//jquery.com/j.js', 'http://192.168.1.2/'].each do |url|
51 it "returns false for #{url}" do
52 expect(target.in_scope?(url)).to eql false
53 end
54 end
55
56 %w[http://e.org //cdn.e.org/f.txt http://s.e.org/ https://192.168.1.12/h].each do |url|
57 it "returns true for #{url}" do
58 expect(target.in_scope?(url)).to eql true
59 end
60 end
61 end
62 end
63
64 describe '#in_scope_urls' do
65 let(:res) { Typhoeus::Response.new(body: File.read(fixtures.join('index.html'))) }
66
67 context 'when block given' do
68 it 'yield the url' do
69 expect { |b| target.in_scope_urls(res, &b) }
70 .to yield_successive_args(
71 ['http://e.org/f.txt', Nokogiri::XML::Element],
72 ['http://e.org/script/s.js', Nokogiri::XML::Element],
73 ['http://e.org/feed', Nokogiri::XML::Element]
74 )
75 end
76 end
77
78 context 'when xpath argument given' do
79 it 'returns the expected array' do
80 xpath = '//link[@rel="alternate" and @type="application/rss+xml"]/@href'
81
82 expect(target.in_scope_urls(res, xpath)).to eql(%w[http://e.org/feed])
83 end
84 end
85
86 context 'when no block given' do
87 after { expect(target.in_scope_urls(res)).to eql @expected }
88
89 context 'when default scope' do
90 it 'returns the expected array' do
91 @expected = %w[http://e.org/f.txt http://e.org/script/s.js http://e.org/feed]
92 end
93 end
94
95 context 'when supplied scope' do
96 let(:opts) { super().merge(scope: ['*.e.org', 'wp-lamp']) }
97
98 it 'returns the expected array' do
99 @expected = %w[http://e.org/f.txt https://cdn.e.org/f2.js http://e.org/script/s.js
100 http://wp-lamp/robots.txt http://e.org/feed]
101 end
102 end
103 end
104 end
105 end
0 require 'spec_helper'
1
2 %i[Generic Apache IIS Nginx].each do |server|
3 describe CMSScanner::Target do
4 subject(:target) do
5 described_class.new(url).extend(described_class::Server.const_get(server))
6 end
7
8 let(:url) { 'http://e.org' }
9 let(:fixtures) { FIXTURES.join('target', 'server', server.to_s.downcase) }
10
11 it_behaves_like described_class::Server.const_get(server)
12 end
13 end
0 require 'spec_helper'
1
2 describe CMSScanner::Target do
3 subject(:target) { described_class.new(url) }
4 let(:url) { 'http://e.org' }
5 let(:fixtures) { FIXTURES.join('target') }
6
7 describe '#interesting_findings' do
8 before do
9 expect(CMSScanner::Finders::InterestingFindings::Base).to receive(:find).and_return(stubbed)
10 end
11
12 context 'when no findings' do
13 let(:stubbed) { [] }
14
15 its(:interesting_findings) { should eq stubbed }
16 end
17
18 context 'when findings' do
19 let(:stubbed) { ['yolo'] }
20
21 it 'allows findings to be added with <<' do
22 expect(target.interesting_findings).to eq stubbed
23
24 target.interesting_findings << 'other-finding'
25
26 expect(target.interesting_findings).to eq(stubbed << 'other-finding')
27 end
28 end
29 end
30
31 describe '#xpath_pattern_from_page' do
32 # Handled in #comments_from_page & #javascripts_from_page
33 end
34
35 describe '#comments_from_page' do
36 let(:fixture) { fixtures.join('comments.html') }
37 let(:page) { Typhoeus::Response.new(body: File.read(fixture)) }
38
39 context 'when the pattern does not match anything' do
40 it 'returns an empty array' do
41 expect(target.comments_from_page(/none/, page)).to eql([])
42 end
43 end
44
45 context 'when the pattern matches' do
46 let(:pattern) { /all in one seo pack/i }
47 let(:s1) { 'All in One SEO Pack 2.2.5.1 by Michael Torbert of Semper Fi Web Design' }
48 let(:s2) { '/all in one seo pack' }
49
50 context 'when no block given' do
51 it 'returns the expected matches' do
52 results = target.comments_from_page(pattern, page)
53
54 [s1, s2].each_with_index do |s, i|
55 expect(results[i].first).to eql s.match(pattern)
56 expect(results[i].last.to_s).to eql "<!-- #{s} -->"
57 end
58 end
59 end
60
61 # The below doesn't work, dunno why
62 # Would need to find a way to ensure the MatchData and XML::Comment are correct
63 context 'when block given' do
64 it 'yield the MatchData' do
65 expect { |b| target.comments_from_page(pattern, page, &b) }
66 .to yield_successive_args(
67 [MatchData, Nokogiri::XML::Comment],
68 [MatchData, Nokogiri::XML::Comment]
69 )
70 end
71 end
72 end
73
74 context 'when invalid byte sequence' do
75 let(:page) { Typhoeus::Response.new(body: "<!-- \xEB -->") }
76
77 it 'does not raise an error' do
78 expect { target.comments_from_page(/none/, page) }.to_not raise_error
79 end
80 end
81 end
82
83 describe '#javascripts_from_page' do
84 let(:fixture) { fixtures.join('javascripts.html') }
85 let(:page) { Typhoeus::Response.new(body: File.read(fixture)) }
86
87 context 'when the pattern does not match anything' do
88 it 'returns an empty array' do
89 expect(target.javascripts_from_page(/none/, page)).to eql([])
90 end
91 end
92
93 context 'when the pattern matches' do
94 let(:pattern) { /_version =/i }
95 let(:s) { "var _version = '1.2.4';" }
96
97 context 'when no block given' do
98 it 'returns the expected matches' do
99 results = target.javascripts_from_page(pattern, page)
100
101 expect(results[0].first).to eql s.match(pattern)
102 expect(results[0].last.text.to_s).to eql s
103 end
104 end
105
106 # The below doesn't work, dunno why
107 # # Would need to find a way to ensure the MatchData and XML::Element are correct
108 context 'when block given' do
109 it 'yield the MatchData' do
110 expect { |b| target.javascripts_from_page(pattern, page, &b) }
111 .to yield_successive_args(
112 [MatchData, Nokogiri::XML::Element]
113 )
114 end
115 end
116 end
117 end
118
119 describe '#urls_from_page' do
120 let(:page) { Typhoeus::Response.new(body: File.read(fixtures.join('urls_from_page.html'))) }
121
122 context 'when block given' do
123 it 'yield the url' do
124 expect { |b| target.urls_from_page(page, &b) }
125 .to yield_successive_args(
126 ['http://e.org/f.txt', Nokogiri::XML::Element],
127 ['https://cdn.e.org/f2.js', Nokogiri::XML::Element],
128 ['http://e.org/script/s.js', Nokogiri::XML::Element],
129 ['http://wp-lamp/feed.xml', Nokogiri::XML::Element],
130 ['http://g.com/img.jpg', Nokogiri::XML::Element],
131 ['http://g.org/logo.png', Nokogiri::XML::Element]
132 )
133 end
134 end
135
136 context 'when no block given' do
137 it 'returns the expected array' do
138 expect(target.urls_from_page(page)).to eql(
139 %w[
140 http://e.org/f.txt https://cdn.e.org/f2.js http://e.org/script/s.js
141 http://wp-lamp/feed.xml http://g.com/img.jpg http://g.org/logo.png
142 ]
143 )
144 end
145
146 context 'when xpath argument given' do
147 it 'returns the expected array' do
148 xpath = '//link[@rel="alternate" and @type="application/rss+xml"]/@href'
149
150 expect(target.urls_from_page(page, xpath)).to eql(%w[http://wp-lamp/feed.xml])
151 end
152 end
153 end
154 end
155 end
0 require 'spec_helper'
1
2 describe CMSScanner::Vulnerability do
3 subject(:vuln) { described_class.new(title, references) }
4 let(:title) { 'Test Vuln' }
5 let(:references) { {} }
6
7 it_behaves_like CMSScanner::References
8
9 describe '#new' do
10 its(:title) { should eql title }
11 its(:references) { should eql({}) }
12 its(:type) { should eql nil }
13 its(:fixed_in) { should eql nil }
14 end
15
16 describe '#==' do
17 context 'when te same vuln' do
18 it 'returns true' do
19 expect(vuln).to eq vuln.dup
20 end
21 end
22
23 context 'when not equal' do
24 it 'returns false' do
25 expect(vuln).to_not eq described_class.new('not eq')
26 end
27 end
28 end
29 end
0 require 'spec_helper'
1
2 describe CMSScanner::WebSite do
3 subject(:web_site) { described_class.new(url, opts) }
4 let(:url) { 'http://e.org' }
5 let(:opts) { {} }
6
7 describe '#url=' do
8 context 'when the url is incorrect' do
9 after do
10 expect { web_site.url = @url }.to raise_error Addressable::URI::InvalidURIError
11 end
12
13 it 'raises an error if empty' do
14 @url = ''
15 end
16
17 it 'raises an error if wrong format' do
18 @url = 'jj'
19 end
20 end
21
22 context 'when valid' do
23 it 'creates an Addressable object and adds a traling slash' do
24 web_site.url = 'http://site.com'
25
26 expect(web_site.url).to eq('http://site.com/')
27 expect(web_site.uri).to be_a Addressable::URI
28 end
29 end
30
31 context 'when non ascii chars' do
32 it 'normalize it' do
33 web_site.url = 'http://пример.испытание/'
34
35 expect(web_site.url).to eql 'http://xn--e1afmkfd.xn--80akhbyknj4f/'
36 end
37 end
38 end
39
40 describe '#url' do
41 context 'when no path argument' do
42 its(:url) { should eql 'http://e.org/' }
43 end
44
45 context 'when a path argument' do
46 it 'appends the path' do
47 expect(web_site.url('file.txt')).to eql "#{url}/file.txt"
48 end
49
50 it 'encodes the path' do
51 expect(web_site.url('f ile.txt')).to eql "#{url}/f%20ile.txt"
52 expect(web_site.url('s/a%.txt')).to eql "#{url}/s/a%25.txt"
53 expect(web_site.url('#file.txt#')).to eql "#{url}/%23file.txt%23"
54 end
55
56 context 'when relative path' do
57 let(:url) { 'http://e.org/dir/' }
58
59 it 'appends it from the host/domain' do
60 expect(web_site.url('/sub/file.txt')).to eql 'http://e.org/sub/file.txt'
61 end
62 end
63 end
64 end
65
66 describe '#opts' do
67 its(:opts) { should eql({}) }
68
69 context 'when opts' do
70 let(:opts) { { test: 'mm' } }
71
72 its(:opts) { should eql opts }
73 end
74 end
75
76 describe '#online?, #http_auth?, #access_forbidden?, #proxy_auth?' do
77 before { stub_request(:get, web_site.url(path)).to_return(status: status) }
78
79 [nil, 'file-path.txt'].each do |p|
80 context "when path = #{p}" do
81 let(:path) { p }
82
83 context 'when response status is a 200' do
84 let(:status) { 200 }
85
86 it 'is considered fine' do
87 expect(web_site.online?(path)).to be true
88 expect(web_site.http_auth?(path)).to be false
89 expect(web_site.access_forbidden?(path)).to be false
90 expect(web_site.proxy_auth?(path)).to be false
91 end
92 end
93
94 context 'when offline' do
95 let(:status) { 0 }
96
97 it 'returns false' do
98 expect(web_site.online?(path)).to be false
99 end
100 end
101
102 context 'when http auth required' do
103 let(:status) { 401 }
104
105 it 'returns true' do
106 expect(web_site.http_auth?(path)).to be true
107 end
108 end
109
110 context 'when access is forbidden' do
111 let(:status) { 403 }
112
113 it 'return true' do
114 expect(web_site.access_forbidden?(path)).to be true
115 end
116 end
117
118 context 'when proxy auth required' do
119 let(:status) { 407 }
120
121 it 'returns true' do
122 expect(web_site.proxy_auth?(path)).to be true
123 end
124 end
125 end
126 end
127 end
128 end
0 [+] Finished: Thu Oct 30 12:02:03 2014
1 [+] Requests Done: 10
2 [+] Memory used: 100 B
3 [+] Elapsed time: 00:00:02
0 {
1 "stop_time": 1414670523,
2 "elapsed": 2,
3 "requests_done": 10,
4 "used_memory": 100
5 }
0 Help Message from OptionParser
0 {
1 "help": "Help Message from OptionParser"
2 }
0 [+] URL: http://e.org/
1 [+] Started: Thu Oct 30 12:02:01 2014
2
0 {
1 "start_time": 1414670521,
2 "start_memory": 10,
3 "target_url": "http://e.org/",
4 "effective_url": "http://e.org/"
5 }
0 [+] URL: http://e.org/
1 [+] Effective URL: http://e.org/home
2 [+] Started: Thu Oct 30 12:02:01 2014
3
0 {
1 "start_time": 1414670521,
2 "start_memory": 10,
3 "target_url": "http://e.org/",
4 "effective_url": "http://e.org/home"
5 }
0 {
1 "interesting_findings": [
2
3 ]
4 }
0 Interesting Finding(s):
1
2 [+] F1_to_s
3 | Found By: Spec
4 | Confidence: 10%
5
6 [+] F2
7 | Interesting Entry: IE1
8 | Found By: Spec
9 | Confidence: 20%
10 | Confirmed By: Spec2, 10% confidence
11 | Reference: R1
12
13 [+] F3
14 | Interesting Entries:
15 | - IE1
16 | - IE2
17 | Found By: Spec
18 | Confidence: 100%
19 | Confirmed By:
20 | - Spec2, 100% confidence
21 | - Spec3, 10% confidence
22 | References:
23 | - R1
24 | - R2
25
26 [+] F4
27 | Found By: Spec
28 | Confirmed By: Spec2
29
0 {
1 "interesting_findings": [
2 {
3 "url": "F1",
4 "to_s": "F1_to_s",
5 "found_by": "Spec",
6 "confidence": 10,
7 "confirmed_by": {
8
9 },
10 "references": {
11
12 },
13 "interesting_entries": [
14
15 ]
16 },
17 {
18 "url": "F2",
19 "to_s": "F2",
20 "found_by": "Spec",
21 "confidence": 20,
22 "confirmed_by": {
23 "Spec2": {
24 "confidence": 10
25 }
26 },
27 "references": {
28 "url": [
29 "R1"
30 ]
31 },
32 "interesting_entries": [
33 "IE1"
34 ]
35 },
36 {
37 "url": "F3",
38 "to_s": "F3",
39 "found_by": "Spec",
40 "confidence": 100,
41 "confirmed_by": {
42 "Spec2": {
43 "confidence": 100
44 },
45 "Spec3": {
46 "confidence": 10
47 }
48 },
49 "references": {
50 "url": [
51 "R1",
52 "R2"
53 ]
54 },
55 "interesting_entries": [
56 "IE1",
57 "IE2"
58 ]
59 },
60 {
61 "url": "F4",
62 "to_s": "F4",
63 "found_by": "Spec",
64 "confidence": 0,
65 "confirmed_by": {
66 "Spec2": {
67 "confidence": 0
68 }
69 },
70 "references": {
71
72 },
73 "interesting_entries": [
74
75 ]
76 }
77 ]
78 }
0 shared_examples CMSScanner::Browser::Actions do
1 let(:url) { 'http://example.com/file.txt' }
2 let(:browser) { CMSScanner::Browser }
3
4 describe '#get, #post, #head' do
5 %i[get post head].each do |method|
6 it 'calls the method and returns a Typhoeus::Response' do
7 stub_request(method, url)
8
9 expect(browser.send(method, url)).to be_a Typhoeus::Response
10 end
11 end
12 end
13
14 describe '#get_and_follow_location' do
15 let(:redirection) { 'http://redirect.me' }
16
17 it 'follows the location' do
18 stub_request(:get, url).to_return(status: 301, headers: { location: redirection })
19 stub_request(:get, redirection).to_return(status: 200, body: 'Got me')
20
21 response = browser.get_and_follow_location(url)
22 expect(response).to be_a Typhoeus::Response
23 # Line below is not working due to an issue in Typhoeus/Webmock
24 # See https://github.com/typhoeus/typhoeus/issues/279
25 # expect(response.body).to eq 'Got me'
26 end
27 end
28 end
0 shared_examples CMSScanner::Finders::Finding do
1 it_behaves_like CMSScanner::References do
2 let(:opts) { { references: references } }
3 let(:references) { {} }
4 end
5
6 %i[confirmed_by interesting_entries].each do |opt|
7 describe "##{opt}" do
8 its(opt) { should eq [] }
9
10 context 'when supplied in the #new' do
11 let(:opts) { { opt => 'test' } }
12
13 its(opt) { should eq 'test' }
14 end
15 end
16 end
17
18 describe '#confidence, #confidence=' do
19 its(:confidence) { should eql 0 }
20
21 context 'when already set' do
22 before { subject.confidence = 10 }
23
24 its(:confidence) { should eql 10 }
25 end
26
27 context 'when another confidence added' do
28 it 'adds it the the actual' do
29 subject.confidence += 30
30 expect(subject.confidence).to eql 30
31 end
32
33 it 'sets it to 100 if >= 100' do
34 subject.confidence += 120
35 expect(subject.confidence).to eql 100
36 end
37 end
38 end
39
40 describe '#parse_finding_options' do
41 xit
42 end
43
44 describe '#eql?' do
45 before do
46 subject.confidence = 10
47 subject.found_by = 'test'
48 end
49
50 context 'when eql' do
51 it 'returns true' do
52 expect(subject).to eql subject
53 end
54 end
55
56 context 'when not eql' do
57 it 'returns false' do
58 other = subject.dup
59 other.confidence = 20
60
61 expect(subject).to_not eql other
62 end
63 end
64 end
65
66 describe '#<=>' do
67 # Handled in spec/app/models/interesting_findings_spec
68 end
69 end
0 shared_examples CMSScanner::Formatter::Buffer do
1 describe '#buffer' do
2 its(:buffer) { should be_empty }
3 end
4 end
0 shared_examples CMSScanner::Formatter::ClassMethods do
1 describe '#load' do
2 context 'w/o parameter' do
3 it 'loads the default formatter' do
4 expect(subject.load).to be_a subject::Cli
5 end
6 end
7
8 it 'loads the correct formatter' do
9 expect(subject.load('cli_no_colour')).to be_a subject::CliNoColour
10 end
11
12 it 'adds the custom_views' do
13 formatter = subject.load(nil, %w[/path/views1 /path2/views])
14
15 expect(formatter.views_directories).to include('/path/views1', '/path2/views')
16 end
17 end
18
19 describe '#availables' do
20 it 'returns the right list' do
21 expect(subject.availables).to match_array(%w[json cli-no-colour cli-no-color cli])
22 end
23 end
24 end
0 shared_examples CMSScanner::Finders::IndependentFinder do
1 describe '::find' do
2 it 'creates a new object and call finders#find' do
3 created = described_class.new(target)
4
5 expect(described_class).to receive(:new).and_return(created)
6 expect(created).to receive(:find)
7
8 described_class.find(target)
9 end
10 end
11
12 describe '#find' do
13 it 'calls finders#run' do
14 expect(subject.finders).to receive(:run).with({})
15 subject.find
16 end
17 end
18
19 describe '#finders' do
20 its(:finders) { should be_a expected_finders_class }
21
22 it 'returns the correct finders' do
23 finders = subject.finders
24
25 expect(finders.size).to eq expected_finders.size
26 expect(finders.map { |f| f.class.to_s.demodulize }).to eq expected_finders
27 end
28 end
29 end
0 shared_examples CMSScanner::References do
1 describe '#references_keys' do
2 it 'returns the expected array of symbols' do
3 expect(subject.class.references_keys)
4 .to eql %i[cve secunia osvdb exploitdb url metasploit packetstorm securityfocus]
5 end
6 end
7
8 describe 'references' do
9 context 'when no references' do
10 %i[cves secunia_ids osvdb_ids exploitdb_ids urls
11 msf_modules packetstorm_ids securityfocus_ids].each do |attribute|
12 its(attribute) { should eql([]) }
13 end
14
15 %i[cve_urls secunia_urls osvdb_urls exploitdb_urls msf_urls
16 packetstorm_urls secunia_urls].each do |attribute|
17 its(attribute) { should eql([]) }
18 end
19
20 its(:references_urls) { should eql([]) }
21 end
22
23 context 'when an unknown reference key is provided' do
24 let(:references) { { cve: 1, unknown: 12 } }
25
26 its(:references) { should eql(cve: %w[1]) }
27 end
28
29 context 'when references provided as string' do
30 let(:references) do
31 {
32 cve: 11,
33 secunia: 12,
34 osvdb: 13,
35 exploitdb: 14,
36 url: 'single-url',
37 metasploit: '/exploit/yolo',
38 packetstorm: 15,
39 securityfocus: 16
40 }
41 end
42
43 its(:cves) { should eql %w[11] }
44 its(:cve_urls) { should eql %w[https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-11] }
45
46 its(:secunia_ids) { should eql %w[12] }
47 its(:secunia_urls) { should eql %w[https://secunia.com/advisories/12/] }
48
49 its(:osvdb_ids) { should eql %w[13] }
50 its(:osvdb_urls) { should eql %w[http://osvdb.org/show/osvdb/13] }
51
52 its(:exploitdb_ids) { should eql %w[14] }
53 its(:exploitdb_urls) { should eql %w[https://www.exploit-db.com/exploits/14/] }
54
55 its(:urls) { should eql %w[single-url] }
56
57 its(:msf_modules) { should eql %w[/exploit/yolo] }
58 its(:msf_urls) { should eql %w[https://www.rapid7.com/db/modules/exploit/yolo] }
59
60 its(:packetstorm_ids) { should eq %w[15] }
61 its(:packetstorm_urls) { should eql %w[http://packetstormsecurity.com/files/15/] }
62
63 its(:securityfocus_ids) { should eq %w[16] }
64 its(:securityfocus_urls) { should eql %w[http://www.securityfocus.com/bid/16/] }
65
66 its(:references_urls) do
67 should eql [
68 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-11',
69 'https://secunia.com/advisories/12/',
70 'http://osvdb.org/show/osvdb/13',
71 'https://www.exploit-db.com/exploits/14/',
72 'single-url',
73 'https://www.rapid7.com/db/modules/exploit/yolo',
74 'http://packetstormsecurity.com/files/15/',
75 'http://www.securityfocus.com/bid/16/'
76 ]
77 end
78 end
79
80 context 'when references provided as array' do
81 xit
82 end
83 end
84 end
0 shared_examples CMSScanner::Target::Platform::PHP do
1 before { stub_request(:get, target.url(path)).to_return(body: body) }
2
3 describe '#debug_log?' do
4 let(:path) { 'd.log' }
5
6 context 'when the body matches' do
7 %w[debug.log].each do |file|
8 context "when #{file} body" do
9 let(:body) { File.read(fixtures.join('debug_log', file)) }
10
11 it 'returns true' do
12 expect(target.debug_log?(path)).to be true
13 end
14 end
15 end
16 end
17
18 context 'when the body does not match' do
19 let(:body) { '' }
20
21 it 'returns false' do
22 expect(target.debug_log?(path)).to be false
23 end
24 end
25 end
26
27 describe '#error_log?' do
28 let(:path) { 'error.log' }
29
30 context 'when the body matches' do
31 %w[error.log].each do |file|
32 context "when #{file} body" do
33 let(:body) { File.read(fixtures.join('error_log', file)) }
34
35 it 'returns true' do
36 expect(target.error_log?(path)).to be true
37 end
38 end
39 end
40 end
41
42 context 'when the body does not match' do
43 let(:body) { '' }
44
45 it 'returns false' do
46 expect(target.error_log?(path)).to be false
47 end
48 end
49 end
50
51 describe '#full_path_disclosure?, #full_path_disclosure_entries' do
52 let(:path) { 'p.php' }
53
54 context 'when the body matches a FPD' do
55 {
56 'wp_rss_functions.php' => %w[/short-path/rss-f.php]
57 }.each do |file, expected|
58 context "when #{file} body" do
59 let(:body) { File.read(fixtures.join('fpd', file)) }
60
61 it 'returns the expected array' do
62 expect(target.full_path_disclosure_entries(path)).to eql expected
63 expect(target.full_path_disclosure?(path)).to be true
64 end
65 end
66 end
67 end
68
69 context 'when no FPD' do
70 let(:body) { '' }
71
72 it 'returns an empty array' do
73 expect(target.full_path_disclosure_entries(path)).to eq []
74 expect(target.full_path_disclosure?(path)).to be false
75 end
76 end
77 end
78 end
0 require 'spec_helper'
1
2 shared_examples CMSScanner::Target::Server::Apache do
3 describe '#server' do
4 its(:server) { should eq :Apache }
5 end
6
7 describe '#directory_listing?, #directory_listing_entries' do
8 before { stub_request(:get, target.url(path)).to_return(body: body, status: status) }
9 let(:path) { 'somedir' }
10
11 context 'when not a 200' do
12 let(:status) { 404 }
13 let(:body) { '' }
14
15 it 'returns false and an empty array' do
16 expect(target.directory_listing?(path)).to be false
17 expect(target.directory_listing_entries(path)).to eql []
18 end
19 end
20
21 context 'when 200' do
22 let(:status) { 200 }
23 let(:body) { File.read(fixtures.join('directory_listing', '2.2.16.html')) }
24
25 it 'returns true and the expected array' do
26 expect(target.directory_listing?(path)).to be true
27 expect(target.directory_listing_entries(path)).to eq %w[backup.php database-empty.php]
28 end
29 end
30
31 context 'when no files nor folders' do
32 let(:status) { 200 }
33 let(:body) { File.read(fixtures.join('directory_listing', 'empty.html')) }
34
35 it 'returns true and the an empty array' do
36 expect(target.directory_listing?(path)).to be true
37 expect(target.directory_listing_entries(path)).to eql []
38 end
39 end
40 end
41 end
0 require 'spec_helper'
1
2 shared_examples CMSScanner::Target::Server::Generic do
3 describe '#server' do
4 before { stub_request(:head, target.url).to_return(headers: parse_headers_file(fixture)) }
5
6 context 'when apache headers' do
7 %w[basic.txt].each do |file|
8 context "when #{file} headers" do
9 let(:fixture) { fixtures.join('server', 'apache', file) }
10
11 its(:server) { should eq :Apache }
12 end
13 end
14 end
15
16 context 'when iis headers' do
17 %w[basic.txt].each do |file|
18 context "when #{file} headers" do
19 let(:fixture) { fixtures.join('server', 'iis', file) }
20
21 its(:server) { should eq :IIS }
22 end
23 end
24 end
25
26 context 'when nginx headers' do
27 %w[basic.txt].each do |file|
28 context "when #{file} headers" do
29 let(:fixture) { fixtures.join('server', 'nginx', file) }
30
31 its(:server) { should eq :Nginx }
32 end
33 end
34 end
35
36 context 'not detected' do
37 let(:fixture) { fixtures.join('server', 'not_detected.txt') }
38
39 its(:server) { should be nil }
40 end
41 end
42
43 describe '#directory_listing?' do
44 # Handled in shared_examples/target/server/apache & nginx
45 end
46 end
0 require 'spec_helper'
1
2 shared_examples CMSScanner::Target::Server::IIS do
3 describe '#server' do
4 its(:server) { should eq :IIS }
5 end
6
7 describe '#directory_listing?, #directory_listing_entries' do
8 before { stub_request(:get, target.url(path)).to_return(body: body, status: status) }
9 let(:path) { 'dir' }
10
11 context 'when not a 200' do
12 let(:status) { 404 }
13 let(:body) { '' }
14
15 it 'returns false and an empty array' do
16 expect(target.directory_listing?(path)).to be false
17 expect(target.directory_listing_entries(path)).to eql []
18 end
19 end
20
21 context 'when 200' do
22 let(:status) { 200 }
23
24 %w[with_parent.html no_parent.html].each do |file|
25 context "when #{file} body" do
26 let(:body) { File.read(fixtures.join('directory_listing', file)) }
27
28 it 'returns true and the expected array' do
29 expect(target.directory_listing?(path)).to be true
30 expect(target.directory_listing_entries(path)).to eq %w[sub-dir web.config]
31 end
32 end
33 end
34 end
35 end
36 end
0 require 'spec_helper'
1
2 shared_examples CMSScanner::Target::Server::Nginx do
3 describe '#server' do
4 its(:server) { should eq :Nginx }
5 end
6
7 describe '#directory_listing?, #directory_listing_entries' do
8 before { stub_request(:get, target.url(path)).to_return(body: body, status: status) }
9 let(:path) { 'somedir' }
10
11 context 'when not a 200' do
12 let(:status) { 404 }
13 let(:body) { '' }
14
15 it 'returns false and an empty array' do
16 expect(target.directory_listing?(path)).to be false
17 expect(target.directory_listing_entries(path)).to eql []
18 end
19 end
20
21 context 'when 200' do
22 let(:status) { 200 }
23 let(:body) { File.read(fixtures.join('directory_listing', 'fanart.html')) }
24
25 it 'returns true and the expected array' do
26 expect(target.directory_listing?(path)).to be true
27 expect(target.directory_listing_entries(path)).to eql %w[1931/ 720/ down]
28 end
29 end
30
31 context 'when no files nor folders' do
32 let(:status) { 200 }
33 let(:body) { File.read(fixtures.join('directory_listing', 'empty.html')) }
34
35 it 'returns true and the an empty array' do
36 expect(target.directory_listing?(path)).to be true
37 expect(target.directory_listing_entries(path)).to eql []
38 end
39 end
40 end
41 end
0 shared_examples 'App::Views::Core' do
1 let(:controller) { CMSScanner::Controller::Core.new }
2 let(:start) { Time.at(1_414_670_521).in_time_zone('Europe/London') }
3 let(:tpl_vars) { { url: target_url, start_time: start } }
4
5 describe 'version' do
6 let(:view) { 'version' }
7
8 it 'outputs the expected string' do
9 @tpl_vars = {}
10
11 @expected_output = if parsed_options[:format] == 'json'
12 JSON.pretty_generate('version' => CMSScanner::VERSION)
13 else
14 "Version: #{CMSScanner::VERSION}\n"
15 end
16 end
17 end
18
19 describe 'help' do
20 let(:view) { 'help' }
21
22 it 'outputs the expected string' do
23 @tpl_vars = { help: 'Help Message from OptionParser' }
24 end
25 end
26
27 describe 'started' do
28 let(:view) { 'started' }
29
30 context 'when the target url and the effective_url are the same' do
31 it 'outputs the expected string' do
32 @tpl_vars = tpl_vars.merge(start_memory: 10, effective_url: target_url)
33 end
34 end
35
36 context 'when target url != effective_url' do
37 let(:expected_view) { 'started_effective_url' }
38
39 it 'outputs the expected string' do
40 @tpl_vars = tpl_vars.merge(start_memory: 10, effective_url: "#{target_url}home")
41 end
42 end
43 end
44
45 describe 'finished' do
46 let(:view) { 'finished' }
47
48 it 'outputs the expected string' do
49 @tpl_vars = tpl_vars.merge(
50 stop_time: Time.at(1_414_670_523).in_time_zone('Europe/London'),
51 used_memory: 100,
52 elapsed: 2,
53 requests_done: 10
54 )
55 end
56 end
57 end
0 shared_examples 'App::Views::InterestingFindings' do
1 let(:controller) { CMSScanner::Controller::InterestingFindings.new }
2 let(:tpl_vars) { { url: target_url } }
3 let(:interesting_file) { CMSScanner::InterestingFinding }
4
5 describe 'findings' do
6 let(:view) { 'findings' }
7 let(:opts) { { confidence: 10, found_by: 'Spec' } }
8
9 context 'when empty results' do
10 let(:expected_view) { 'empty' }
11
12 it 'outputs the expected string' do
13 @tpl_vars = tpl_vars.merge(findings: [])
14 end
15 end
16
17 it 'outputs the expected string' do
18 findings = CMSScanner::Finders::Findings.new
19
20 findings <<
21 interesting_file.new('F1', opts.merge(to_s: 'F1_to_s')) <<
22 interesting_file.new('F2', opts.merge(references: { url: 'R1' }, interesting_entries: %w[IE1])) <<
23 interesting_file.new('F2', opts.merge(found_by: 'Spec2')) <<
24 interesting_file.new('F3',
25 opts.merge(references: { url: %w[R1 R2] }, interesting_entries: %w[IE1 IE2])) <<
26 interesting_file.new('F3', opts.merge(found_by: 'Spec2', confidence: 100)) <<
27 interesting_file.new('F3', opts.merge(found_by: 'Spec3')) <<
28 interesting_file.new('F4', opts.merge(confidence: 0)) <<
29 interesting_file.new('F4', opts.merge(confidence: 0, found_by: 'Spec2'))
30
31 @tpl_vars = tpl_vars.merge(findings: findings)
32 end
33 end
34 end
0 require 'shared_examples/browser_actions'
1 require 'shared_examples/formatter_buffer'
2 require 'shared_examples/formatter_class_methods'
3 require 'shared_examples/finding'
4 require 'shared_examples/independent_finder'
5 require 'shared_examples/target/platform/php'
6 require 'shared_examples/target/server/generic'
7 require 'shared_examples/target/server/apache'
8 require 'shared_examples/target/server/iis'
9 require 'shared_examples/target/server/nginx'
10 require 'shared_examples/views/core'
11 require 'shared_examples/views/interesting_findings'
12 require 'shared_examples/references'
0 $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
1
2 require 'simplecov'
3 require 'rspec/its'
4 require 'webmock/rspec'
5 require 'active_support/time'
6
7 if ENV['TRAVIS']
8 require 'coveralls'
9 SimpleCov.formatter = Coveralls::SimpleCov::Formatter
10 end
11
12 SimpleCov.start do
13 add_filter '/example/'
14 add_filter '/spec/'
15 add_filter 'helper'
16 end
17
18 # See http://betterspecs.org/
19 RSpec.configure do |config|
20 config.expect_with :rspec do |c|
21 c.syntax = :expect
22 end
23
24 config.before(:each) do
25 # Needed for rspec to run w/o error due to the at_exit hook calling the controller#target
26 CMSScanner::Controller::Core.parsed_options = { url: 'http://ex.lo' }
27 end
28 end
29
30 def count_files_in_dir(absolute_dir_path, files_pattern = '*')
31 Dir.glob(File.join(absolute_dir_path, files_pattern)).count
32 end
33
34 # Parse a file containing raw headers and return the associated Hash
35 # @return [ Hash ]
36 def parse_headers_file(filepath)
37 Typhoeus::Response::Header.new(File.read(filepath))
38 end
39
40 require 'cms_scanner'
41 require 'shared_examples'
42
43 def rspec_parsed_options(args)
44 controllers = CMSScanner::Controllers.new <<
45 CMSScanner::Controller::Core.new <<
46 CMSScanner::Controller::InterestingFindings.new
47
48 controllers.option_parser.results(args.split(' '))
49 end
50
51 # TODO: remove when https://github.com/bblimke/webmock/issues/552 fixed
52 # rubocop:disable all
53 module WebMock
54 module HttpLibAdapters
55 class TyphoeusAdapter < HttpLibAdapter
56 def self.effective_url(effective_uri)
57 effective_uri.port = nil if effective_uri.scheme == 'http' && effective_uri.port == 80
58 effective_uri.port = nil if effective_uri.scheme == 'https' && effective_uri.port == 443
59
60 effective_uri.to_s
61 end
62
63 def self.generate_typhoeus_response(request_signature, webmock_response)
64 response = if webmock_response.should_timeout
65 ::Typhoeus::Response.new(
66 code: 0,
67 status_message: '',
68 body: '',
69 headers: {},
70 return_code: :operation_timedout
71 )
72 else
73 ::Typhoeus::Response.new(
74 code: webmock_response.status[0],
75 status_message: webmock_response.status[1],
76 body: webmock_response.body,
77 headers: webmock_response.headers,
78 effective_url: effective_url(request_signature.uri)
79 )
80 end
81 response.mock = :webmock
82 response
83 end
84 end
85 end
86 end
87 # rubocop:enabled all
88
89 SPECS = Pathname.new(__FILE__).dirname
90 CACHE = SPECS.join('cache')
91 FIXTURES = SPECS.join('fixtures')
92 FIXTURES_VIEWS = FIXTURES.join('views')
93 FIXTURES_FINDERS = FIXTURES.join('finders')
94 FIXTURES_MODELS = FIXTURES.join('models')
95 APP_VIEWS = File.join(CMSScanner::APP_DIR, 'views')