Breaking PHP internals

A vulnerable application allows an attacker to load arbitraty PHP classes by sending out specially crafted requested.
Exploiting PHP autoloader we can turn it into a Local File Inclusion.

Context

The application under exam is Joomla CMS version 3.8.7 and in my site setup I configured it to send me an email every time a PHP exception happens. Since I'm running PHP 7, I can intercept almost every exception and report it back.
One day I got this report in the email:

Exception Type: Error
File: /var/www/vhosts/mysite/components/com_users/models/login.php
Line: 62
Message: Call to a member function get() on null 
#0 /var/www/vhosts/mysite/libraries/src/MVC/Model/FormModel.php(233): UsersModelLogin->loadFormData()
#1 /var/www/vhosts/mysite/components/com_users/models/login.php(35): Joomla\CMS\MVC\Model\FormModel->loadForm('com_users.login', 'login', Array)

This is pretty strange, since Joomla tries its best to catch every exception and avoid them from bubbling up and being displayed to the user. So I decided to investigate further.
These are the lines causing the exception:

$input = $app->input;
$method = $input->getMethod();

// Check for return URL from the request first
if ($return = $input->$method->get('return', '', 'BASE64'))

Mhm... so we're invoking an invalid method on the Input class. Where we're getting such method?

public function getMethod()
{
    $method = strtoupper($_SERVER['REQUEST_METHOD']);

    return $method;
}

Ah! User input being used without being sanitized!
That could be interesting.

Digging into the issue

Ok, hold your hat because we're going down into a really long rabbit hole.
First of all, since we're invoking a method that's not defined, we're going through \Joomla\CMS\Input\Input::__get magic method.
This in turn will construct a class name from our HTTP method name, and then try to load it from the autoloader:

$className = '\\Joomla\\CMS\\Input\\' . ucfirst($name);
if (class_exists($className))
{
    $this->inputs[$name] = new $className(null, $this->options);

    return $this->inputs[$name];
}

Joomla! registers several autoloaders, which will try to load the file from disk to include the required classname based on different conventions.
The function we're interested in is the following one \JLoader::loadByPsr4():

$classPath .= $className . '.php';

// Loop through paths registered to this namespace until we find a match.
foreach ($paths as $path)
{
    // Base folder will be SITE_ROOT/libraries/src/Input. From here we can start to traverse
    $classFilePath = $path . DIRECTORY_SEPARATOR . substr_replace($classPath, '', 0, strlen($nsPath) + 1);

    // We check for class_exists to handle case-sensitive file systems
    if (file_exists($classFilePath) && !class_exists($class, false))
    {
        $found = (bool) include_once $classFilePath;

        if ($found)
        {
            self::loadAliasFor($class);
        }

        return $found;
    }
}

What if our HTTP method contains some double dots, trying to do some path traversal?

Exploiting the autoloader

I immediately tried to supply the following command:


curl -X "../something" "http://dev3.vagrant.up/index.php?option=com_users" -H "Cookie: XDEBUG_SESSION=PHPSTORM"

And watched the code flow with the debugger attached.

When the interpreter arrived to the line with class_exists($className).... nothing happened.
I mean, literally nothing: I wasn't able to step inside the function and follow all the autoload functions.
Absolutely nothing.

That's a bummer.

I'll spare you the countess tests and tries, but the final note is the following: in newer versions, PHP organization patched the class_exists function to only accept strings that contain valid characters for a class name. Testing all the available versions, the only vulnerable one is PHP 5.3 and older.
So I quickly switched PHP version on my box and tried again: now everything works as expected!

Local File Inclusion

By using the HTTP method ../../something I can force Joomla! autoloader to include the file /var/www/dev3/libraries/src/Input/../../SOMETHING.php.
As you can see the method is transformed into uppercase, which shrinks the attack surface, but would work pretty reliable under Windows systems (pretty common in enterprise installs).

Conclusions

Sometimes we tend to forget it, but PHP is an interpreter and, as all softwares, it could have its own flaws and security issues.
This is why it's very important to consider the server environment we're facing, because some exploits all of sudden become available in outdated installations.

Comments:

Blog Comments powered by Disqus.