PHPMyAdmin 4.8.0 ~ 4.8.1 Remote Code Execution

TL;DR

I discovered a file inclusion vulnerability in index.php from PMA 4.8.0 ~ 4.8.1, and it is assigned CVE-2018–12613. It is caused by a validation bypass in the vulnerable path checking function Core::checkPageValidity. This vulnerability enables an authenticated remote attacker to execute arbitrary PHP code on the server.

Vulnerability Explained

There is a file inclusion in index.php:

if (! empty($_REQUEST['target'])
&& is_string($_REQUEST['target'])
&& ! preg_match('/^index/', $_REQUEST['target'])
&& ! in_array($_REQUEST['target'], $target_blacklist)
&& Core::checkPageValidity($_REQUEST['target'])
) {
include $_REQUEST['target'];
exit;
}
// ...

This include used to be properly protected by the conditions in the if statement, but in the 4.8.0 release, the last check is changed to reuse the existing function, Core::checkPageValidity, which (I think) is meant to check URL paths. Hence we can exploit URL features to reach arbitrary file inclusion. The functions goes:

public static function checkPageValidity(&$page, array $whitelist = [])
{
// ...
$_page = mb_substr(
$page,
0,
mb_strpos($page . '?', '?')
);
// $whitelist == array('db_datadict.php', 'sql.php', ...)
if (in_array($_page, $whitelist)) {
return true;
}
// ...
return false;
}

The function strips everything behind ? from $page (everything behind ? is a query string, which is not part of the URL path), and checks if it is in the whitelist. The whitelist is a list:'db_datadict.php', 'sql.php', ....

Attack

Now, since we have complete control over $page, which comes directly from $_REQUEST['target'], we can set it to:

$page = 'sql.php?/../../../etc/passwd'

The function then performs its checks:

  1. Strip everything behind ? and assign 'sql.php' to $_page
  2. Check if $_page, i.e. 'sql.php', is in whitelist? YES

After passing the check, back to index.php:

include $_REQUEST['target'];

It includes the non-stripped version, bingo!

So the whole exploit goes:

GET /index.php?target=sql.php%3f/../../etc/passwd
  • %3f will be decoded and become ?
  • Core::checkPageValidity strips everything behind ? and finds sql.php within whitelist: check is bypassed!
  • index.php runs include 'sql.php?/../../etc/passwd', and PHP has this magic to convert the path to ../etc/passwd, without checking if the directorysql.php? exists or not. Finally, it includes ../etc/passwd successfully.

Ideas for Exploit Writing

To write an exploit, you can enumerate file paths like:

  • ../etc/passwd
  • ../../etc/passwd
  • ../windows/win.ini
  • ../../windows/win.ini

Once you locate the number of ..s you have to prepend, you can inject your php payload into access log, or run a query like SELECT ‘<?php phpinfo();?>' in sql.php and include your own session file (e.g. /var/lib/php5/sess_<PHPSESSID>), which contains your SQL query, to execute arbitrary PHP code.

Root Cause: Inconsistency

The root cause is the inconsistency between the checked path and the actual path for inclusion. The checked path should be used AS the actual path for inclusion, otherwise the check function might be bypassed or exploited.

This kind of inconsistency is a common pattern in various web vulnerabilities, as pointed out by orange in his epic talk on SSRF bypassing in Blackhat 2017.

Disclosure

I reported this vulnerability to SSD, and they went through the whole responsible disclosure process with phpmyadmin, many thanks to them!!

Bug Clash

Interestingly, ChaMd5 found the same vulnerability almost at the same time, and they wrote a pretty good write-up, too!

Reference