WPScan files

This commit is contained in:
ethicalhack3r
2012-07-11 22:49:18 +02:00
parent 6da2da90f7
commit 3d78cbc4ac
190 changed files with 43701 additions and 0 deletions

218
lib/browser.rb Normal file
View 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
View 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
View 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
View 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

View 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

View 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
View 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

View 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
View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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
View 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
View 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
View 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
View 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
View 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
View 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

View 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

View 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

View 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

View 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

View 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