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:
- X-Cart Changelog v5.4.1.8: https://devs.x-cart.com/changelog/5.4.1.8_-_21_july_2020.html
- X-Cart Changelog v5.4.0.13: https://devs.x-cart.com/changelog/5.4.0.13_-_21_july_2020.html
Something Familiar About That Data
Immediately after installing X-Cart I spotted the following cookie in a request:

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

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.

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.

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.

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.

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:

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:

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
:

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
- X-Cart (https://www.x-cart.com/)
- X-Cart Changelog for v5.1.4.8 (https://devs.x-cart.com/changelog/5.4.1.8_-_21_july_2020.html)
- X-Cart Changelog for v5.4.0.13 (https://devs.x-cart.com/changelog/5.4.0.13_-_21_july_2020.html)
- PHPGGC (https://github.com/ambionics/phpggc)
- Archive_Tar PEAR Library (https://github.com/pear/Archive_Tar)
- PHP strip_tags Function (https://www.php.net/manual/en/function.strip-tags.php)
- Source for unserialize of
S:
strings (https://github.com/php/php-src/blob/master/ext/standard/var_unserializer.re#L958) - Source for hex escaping in
S:
strings (https://github.com/php/php-src/blob/master/ext/standard/var_unserializer.re#L324-L336)
Discussion