SA: Shipstation plugin for CS-Cart - Incorrect Access Control

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
/***************************************************************************
*                                                                          *
*   (c) 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;