Exploitation

X-Cart 5 <= 5.4.0.12/5.4.1.7 Unauthenticated RCE via File Write

This one was a fun little hack. Versions 5.4.1.7 and below, and 5.4.0.12 and below of the X-Cart PHP ecommerce platform are affected by an unauthenticated vulnerability that allows an attacker to control the path and partial contents of a file write operation. The vulnerability was fixed in versions 5.4.1.8 and 5.4.0.13, but read on for the details.

Update: Links to the vendor’s changelog for the updated versions:

Something Familiar About That Data

Immediately after installing X-Cart I spotted the following cookie in a request:

X-Cart viewedResources Cookie
X-Cart viewedResources Cookie

The URL encoding is what caught my eye. Switching over to the “params” tab in Burp Suite showed the following:

Decoded X-Cart Cookie
Decoded X-Cart Cookie

For those not familiar with the format, the contents of the viewedResources cookie are in PHP’s native serialization format. If the application passes this cookie through the unserialize() function then I could use the cookie to instantiate PHP objects on the server and potentially use the properties of those objects to gain some influence over the execution of existing code.

Finding an Entry Point

First I needed to find and verify the entry point to ensure that I could reliably pass data to unserialize(). There’s no point throwing payloads at a given request if it won’t pass the cookie through unserialize().

I started my search with a recursive grep of the code for the cookie name: viewedResources. The grep output included four lines of code in two files where a variable of this name was passed through unserialize(). This was a test environment and it didn’t matter if I broke something, so I made some quick code edits to work out if any of those calls to unserialize() were reached through a given HTTP request.

Inserted Call to die()
Inserted Call to die()

When I refreshed the page I was presented with the string “foo2”, confirming that execution reached the call to unserialize() in the isResourceKeyInCookie() function in the file var/run/classes/XLite/View/AResourceContainerAbstract.php. Bingo!

I edited the code again to output the contents of the $viewedResources variable and verify that it contained my cookie data before reverting my edits so that I could start passing data through to the unserialize() function.

Gadgets

Next I needed one or more gadgets that I could influence to do something useful. While I was installing X-Cart I noticed a vendor directory containing various third-party libraries, so I figured the easy option would be to fire up PHPGGC (essentially ysoserial but for PHP), and go in guns blazing with all available gadget chains.

Testing PHPGGC Gadget Chains
Testing PHPGGC Gadget Chains

This revealed three potentially interesting options. The first two that immediately stood out resulted in HTTP 500 errors as shown in the screenshot above. These were Guzzle/FW1 and WordPress/RCE2, although the latter relied on a WordPress-specific gadget and was unlikely to be of use unless a useful alternative could be found within X-Cart.

The third potential option didn’t cause an error but instead was identified when a test file was deleted from the server. Interestingly it was Drupal/FD1, which turned out to be a gadget in the Archive_Tar PEAR library rather than a Drupal-specific gadget.

So, without hunting for gadgets specific to X-Cart, I had a file delete gadget and potentially a file write gadget (Guzzle/FW1).

Debugging

The HTTP 500 error on the Guzzle/FW1 gadget chain indicated that something worked enough to cause an error and it was therefore worth further investigation.

The outer class used in the chain was GuzzleHttp\Cookie\FileCookieJar, and PHPGGC tells us that the entry point for the gadget is the __destruct() method. A quick recursive grep found the relevant class and methods.

The Guzzle FileCookieJar Gadget
The Guzzle FileCookieJar Gadget

When a FileCookieJar object goes out of scope, the __destruct() method is called, which passes $this->filename to $this->save(). The save() method creates an array of cookies from the FileCookieJar object, encodes the array as JSON, then writes it to a file at the path in the filename property. Effectively this means we should be able to write a JSON file containing embedded PHP code to an arbitrary path.

As I did before, I made a quick code edit to quickly get a better understanding of what was going on. I wanted to verify that the FileCookieJar::save() method was being called, and if so I wanted to know what $this looked like.

Dumping the FileCookieJar
Dumping the FileCookieJar

When I replayed the HTTP request in Burp Suite with my payload in the viewedResources cookie I triggered the die() call above, however the properties of the FileCookieJar had not been set according to my payload. Both the filename and cookies properties were null.

I added some similar debug output back in the isResourceKeyInCookie() method mentioned earlier in order to dump the $viewedResources variable before it was passed to unserialize(). There was a subtle difference between what I’d passed in the cookie and what the isResourceKeyInCookie() method saw. The PHP serialization format includes null bytes in the names of non-public class/object properties. Those null bytes were being stripped out somewhere which meant that unserialize() wasn’t reading the values and setting the properties I needed.

The value of the $viewedResources variable was retrieved through an instance of the class XLite\Core\Request. A review of this class found that it was filtering all request parameters and cookies. In particular, the value of the viewedResources cookie was being passed through the strip_tags() PHP function, which removes all HTML tags, PHP tags, and null bytes.

Bypassing strip_tags()

Fortunately, PHP’s serialization format supports two string representations. One has the prefix s: followed by the string length, followed by the raw bytes of the string verbatim (including the null bytes that were being stripped away). The other has the prefix S: followed by the string length, followed by the string. Most importantly, the latter representation supports hex escape sequences such as \00.

By switching all of the s: strings in our payload to S: strings, the strip_tags() function will see the string \00 in place of each of the null bytes, and will ignore them as they’re no longer null bytes (strip_tags() sees 0x5c3030 rather than 0x00). The unserialize() function will then interpret the \00 sequence as a null byte and successfully deserialize the data, enabling us to create a FileCookieJar object and set the non-public properties. This alternative encoding is supported by PHPGGC using the -a flag.

After generating a new payload, I had to do some additional encoding in order to get the <?php and ?> tags through strip_tags() untouched. Once that was done I successfully wrote a PHP script to the server:

PHP Script Successfully Uploaded to X-Cart via Unsafe Pre-Auth Cookie Deserialization
PHP Script Successfully Uploaded to X-Cart via Unsafe Pre-Auth Cookie Deserialization

Improving Reliability

A hardened X-Cart installation might restrict write access to files and directories, but certain directories must be writable for normal use of X-Cart. For example var/log where X-Cart writes log files, or the various directories where product images might be uploaded to. Fortunately the X-Cart team thought of this and made heavy use of .htaccess files to prevent PHP files from being executed in the event that someone were able to upload a PHP script to one of these locations.

Here’s the content of files/attachments/.htaccess which denies access to files with certain extensions:

Example of an X-Cart .htaccess File
Example of an X-Cart .htaccess File

These aren’t the only file extensions that might be passed through the PHP interpreter though. In particular, blacklists like this always seem to miss the extension .phtml which, unlike some alternative PHP file extensions, is still passed to the PHP interpreter under default configurations.

Here’s another example of a .htaccess file used by X-Cart, this time from skins/.htaccess:

Another X-Cart .htaccess File
Another X-Cart .htaccess File

The file extension blacklist here still doesn’t include .phtml, but this one also doesn’t include .php3. The inconsistency makes me wonder whether a proof-of-concept for an older X-Cart vulnerability uploaded a shell with the .php3 extension to the files/attachments directory…

Of course, none of this matters, because we can just use the Drupal/FD1 gadget to delete our choice of .htaccess file and upload a shell to any writable directory we want.

References

Discussion

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.