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