Headline
CVE-2020-8889: SA: Shipstation plugin for CS-Cart - Incorrect Access Control
The ShipStation.com plugin 1.0 for CS-Cart allows remote attackers to obtain sensitive information (via action=export) because a typo results in a successful comparison of a blank password and NULL.
Description:
The ShipStation.com plugin 1.0 for CS-Cart allows remote attackers to obtain sensitive information (via action=export) because a typo results in a successful comparison of a blank password and NULL.
Additional information:
Multiple access bypass vulnerabilities in the file named shipstation.php, with trigger points at line 36 as well as line 31 and as explained in more detail below, in the ShipStation.com CS-Cart plugin 1.0 allow remote attackers to access sensitive user and purchase data from a CS-Cart installation via accessing the front page of the store with approximately four key/value pairs appended to the query string of the URL: "dispatch=shipstation", "action=export", "start_date=START_DATE_HERE", and "end_date=END_DATE_HERE", where START_DATE_HERE and END_DATE_HERE are replaced with a date readable by PHP’s strtotime() function. Trigger point specifics as previously referenced are as follows. (1) Line 36 of shipstation.php seeks to deny access by way of comparing the remote username and the remote password each to its respective counterpart as stored in the CS-Cart database; however instead of validating whether either of those exclusive values are not a match (thereby denying access in either case), line 36 denies access only when both the remote username does not match the local username and when the remote password does not match the local password. In other words, if either one is a mismatch then access is not denied. (2) Line 36 of shipstation.php does utilize strict type comparison operators during authentication, the importance of which being explained shortly hereafter. (3) Line 31 of shipstation.php attempts to retrieve a password value from CS-Cart’s registry whose key is "addons.shipstations.password". However, while this is a simple typo (“shipstations” instead of “shipstation”), the result is that said registry key’s value becomes unretrievable since it is unable to be set from anywhere within the administrative control panel, and its returned value will always be NULL. Further, since that same value is later used on line 36 for comparison to the remote password for authentication purposes, even if numbers 1 and 2 as described above were non-existent, a remote attacker could still bypass access restrictions by providing an empty string as the remote password since an empty string and NULL would technically match each other in PHP since strict type comparison operators are not utilized here.
Solution:
This advisory only applies to the plugin which was previously provided directly by ShipStation’s UI. The plugin has since been moved to Github.
For users who obtained the plugin directly from the ShipStation UI, install version 1.0.10 which is now hosted on Github at https://github.com/shipstation/plugin-cs-cart.
Original source code:
<?php /*************************************************************************** * * * © 2004 Vladimir V. Kalynyak, Alexey V. Vinokurov, Ilya M. Shalnev * * * * This is commercial software, only users who have purchased a valid * * license and accept to the terms of the License Agreement can install * * and use this program. * * * **************************************************************************** * PLEASE READ THE FULL TEXT OF THE SOFTWARE LICENSE AGREEMENT IN THE * * “copyright.txt” FILE PROVIDED WITH THIS DISTRIBUTION PACKAGE. * ****************************************************************************/
header(‘Content-Type: text/xml’);
if (!defined(‘BOOTSTRAP’)) { die(‘Access denied’); }
use Tygh\Registry; $action = strtolower($_REQUEST[‘action’]);
$post_data = '’;
if ($action == ‘export’) {
$username = empty($\_SERVER\['PHP\_AUTH\_USER'\]) ? $\_SERVER\['HTTP\_SS\_AUTH\_USER'\] : $\_SERVER\['PHP\_AUTH\_USER'\];
$password = empty($\_SERVER\['PHP\_AUTH\_PW'\]) ? $\_SERVER\['HTTP\_SS\_AUTH\_PW'\] : $\_SERVER\['PHP\_AUTH\_PW'\];
//if (empty($\_REQUEST\['vendor'\]) || !fn\_allowed\_for('MULTIVENDOR')) {
$addon\_username = Registry::get('addons.shipstation.username');
$addon\_password = Registry::get('addons.shipstations.password');
//} elseif (fn\_allowed\_for('MULTIVENDOR')) {
//list($addon\_username, $addon\_password) = db\_get\_row("SELECT shipstation\_username, shipstation\_password FROM ?:companies WHERE company\_id = ?i", $\_REQUEST\['vendor'\]);
//}
if ($username != $addon\_username && $password != $addon\_password) {
die('Access denied - Wrong username or password');
}
if (isset($\_REQUEST\['start\_date'\])) {
$start\_date = $\_REQUEST\['start\_date'\];
}
if (isset($\_REQUEST\['end\_date'\])) {
$end\_date = $\_REQUEST\['end\_date'\];
}
$page = empty($\_REQUEST\['page'\]) ? 1 : $\_REQUEST\['page'\];
$items\_per\_page = Registry::get('settings.Appearance.admin\_orders\_per\_page');
$limit = db\_paginate($page, $items\_per\_page);
$condition = " AND is\_parent\_order != 'Y'";
$condition .= db\_quote(" AND ((timestamp >= ?i AND timestamp <= ?i) OR (last\_modify != 0 AND last\_modify >= ?i AND last\_modify <= ?i))", strtotime($start\_date), strtotime($end\_date), strtotime($start\_date), strtotime($end\_date));
if (fn\_allowed\_for('MULTIVENDOR') && !empty($\_REQUEST\['vendor'\])) {
$condition .= db\_quote(" AND company\_id = ?i ", $\_REQUEST\['vendor'\]);
}
$order\_ids = db\_get\_fields("SELECT order\_id FROM ?:orders "
. " WHERE 1 $condition $limit");
$total = db\_get\_field("SELECT COUNT(DISTINCT(order\_id)) FROM ?:orders WHERE 1 $condition");
$total\_pages = ceil(($total \* 1.0)/ $items\_per\_page);
$post\_data = fn\_shipstation\_xml\_header();
$post\_data .= fn\_shipstation\_add\_tag("Orders", ($total\_pages > 1 && $page == 1 ? array('pages' => $total\_pages) : array())); // TODO split to pages
foreach ($order\_ids as $order\_id) {
$post\_data .= fn\_shipstation\_add\_order($order\_id);
}
$post\_data .= fn\_shipstation\_close\_tag("Orders");
} elseif ($action == ‘shipnotify’) { $body = '’; $fh = @fopen('php://input’, ‘r’); if ($fh) { while (!feof($fh)) { $s = fread($fh, 1024); if (is_string($s)) { $body .= $s; } } fclose($fh); } $order_id = $_REQUEST[‘order_number’];
$tracking\_number = $\_REQUEST\['tracking\_number'\];
if (empty($order\_id) || empty($tracking\_number)) {
header("HTTP/1.0 404", true, 404);
exit;
} else {
$order\_info = fn\_get\_order\_info($order\_id);
if (!empty($order\_info)) {
$products = $order\_info\['products'\];
$order\_shipments = db\_get\_hash\_array("SELECT sum(amount) as amount, item\_id FROM ?:shipment\_items WHERE order\_id = ?i GROUP BY item\_id", 'item\_id', $order\_id);
$all\_shipped = true;
foreach ($products as $item\_id => $product) {
if (isset($order\_shipments\[$item\_id\])) {
$order\_amount = $product\['amount'\];
$shipped\_amount = $order\_shipments\[$item\_id\]\['amount'\];
if (($order\_amount > $shipped\_amount) || ($order\_amount == $shipped\_amount)) {
$all\_shipped = false;
break;
}
} else {
$all\_shipped = false;
break;
}
}
if ($all\_shipped) {
header("HTTP/1.0 404", true, 404);
die('All shipped');
}
$carriers = fn\_get\_carriers();
$carrier = '';
foreach ($carriers as $s\_carrier) {
if (strtolower($s\_carrier) == strtolower($\_REQUEST\['carrier'\])) {
$carrier = $s\_carrier;
break;
}
}
if (empty($carrier) && !empty($\_carrier)) {
$carrier = $\_carrier;
}
$comments = '';
$doc = new DomDocument('1.0', 'utf-8');
$doc->loadXML($body);
$xp = new DomXPath($doc);
foreach ($xp->query('//NotesToCustomer') as $node) {
$comments = $node->nodeValue;
}
foreach ($xp->query('//ShipDate') as $node) {
$shipdate = $node->nodeValue;
}
$\_products = array();
foreach ($xp->query('//Items') as $node) {
foreach ($node->childNodes as $item) {
$amount = 0;
$item\_id = 0;
foreach ($item->childNodes as $subnode) {
if ($subnode->nodeName == 'Quantity') {
$amount = $subnode->nodeValue;
}
if ($subnode->nodeName == 'SKU') {
$order\_details = db\_get\_row("SELECT item\_id, amount FROM ?:order\_details WHERE order\_id = ?i AND product\_code = ?s", $order\_id, $subnode->nodeValue);
if (!empty($order\_details\['item\_id'\])) {
$item\_id = $order\_details\['item\_id'\];
}
}
}
if (!empty($item\_id)) {
$\_products\[$item\_id\] = $amount;
}
}
}
$ship\_data = array(
'shipping\_id' => $order\_info\['shipping\_ids'\],
'tracking\_number' => $tracking\_number,
'carrier' => $carrier,
'comments' => $comments,
'timestamp' => !empty($shipdate) ? strtotime($shipdate) : time()
);
$shipment\_id = db\_query("INSERT INTO ?:shipments ?e", $ship\_data);
foreach ($\_products as $key => $amount) {
if (isset($order\_info\['products'\]\[$key\])) {
$amount = intval($amount);
}
if ($amount == 0) {
continue;
}
$\_data = array(
'item\_id' => $key,
'shipment\_id' => $shipment\_id,
'order\_id' => $order\_id,
'product\_id' => $order\_info\['products'\]\[$key\]\['product\_id'\],
'amount' => $amount,
);
db\_query("INSERT INTO ?:shipment\_items ?e", $\_data);
}
$order\_shipments = db\_get\_hash\_array("SELECT sum(amount) as amount, item\_id FROM ?:shipment\_items WHERE order\_id = ?i GROUP BY item\_id", 'item\_id', $order\_id);
$all\_shipped = true;
foreach ($products as $item\_id => $product) {
if (isset($order\_shipments\[$item\_id\])) {
$order\_amount = $product\['amount'\];
$shipped\_amount = $order\_shipments\[$item\_id\]\['amount'\];
if ($order\_amount > $shipped\_amount) {
$all\_shipped = false;
break;
}
} else {
$all\_shipped = false;
break;
}
}
if ($all\_shipped) {
$shipped\_status = Registry::get('addons.shipstation.shipped\_statuses');
if (is\_array($shipped\_status)) {
$shipped\_status = reset(array\_keys($shipped\_status));
$shipped\_status = str\_replace('status\_', '', $shipped\_status);
}
if (empty($shipped\_status)) {
$shipped\_status = 'C';
}
fn\_change\_order\_status($order\_id, $shipped\_status, '', true);
}
header("HTTP/1.0 200", true, 200);
exit;
} else {
header("HTTP/1.0 404", true, 404);
exit;
}
}
} else { header("HTTP/1.0 200", true, 200); exit; }
echo $post_data;
exit;