# Variant Root Cause Analysis: GHSA-jf2j-jhvf-rc56 / CVE-2026-23698

## Summary

Vtiger CRM 8.4.0's authenticated admin **Module Manager → Import** feature is not only exploitable by uploading a malicious module ZIP (the original CVE-2026-23696 proof of concept), but also by uploading a **language pack ZIP** or a **layout/theme ZIP** through the same endpoint. Both alternate paths extract attacker-controlled PHP files into the web root without adequate validation. The language import path performs no content validation at all; the layout import path attempts a regex-based PHP detection that is bypassed by the PHP short-echo tag `<?=`. In both cases, the extracted PHP file is reachable directly via HTTP and executes as the web-server user, producing the same persistent, unauthenticated RCE outcome as the original module-import vulnerability. No vendor patch is available, so a true bypass of a fixed version could not be demonstrated; the confirmed findings are **alternate triggers on the vulnerable 8.4.0 release**.

## Fix Coverage / Assumptions

The original advisory does not reference a vendor fix. Public disclosure (Jiva Security, 2026-07-06) confirms that Vtiger did not respond to the 90-day disclosure window and that no patch was released. A narrow fix that only inspected the module import path (`Vtiger_PackageImport`) would leave two structurally similar sinks exposed:

- `Vtiger_LanguageImport::import_Language()` — no file content or extension validation.
- `Vtiger_LayoutImport::import_Layout()` — partial PHP content validation that is bypassable.

The current code assumes that the import type alone (`module`, `language`, `layout`) is enough to determine which extraction logic to run, but it does not enforce a uniform safe-extraction policy across all three types.

## Variant / Alternate Trigger

Two distinct, confirmed alternate triggers were found through the same authenticated admin endpoint.

### Variant 1 — Language Pack Import

- **Entry point**: `index.php?module=ModuleManager&parent=Settings&view=ModuleImport&mode=importUserModuleStep1` (HTTP POST to `importUserModuleStep2`, then `Basic&mode=importUserModuleStep3`).
- **Dispatch**: `modules/Settings/ModuleManager/actions/Basic.php::importUserModuleStep3` creates `new Vtiger_Language()` when `module_import_type == 'language'`.
- **Extraction sink**: `vtlib/Vtiger/LanguageImport.php::import_Language()`.
- **Trigger**: Upload a ZIP containing a valid `manifest.xml` with `<type>language</type>` and a `<prefix>` value, plus a PHP file under `modules/shell.php`. The sink rewrites the path to `languages/<prefix>/shell.php`.
- **Execution**: Access `http://<host>/languages/<prefix>/shell.php?cmd=<command>`.

### Variant 2 — Layout Import PHP-Filter Bypass

- **Entry point**: Same Module Manager Import endpoint as above.
- **Dispatch**: `modules/Settings/ModuleManager/actions/Basic.php::importUserModuleStep3` creates `new Vtiger_Layout()` when `module_import_type == 'layout'`.
- **Extraction sink**: `vtlib/Vtiger/LayoutImport.php::import_Layout()`.
- **Trigger**: Upload a ZIP containing a valid `manifest.xml` with `<type>layout</type>` and `<parent>v7</parent>`, plus a PHP file under `layouts/<name>/skins/<name>/shell.php` that uses the short-echo tag `<?= ... ?>`. The regex `/(<\?php?(.*?))/i` in `import_Layout()` does not detect `<?=`, so the file is extracted to `layouts/<name>/skins/<name>/shell.php`.
- **Execution**: Access `http://<host>/layouts/<name>/skins/<name>/shell.php?cmd=<command>`.

## Impact

- **Product/component**: Vtiger CRM 8.4.0 (open-source PHP/MySQL CRM)
- **Affected versions**: through 8.4.0 (tested on the 8.4.0 SourceForge release tarball)
- **Risk level**: High
- **Consequences**: Any authenticated administrator who can access the Module Manager Import feature can plant a persistent, unauthenticated web shell in `languages/` or `layouts/` in addition to `modules/`, achieving arbitrary remote code execution as the web-server user (`www-data`).

## Impact Parity

- **Disclosed/claimed maximum impact**: Authenticated remote code execution via a crafted module ZIP (code execution).
- **Reproduced impact from this variant run**: The variant script logged into Vtiger 8.4.0 as admin, uploaded a language pack ZIP and a layout pack ZIP through the same Module Manager Import endpoint, and confirmed that both produced a working webshell returning the marker `PRUVA_LANG_SHELL_OK` and `PRUVA_LAYOUT_SHELL_OK` respectively, plus the output of the injected command (`www-data`).
- **Parity**: `full` — the reproduced impact matches the claimed RCE impact.
- **Not demonstrated**: A true bypass of a released vendor patch, because no patch is available.

## Root Cause

The root cause is the same as the original vulnerability: **Vtiger extracts attacker-supplied ZIP archives directly into web-accessible directories without a uniform, robust allowlist of safe file types and contents.** The Module Manager Import dispatcher treats the three package types (module, language, layout) as separate code paths, but none of them:

1. Stages the archive outside the web root before validation.
2. Maintains a per-type allowlist of expected file extensions and relative paths.
3. Rejects PHP files that are not legitimate, known source files for the package type.

