Codebase list ruby-cms-scanner / d472aae
New upstream version 0.12.0 Sophie Brun 3 years ago
19 changed file(s) with 77 addition(s) and 63 deletion(s). Raw diff Collapse all Expand all
00 require: rubocop-performance
11 AllCops:
2 NewCops: enable
23 TargetRubyVersion: 2.5
34 Exclude:
45 - '*.gemspec'
56 - 'vendor/**/*'
67 - 'example/**/*'
7 Layout/EmptyLinesAroundAttributeAccessor:
8 Enabled: true
98 Layout/LineLength:
109 Max: 120
11 Layout/SpaceAroundMethodCallOperator:
12 Enabled: true
13 Lint/DeprecatedOpenSSLConstant:
14 Enabled: true
1510 Lint/UriEscapeUnescape:
1611 Enabled: false
17 Lint/RaiseException:
18 Enabled: true
19 Lint/StructNewOverride:
20 Enabled: true
2112 Metrics/AbcSize:
2213 Max: 25
2314 Metrics/BlockLength:
3526 Enabled: false
3627 Style/Documentation:
3728 Enabled: false
38 Style/ExponentialNotation:
39 Enabled: true
4029 Style/FormatStringToken:
4130 Exclude:
4231 - lib/cms_scanner/finders/finder.rb
43 Style/HashEachMethods:
44 Enabled: true
45 Style/HashTransformKeys:
46 Enabled: true
47 Style/HashTransformValues:
48 Enabled: true
4932 Style/MixinUsage:
5033 Exclude:
5134 - lib/cms_scanner/formatter.rb
52 Style/SlicingWithRange:
53 Enabled: true
1818 entries.each do |header, value|
1919 next if known_headers.include?(header.downcase)
2020
21 results << "#{header}: #{[*value].join(', ')}"
21 results << "#{header}: #{Array(value).join(', ')}"
2222 end
2323 results
2424 end
3333 s.add_development_dependency 'rake', '~> 13.0'
3434 s.add_development_dependency 'rspec', '~> 3.9.0'
3535 s.add_development_dependency 'rspec-its', '~> 1.3.0'
36 s.add_development_dependency 'rubocop', '~> 0.85.0'
37 s.add_development_dependency 'rubocop-performance', '~> 1.6.0'
36 s.add_development_dependency 'rubocop', '~> 0.88.0'
37 s.add_development_dependency 'rubocop-performance', '~> 1.7.0'
3838 s.add_development_dependency 'simplecov', '~> 0.18.2'
3939 s.add_development_dependency 'simplecov-lcov', '~> 0.8.0'
4040 s.add_development_dependency 'webmock', '~> 3.8.0'
2020
2121 return symbols if mode.nil? || mode == :mixed
2222
23 symbols.include?(mode) ? [*mode] : []
23 symbols.include?(mode) ? Array(mode) : []
2424 end
2525
2626 # @param [ CMSScanner::Finders::Finder ] finder
2727 # @param [ Symbol ] symbol See return values of #symbols_from_mode
2828 # @param [ Hash ] opts
2929 def run_finder(finder, symbol, opts)
30 [*finder.send(symbol, opts.merge(found: findings))].compact.each do |found|
30 Array(finder.send(symbol, opts.merge(found: findings))).compact.each do |found|
3131 findings << found
3232 end
3333 end
55 # Module to provide an easy way to perform password attacks
66 module BreadthFirstDictionaryAttack
77 # @param [ Array<CMSScanner::Model::User> ] users
8 # @param [ Array<String> ] passwords
8 # @param [ String ] wordlist_path
99 # @param [ Hash ] opts
1010 # @option opts [ Boolean ] :show_progression
1111 #
1212 # @yield [ CMSScanner::User ] When a valid combination is found
1313 #
1414 # Due to Typhoeus threads shenanigans, in rare cases the progress-bar might
15 # be incorrect updated, hence the 'rescue ProgressBar::InvalidProgressError'
15 # be incorrectly updated, hence the 'rescue ProgressBar::InvalidProgressError'
1616 #
1717 # TODO: Make rubocop happy about metrics etc
1818 #
1919 # rubocop:disable all
20 def attack(users, passwords, opts = {})
21 create_progress_bar(total: users.size * passwords.size, show_progression: opts[:show_progression])
20 def attack(users, wordlist_path, opts = {})
21 wordlist = File.open(wordlist_path)
22
23 create_progress_bar(total: users.size * wordlist.count, show_progression: opts[:show_progression])
2224
2325 queue_count = 0
2426 # Keep the number of requests sent for each users
2729
2830 users.each { |u| user_requests_count[u.username] = 0 }
2931
30 passwords.each do |password|
32 File.foreach(wordlist) do |password|
33 password.chomp!
3134 remaining_users = users.select { |u| u.password.nil? }
3235
3336 break if remaining_users.empty?
4649 user.password = password
4750
4851 begin
49 progress_bar.total -= passwords.size - user_requests_count[user.username]
52 progress_bar.total -= wordlist.count - user_requests_count[user.username]
5053 rescue ProgressBar::InvalidProgressError
5154 end
5255
104107 "No response from remote server. WAF/IPS? (#{response.return_message})"
105108 elsif response.code.to_s.start_with?('50')
106109 'Server error, try reducing the number of threads.'
110 elsif NS::ParsedCli.verbose?
111 "Unknown response received Code: #{response.code}\nBody: #{response.body}"
107112 else
108 "Unknown response received Code: #{response.code}\nBody: #{response.body}"
113 "Unknown response received Code: #{response.code}"
109114 end
110115
111116 progress_bar.log("Error: #{error}")
5454 # @return [ Typhoeus::Response, nil ]
5555 def maybe_get_full_response(head_res, opts)
5656 return head_res unless opts[:check_full_response] == true ||
57 [*opts[:check_full_response]].include?(head_res.code)
57 Array(opts[:check_full_response]).include?(head_res.code)
5858
5959 full_res = NS::Browser.get(head_res.effective_url, full_request_params)
6060
1616 def log(string = nil)
1717 return logs if string.nil?
1818
19 logs << string
19 logs << string unless logs.include?(string)
2020 end
2121 end
2222 end
2020 next unless refs.key?(key)
2121
2222 @references[key] = if key == :youtube
23 [*refs[:youtube]].map { |id| youtube_url(id) }
23 Array(refs[:youtube]).map { |id| youtube_url(id) }
2424 else
25 [*refs[key]].map(&:to_s)
25 Array(refs[key]).map(&:to_s)
2626 end
2727 end
2828 end
44 module Platform
55 # Some PHP specific implementation
66 module PHP
7 DEBUG_LOG_PATTERN = /(?:\[\d{2}\-[a-zA-Z]{3}\-\d{4}\s\d{2}\:\d{2}:\d{2}\s[A-Z]{3}\]|
7 DEBUG_LOG_PATTERN = /(?:\[\d{2}-[a-zA-Z]{3}-\d{4}\s\d{2}:\d{2}:\d{2}\s[A-Z]{3}\]|
88 PHP\s(?:Fatal|Warning|Strict|Error|Notice):)/x.freeze
99 FPD_PATTERN = /Fatal error:.+? in (.+?) on/.freeze
1010 ERROR_LOG_PATTERN = /PHP Fatal error/i.freeze
5252 domains = [uri.host + uri.path]
5353
5454 domains += if scope.domains.empty?
55 [*scope.invalid_domains[1..-1]]
55 Array(scope.invalid_domains[1..-1])
5656 else
57 [*scope.domains[1..-1]].map(&:to_s) + scope.invalid_domains
57 Array(scope.domains[1..-1]).map(&:to_s) + scope.invalid_domains
5858 end
5959
6060 domains.map! { |d| Regexp.escape(d.delete_suffix('/')).gsub('\*', '.*').gsub('/', '\\\\\?/') }
4040 def directory_listing?(path = nil, params = {})
4141 res = NS::Browser.get(url(path), params)
4242
43 res.code == 200 && res.body =~ /<h1>Index of/ ? true : false
43 res.code == 200 && res.body.include?('<h1>Index of') ? true : false
4444 end
4545
4646 # @param [ String ] path
1717 super(url, opts)
1818
1919 scope << uri.host
20 [*opts[:scope]].each { |s| scope << s }
20 Array(opts[:scope]).each { |s| scope << s }
2121 end
2222
2323 # @param [ Hash ] opts
11
22 # Version
33 module CMSScanner
4 VERSION = '0.10.1'
4 VERSION = '0.12.0'
55 end
0 pwd
1 admin
2 P@ssw0rd
1919 %i[passive aggressive].each do |symbol|
2020 it 'returns it in an array' do
2121 @mode = symbol
22 @expected = [*symbol]
22 @expected = Array(symbol)
2323 end
2424 end
2525 end
1515 end
1616
1717 def errored_response?(response)
18 response.timed_out? || response.body =~ /Error:/
18 response.timed_out? || response.body.include?('Error:')
1919 end
2020 end
2121
2525
2626 describe '#attack' do
2727 let(:users) { %w[admin root user].map { |u| CMSScanner::Model::User.new(u) } }
28 let(:passwords) { %w[pwd admin P@ssw0rd] }
28 let(:wordlist_path) { FIXTURES.join('passwords.txt').to_s }
2929
3030 before do
3131 # Mock all login requests to 401
32 passwords.each do |password|
32 File.foreach(wordlist_path) do |password|
3333 users.each do |user|
3434 stub_request(:post, login_url)
35 .with(body: { username: user.username, pwd: password })
35 .with(body: { username: user.username, pwd: password.chomp })
3636 .to_return(status: 401)
3737 end
3838 end
4040
4141 context 'when no valid credentials' do
4242 it 'does not yield anything' do
43 expect { |block| finder.attack(users, passwords, &block) }.not_to yield_control
43 expect { |block| finder.attack(users, wordlist_path, &block) }.not_to yield_control
4444 end
4545
4646 context 'when trying to increment above current progress' do
4949 expect_any_instance_of(ProgressBar::Base)
5050 .to receive(:progress)
5151 .at_least(1)
52 .and_return(users.size * passwords.size)
52 .and_return(users.size * File.open(wordlist_path).count)
5353
5454 expect_any_instance_of(ProgressBar::Base)
5555 .not_to receive(:increment)
5656
57 expect { |block| finder.attack(users, passwords, &block) }.not_to yield_control
57 expect { |block| finder.attack(users, wordlist_path, &block) }.not_to yield_control
5858 end
5959 end
6060 end
6767 end
6868
6969 it 'yields the matching user' do
70 expect { |block| finder.attack(users, passwords, &block) }
70 expect { |block| finder.attack(users, wordlist_path, &block) }
7171 .to yield_with_args(CMSScanner::Model::User.new('admin', password: 'admin'))
7272 end
7373
7575 it 'does not raise an error' do
7676 expect_any_instance_of(ProgressBar::Base).to receive(:total=).and_raise ProgressBar::InvalidProgressError
7777
78 expect { |block| finder.attack(users, passwords, &block) }
78 expect { |block| finder.attack(users, wordlist_path, &block) }
7979 .to yield_with_args(CMSScanner::Model::User.new('admin', password: 'admin'))
8080 end
8181 end
9393 .to_timeout
9494 end
9595
96 finder.attack(users, passwords)
96 CMSScanner::ParsedCli.options = { verbose: defined?(verbose) ? verbose : false }
97
98 finder.attack(users, wordlist_path)
9799 end
98100
99101 context 'when request timeout' do
127129 context 'when unknown error' do
128130 let(:stub_params) { { status: 200, body: 'Error: Something went wrong' } }
129131
130 it 'logs to correct message' do
131 expect(finder.progress_bar.log).to eql [
132 "Error: Unknown response received Code: 200\nBody: Error: Something went wrong"
133 ]
132 context 'when no --verbose' do
133 let(:verbose) { false }
134
135 it 'logs to correct message' do
136 expect(finder.progress_bar.log).to eql [
137 'Error: Unknown response received Code: 200'
138 ]
139 end
140 end
141
142 context 'when --verbose' do
143 let(:verbose) { true }
144
145 it 'logs to correct message' do
146 expect(finder.progress_bar.log).to eql [
147 "Error: Unknown response received Code: 200\nBody: Error: Something went wrong"
148 ]
149 end
134150 end
135151 end
136152 end
208208 end
209209
210210 context 'when one header matches but the other not, using negative look-arounds' do
211 let(:opts) { super().merge(exclude_content: /\A((?!x\-cacheable)[\s\S])*\z/i) }
211 let(:opts) { super().merge(exclude_content: /\A((?!x-cacheable)[\s\S])*\z/i) }
212212
213213 before do
214214 stub_request(:head, target_urls.keys.last).and_return(status: 200, headers: { 'x-cacheable' => 'YES' })
1717
1818 expect(output.log).to eql(%w[M1 M2])
1919 end
20
21 it 'does not add duplicate' do
22 output.log 'M1'
23 output.log 'M1'
24 output.log 'M2'
25
26 expect(output.logs).to eql(%w[M1 M2])
27 expect(output.log).to eql(%w[M1 M2])
28 end
2029 end
2130 end
2231 end
113113 its(:scope_url_pattern) { should eql %r{https?:\\?/\\?/(?:e\.org)\\?/?}i }
114114
115115 context 'when target is an invalid domain for PublicSuffix' do
116 let(:url) { 'http://wp-lab/' }
116 let(:url) { 'http://wp_lab/' }
117117
118 its(:scope_url_pattern) { should eql %r{https?:\\?/\\?/(?:wp\-lab)\\?/?}i }
118 its(:scope_url_pattern) { should eql %r{https?:\\?/\\?/(?:wp_lab)\\?/?}i }
119119 end
120120
121121 context 'when a port is present in the target URL' do
127127 end
128128
129129 context 'when scope given' do
130 let(:opts) { super().merge(scope: ['*.cdn.org', 'wp-lamp', '192.168.1.1']) }
130 let(:opts) { super().merge(scope: ['*.cdn.org', 'wp_lamp', '192.168.1.1']) }
131131
132 its(:scope_url_pattern) { should eql %r{https?:\\?/\\?/(?:e\.org|.*\.cdn\.org|192\.168\.1\.1|wp\-lamp)\\?/?}i }
132 its(:scope_url_pattern) { should eql %r{https?:\\?/\\?/(?:e\.org|.*\.cdn\.org|192\.168\.1\.1|wp_lamp)\\?/?}i }
133133
134134 context 'when target URL has a subdir' do
135135 let(:url) { 'https://e.org/blog/test' }
136136
137137 its(:scope_url_pattern) do
138 should eql %r{https?:\\?/\\?/(?:e\.org\\?/blog\\?/test|.*\.cdn\.org|192\.168\.1\.1|wp\-lamp)\\?/?}i
138 should eql %r{https?:\\?/\\?/(?:e\.org\\?/blog\\?/test|.*\.cdn\.org|192\.168\.1\.1|wp_lamp)\\?/?}i
139139 end
140140 end
141141 end