From 37a72f0c7229f49108df48147759f709065fe87b Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Wed, 9 May 2018 16:34:30 +0100 Subject: [PATCH] Add /.well-known/security.txt check See https://securitytxt.org/ --- lib/wpscan/web_site.rb | 2 + lib/wpscan/web_site/security_txt.rb | 39 +++++++ spec/shared_examples/web_site/security_txt.rb | 108 ++++++++++++++++++ wpscan.rb | 8 ++ 4 files changed, 157 insertions(+) create mode 100644 lib/wpscan/web_site/security_txt.rb create mode 100644 spec/shared_examples/web_site/security_txt.rb diff --git a/lib/wpscan/web_site.rb b/lib/wpscan/web_site.rb index 77125c98..3779210b 100644 --- a/lib/wpscan/web_site.rb +++ b/lib/wpscan/web_site.rb @@ -2,11 +2,13 @@ require 'web_site/robots_txt' require 'web_site/humans_txt' +require 'web_site/security_txt' require 'web_site/interesting_headers' class WebSite include WebSite::RobotsTxt include WebSite::HumansTxt + include WebSite::SecurityTxt include WebSite::InterestingHeaders attr_reader :uri diff --git a/lib/wpscan/web_site/security_txt.rb b/lib/wpscan/web_site/security_txt.rb new file mode 100644 index 00000000..3464fd72 --- /dev/null +++ b/lib/wpscan/web_site/security_txt.rb @@ -0,0 +1,39 @@ +# encoding: UTF-8 + +class WebSite + module SecurityTxt + + # Checks if a security.txt file exists + # @return [ Boolean ] + def has_security? + Browser.get(security_url).code == 200 + end + + # Gets a security.txt URL + # @return [ String ] + def security_url + @uri.clone.merge('.well-known/security.txt').to_s + end + + # Parse security.txt + # @return [ Array ] URLs generated from security.txt + def parse_security_txt + return unless has_security? + + return_object = [] + response = Browser.get(security_url.to_s) + entries = response.body.split(/\n/) + if entries + entries.flatten! + entries.uniq! + + entries.each do |d| + temp = d.strip + return_object << temp.to_s + end + end + return_object + end + + end +end diff --git a/spec/shared_examples/web_site/security_txt.rb b/spec/shared_examples/web_site/security_txt.rb new file mode 100644 index 00000000..3bca86dd --- /dev/null +++ b/spec/shared_examples/web_site/security_txt.rb @@ -0,0 +1,108 @@ +# encoding: UTF-8 + +shared_examples 'WebSite::SecurityTxt' do + let(:known_dirs) { WebSite::SecurityTxt.known_dirs } + + describe '#security_url' do + it 'returns the correct url' do + expect(web_site.security_url).to eql 'http://example.localhost/security.txt' + end + end + + describe '#has_security?' do + it 'returns true' do + stub_request(:get, web_site.security_url).to_return(status: 200) + expect(web_site.has_security?).to be_truthy + end + + it 'returns false' do + stub_request(:get, web_site.security_url).to_return(status: 404) + expect(web_site.has_security?).to be_falsey + end + end + + describe '#parse_security_txt' do + + context 'installed in root' do + after :each do + stub_request_to_fixture(url: web_site.security_url, fixture: @fixture) + security = web_site.parse_security_txt + expect(security).to match_array @expected + end + + it 'returns an empty Array (empty security.txt)' do + @fixture = fixtures_dir + '/security_txt/empty_security.txt' + @expected = [] + end + + it 'returns an empty Array (invalid security.txt)' do + @fixture = fixtures_dir + '/security_txt/invalid_security.txt' + @expected = [] + end + + it 'returns some urls and some strings' do + @fixture = fixtures_dir + '/security_txt/invalid_security_2.txt' + @expected = %w( + /ÖÜ()=? + http://10.0.0.0/wp-includes/ + http://example.localhost/asdf/ + wooooza + ) + end + + it 'returns an Array of urls (valid security.txt)' do + @fixture = fixtures_dir + '/security_txt/security.txt' + @expected = %w( + http://example.localhost/wordpress/admin/ + http://example.localhost/wordpress/wp-admin/ + http://example.localhost/wordpress/secret/ + http://example.localhost/Wordpress/wp-admin/ + http://example.localhost/wp-admin/tralling-space/ + http://example.localhost/asdf/ + ) + end + + it 'removes duplicate entries from security.txt test 1' do + @fixture = fixtures_dir + '/security_txt/security_duplicate_1.txt' + @expected = %w( + http://example.localhost/wordpress/ + http://example.localhost/wordpress/admin/ + http://example.localhost/wordpress/wp-admin/ + http://example.localhost/wordpress/secret/ + http://example.localhost/Wordpress/wp-admin/ + http://example.localhost/wp-admin/tralling-space/ + http://example.localhost/asdf/ + ) + end + + it 'removes duplicate entries from security.txt test 2' do + @fixture = fixtures_dir + '/security_txt/security_duplicate_2.txt' + @expected = nil + end + end + + context 'installed in sub directory' do + it 'returns an Array of urls (valid security.txt, WP installed in subdir)' do + web_site_sub = WebSite.new('http://example.localhost/wordpress/') + fixture = fixtures_dir + '/security_txt/security.txt' + expected = %w( + http://example.localhost/wordpress/admin/ + http://example.localhost/wordpress/secret/ + http://example.localhost/Wordpress/wp-admin/ + http://example.localhost/wp-admin/tralling-space/ + http://example.localhost/asdf/ + ) + stub_request_to_fixture(url: web_site_sub.security_url, fixture: fixture) + security = web_site_sub.parse_security_txt + expect(security).to match_array expected + end + end + end + + describe '#known_dirs' do + it 'does not contain duplicates' do + expect(known_dirs.flatten.uniq.length).to eq known_dirs.length + end + end + +end diff --git a/wpscan.rb b/wpscan.rb index be4a0b0d..257ec151 100755 --- a/wpscan.rb +++ b/wpscan.rb @@ -219,6 +219,14 @@ def main end end + if wp_target.has_security? + puts info("security.txt available under: #{wp_target.security_url}") + + wp_target.parse_security_txt.each do |dir| + puts info("Interesting entry from security.txt: #{dir}") + end + end + if wp_target.has_full_path_disclosure? puts warning("Full Path Disclosure (FPD) in '#{wp_target.full_path_disclosure_url}': #{wp_target.full_path_disclosure_data}") end