This one was a fun little hack. Versions 126.96.36.199 and below, and 188.8.131.52 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 184.108.40.206 and 220.127.116.11, but read on for the details.
Update: Links to the vendor’s changelog for the updated versions:
- X-Cart Changelog v18.104.22.168: https://devs.x-cart.com/changelog/22.214.171.124_-_21_july_2020.html
- X-Cart Changelog v126.96.36.199: https://devs.x-cart.com/changelog/188.8.131.52_-_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
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
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
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
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 (
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
PHPGGC tells us that the entry point for the gadget is the
__destruct() method. A quick recursive
grep found the relevant class and methods.
FileCookieJar object goes out of scope, the
__destruct() method is called, which passes
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
cookies properties were
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.
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
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 (
0x5c3030 rather than
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
After generating a new payload, I had to do some additional encoding in order to get the
?> tags through
strip_tags() untouched. Once that was done I successfully wrote a PHP script to the server:
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
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
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.
- X-Cart (https://www.x-cart.com/)
- X-Cart Changelog for v184.108.40.206 (https://devs.x-cart.com/changelog/220.127.116.11_-_21_july_2020.html)
- X-Cart Changelog for v18.104.22.168 (https://devs.x-cart.com/changelog/22.214.171.124_-_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
- Source for hex escaping in