Vulnerability Details: Joomla! Remote Code Execution

The Joomla! team released a new version of Joomla! CMS yesterday to patch a serious and easy to exploit remote code execution vulnerability that affected pretty much all versions of the platform up to 3.4.5. As soon as the patch was released, we were able to start our investigation and found that it was already being exploited in the wild – 2 days before the disclosure.

We will now shed some light on vulnerability, specifically how it works.

PHP Session 102

By default, Joomla! stores users session in the site’s database. Here’s an example of what a typical session would look like:

1-sessions

A quick look reveals an interesting behavior about PHP’s way of unserializing sessions (which can be reproduced using PHP function session_decode). PHP’s session serialization function is a bit different than the usual serialize() we’re used to, especially when it comes to array indexes. Here’s a comparison of the two for a given array, array( ‘a’ => ‘a’, ‘b’ => ‘b’):

  • A standard serialize() call would give us a:2:{s:1:”a”;s:1:”a”;s:1:”b”;s:1:”b”;}
  • Whereas session_encode() is returning a|s:1:”a”;b|s:1:”b”;

As you can see, the second encoding still uses regular variable serialization but differ in the way it’s declaring indexes for the $_SESSION array. In this case, this is one of the thing that will allow attackers to store arbitrary session data inside the database.

When User Input Meets Session Data

Some of you might have noticed already, but the above session was created by me using Firefox. But wait, how does Joomla! know this?

2-u-a

When it creates a new session, Joomla! takes the client’s user-agent and stores it in the session’s session.client.browser index, which will be saved later on the database. Meaning, one could in theory close the current serialized object/array they are into and start a new one, using a payload similar to “}__test|a:100:{some serialized data}. The problem with this approach, as some will have noticed, is that we leave an extra pipe ( | ) character, which breaks the resulting serialized payload. To get anything malicious in the session, an attacker needs to get rid of all the data located after the injected payload.

Tainted Session Data Meets MySQL

Checking the session table’s collation gives us the last piece of the puzzle:

3-db

WordPress had a very similar issue recently, involving the lack of support of MySQL’s utf8_general_ci collation for 4 byte UTF-8 characters. MySQL’s default behavior when it meets an UTF-8 character that isn’t supported by utf8_general_ci is that it will just truncate all the data, starting from that specific character, exactly what’s needed for the exploit to work. The payload would then become something like: “}__test|a:100:{some serialized data}

What About Achieving Code Execution?

From the moment the attacker can push an arbitrary serialized payload in its session, he’s conducting what is known as an Object Injection attack, which as we have seen in the wild and shown in past disclosures usually allows Remote Code Execution to occur on the victim’s site. We won’t show this part here as there is still a lot of sites not updated, but you get the point.

Update As Soon As Possible

Attackers are already exploiting this vulnerability in the wild, so updating your sites should be done in priority. In the event where this wouldn’t be possible, we strongly encourage you to consider adding other security measures to prevent it from being hacked, such as a Website Application Firewall (WAF).

15 comments
      1. the best part is you guys tried to castrate the payload in your previous post but forgot to remove it from ddecode, probably because you wanted to achieve publicity by doing so, just like what seems to be the new thing with you types of “researchers”… gotta drop a bomb to make your resumes explosive right

        1. ddecode.com is a public tool, Sucuri didn’t put it there. Someone else must have tried to decode it.

  1. If you’re not using a database to store session data (ie, using PHP sessions instead), is this exploit still effective?

    What if your jos_sessions table doesn’t have a “data” column? Is it possible to see if you are exploited to check the data column in jos_sessions for a specific keyword?

    1. If you’re not using a database to store session data YES this exploit still is valid. Its a session issue – not a database issue.

  2. Another great analysis. Were you able to entirely mitigate this attack for your customers? Or at least most of them? Or did their protection depend on them updating Jooma, which has seemed to have had a lot of security exploits lately.

    1. Yes, they were entirely protected from this attack, even before we knew about it (and without patches). That was due to our user-agent/x-forwarded-for filters that blocked / patched the vulnerability.

  3. I think I understand how this is supposed to work, and I certainly see people trying it on our sites. But I don’t see how it CAN work. Where the user-agent string is being inserted is preceded by a type, a length, and an open quote (s:412:”). So even if you close the quotes and close the object php is going to reject the whole thing because it didn’t get 412 characters when reading the value for client.browser. Unless this also requires a broken php deserializer?

  4. I feel a quick look into JDatabaseDriverMysqli would be a good thing to do and check for the IPs in the log that makes Joomla vulnerable to the attacks. If you are a user of Joomla 3.x, update immediately to 3.4.6.

  5. This injection is useless, because as noted @njk it will be stored with wrong length parameter e.g. s:500:”dummy”;} . So such session string cannot be decoded in correct way. Moreover the injected payload will be taken up due to such large size, and PHP interpreter devours session identifier __test|a……. Therefore nor 1-st nor 2-nd session will be decoded properly.
    The described “vulnerability” is not exploitable. I intentionally proved the existing PoC with vulnerable version of Joomla 3.4.4. It does not work. In the database session.counter is always 1 – it proves, that session cannot be unserialized.

Comments are closed.

You May Also Like