New upstream version 0.12.0
Sophie Brun
3 years ago
0 | 0 | require: rubocop-performance |
1 | 1 | AllCops: |
2 | NewCops: enable | |
2 | 3 | TargetRubyVersion: 2.5 |
3 | 4 | Exclude: |
4 | 5 | - '*.gemspec' |
5 | 6 | - 'vendor/**/*' |
6 | 7 | - 'example/**/*' |
7 | Layout/EmptyLinesAroundAttributeAccessor: | |
8 | Enabled: true | |
9 | 8 | Layout/LineLength: |
10 | 9 | Max: 120 |
11 | Layout/SpaceAroundMethodCallOperator: | |
12 | Enabled: true | |
13 | Lint/DeprecatedOpenSSLConstant: | |
14 | Enabled: true | |
15 | 10 | Lint/UriEscapeUnescape: |
16 | 11 | Enabled: false |
17 | Lint/RaiseException: | |
18 | Enabled: true | |
19 | Lint/StructNewOverride: | |
20 | Enabled: true | |
21 | 12 | Metrics/AbcSize: |
22 | 13 | Max: 25 |
23 | 14 | Metrics/BlockLength: |
35 | 26 | Enabled: false |
36 | 27 | Style/Documentation: |
37 | 28 | Enabled: false |
38 | Style/ExponentialNotation: | |
39 | Enabled: true | |
40 | 29 | Style/FormatStringToken: |
41 | 30 | Exclude: |
42 | 31 | - lib/cms_scanner/finders/finder.rb |
43 | Style/HashEachMethods: | |
44 | Enabled: true | |
45 | Style/HashTransformKeys: | |
46 | Enabled: true | |
47 | Style/HashTransformValues: | |
48 | Enabled: true | |
49 | 32 | Style/MixinUsage: |
50 | 33 | Exclude: |
51 | 34 | - lib/cms_scanner/formatter.rb |
52 | Style/SlicingWithRange: | |
53 | Enabled: true |
18 | 18 | entries.each do |header, value| |
19 | 19 | next if known_headers.include?(header.downcase) |
20 | 20 | |
21 | results << "#{header}: #{[*value].join(', ')}" | |
21 | results << "#{header}: #{Array(value).join(', ')}" | |
22 | 22 | end |
23 | 23 | results |
24 | 24 | end |
33 | 33 | s.add_development_dependency 'rake', '~> 13.0' |
34 | 34 | s.add_development_dependency 'rspec', '~> 3.9.0' |
35 | 35 | 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' | |
38 | 38 | s.add_development_dependency 'simplecov', '~> 0.18.2' |
39 | 39 | s.add_development_dependency 'simplecov-lcov', '~> 0.8.0' |
40 | 40 | s.add_development_dependency 'webmock', '~> 3.8.0' |
20 | 20 | |
21 | 21 | return symbols if mode.nil? || mode == :mixed |
22 | 22 | |
23 | symbols.include?(mode) ? [*mode] : [] | |
23 | symbols.include?(mode) ? Array(mode) : [] | |
24 | 24 | end |
25 | 25 | |
26 | 26 | # @param [ CMSScanner::Finders::Finder ] finder |
27 | 27 | # @param [ Symbol ] symbol See return values of #symbols_from_mode |
28 | 28 | # @param [ Hash ] opts |
29 | 29 | 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| | |
31 | 31 | findings << found |
32 | 32 | end |
33 | 33 | end |
5 | 5 | # Module to provide an easy way to perform password attacks |
6 | 6 | module BreadthFirstDictionaryAttack |
7 | 7 | # @param [ Array<CMSScanner::Model::User> ] users |
8 | # @param [ Array<String> ] passwords | |
8 | # @param [ String ] wordlist_path | |
9 | 9 | # @param [ Hash ] opts |
10 | 10 | # @option opts [ Boolean ] :show_progression |
11 | 11 | # |
12 | 12 | # @yield [ CMSScanner::User ] When a valid combination is found |
13 | 13 | # |
14 | 14 | # 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' | |
16 | 16 | # |
17 | 17 | # TODO: Make rubocop happy about metrics etc |
18 | 18 | # |
19 | 19 | # 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]) | |
22 | 24 | |
23 | 25 | queue_count = 0 |
24 | 26 | # Keep the number of requests sent for each users |
27 | 29 | |
28 | 30 | users.each { |u| user_requests_count[u.username] = 0 } |
29 | 31 | |
30 | passwords.each do |password| | |
32 | File.foreach(wordlist) do |password| | |
33 | password.chomp! | |
31 | 34 | remaining_users = users.select { |u| u.password.nil? } |
32 | 35 | |
33 | 36 | break if remaining_users.empty? |
46 | 49 | user.password = password |
47 | 50 | |
48 | 51 | begin |
49 | progress_bar.total -= passwords.size - user_requests_count[user.username] | |
52 | progress_bar.total -= wordlist.count - user_requests_count[user.username] | |
50 | 53 | rescue ProgressBar::InvalidProgressError |
51 | 54 | end |
52 | 55 | |
104 | 107 | "No response from remote server. WAF/IPS? (#{response.return_message})" |
105 | 108 | elsif response.code.to_s.start_with?('50') |
106 | 109 | 'Server error, try reducing the number of threads.' |
110 | elsif NS::ParsedCli.verbose? | |
111 | "Unknown response received Code: #{response.code}\nBody: #{response.body}" | |
107 | 112 | else |
108 | "Unknown response received Code: #{response.code}\nBody: #{response.body}" | |
113 | "Unknown response received Code: #{response.code}" | |
109 | 114 | end |
110 | 115 | |
111 | 116 | progress_bar.log("Error: #{error}") |
54 | 54 | # @return [ Typhoeus::Response, nil ] |
55 | 55 | def maybe_get_full_response(head_res, opts) |
56 | 56 | 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) | |
58 | 58 | |
59 | 59 | full_res = NS::Browser.get(head_res.effective_url, full_request_params) |
60 | 60 |
16 | 16 | def log(string = nil) |
17 | 17 | return logs if string.nil? |
18 | 18 | |
19 | logs << string | |
19 | logs << string unless logs.include?(string) | |
20 | 20 | end |
21 | 21 | end |
22 | 22 | end |
20 | 20 | next unless refs.key?(key) |
21 | 21 | |
22 | 22 | @references[key] = if key == :youtube |
23 | [*refs[:youtube]].map { |id| youtube_url(id) } | |
23 | Array(refs[:youtube]).map { |id| youtube_url(id) } | |
24 | 24 | else |
25 | [*refs[key]].map(&:to_s) | |
25 | Array(refs[key]).map(&:to_s) | |
26 | 26 | end |
27 | 27 | end |
28 | 28 | end |
4 | 4 | module Platform |
5 | 5 | # Some PHP specific implementation |
6 | 6 | 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}\]| | |
8 | 8 | PHP\s(?:Fatal|Warning|Strict|Error|Notice):)/x.freeze |
9 | 9 | FPD_PATTERN = /Fatal error:.+? in (.+?) on/.freeze |
10 | 10 | ERROR_LOG_PATTERN = /PHP Fatal error/i.freeze |
52 | 52 | domains = [uri.host + uri.path] |
53 | 53 | |
54 | 54 | domains += if scope.domains.empty? |
55 | [*scope.invalid_domains[1..-1]] | |
55 | Array(scope.invalid_domains[1..-1]) | |
56 | 56 | else |
57 | [*scope.domains[1..-1]].map(&:to_s) + scope.invalid_domains | |
57 | Array(scope.domains[1..-1]).map(&:to_s) + scope.invalid_domains | |
58 | 58 | end |
59 | 59 | |
60 | 60 | domains.map! { |d| Regexp.escape(d.delete_suffix('/')).gsub('\*', '.*').gsub('/', '\\\\\?/') } |
40 | 40 | def directory_listing?(path = nil, params = {}) |
41 | 41 | res = NS::Browser.get(url(path), params) |
42 | 42 | |
43 | res.code == 200 && res.body =~ /<h1>Index of/ ? true : false | |
43 | res.code == 200 && res.body.include?('<h1>Index of') ? true : false | |
44 | 44 | end |
45 | 45 | |
46 | 46 | # @param [ String ] path |
17 | 17 | super(url, opts) |
18 | 18 | |
19 | 19 | scope << uri.host |
20 | [*opts[:scope]].each { |s| scope << s } | |
20 | Array(opts[:scope]).each { |s| scope << s } | |
21 | 21 | end |
22 | 22 | |
23 | 23 | # @param [ Hash ] opts |
19 | 19 | %i[passive aggressive].each do |symbol| |
20 | 20 | it 'returns it in an array' do |
21 | 21 | @mode = symbol |
22 | @expected = [*symbol] | |
22 | @expected = Array(symbol) | |
23 | 23 | end |
24 | 24 | end |
25 | 25 | end |
15 | 15 | end |
16 | 16 | |
17 | 17 | def errored_response?(response) |
18 | response.timed_out? || response.body =~ /Error:/ | |
18 | response.timed_out? || response.body.include?('Error:') | |
19 | 19 | end |
20 | 20 | end |
21 | 21 | |
25 | 25 | |
26 | 26 | describe '#attack' do |
27 | 27 | 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 } | |
29 | 29 | |
30 | 30 | before do |
31 | 31 | # Mock all login requests to 401 |
32 | passwords.each do |password| | |
32 | File.foreach(wordlist_path) do |password| | |
33 | 33 | users.each do |user| |
34 | 34 | stub_request(:post, login_url) |
35 | .with(body: { username: user.username, pwd: password }) | |
35 | .with(body: { username: user.username, pwd: password.chomp }) | |
36 | 36 | .to_return(status: 401) |
37 | 37 | end |
38 | 38 | end |
40 | 40 | |
41 | 41 | context 'when no valid credentials' do |
42 | 42 | 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 | |
44 | 44 | end |
45 | 45 | |
46 | 46 | context 'when trying to increment above current progress' do |
49 | 49 | expect_any_instance_of(ProgressBar::Base) |
50 | 50 | .to receive(:progress) |
51 | 51 | .at_least(1) |
52 | .and_return(users.size * passwords.size) | |
52 | .and_return(users.size * File.open(wordlist_path).count) | |
53 | 53 | |
54 | 54 | expect_any_instance_of(ProgressBar::Base) |
55 | 55 | .not_to receive(:increment) |
56 | 56 | |
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 | |
58 | 58 | end |
59 | 59 | end |
60 | 60 | end |
67 | 67 | end |
68 | 68 | |
69 | 69 | it 'yields the matching user' do |
70 | expect { |block| finder.attack(users, passwords, &block) } | |
70 | expect { |block| finder.attack(users, wordlist_path, &block) } | |
71 | 71 | .to yield_with_args(CMSScanner::Model::User.new('admin', password: 'admin')) |
72 | 72 | end |
73 | 73 | |
75 | 75 | it 'does not raise an error' do |
76 | 76 | expect_any_instance_of(ProgressBar::Base).to receive(:total=).and_raise ProgressBar::InvalidProgressError |
77 | 77 | |
78 | expect { |block| finder.attack(users, passwords, &block) } | |
78 | expect { |block| finder.attack(users, wordlist_path, &block) } | |
79 | 79 | .to yield_with_args(CMSScanner::Model::User.new('admin', password: 'admin')) |
80 | 80 | end |
81 | 81 | end |
93 | 93 | .to_timeout |
94 | 94 | end |
95 | 95 | |
96 | finder.attack(users, passwords) | |
96 | CMSScanner::ParsedCli.options = { verbose: defined?(verbose) ? verbose : false } | |
97 | ||
98 | finder.attack(users, wordlist_path) | |
97 | 99 | end |
98 | 100 | |
99 | 101 | context 'when request timeout' do |
127 | 129 | context 'when unknown error' do |
128 | 130 | let(:stub_params) { { status: 200, body: 'Error: Something went wrong' } } |
129 | 131 | |
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 | |
134 | 150 | end |
135 | 151 | end |
136 | 152 | end |
208 | 208 | end |
209 | 209 | |
210 | 210 | 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) } | |
212 | 212 | |
213 | 213 | before do |
214 | 214 | stub_request(:head, target_urls.keys.last).and_return(status: 200, headers: { 'x-cacheable' => 'YES' }) |
17 | 17 | |
18 | 18 | expect(output.log).to eql(%w[M1 M2]) |
19 | 19 | 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 | |
20 | 29 | end |
21 | 30 | end |
22 | 31 | end |
113 | 113 | its(:scope_url_pattern) { should eql %r{https?:\\?/\\?/(?:e\.org)\\?/?}i } |
114 | 114 | |
115 | 115 | context 'when target is an invalid domain for PublicSuffix' do |
116 | let(:url) { 'http://wp-lab/' } | |
116 | let(:url) { 'http://wp_lab/' } | |
117 | 117 | |
118 | its(:scope_url_pattern) { should eql %r{https?:\\?/\\?/(?:wp\-lab)\\?/?}i } | |
118 | its(:scope_url_pattern) { should eql %r{https?:\\?/\\?/(?:wp_lab)\\?/?}i } | |
119 | 119 | end |
120 | 120 | |
121 | 121 | context 'when a port is present in the target URL' do |
127 | 127 | end |
128 | 128 | |
129 | 129 | 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']) } | |
131 | 131 | |
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 } | |
133 | 133 | |
134 | 134 | context 'when target URL has a subdir' do |
135 | 135 | let(:url) { 'https://e.org/blog/test' } |
136 | 136 | |
137 | 137 | 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 | |
139 | 139 | end |
140 | 140 | end |
141 | 141 | end |