The language import path has no validation at all. The layout import path has an incomplete regex that can be bypassed by the PHP short-echo tag. The module import path has no validation at all. The architectural trust boundary is identical: an authenticated admin can turn any of these imports into a persistent web shell.

## Reproduction Steps

The full, automated reproduction is in `bundle/vuln_variant/reproduction_steps.sh`. At a high level it:

1. Logs in to the already-running Vtiger 8.4.0 instance as the admin user.
2. Builds a malicious **language pack** ZIP with `<type>language</type>`, a `<prefix>`, and `modules/shell.php` containing a PHP shell.
3. Uploads the ZIP through the Module Manager Import endpoint and completes the import.
4. Verifies the shell by accessing `http://localhost/languages/<prefix>/shell.php?cmd=whoami`.
5. Builds a malicious **layout pack** ZIP with `<type>layout</type>`, `<parent>v7</parent>`, and `layouts/<name>/skins/<name>/shell.php` using the `<?=` short-echo tag.
6. Uploads the ZIP through the same endpoint and completes the import.
7. Verifies the shell by accessing `http://localhost/layouts/<name>/skins/<name>/shell.php?cmd=whoami`.
8. Records the results to `bundle/vuln_variant/proof.log` and exits with code 1 because no vendor patch is available to test a true bypass.

**Expected evidence of reproduction**: Both webshells respond with their respective marker (`PRUVA_LANG_SHELL_OK` or `PRUVA_LAYOUT_SHELL_OK`) and the output of the injected command (e.g., `www-data`).

## Evidence

- `bundle/logs/vuln_variant_reproduction_steps.log` — full trace of the two variant tests.
- `bundle/vuln_variant/proof.log` — structured result file (`LANGUAGE_VARIANT=True`, `LAYOUT_VARIANT=True`).
- `bundle/vuln_variant/reproduction_steps.sh` — the idempotent reproduction script.
- Key excerpts from `bundle/logs/vuln_variant_reproduction_steps.log`:

```
[+] Upload accepted: file=usermodule_1783466053.zip name=LangycQzbm type=language
[+] Install response: {"success":true,"result":{"success":true,"importModuleName":"LangycQzbm"}}
[*] Triggering language webshell: http://localhost/languages/langycqzbm/shell.php?cmd=whoami
[+] Webshell output: 200 PRUVA_LANG_SHELL_OK
www-data
www-data

[+] Upload accepted: file=usermodule_1783466053.zip name=LaynpRQix type=layout
[+] Install response: {"success":true,"result":{"success":true,"importModuleName":"LaynpRQix"}}
[*] Triggering layout webshell: http://localhost/layouts/LaynpRQix/skins/LaynpRQix/shell.php?cmd=whoami
[+] Webshell output: 200 PRUVA_LAYOUT_SHELL_OK
www-data
www-data
```

Environment captured during the run:

- Vtiger CRM 8.4.0 (SourceForge tarball, md5 `360f394f5f7ffabc89eaca05b18523f9`)
- Apache 2.4.66 + PHP 8.5 (mod_php)
- MariaDB 11.8.6
- Ubuntu 26.04 (resolute)

## Recommendations / Next Steps

- **Immediate fix**: Add a uniform, pre-extraction validation layer in the Module Manager import dispatcher (`modules/Settings/ModuleManager/actions/Basic.php`) before calling `Vtiger_Package::import()`, `Vtiger_Language::import()`, or `Vtiger_Layout::import()`. The validator should enforce a per-type allowlist of expected file extensions and relative paths, and reject any PHP file that is not a legitimate module/language/layout source file.
- **Fix layout content check**: Replace the bypassable regex `/(<\?php?(.*?))/i` in `vtlib/Vtiger/LayoutImport.php` with a robust check (e.g., tokenization via `token_get_all()`) or, better, reject PHP files entirely in layout packs.
- **Fix MIME validation**: `Vtiger_Functions::verifyClaimedMIME()` should compare file MIME types against a MIME type allowlist, not against the `$upload_badext` extension list.
- **Architectural fix**: Stage all user-uploaded module/language/layout archives outside the document root. Copy only validated files into the application tree, and serve assets through an authenticated PHP handler that sets `Content-Disposition: attachment` for non-executable files.
- **Regression tests**: Add tests that attempt to import each package type with an embedded PHP shell and assert rejection and that the PHP file is never created under `modules/`, `languages/`, or `layouts/`.

## Additional Notes

- **Idempotency**: The reproduction script was run twice consecutively against the same Vtiger 8.4.0 installation. Both runs succeeded, using unique module/language/layout names each time to avoid collisions.
- **Fixed version test**: No vendor patch is available. The script therefore cannot demonstrate a true bypass of a fixed version and exits with code 1 even though the alternate triggers are confirmed on the vulnerable version.
- **No repro state mutation**: The variant stage reused the existing 8.4.0 deployment created by the repro stage; it did not check out a different source ref or alter the original repro artifacts.
- **Third-party research alignment**: The Jiva Security disclosure for CVE-2026-23698 explicitly flagged the language and layout import paths as structurally similar alternate routes, but did not publish a full PoC for them. This run provides that validation.
