From d9fd20c6fe7699cccc830de432a09b1796b5f701 Mon Sep 17 00:00:00 2001 From: erwanlr Date: Thu, 17 Jan 2013 13:08:01 +0100 Subject: [PATCH] WPSTools plugins mode activated --- lib/common/custom_option_parser.rb | 89 +++++++ lib/common/plugins/plugin.rb | 40 ++++ lib/common/plugins/plugins.rb | 55 +++++ lib/common_helper.rb | 38 +-- lib/environment.rb | 1 + .../plugins/checker/checker_plugin.rb | 148 ++++++++++++ .../list_generator}/generate_list.rb | 6 +- .../list_generator/list_generator_plugin.rb | 69 ++++++ .../list_generator/svn_parser.rb} | 2 +- lib/wpstools/wpstools_helper.rb | 32 +-- spec/lib/common/custom_option_parser_spec.rb | 153 ++++++++++++ spec/lib/common/plugins/plugin_spec.rb | 46 ++++ spec/lib/common/plugins/plugins_spec.rb | 95 ++++++++ .../list_generator/generate_list_spec.rb | 3 + .../plugins/list_generator/svn_parser_spec.rb | 3 + spec/spec_helper.rb | 1 + wpstools.rb | 219 ++---------------- 17 files changed, 749 insertions(+), 251 deletions(-) create mode 100644 lib/common/custom_option_parser.rb create mode 100644 lib/common/plugins/plugin.rb create mode 100644 lib/common/plugins/plugins.rb create mode 100644 lib/wpstools/plugins/checker/checker_plugin.rb rename lib/wpstools/{ => plugins/list_generator}/generate_list.rb (96%) create mode 100644 lib/wpstools/plugins/list_generator/list_generator_plugin.rb rename lib/wpstools/{parse_svn.rb => plugins/list_generator/svn_parser.rb} (99%) create mode 100644 spec/lib/common/custom_option_parser_spec.rb create mode 100644 spec/lib/common/plugins/plugin_spec.rb create mode 100644 spec/lib/common/plugins/plugins_spec.rb create mode 100644 spec/lib/wpstools/plugins/list_generator/generate_list_spec.rb create mode 100644 spec/lib/wpstools/plugins/list_generator/svn_parser_spec.rb diff --git a/lib/common/custom_option_parser.rb b/lib/common/custom_option_parser.rb new file mode 100644 index 00000000..4a20358a --- /dev/null +++ b/lib/common/custom_option_parser.rb @@ -0,0 +1,89 @@ +# WPScan - WordPress Security Scanner +# Copyright (C) 2012-2013 +# +# 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 . +#++ + +class CustomOptionParser < OptionParser + + attr_reader :symbols_used + + def initialize(banner = nil, width = 32, indent = ' ' * 4) + @results = {} + @symbols_used = [] + super(banner, width, indent) + end + + + # param Array(Array) or Array options + def add(options) + if options.is_a?(Array) + if options[0].is_a?(Array) + options.each do |option| + add_option(option) + end + else + add_option(options) + end + else + raise "Options must be at least an Array, or an Array(Array). #{options.class} supplied" + end + end + + # param Array option + def add_option(option) + if option.is_a?(Array) + option_symbol = CustomOptionParser::option_to_symbol(option) + + unless @symbols_used.include?(option_symbol) + @symbols_used << option_symbol + + self.on(*option) do |arg| + @results[option_symbol] = arg + end + else + raise "The option #{option_symbol} is already used !" + end + else + raise "The option must be an array, #{option.class} supplied : '#{option}'" + end + end + + # return Hash + def results(argv = default_argv) + self.parse!(argv) if @results.empty? + + @results + end + + protected + # param Array option + def self.option_to_symbol(option) + option_name = nil + + option.each do |option_attr| + if option_attr =~ /^--/ + option_name = option_attr + break + end + end + + if option_name + option_name = option_name.gsub(/^--/, '').gsub(/-/, '_').gsub(/ .*$/, '') + :"#{option_name}" + else + raise "Could not find the option name for #{option}" + end + end +end diff --git a/lib/common/plugins/plugin.rb b/lib/common/plugins/plugin.rb new file mode 100644 index 00000000..97c259c7 --- /dev/null +++ b/lib/common/plugins/plugin.rb @@ -0,0 +1,40 @@ +# WPScan - WordPress Security Scanner +# Copyright (C) 2012-2013 +# +# 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 . +#++ + +class Plugin + + attr_reader :author, :registered_options + + def initialize(infos = {}) + @author = infos[:author] + end + + def run(options = {}) + raise NotImplementedError + end + + # param Array options + def register_options(*options) + options.each do |option| + unless option.is_a?(Array) + raise "Each option must be an array, #{option.class} supplied" + end + end + @registered_options = options + end + +end diff --git a/lib/common/plugins/plugins.rb b/lib/common/plugins/plugins.rb new file mode 100644 index 00000000..5c1791e8 --- /dev/null +++ b/lib/common/plugins/plugins.rb @@ -0,0 +1,55 @@ +# WPScan - WordPress Security Scanner +# Copyright (C) 2012-2013 +# +# 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 . +#++ + +class Plugins < Array + + attr_reader :option_parser + + def initialize(option_parser = nil) + if option_parser + if option_parser.is_a?(CustomOptionParser) + @option_parser = option_parser + else + raise "The parser must be an instance of CustomOptionParser, #{option_parser.class} supplied" + end + else + @option_parser = CustomOptionParser.new + end + end + + # param Array(Plugin) plugins + def register(*plugins) + plugins.each do |plugin| + register_plugin(plugin) + end + end + + # param Plugin plugin + def register_plugin(plugin) + if plugin.is_a?(Plugin) + self << plugin + + # A plugin may not have options + if plugin_options = plugin.registered_options + @option_parser.add(plugin_options) + end + else + raise "The argument must be an instance of Plugin, #{plugin.class} supplied" + end + end + +end diff --git a/lib/common_helper.rb b/lib/common_helper.rb index 08f2c573..31335186 100644 --- a/lib/common_helper.rb +++ b/lib/common_helper.rb @@ -16,17 +16,22 @@ # along with this program. If not, see . #++ -LIB_DIR = File.dirname(__FILE__) -ROOT_DIR = File.expand_path(LIB_DIR + '/..') # expand_path is used to get "wpscan/" instead of "wpscan/lib/../" -DATA_DIR = ROOT_DIR + "/data" -CONF_DIR = ROOT_DIR + "/conf" -CACHE_DIR = ROOT_DIR + "/cache" -WPSCAN_LIB_DIR = LIB_DIR + "/wpscan" -WPSTOOLS_LIB_DIR = LIB_DIR + "/wpstools" -UPDATER_LIB_DIR = LIB_DIR + "/updater" -LOG_FILE = ROOT_DIR + "/log.txt" +LIB_DIR = File.dirname(__FILE__) +ROOT_DIR = File.expand_path(LIB_DIR + '/..') # expand_path is used to get "wpscan/" instead of "wpscan/lib/../" +DATA_DIR = ROOT_DIR + "/data" +CONF_DIR = ROOT_DIR + "/conf" +CACHE_DIR = ROOT_DIR + "/cache" +WPSCAN_LIB_DIR = LIB_DIR + "/wpscan" +WPSTOOLS_LIB_DIR = LIB_DIR + "/wpstools" +UPDATER_LIB_DIR = LIB_DIR + "/updater" +COMMON_LIB_DIR = LIB_DIR + "/common" +LOG_FILE = ROOT_DIR + "/log.txt" +# Plugins directories +COMON_PLUGINS_DIR = COMMON_LIB_DIR + "/plugins" +WPSCAN_PLUGINS_DIR = WPSCAN_LIB_DIR + "/plugins" +WPSTOOLS_PLUGINS_DIR = WPSTOOLS_LIB_DIR + "/plugins" -WPSCAN_VERSION = "2.0" +WPSCAN_VERSION = "2.0" require "#{LIB_DIR}/environment" @@ -39,6 +44,9 @@ def require_files_from_directory(absolute_dir_path, files_pattern = "*.rb") end end +#require_files_from_directory(COMMON_LIB_DIR) +require_files_from_directory(COMMON_LIB_DIR, "**/*.rb") + # Add protocol def add_http_protocol(url) url =~ /^https?:/ ? url : "http://#{url}" @@ -148,9 +156,9 @@ def get_metasploit_url(module_path) end # Override for puts to enable logging -def puts(o = "") +#def puts(o = "") # remove color for logging - temp = o.gsub(/\e\[\d+m(?.*)?\e\[0m/, '\k') - File.open(LOG_FILE, "a+") { |f| f.puts(temp) } - super(o) -end \ No newline at end of file + #temp = o.gsub(/\e\[\d+m(?.*)?\e\[0m/, '\k') + #File.open(LOG_FILE, "a+") { |f| f.puts(temp) } + #super(o) +#end diff --git a/lib/environment.rb b/lib/environment.rb index 3b611cca..be1b58c7 100644 --- a/lib/environment.rb +++ b/lib/environment.rb @@ -20,6 +20,7 @@ begin # Standard libs require 'rubygems' require 'getoptlong' + require 'optparse' # Will replace getoptlong require 'uri' require 'time' require 'resolv' diff --git a/lib/wpstools/plugins/checker/checker_plugin.rb b/lib/wpstools/plugins/checker/checker_plugin.rb new file mode 100644 index 00000000..736f7464 --- /dev/null +++ b/lib/wpstools/plugins/checker/checker_plugin.rb @@ -0,0 +1,148 @@ +# WPScan - WordPress Security Scanner +# Copyright (C) 2012-2013 +# +# 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 . +#++ + +class CheckerPlugin < Plugin + + def initialize + super( + :author => "@wpscanteam - @erwanlr" + ) + + register_options( + ["--check-vuln-ref-urls", "--cvru", "Check all the vulnerabilities reference urls for 404"], + ["--check-local-vulnerable-files LOCAL_DIRECTORY", "--clvf", "Perform a recursive scan in the LOCAL_DIRECTORY to find vulnerable files or shells"] + ) + end + + def run(options = {}) + if options[:check_vuln_ref_urls] + check_vuln_ref_urls + end + + if options[:check_local_vulnerable_files] + check_local_vulnerable_files(options[:check_local_vulnerable_files]) + end + end + + def check_vuln_ref_urls + vuln_ref_files = ["plugin_vulns.xml", "theme_vulns.xml", "wp_vulns.xml"] + error_codes = [404, 500, 403] + not_found_regexp = %r{No Results Found|error 404|ID Invalid or Not Found}i + + puts "[+] Checking vulnerabilities reference urls" + + vuln_ref_files.each do |vuln_ref_file| + xml = Nokogiri::XML(File.open(DATA_DIR + '/' + vuln_ref_file)) do |config| + config.noblanks + end + + urls = [] + xml.xpath("//reference").each { |node| urls << node.text } + + urls.uniq! + + dead_urls = [] + queue_count = 0 + request_count = 0 + browser = Browser.instance + hydra = browser.hydra + number_of_urls = urls.size + + urls.each do |url| + request = browser.forge_request(url, { :cache_timeout => 0, :follow_location => true }) + request_count += 1 + + request.on_complete do |response| + print "\r [+] Checking #{vuln_ref_file} #{number_of_urls} total ... #{(request_count * 100) / number_of_urls}% complete." + + if error_codes.include?(response.code) or not_found_regexp.match(response.body) + dead_urls << url + end + end + + hydra.queue(request) + queue_count += 1 + + if queue_count == browser.max_threads + hydra.run + queue_count = 0 + end + end + + hydra.run + puts + unless dead_urls.empty? + dead_urls.each { |url| puts " Not Found #{url}" } + end + end + end + + def check_local_vulnerable_files(dir_to_scan) + if Dir::exist?(dir_to_scan) + xml_file = DATA_DIR + "/local_vulnerable_files.xml" + local_hashes = {} + file_extension_to_scan = "*.{js,php,swf,html,htm}" + + print "[+] Generating local hashes ... " + + Dir[File::join(dir_to_scan, "**", file_extension_to_scan)].each do |filename| + sha1sum = Digest::SHA1.file(filename).hexdigest + + if local_hashes.has_key?(sha1sum) + local_hashes[sha1sum] << filename + else + local_hashes[sha1sum] = [filename] + end + end + + puts "done." + + puts "[+] Checking for vulnerable files ..." + + xml = Nokogiri::XML(File.open(xml_file)) do |config| + config.noblanks + end + + xml.xpath("//hash").each do |node| + sha1sum = node.attribute("sha1").text + + if local_hashes.has_key?(sha1sum) + local_filenames = local_hashes[sha1sum] + vuln_title = node.search("title").text + vuln_filename = node.search("file").text + vuln_refrence = node.search("reference").text + + puts " #{vuln_filename} found :" + puts " | Location(s):" + local_filenames.each do |file| + puts " | - #{file}" + end + puts " |" + puts " | Title: #{vuln_title}" + puts " | Refrence: #{vuln_refrence}" if !vuln_refrence.empty? + puts + end + end + + puts "done." + + else + puts "The supplied directory '#{dir_to_scan}' does not exist" + end + end + +end diff --git a/lib/wpstools/generate_list.rb b/lib/wpstools/plugins/list_generator/generate_list.rb similarity index 96% rename from lib/wpstools/generate_list.rb rename to lib/wpstools/plugins/list_generator/generate_list.rb index 7c52f933..f479cd12 100644 --- a/lib/wpstools/generate_list.rb +++ b/lib/wpstools/plugins/list_generator/generate_list.rb @@ -19,7 +19,7 @@ #++ # This tool generates a list to use for plugin and theme enumeration -class Generate_List +class GenerateList attr_accessor :verbose @@ -70,14 +70,14 @@ class Generate_List def generate_full_list set_file_name(:full) - items = Svn_Parser.new(@svn_url, @verbose).parse + items = SvnParser.new(@svn_url, @verbose).parse save items end def generate_popular_list(pages) set_file_name(:popular) popular = get_popular_items(pages) - items = Svn_Parser.new(@svn_url, @verbose).parse(popular) + items = SvnParser.new(@svn_url, @verbose).parse(popular) save items end diff --git a/lib/wpstools/plugins/list_generator/list_generator_plugin.rb b/lib/wpstools/plugins/list_generator/list_generator_plugin.rb new file mode 100644 index 00000000..eaa96347 --- /dev/null +++ b/lib/wpstools/plugins/list_generator/list_generator_plugin.rb @@ -0,0 +1,69 @@ +# WPScan - WordPress Security Scanner +# Copyright (C) 2012-2013 +# +# 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 . +#++ + +class ListGeneratorPlugin < Plugin + + def initialize + super( + :author => "WPScanTeam - @FireFart" + ) + + register_options( + ["--generate-plugin-list [NUMBER_OF_PAGES]", "--gpl", Integer, "Generate a new data/plugins.txt file. (supply number of *pages* to parse, default : 150)"], + ["--generate-full-plugin-list", "--gfpl", "Generate a new full data/plugins.txt file"], + + ["--generate-theme-list [NUMBER_OF_PAGES]", "--gtl", Integer, "Generate a new data/themes.txt file. (supply number of *pages* to parse, default : 150)"], + ["--generate-full-theme-list", "--gftl", "Generate a new full data/themes.txt file"], + + ["--generate-all", "--ga", "Generate a new full plugins, full themes, popular plugins and popular themes list"], + ) + end + + def run(options = {}) + verbose = options[:verbose] || false + generate_all = options[:generate_all] || false + + if options.has_key?(:generate_plugin_list) || generate_all + number_of_pages = options[:generate_plugin_list] || 150 + + puts "[+] Generating new most popular plugin list" + puts + GenerateList.new('plugins', verbose).generate_popular_list(number_of_pages) + end + + if options[:generate_full_plugin_list] || generate_all + puts "[+] Generating new full plugin list" + puts + GenerateList.new('plugins', verbose).generate_full_list + end + + if options.has_key?(:generate_theme_list) || generate_all + number_of_pages = options[:generate_theme_list] || 150 + + puts "[+] Generating new most popular theme list" + puts + GenerateList.new('themes', verbose).generate_popular_list(number_of_pages) + end + + if options[:generate_full_theme_list] || generate_all + puts "[+] Generating new full theme list" + puts + GenerateList.new('themes', verbose).generate_full_list + end + end + +end diff --git a/lib/wpstools/parse_svn.rb b/lib/wpstools/plugins/list_generator/svn_parser.rb similarity index 99% rename from lib/wpstools/parse_svn.rb rename to lib/wpstools/plugins/list_generator/svn_parser.rb index b67243cd..d3866d25 100644 --- a/lib/wpstools/parse_svn.rb +++ b/lib/wpstools/plugins/list_generator/svn_parser.rb @@ -19,7 +19,7 @@ #++ # This Class Parses SVN Repositories via HTTP -class Svn_Parser +class SvnParser attr_accessor :verbose, :svn_root, :keep_empty_dirs diff --git a/lib/wpstools/wpstools_helper.rb b/lib/wpstools/wpstools_helper.rb index fac49420..206697f2 100644 --- a/lib/wpstools/wpstools_helper.rb +++ b/lib/wpstools/wpstools_helper.rb @@ -19,6 +19,7 @@ require File.expand_path(File.dirname(__FILE__) + '/../common_helper') require_files_from_directory(WPSTOOLS_LIB_DIR) +require_files_from_directory(WPSTOOLS_PLUGINS_DIR, "**/*.rb") def usage() script_name = $0 @@ -28,19 +29,19 @@ def usage() puts "Examples:" puts puts "- Generate a new 'most popular' plugin list, up to 150 pages ..." - puts "ruby #{script_name} --generate_plugin_list 150" + puts "ruby #{script_name} --generate-plugin-list 150" puts puts "- Generate a new full plugin list" - puts "ruby #{script_name} --generate_full_plugin_list" + puts "ruby #{script_name} --generate-full-plugin-list" puts puts "- Generate a new 'most popular' theme list, up to 150 pages ..." - puts "ruby #{script_name} --generate_theme_list 150" + puts "ruby #{script_name} --generate-theme-list 150" puts puts "- Generate a new full theme list" - puts "ruby #{script_name} --generate_full_theme_list" + puts "ruby #{script_name} --generate-full-theme-list" puts puts "- Generate all list" - puts "ruby #{script_name} --generate_all" + puts "ruby #{script_name} --generate-all" puts puts "Locally scan a wordpress installation for vulnerable files or shells" puts "ruby #{script_name} --check-local-vulnerable-files /var/www/wordpress/" @@ -48,24 +49,3 @@ def usage() puts "See README for further information." puts end - -def help() - puts "Help :" - puts - puts "--help | -h This help screen." - puts "--Verbose | -v Verbose output." - puts "--update | -u Update to the latest revision." - puts "--generate_plugin_list [number of pages] Generate a new data/plugins.txt file. (supply number of *pages* to parse, default : 150)" - puts "--gpl Alias for --generate_plugin_list" - puts "--generate_full_plugin_list Generate a new full data/plugins.txt file" - puts "--gfpl Alias for --generate_full_plugin_list" - puts "--generate_theme_list [number of pages] Generate a new data/themes.txt file. (supply number of *pages* to parse, default : 150)" - puts "--gtl Alias for --generate_theme_list" - puts "--generate_full_theme_list Generate a new full data/themes.txt file" - puts "--gftl Alias for --generate_full_theme_list" - puts "--generate_all Generate a new full plugins, full themes, popular plugins and popular themes list" - puts "--ga Alias for --generate_all" - puts "--check-vuln-ref-urls | --cvru Check all the vulnerabilities reference urls for 404" - puts "--check-local-vulnerable-files | --clvf Perform a recursive scan in the to find vulnerable files or shells" - puts -end diff --git a/spec/lib/common/custom_option_parser_spec.rb b/spec/lib/common/custom_option_parser_spec.rb new file mode 100644 index 00000000..7f30a720 --- /dev/null +++ b/spec/lib/common/custom_option_parser_spec.rb @@ -0,0 +1,153 @@ +require "spec_helper" + +describe CustomOptionParser do + + let(:parser) { CustomOptionParser.new } + + describe "#new" do + + end + + describe "::option_to_symbol" do + after :each do + if @exception + expect { CustomOptionParser::option_to_symbol(@option) }.to raise_error(@exception) + else + CustomOptionParser::option_to_symbol(@option).should === @expected + end + end + + context "without REQUIRED or OPTIONAL arguments" do + context "with short option" do + it "should return :test" do + @option = ["-t", "--test", "Testing"] + @expected = :test + end + + it "should :its_a_long_option" do + @option = ["-l", "--its-a-long-option", "Testing '-' replacement"] + @expected = :its_a_long_option + end + end + + context "without short option" do + it "should return :long" do + @option = ["--long", "The method should find the option name ('long')"] + @expected = :long + end + + it "should return :long_option" do + @option = ["--long-option", "No short !"] + @expected = :long_option + end + end + + context "without long option" do + it "should raise an arror" do + @option = ["-v", "The long option is missing there"] + @exception = "Could not find the option name for [\"-v\", \"The long option is missing there\"]" + end + + it "should raise an error" do + @option = ["The long option is missing there"] + @exception = "Could not find the option name for [\"The long option is missing there\"]" + end + end + + context "with multiple long option names (like alias)" do + it "should return :check_long and not :cl" do + @option = ["--check-long", "--cl"] + @expected = :check_long + end + end + end + + context "with REQUIRED or OPTIONAL arguments" do + it "should removed the OPTIONAL argument" do + @option = ["-p", "--page [PAGE_NUMBER]"] + @expected = :page + end + + it "should removed the REQUIRED argument" do + @option = ["--url TARGET_URL"] + @expected = :url + end + end + + end + + describe "#add_option" do + context "exception throwing if" do + after :each do + expect { parser.add_option(@option) }.to raise_error(@exception) + end + + it "argument passed is not an Array" do + @option = "a simple String" + @exception = "The option must be an array, String supplied : 'a simple String'" + end + + it "option name is already used" do + @option = ["-v", "--verbose", "Verbose mode"] + parser.add_option(@option) + @exception = "The option verbose is already used !" + end + end + + it "should have had 2 symbols (:verbose, :url) to @symbols_used" do + parser.add_option(["-v", "--verbose"]) + parser.add_option(["--url TARGET_URL"]) + + parser.symbols_used.sort.should === [:url, :verbose] + end + + context "parsing" do + before :each do + parser.add_option(["-u", "--url TARGET_URL", "Set the target url"]) + end + + it "should raise an error if an unknown option is supplied" do + expect { parser.parse!(["--verbose"]) }.to raise_error(OptionParser::InvalidOption) + end + + it "should raise an error if an option require an argument which is not supplied" do + expect { parser.parse!(["--url"]) }.to raise_error(OptionParser::MissingArgument) + end + + it "should retrieve the correct argument" do + parser.parse!(["-u", "iam_the_target"]) + parser.results.should === { :url => "iam_the_target" } + end + end + end + + describe "#add" do + it "should raise an error if the argument is not an Array or Array(Array)" do + expect { parser.add("Hello") }.to raise_error("Options must be at least an Array, or an Array(Array). String supplied") + end + + before :each do + parser.add(["-u", "--url TARGET_URL"]) + end + + context "single option" do + it "should add the :url option, and retrieve the correct argument" do + parser.symbols_used.should === [ :url ] + parser.results(["-u", "target.com"]).should === { :url => "target.com" } + end + end + + context "multiple options" do + it "should add 2 options, and retrieve the correct arguments" do + parser.add([ + ["-v", "--verbose"], + ["--test [TEST_NUMBER]"] + ]) + + parser.symbols_used.sort.should === [:test, :url, :verbose] + parser.results(["-u", "wp.com", "-v", "--test"]).should === { :test => nil, :url => "wp.com", :verbose => true } + end + end + end + +end diff --git a/spec/lib/common/plugins/plugin_spec.rb b/spec/lib/common/plugins/plugin_spec.rb new file mode 100644 index 00000000..afd43bd5 --- /dev/null +++ b/spec/lib/common/plugins/plugin_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +describe Plugin do + subject(:plugin) { Plugin.new } + + describe "#new" do + context "with some infos" do + subject(:plugin) { Plugin.new(infos) } + let(:infos) { {:author => "John"} } + + its(:author) { should === infos[:author] } + end + end + + describe "#run" do + it "should raise a NotImplementedError" do + expect { plugin.run }.to raise_error(NotImplementedError) + end + end + + describe "#register_options" do + after :each do + if @exception + expect { plugin.register_options(*@options) }.to raise_error(@exception) + else + plugin.register_options(*@options) + plugin.registered_options.sort.should === @expected.sort + end + end + + context "when an option is not an Array" do + it "should raise an error" do + @options = [["-v", "--verbose", "It's a valid option"], "Not a valid one"] + @exception = "Each option must be an array, String supplied" + end + end + + context "when options are Arrays" do + it "should register the options" do + @options = [["-v", "--verbose", "Verbose mode"], ["-u", "--url TARGET_URL"]] + @expected = *@options + end + end + end + +end diff --git a/spec/lib/common/plugins/plugins_spec.rb b/spec/lib/common/plugins/plugins_spec.rb new file mode 100644 index 00000000..2c3b71a7 --- /dev/null +++ b/spec/lib/common/plugins/plugins_spec.rb @@ -0,0 +1,95 @@ +require "spec_helper" + +class TestPlugin < Plugin + def initialize + register_options(["-u", "--url"]) + end +end + +class AnotherPlugin < Plugin + def initialize + super(:author => "John") + # No Options + end +end + +describe Plugins do + subject(:plugins) { Plugins.new } + + let(:test_plugin) { TestPlugin.new } + let(:another_plugin) { AnotherPlugin.new } + + describe "#new" do + context "without argument" do + its(:option_parser) { should be_a CustomOptionParser } + + it "should be an Array" do + plugins.should be_an Array + end + end + + context "with an option_parser argument" do + subject(:plugin) { Plugins.new(CustomOptionParser.new("the banner")) } + + its(:option_parser) { should be_a CustomOptionParser } + its("option_parser.banner") { should === "the banner" } + + it "should raise an eror if the parser is not an instance of CustomOptionParser" do + expect { Plugins.new(OptionParser.new) }.to raise_error("The parser must be an instance of CustomOptionParser, OptionParser supplied") + end + end + end + + describe "#register_plugin" do + after :each do + if @exception + expect { plugins.register_plugin(@plugin) }.to raise_error(@exception) + else + plugins.register_plugin(@plugin) + plugins.should include(@plugin) + plugins.should === @expected + end + end + + context "when the argument supplied is not an instance of Plugin" do + it "should raise an error" do + @plugin = "I'am a String" + @exception = "The argument must be an instance of Plugin, String supplied" + end + end + + it "should register the plugin" do + @plugin = TestPlugin.new + @expected = [@plugin] + end + + it "should register 2 plugins (the order is important)" do + plugins.register_plugin(test_plugin) + + @plugin = another_plugin + @expected = [test_plugin, @plugin] + end + end + + describe "#register" do + after :each do + plugins.register(*@plugins_to_register) + + @plugins_to_register.each do |plugin| + plugins.should include(plugin) + end + + # For the correct order + plugins.should === @plugins_to_register + end + + it "should register 1 plugin" do + @plugins_to_register = [test_plugin] + end + + it "should register 2 plugins" do + @plugins_to_register = [another_plugin, test_plugin] + end + end + +end diff --git a/spec/lib/wpstools/plugins/list_generator/generate_list_spec.rb b/spec/lib/wpstools/plugins/list_generator/generate_list_spec.rb new file mode 100644 index 00000000..79d0ea42 --- /dev/null +++ b/spec/lib/wpstools/plugins/list_generator/generate_list_spec.rb @@ -0,0 +1,3 @@ +require File.expand_path(File.dirname(__FILE__) + "/../../wpstools_helper") + +# TODO diff --git a/spec/lib/wpstools/plugins/list_generator/svn_parser_spec.rb b/spec/lib/wpstools/plugins/list_generator/svn_parser_spec.rb new file mode 100644 index 00000000..79d0ea42 --- /dev/null +++ b/spec/lib/wpstools/plugins/list_generator/svn_parser_spec.rb @@ -0,0 +1,3 @@ +require File.expand_path(File.dirname(__FILE__) + "/../../wpstools_helper") + +# TODO diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 140965e8..d1f6f96c 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -26,6 +26,7 @@ if RUBY_VERSION >= "1.9" add_filter "/spec/" add_filter "_helper.rb" add_filter "environment.rb" + add_filter "_plugin.rb" # Unused files at this time add_filter "exploit.rb" diff --git a/wpstools.rb b/wpstools.rb index 5fd2a62d..b67c8a41 100755 --- a/wpstools.rb +++ b/wpstools.rb @@ -19,223 +19,30 @@ #++ $: << '.' -require File.dirname(__FILE__) +'/lib/wpstools/wpstools_helper' +require File.dirname(__FILE__) + "/lib/wpstools/wpstools_helper" begin banner() - if ARGV.length == 0 - raise "No argument supplied\n#{usage()}" - end + option_parser = CustomOptionParser.new("Usage: ./wpstools.rb [options]", 60) + option_parser.separator "" + option_parser.add(["-v", "--verbose", "Verbose output"]) - # A better way to do that should be to create a wpstools_options.rb file like wpscan_options.rb - # and a wps_options.rb with common options code - options = GetoptLong.new( - ["--help", "-h", GetoptLong::NO_ARGUMENT], - ["--verbose", "-v", GetoptLong::NO_ARGUMENT], - ["--generate_plugin_list", GetoptLong::OPTIONAL_ARGUMENT], - ["--generate_full_plugin_list", GetoptLong::NO_ARGUMENT], - ["--generate_theme_list", GetoptLong::OPTIONAL_ARGUMENT], - ["--generate_full_theme_list", GetoptLong::NO_ARGUMENT], - ["--generate_all", GetoptLong::NO_ARGUMENT], - ["--gpl", GetoptLong::OPTIONAL_ARGUMENT], # Alias for --generate_plugin_list - ["--gfpl", GetoptLong::OPTIONAL_ARGUMENT], # Alias for --generate_full_plugin_list - ["--gtl", GetoptLong::OPTIONAL_ARGUMENT], # Alias for --generate_theme_list - ["--gftl", GetoptLong::OPTIONAL_ARGUMENT], # Alias for --generate_full_theme_list - ["--ga", GetoptLong::OPTIONAL_ARGUMENT], # Alias for --generate_all - ["--update", "-u", GetoptLong::NO_ARGUMENT], - ["--check-vuln-ref-urls", GetoptLong::NO_ARGUMENT], - ["--cvru", GetoptLong::NO_ARGUMENT], # Alias for --check-vuln-ref-urls - ["--check-local-vulnerable-files", GetoptLong::REQUIRED_ARGUMENT], - ["--clvf", GetoptLong::REQUIRED_ARGUMENT] # Alias for --check-local-vulnerable-files + plugins = Plugins.new(option_parser) + plugins.register( + CheckerPlugin.new, + ListGeneratorPlugin.new ) - options.each do |option, argument| - case option - when "--help" - help() - exit - when "--verbose" - @verbose = true - when "--generate_plugin_list", "--gpl" - if argument == '' - puts "Number of pages not supplied, defaulting to 150 pages ..." - @number_of_pages = 150 - else - @number_of_pages = argument.to_i - end + options = option_parser.results - @generate_plugin_list = true - when "--generate_theme_list", "--gtl" - if argument == '' - puts "Number of pages not supplied, defaulting to 150 pages ..." - @number_of_pages = 150 - else - @number_of_pages = argument.to_i - end - - @generate_theme_list = true - when "--update" - @update = true - when "--generate_full_plugin_list", "--gfpl" - @generate_full_plugin_list = true - when "--generate_full_theme_list", "--gftl" - @generate_full_theme_list = true - when "--generate_all", "--ga" - @generate_plugin_list = true - @generate_theme_list = true - @number_of_pages = 150 - @generate_full_theme_list = true - @generate_full_plugin_list = true - when "--check-vuln-ref-urls", "--cvru" - @check_vuln_ref_urls = true - when "--check-local-vulnerable-files", "--clvf" - @check_local_vulnerable_files = true - @dir_to_scan = argument - end + if options.empty? + raise "No option supplied\n\n#{option_parser}" end - if @update - unless @updater.nil? - puts @updater.update() - else - puts "Svn / Git not installed, or wpscan has not been installed with one of them." - puts "Update aborted" - end - end - - if @generate_plugin_list - puts "[+] Generating new most popular plugin list" - puts - Generate_List.new('plugins', @verbose).generate_popular_list(@number_of_pages) - end - - if @generate_full_plugin_list - puts "[+] Generating new full plugin list" - puts - Generate_List.new('plugins', @verbose).generate_full_list - end - - if @generate_theme_list - puts "[+] Generating new most popular theme list" - puts - Generate_List.new('themes', @verbose).generate_popular_list(@number_of_pages) - end - - if @generate_full_theme_list - puts "[+] Generating new full theme list" - puts - Generate_List.new('themes', @verbose).generate_full_list - end - - # seclists.org redirects to the homepage if the reference does not exist - # TODO : the special case above - if @check_vuln_ref_urls - vuln_ref_files = ["plugin_vulns.xml", "theme_vulns.xml", "wp_vulns.xml"] - error_codes = [404, 500, 403] - not_found_regexp = %r{No Results Found|error 404|ID Invalid or Not Found}i - - puts "[+] Checking vulnerabilities reference urls" - - vuln_ref_files.each do |vuln_ref_file| - xml = Nokogiri::XML(File.open(DATA_DIR + '/' + vuln_ref_file)) do |config| - config.noblanks - end - - urls = [] - xml.xpath("//reference").each { |node| urls << node.text } - - urls.uniq! - - dead_urls = [] - queue_count = 0 - request_count = 0 - browser = Browser.instance - hydra = browser.hydra - number_of_urls = urls.size - - urls.each do |url| - request = browser.forge_request(url, { :cache_timeout => 0, :follow_location => true }) - request_count += 1 - - request.on_complete do |response| - print "\r [+] Checking #{vuln_ref_file} #{number_of_urls} total ... #{(request_count * 100) / number_of_urls}% complete." - - if error_codes.include?(response.code) or not_found_regexp.match(response.body) - dead_urls << url - end - end - - hydra.queue(request) - queue_count += 1 - - if queue_count == browser.max_threads - hydra.run - queue_count = 0 - end - end - - hydra.run - puts - unless dead_urls.empty? - dead_urls.each { |url| puts " Not Found #{url}" } - end - end - end - - if @check_local_vulnerable_files - if Dir::exist?(@dir_to_scan) - xml_file = DATA_DIR + "/local_vulnerable_files.xml" - local_hashes = {} - file_extension_to_scan = "*.{js,php,swf,html,htm}" - - print "[+] Generating local hashes ... " - - Dir[File::join(@dir_to_scan, "**", file_extension_to_scan)].each do |filename| - sha1sum = Digest::SHA1.file(filename).hexdigest - - if local_hashes.has_key?(sha1sum) - local_hashes[sha1sum] << filename - else - local_hashes[sha1sum] = [filename] - end - end - - puts "done." - - puts "[+] Checking for vulnerable files ..." - - xml = Nokogiri::XML(File.open(xml_file)) do |config| - config.noblanks - end - - xml.xpath("//hash").each do |node| - sha1sum = node.attribute("sha1").text - - if local_hashes.has_key?(sha1sum) - local_filenames = local_hashes[sha1sum] - vuln_title = node.search("title").text - vuln_filename = node.search("file").text - vuln_refrence = node.search("reference").text - - puts " #{vuln_filename} found :" - puts " | Location(s):" - local_filenames.each do |file| - puts " | - #{file}" - end - puts " |" - puts " | Title: #{vuln_title}" - puts " | Refrence: #{vuln_refrence}" if !vuln_refrence.empty? - puts - end - end - - puts "done." - - else - puts "The supplied directory '#{@dir_to_scan}' does not exist" - end + plugins.each do |plugin| + plugin.run(options) end rescue => e