diff --git a/CREDITS b/CREDITS index aa48164e..02088591 100644 --- a/CREDITS +++ b/CREDITS @@ -15,3 +15,4 @@ michee08 - Reported and gave potential solutions to bugs. Callum Pember - Implemented proxy support - callumpember at gmail.com g0tmi1k - Additional timthumb checks + bug reports. Melvin Lammerts - Reported a couple of fake vulnerabilities - melvin at 12k.nl +Christian Mehlmauer - @_FireFart_ - Theme enumeration \ No newline at end of file diff --git a/lib/wpscan/wp_item.rb b/lib/wpscan/wp_item.rb index 8793da2c..588ef53d 100644 --- a/lib/wpscan/wp_item.rb +++ b/lib/wpscan/wp_item.rb @@ -17,23 +17,37 @@ #++ class WpItem < Vulnerable - attr_accessor :path, :url, :wp_content_dir + attr_accessor :path, :url, :wp_content_dir, :name @version = nil + def initialize(options = {}) + @wp_content_dir = options[:wp_content_dir] + @url = options[:url] + @path = options[:path] + @name = options[:name] || extract_name_from_url + end + + # Get the full url for this item def get_url - URI.parse("#{@url.to_s}#@wp_content_dir/#@path") + url = @url.to_s.end_with?("/") ? @url.to_s : "#@url/" + # remove first and last / + wp_content_dir = @wp_content_dir.sub(/^\//, "").sub(/\/$/, "") + # remove first / + path = @path.sub(/^\//, "") + URI.parse("#{url}#{wp_content_dir}/#{path}") end + # Gets the full url for this item without filenames def get_url_without_filename - matches = @path.match(%r{^(.*/).*$}) - if matches == nil or matches.length < 2 - dirname = @path - else - dirname = matches[1] + location_url = get_url.to_s + valid_location_url = location_url[%r{^(https?://.*/)[^.]+\.[^/]+$}, 1] + unless valid_location_url + valid_location_url = add_trailing_slash(location_url) end - URI.parse("#{@url.to_s}#@wp_content_dir/#{dirname}") + URI.parse(valid_location_url) end + # Returns version number from readme.txt if it exists def version unless @version response = Browser.instance.get(get_url.merge("readme.txt").to_s) @@ -45,42 +59,36 @@ class WpItem < Vulnerable # Is directory listing enabled? def directory_listing? # Need to remove to file part from the url - Browser.instance.get(location_uri_from_file_url(get_url.to_s)).body[%r{Index of}] ? true : false + Browser.instance.get(get_url_without_filename).body[%r{<title>Index of}] ? true : false end - def extract_name_from_url(url) - url.to_s[%r{^(https?://.*/([^/]+)/)}i, 2] + # Extract item name from a url + def extract_name_from_url + get_url.to_s[%r{^(https?://.*/([^/]+)/)}i, 2] end + # To string. Adds a version number if detected def to_s item_version = version "#@name#{' v' + item_version.strip if item_version}" end + # Object comparer def ==(item) item.name == @name end - def <=>(item) - item.name <=> @name - end - - def location_uri_from_file_url(location_url) - valid_location_url = location_url[%r{^(https?://.*/)[^.]+\.[^/]+$}, 1] - unless valid_location_url - valid_location_url = add_trailing_slash(location_url) - end - URI.parse(valid_location_url) - end - + # Url for readme.txt def readme_url get_url_without_filename.merge("readme.txt") end + # Url for changelog.txt def changelog_url get_url_without_filename.merge("changelog.txt") end + # readme.txt present? def has_readme? unless @readme status = Browser.instance.get(readme_url).code @@ -89,6 +97,7 @@ class WpItem < Vulnerable @readme end + # changelog.txt present? def has_changelog? unless @changelog status = Browser.instance.get(changelog_url).code diff --git a/lib/wpscan/wp_plugin.rb b/lib/wpscan/wp_plugin.rb index f344c082..e2764d6b 100644 --- a/lib/wpscan/wp_plugin.rb +++ b/lib/wpscan/wp_plugin.rb @@ -26,7 +26,6 @@ class WpPlugin < WpItem @url = options[:url] @path = options[:path] @wp_content_dir = options[:wp_content_dir] - @name = options[:name] || extract_name_from_url(get_url) @vulns_xml = options[:vulns_xml] || DATA_DIR + '/plugin_vulns.xml' @vulns_xpath = "//plugin[@name='#@name']/vulnerability" @version = nil @@ -36,6 +35,8 @@ class WpPlugin < WpItem raise("wp_content_dir not set") unless @wp_content_dir raise("name not set") unless @name raise("vulns_xml not set") unless @vulns_xml + + super(:wp_content_dir => @wp_content_dir, :url => @url, :path => @path) end # Discover any error_log files created by WordPress diff --git a/lib/wpscan/wp_theme.rb b/lib/wpscan/wp_theme.rb index 991809dc..94352ad9 100644 --- a/lib/wpscan/wp_theme.rb +++ b/lib/wpscan/wp_theme.rb @@ -24,7 +24,6 @@ class WpTheme < WpItem def initialize(options = {}) @url = options[:url] - @name = options[:name] || extract_name_from_url(get_url) @path = options[:path] @wp_content_dir = options[:wp_content_dir] @vulns_xml = options[:vulns_xml] || DATA_DIR + '/wp_theme_vulns.xml' @@ -38,6 +37,8 @@ class WpTheme < WpItem raise("wp_content_dir not set") unless @wp_content_dir raise("name not set") unless @name raise("vulns_xml not set") unless @vulns_xml + + super(:wp_content_dir => @wp_content_dir, :url => @url, :path => @path) end def version @@ -49,7 +50,6 @@ class WpTheme < WpItem @version end - def self.find(target_uri) self.methods.grep(/find_from_/).each do |method_to_call| theme = self.send(method_to_call, target_uri) diff --git a/spec/lib/wpscan/wp_item_spec.rb b/spec/lib/wpscan/wp_item_spec.rb new file mode 100644 index 00000000..8b6d900e --- /dev/null +++ b/spec/lib/wpscan/wp_item_spec.rb @@ -0,0 +1,228 @@ +#-- +# WPScan - WordPress Security Scanner +# Copyright (C) 2012 +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +#++ + +require File.expand_path(File.dirname(__FILE__) + '/wpscan_helper') + +describe WpPlugin do + before :all do + @browser = Browser.instance(:config_file => SPEC_FIXTURES_CONF_DIR + '/browser/browser.conf.json') + end + + before :each do + @instance = WpItem.new(:wp_content_dir => "wp-content", + :url => "http://sub.example.com/path/to/wordpress/", + :path => "plugins/test/asdf.php") + end + + describe "#initialize" do + it "should create a correct instance" do + @instance.wp_content_dir.should == "wp-content" + @instance.url.should == "http://sub.example.com/path/to/wordpress/" + @instance.path.should == "plugins/test/asdf.php" + end + end + + describe "#get_url" do + it "should return the correct url" do + @instance.get_url.to_s.should == "http://sub.example.com/path/to/wordpress/wp-content/plugins/test/asdf.php" + end + + it "should return the correct url (custom wp_content_dir)" do + @instance.wp_content_dir = "custom" + @instance.get_url.to_s.should == "http://sub.example.com/path/to/wordpress/custom/plugins/test/asdf.php" + end + + it "should trim / and add missing / before concatenating url" do + @instance.wp_content_dir = "/custom/" + @instance.url = "http://sub.example.com/path/to/wordpress" + @instance.path = "plugins/test/asdf.php" + @instance.get_url.to_s.should == "http://sub.example.com/path/to/wordpress/custom/plugins/test/asdf.php" + end + end + + describe "#get_url_without_filename" do + it "should return the correct url" do + @instance.get_url_without_filename.to_s.should == "http://sub.example.com/path/to/wordpress/wp-content/plugins/test/" + end + + it "should return the correct url (custom wp_content_dir)" do + @instance.wp_content_dir = "custom" + @instance.get_url_without_filename.to_s.should == "http://sub.example.com/path/to/wordpress/custom/plugins/test/" + end + + it "should trim / and add missing / before concatenating url" do + @instance.wp_content_dir = "/custom/" + @instance.url = "http://sub.example.com/path/to/wordpress" + @instance.path = "plugins/test/asdf.php" + @instance.get_url_without_filename.to_s.should == "http://sub.example.com/path/to/wordpress/custom/plugins/test/" + end + + it "should not remove the last foldername" do + @instance.path = "plugins/test/" + @instance.get_url_without_filename.to_s.should == "http://sub.example.com/path/to/wordpress/wp-content/plugins/test/" + end + end + + describe "#version" do + it "should return a version number" do + stub_request(:get, @instance.readme_url.to_s).to_return(:status => 200, :body => "Stable tag: 1.2.4.3.2.1") + @instance.version.should == "1.2.4.3.2.1" + end + + it "should not return a version number" do + stub_request(:get, @instance.readme_url.to_s).to_return(:status => 200, :body => "Stable tag: trunk") + @instance.version.should be nil + end + end + + describe "#directory_listing?" do + it "should return true" do + stub_request(:get, @instance.get_url_without_filename.to_s).to_return(:status => 200, :body => "<html><head><title>Index of asdf") + @instance.directory_listing?.should == true + end + + it "should return false" do + stub_request(:get, @instance.get_url_without_filename.to_s).to_return(:status => 200, :body => "My Wordpress Site") + @instance.directory_listing?.should == false + end + end + + describe "#extract_name_from_url" do + it "should extract the correct name" do + @instance.extract_name_from_url.should == "test" + end + + it "should extract the correct name (custom wp_content_dir)" do + @instance.wp_content_dir = "custom" + @instance.extract_name_from_url.should == "test" + end + + it "should extract the correct name" do + @instance.wp_content_dir = "/custom/" + @instance.url = "http://sub.example.com/path/to/wordpress" + @instance.path = "plugins/test2/asdf.php" + @instance.extract_name_from_url.should == "test2" + end + + it "should extract the correct plugin name" do + @instance.path = "plugins/testplugin/" + @instance.extract_name_from_url.should == "testplugin" + end + + it "should extract the correct theme name" do + @instance.path = "themes/testtheme/" + @instance.extract_name_from_url.should == "testtheme" + end + end + + describe "#to_s" do + it "should return the name including a version number" do + stub_request(:get, @instance.readme_url.to_s).to_return(:status => 200, :body => "Stable tag: 1.2.4.3.2.1") + @instance.to_s.should == "test v1.2.4.3.2.1" + end + + it "should not return the name without a version number" do + stub_request(:get, @instance.readme_url.to_s).to_return(:status => 200, :body => "Stable tag: trunk") + @instance.to_s.should == "test" + end + end + + describe "#==" do + it "should return false" do + instance2 = WpItem.new(:wp_content_dir => "wp-content", + :url => "http://sub.example.com/path/to/wordpress/", + :path => "plugins/newname/asdf.php") + (@instance==instance2).should == false + end + + it "should return true" do + instance2 = WpItem.new(:wp_content_dir => "wp-content", + :url => "http://sub.example.com/path/to/wordpress/", + :path => "plugins/test/asdf.php") + (@instance==instance2).should == true + end + end + + describe "#readme_url" do + it "should return the corrent plugin readme url" do + @instance.readme_url.to_s.should == "http://sub.example.com/path/to/wordpress/wp-content/plugins/test/readme.txt" + end + + it "should return the corrent plugin readme url (custom wp_content)" do + @instance.wp_content_dir = "custom" + @instance.readme_url.to_s.should == "http://sub.example.com/path/to/wordpress/custom/plugins/test/readme.txt" + end + + it "should return the corrent theme readme url" do + @instance.path = "themes/test/asdf.php" + @instance.readme_url.to_s.should == "http://sub.example.com/path/to/wordpress/wp-content/themes/test/readme.txt" + end + + it "should return the corrent theme readme url (custom wp_content)" do + @instance.wp_content_dir = "custom" + @instance.path = "themes/test/asdf.php" + @instance.readme_url.to_s.should == "http://sub.example.com/path/to/wordpress/custom/themes/test/readme.txt" + end + end + + describe "#changelog_url" do + it "should return the corrent plugin changelog url" do + @instance.changelog_url.to_s.should == "http://sub.example.com/path/to/wordpress/wp-content/plugins/test/changelog.txt" + end + + it "should return the corrent plugin changelog url (custom wp_content)" do + @instance.wp_content_dir = "custom" + @instance.changelog_url.to_s.should == "http://sub.example.com/path/to/wordpress/custom/plugins/test/changelog.txt" + end + + it "should return the corrent theme changelog url" do + @instance.path = "themes/test/asdf.php" + @instance.changelog_url.to_s.should == "http://sub.example.com/path/to/wordpress/wp-content/themes/test/changelog.txt" + end + + it "should return the corrent theme changelog url (custom wp_content)" do + @instance.wp_content_dir = "custom" + @instance.path = "themes/test/asdf.php" + @instance.changelog_url.to_s.should == "http://sub.example.com/path/to/wordpress/custom/themes/test/changelog.txt" + end + end + + describe "#has_readme?" do + it "should return true" do + stub_request(:get, @instance.readme_url.to_s).to_return(:status => 200) + @instance.has_readme?.should == true + end + + it "should return false" do + stub_request(:get, @instance.readme_url.to_s).to_return(:status => 403) + @instance.has_readme?.should == false + end + end + + describe "#has_changelog?" do + it "should return true" do + stub_request(:get, @instance.changelog_url.to_s).to_return(:status => 200) + @instance.has_changelog?.should == true + end + + it "should return false" do + stub_request(:get, @instance.changelog_url.to_s).to_return(:status => 403) + @instance.has_changelog?.should == false + end + end +end \ No newline at end of file