Skip to content

Commit 79fe342

Browse files
committed
Land rapid7#3558, @firefart's improvements to wordpress mixin
2 parents faee2c7 + 2d5fd5e commit 79fe342

File tree

12 files changed

+411
-128
lines changed

12 files changed

+411
-128
lines changed

lib/msf/http/wordpress.rb

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,20 @@ def initialize(info = {})
2525
super
2626

2727
register_options(
28-
[
29-
Msf::OptString.new('TARGETURI', [true, 'The base path to the wordpress application', '/']),
30-
], HTTP::Wordpress
28+
[
29+
Msf::OptString.new('TARGETURI', [true, 'The base path to the wordpress application', '/'])
30+
], HTTP::Wordpress
3131
)
32+
33+
register_advanced_options(
34+
[
35+
Msf::OptString.new('WPCONTENTDIR', [true, 'The name of the wp-content directory', 'wp-content'])
36+
], HTTP::Wordpress
37+
)
38+
end
39+
40+
def wp_content_dir
41+
datastore['WPCONTENTDIR']
3242
end
3343
end
3444
end

lib/msf/http/wordpress/base.rb

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,23 @@
11
# -*- coding: binary -*-
22

33
module Msf::HTTP::Wordpress::Base
4-
54
# Checks if the site is online and running wordpress
65
#
76
# @return [Rex::Proto::Http::Response,nil] Returns the HTTP response if the site is online and running wordpress, nil otherwise
87
def wordpress_and_online?
9-
begin
10-
res = send_request_cgi({
11-
'method' => 'GET',
12-
'uri' => normalize_uri(target_uri.path)
13-
})
14-
return res if res and
15-
res.code == 200 and
16-
(
17-
res.body =~ /["'][^"']*\/wp-content\/[^"']*["']/i or
18-
res.body =~ /<link rel=["']wlwmanifest["'].*href=["'].*\/wp-includes\/wlwmanifest\.xml["'] \/>/i or
19-
res.body =~ /<link rel=["']pingback["'].*href=["'].*\/xmlrpc\.php["'] \/>/i
20-
)
21-
return nil
22-
rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout
23-
print_error("#{peer} - Error connecting to #{target_uri}")
24-
return nil
25-
end
8+
res = send_request_cgi(
9+
'method' => 'GET',
10+
'uri' => normalize_uri(target_uri.path)
11+
)
12+
wordpress_detect_regexes = [
13+
/["'][^"']*\/#{Regexp.escape(wp_content_dir)}\/[^"']*["']/i,
14+
/<link rel=["']wlwmanifest["'].*href=["'].*\/wp-includes\/wlwmanifest\.xml["'] \/>/i,
15+
/<link rel=["']pingback["'].*href=["'].*\/xmlrpc\.php["'](?: \/)*>/i
16+
]
17+
return res if res && res.code == 200 && res.body && wordpress_detect_regexes.any? { |r| res.body =~ r }
18+
return nil
19+
rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout => e
20+
print_error("#{peer} - Error connecting to #{target_uri}: #{e}")
21+
return nil
2622
end
27-
2823
end

lib/msf/http/wordpress/helpers.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def wordpress_helper_post_comment(comment, comment_post_id, login_cookie, author
4949
options.merge!({'vars_post' => vars_post})
5050
options.merge!({'cookie' => login_cookie}) if login_cookie
5151
res = send_request_cgi(options)
52-
if res and (res.code == 301 or res.code == 302) and res.headers['Location']
52+
if res && res.redirect? && res.redirection
5353
return wordpress_helper_parse_location_header(res)
5454
else
5555
message = "#{peer} - Post comment failed."
@@ -101,7 +101,7 @@ def wordpress_helper_check_post_id(uri, comments_enabled=false, login_cookie=nil
101101
else
102102
return res.body
103103
end
104-
elsif res and (res.code == 301 or res.code == 302) and res.headers['Location']
104+
elsif res && res.redirect? && res.redirection
105105
path = wordpress_helper_parse_location_header(res)
106106
return wordpress_helper_check_post_id(path, comments_enabled, login_cookie)
107107
end
@@ -113,9 +113,9 @@ def wordpress_helper_check_post_id(uri, comments_enabled=false, login_cookie=nil
113113
# @param res [Rex::Proto::Http::Response] The HTTP response
114114
# @return [String,nil] the path and query, nil on error
115115
def wordpress_helper_parse_location_header(res)
116-
return nil unless res and (res.code == 301 or res.code == 302) and res.headers['Location']
116+
return nil unless res && res.redirect? && res.redirection
117117

118-
location = res.headers['Location']
118+
location = res.redirection
119119
path_from_uri(location)
120120
end
121121

lib/msf/http/wordpress/login.rb

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,30 @@
11
# -*- coding: binary -*-
2-
module Msf::HTTP::Wordpress::Login
32

3+
module Msf::HTTP::Wordpress::Login
44
# performs a wordpress login
55
#
66
# @param user [String] Username
77
# @param pass [String] Password
88
# @return [String,nil] the session cookies as a single string on successful login, nil otherwise
99
def wordpress_login(user, pass)
1010
redirect = "#{target_uri}#{Rex::Text.rand_text_alpha(8)}"
11-
res = send_request_cgi({
11+
res = send_request_cgi(
1212
'method' => 'POST',
1313
'uri' => wordpress_url_login,
1414
'vars_post' => wordpress_helper_login_post_data(user, pass, redirect)
15-
})
16-
17-
if res and (res.code == 301 or res.code == 302) and res.headers['Location'] == redirect
15+
)
16+
if res && res.redirect? && res.redirection && res.redirection.to_s == redirect
1817
cookies = res.get_cookies
1918
# Check if a valid wordpress cookie is returned
20-
return cookies if cookies =~ /wordpress(?:_sec)?_logged_in_[^=]+=[^;]+;/i ||
19+
return cookies if
20+
# current Wordpress
21+
cookies =~ /wordpress(?:_sec)?_logged_in_[^=]+=[^;]+;/i ||
22+
# Wordpress 2.0
2123
cookies =~ /wordpress(?:user|pass)_[^=]+=[^;]+;/i ||
24+
# Wordpress 2.5
2225
cookies =~ /wordpress_[a-z0-9]+=[^;]+;/i
2326
end
2427

25-
return nil
28+
nil
2629
end
27-
2830
end

lib/msf/http/wordpress/posts.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ def wordpress_get_all_blog_posts_via_feed(max_redirects = 10)
112112
count = max_redirects
113113

114114
# Follow redirects
115-
while (res.code == 301 || res.code == 302) and res.headers['Location'] and count != 0
115+
while res.redirect? && res.redirection && count != 0
116116
path = wordpress_helper_parse_location_header(res)
117117
return nil unless path
118118

lib/msf/http/wordpress/users.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def wordpress_userid_exists?(user_id)
3333
'uri' => url
3434
})
3535

36-
if res and res.code == 301
36+
if res and res.redirect?
3737
uri = wordpress_helper_parse_location_header(res)
3838
return nil unless uri
3939
# try to extract username from location

lib/msf/http/wordpress/version.rb

Lines changed: 80 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,63 +2,124 @@
22

33
module Msf::HTTP::Wordpress::Version
44

5+
# Used to check if the version is correct: must contain at least one dot
6+
WORDPRESS_VERSION_PATTERN = '([^\r\n"\']+\.[^\r\n"\']+)'
7+
58
# Extracts the Wordpress version information from various sources
69
#
710
# @return [String,nil] Wordpress version if found, nil otherwise
811
def wordpress_version
912
# detect version from generator
10-
version = wordpress_version_helper(normalize_uri(target_uri.path), /<meta name="generator" content="WordPress #{wordpress_version_pattern}" \/>/i)
13+
version = wordpress_version_helper(normalize_uri(target_uri.path), /<meta name="generator" content="WordPress #{WORDPRESS_VERSION_PATTERN}" \/>/i)
1114
return version if version
1215

1316
# detect version from readme
14-
version = wordpress_version_helper(wordpress_url_readme, /<br \/>\sversion #{wordpress_version_pattern}/i)
17+
version = wordpress_version_helper(wordpress_url_readme, /<br \/>\sversion #{WORDPRESS_VERSION_PATTERN}/i)
1518
return version if version
1619

1720
# detect version from rss
18-
version = wordpress_version_helper(wordpress_url_rss, /<generator>http:\/\/wordpress.org\/\?v=#{wordpress_version_pattern}<\/generator>/i)
21+
version = wordpress_version_helper(wordpress_url_rss, /<generator>http:\/\/wordpress.org\/\?v=#{WORDPRESS_VERSION_PATTERN}<\/generator>/i)
1922
return version if version
2023

2124
# detect version from rdf
22-
version = wordpress_version_helper(wordpress_url_rdf, /<admin:generatorAgent rdf:resource="http:\/\/wordpress.org\/\?v=#{wordpress_version_pattern}" \/>/i)
25+
version = wordpress_version_helper(wordpress_url_rdf, /<admin:generatorAgent rdf:resource="http:\/\/wordpress.org\/\?v=#{WORDPRESS_VERSION_PATTERN}" \/>/i)
2326
return version if version
2427

2528
# detect version from atom
26-
version = wordpress_version_helper(wordpress_url_atom, /<generator uri="http:\/\/wordpress.org\/" version="#{wordpress_version_pattern}">WordPress<\/generator>/i)
29+
version = wordpress_version_helper(wordpress_url_atom, /<generator uri="http:\/\/wordpress.org\/" version="#{WORDPRESS_VERSION_PATTERN}">WordPress<\/generator>/i)
2730
return version if version
2831

2932
# detect version from sitemap
30-
version = wordpress_version_helper(wordpress_url_sitemap, /generator="wordpress\/#{wordpress_version_pattern}"/i)
33+
version = wordpress_version_helper(wordpress_url_sitemap, /generator="wordpress\/#{WORDPRESS_VERSION_PATTERN}"/i)
3134
return version if version
3235

3336
# detect version from opml
34-
version = wordpress_version_helper(wordpress_url_opml, /generator="wordpress\/#{wordpress_version_pattern}"/i)
37+
version = wordpress_version_helper(wordpress_url_opml, /generator="wordpress\/#{WORDPRESS_VERSION_PATTERN}"/i)
3538
return version if version
3639

3740
nil
3841
end
3942

40-
private
43+
# Checks a readme for a vulnerable version
44+
#
45+
# @param [String] plugin_name The name of the plugin
46+
# @param [String] fixed_version The version the vulnerability was fixed in
47+
# @param [String] vuln_introduced_version Optional, the version the vulnerability was introduced
48+
#
49+
# @return [ Msf::Exploit::CheckCode ]
50+
def check_plugin_version_from_readme(plugin_name, fixed_version, vuln_introduced_version = nil)
51+
check_version_from_readme(:plugin, plugin_name, fixed_version, vuln_introduced_version)
52+
end
4153

42-
# Used to check if the version is correct: must contain at least one dot.
54+
# Checks a readme for a vulnerable version
4355
#
44-
# @return [ String ]
45-
def wordpress_version_pattern
46-
'([^\r\n"\']+\.[^\r\n"\']+)'
56+
# @param [String] theme_name The name of the theme
57+
# @param [String] fixed_version 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_readme(theme_name, fixed_version, vuln_introduced_version = nil)
62+
check_version_from_readme(:theme, theme_name, fixed_version, vuln_introduced_version)
4763
end
4864

65+
private
66+
4967
def wordpress_version_helper(url, regex)
50-
res = send_request_cgi({
51-
'method' => 'GET',
52-
'uri' => url
53-
})
68+
res = send_request_cgi(
69+
'method' => 'GET',
70+
'uri' => url
71+
)
5472
if res
5573
match = res.body.match(regex)
56-
if match
57-
return match[1]
58-
end
74+
return match[1] if match
5975
end
6076

6177
nil
6278
end
6379

80+
def check_version_from_readme(type, name, fixed_version, vuln_introduced_version = nil)
81+
case type
82+
when :plugin
83+
folder = 'plugins'
84+
when :theme
85+
folder = 'themes'
86+
else
87+
fail("Unknown readme type #{type}")
88+
end
89+
90+
readme_url = normalize_uri(target_uri.path, wp_content_dir, folder, name, 'readme.txt')
91+
res = send_request_cgi(
92+
'uri' => readme_url,
93+
'method' => 'GET'
94+
)
95+
# no readme.txt present
96+
return Msf::Exploit::CheckCode::Unknown if res.nil? || res.code != 200
97+
98+
# try to extract version from readme
99+
# Example line:
100+
# Stable tag: 2.6.6
101+
version = res.body.to_s[/stable tag: ([^\r\n"\']+\.[^\r\n"\']+)/i, 1]
102+
103+
# readme present, but no version number
104+
return Msf::Exploit::CheckCode::Detected if version.nil?
105+
106+
vprint_status("#{peer} - Found version #{version} of the #{type}")
107+
108+
# Version older than fixed version
109+
if Gem::Version.new(version) < Gem::Version.new(fixed_version)
110+
if vuln_introduced_version.nil?
111+
# All versions are vulnerable
112+
return Msf::Exploit::CheckCode::Appears
113+
# vuln_introduced_version provided, check if version is newer
114+
elsif Gem::Version.new(version) >= Gem::Version.new(vuln_introduced_version)
115+
return Msf::Exploit::CheckCode::Appears
116+
else
117+
# Not in range, nut vulnerable
118+
return Msf::Exploit::CheckCode::Safe
119+
end
120+
# version newer than fixed version
121+
else
122+
return Msf::Exploit::CheckCode::Safe
123+
end
124+
end
64125
end

0 commit comments

Comments
 (0)