Title: South River Technologies Titan MFT Server 26.0.0 - code execution
Author: 0day Rubbish Research Team
Contact: disclosure@0day-rubbish.com
Type: remote
Platform: Windows
CVE: pending
Description:
South River Technologies Titan MFT Server 26.0.0.364, the NextGen generation of the commercial managed file transfer server from South River Technologies (Annapolis, Maryland, United States), runs as a single .NET 8 process, titanmft.exe, registered as the Windows service SrtTitanMFTServerService under LocalSystem and serving a Blazor Server administration application on a TLS listener bound to 0.0.0.0:47443 with the product's IP-restriction feature disabled. Two independent defects chain into operating-system command execution as NT AUTHORITY\SYSTEM. The first is CWE-306 on the first-run administrator-creation path: while the administration database's nxUserParams table holds zero rows, the login route renders an "Initial Admin User Setup" screen to any client reaching the listener, gated only by `RunWizard = !nxUserParams.AdminUserExists() && !CreateAdminAtConsole` at MxSvrParams.cs:518-520, with no EULA block, no bootstrap ticket, no CAPTCHA, no first-boot token, no localhost restriction and no channel binding; submitting it calls AppState.CreateAdminUser followed by AppState.Login inside the same in-process circuit and lands the anonymous client in the management console. The second is CWE-78 on the job post-command path: the console field labelled "Execute After File Transfer Task" is persisted verbatim into nxJobs.document.PostCommandText and, when the job runs, is passed by MxUtilSvrJobHandler.RunCustomProcessAsync at MxUtilSvrJobHandler.cs:991 (a second pass over the same decompilation records :996), reached from the predicate at :970 via the call site at :979-982, to process creation with UseShellExecute = false, a plain Arguments string rather than ArgumentList, no allow-list and a cmd-prefix special case that concatenates "/c " + command.Substring(3).Trim(). That special case reconstitutes full shell semantics, so redirection, chaining and percent-expansion are all live in the payload space, and the child inherits the LocalSystem service token because nothing on the path impersonates, restricts the token or downgrades integrity. The framing that governs both halves: this is a first-run window, meaning a deployment state, and not an absence of authentication. The window opens when installation completes and closes the instant the first administrator exists, after which the login route renders a credential form and the account-creation half of the chain has no route whatsoever. The product's message-level authorisation is separately intact and was measured intact during the same run in which the wizard was available, which is precisely why the chain is described as a state-conditioned finding rather than as an authentication bypass. Zero [Authorize] attributes exist across the 1,880 decompiled files of the ServerCore assembly, so every authorisation decision lives in application logic and, for the setup screen, that logic is an empty-table test. The legacy /WebApi/CreateAdmin route carries the same predicate and is reachable unauthenticated but is functionally dead from a NullReferenceException in model binding, characterised across four body states, so it is a dead sibling rather than an alternative path - and repairing the binding without repairing the predicate would revive it. One detection trap is worth repeating for anyone writing a scanner or an operator self-check: a plain GET /Login returns only the 3,869-byte Blazor host shell containing none of the setup text, because the component tree renders only after the circuit boots client-side, so a raw-response grep reports "no wizard" on an installation that is in fact exposed. The authoritative check is the rendered text in a real browser, which is what this exploit's default gate mode performs.
CVSS: 8.1 High (CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H) as the primary published reading, because the chain requires a deployment state the attacker can discover but cannot create, cannot extend and cannot re-open: an installation that has not yet completed first-time setup. CVSS 3.1 places "conditions beyond the attacker's control" in Attack Complexity High, and the window's duration in real deployments was never measured, so AC:L cannot be supported as a headline. Arithmetic: ISS = 1 - (0.44)^3 = 0.914816; Impact at S:U = 6.42 x 0.914816 = 5.87311872; Exploitability = 8.22 x 0.85 x 0.44 x 0.85 x 0.85 = 2.2211673; Base = 8.09428602; Roundup to the tenth = 8.1, band High. Conditional reading, published as conditional and never as the headline figure: 9.8 Critical (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H), Exploitability 3.887042775, Base 9.760161495, Roundup 9.8, applicable only to one installation confirmed to be inside its first-run window, where the state condition is already satisfied and the remaining attack is four form submissions and a menu click. Rejected readings, named so the rejection is auditable: Scope Changed would compute to 9.1 under AC:H and to 10.0 under the conditional AC:L, and is rejected because the child process is created by the service inside the service's own LocalSystem token on the service's own host, so executing as the same service identity on the same host does not change scope - and because adopting Changed would put a conditional 10.0 on the table and invite the "this product has no authentication" misreading. The sink-only reading CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H computes to 7.2 and is rejected because the post-command field on its own is a documented administrator feature; an administrator reaching LocalSystem is a privilege-separation weakness, not remote code execution, and publishing either half in isolation would misrepresent the finding. PR:N is a property of the chain start rather than of the sink in isolation: the least privileged principal that reaches the sink by the documented path is a principal that did not exist before the request. Contributing classifications named but deliberately excluded from the score: CWE-250/CWE-269 for LocalSystem execution with no privilege separation, and CWE-209/CWE-200 for an unauthenticated stack-trace echo and a pre-authorised window-state leak, both reported as separate defects with no score attached here. CWE-1188 is deliberately not used because no insecure default credential ships with the product - the exposure is a state, not a secret - and using it would imply that rotating a password closes the window when in fact creating the first administrator does. There is no recorded-score correction to disclose for this finding, because the material the analysis is built from carries no CVSS vector and no base score for it; both readings were derived from the metric definitions and the measured facts, computed with the CVSS 3.1 base formula and rounded up to the tenth as that formula specifies.
Impact: NT AUTHORITY\SYSTEM on the Windows host carrying an organisation's file-exchange hub, which for this class of product is close to a worst case rather than a generic service identity. Every trading partner's exchange credentials, because connector configuration, SFTP/FTPS/WebDAV account material and the product's SSH and PGP key material - including keys used to encrypt outbound partner files and to verify inbound ones - live in the administration and domain databases, so one host compromise becomes the compromise of every exchange relationship the host participates in, with parties who have no visibility into the incident. The files themselves in transit and at rest, because connector source and destination directories are both readable and writable from the service account, permitting interception, silent substitution of content presented to a partner, and removal of evidence. Transfer configuration, because jobs, schedules, connector paths, notification settings and audit configuration can all be rewritten, which allows a recurring partner transfer to be redirected to an attacker-chosen directory with no partner-visible change, and allows auditing to be disabled for a job before it is abused. Host persistence and pivot, because LocalSystem permits creating local accounts, installing services, writing to startup locations and reading host credential stores, and MFT hosts are routinely granted broad access to internal storage and to domain resources precisely so they can collect and deliver files. Availability of scheduled transfers, because stopping the service, disabling jobs, corrupting the SQLite databases or filling the connector volume halts every exchange, which for partner agreements carrying delivery windows is a business-continuity event rather than a service outage. Detection at the product layer is anomaly-based rather than signature-based, because every step in the chain is a documented administrative action: the product's own audit surface records legitimate administration, and there are no failed logins, no injection payloads and no exploitation signatures. Exposure is a race against deployment, so its length depends on how quickly first-time setup follows installation and was not measured; the same build can be exposed for five minutes or for five days depending on the operator's process. A deployment that finished first-time setup before its listener became reachable to untrusted clients was never exposed at all.
Verification boundary: full-chain dynamic verification on a Windows host with a fresh default installation of 26.0.0.364 from the BitRock unattended package, driven over HTTP and the Blazor circuit by a headless Chromium-family browser under the DevTools protocol, with Python standard-library tooling only and a raw-socket WebSocket client; the attack path is HTTP plus the product's own user interface, with no direct database writes and no direct API calls on it, and database reads taken read-only for forensic comparison. Every step - window preparation, probe set, account creation, job creation, injection, run, marker verification, control run and teardown - executed inside one scripted pass whose output was captured, so no step was proven on a machine that an earlier step's debugging had modified. Identity was measured, not inferred: the injected `cmd /c whoami > <marker>` wrote a 21-byte file containing `nt authority\system` whose modification time sits 76 ms after the job-completion timestamp the product recorded in its own job-history table for that run. A negative control ran alongside, re-authenticating as the administrator the exploit itself created, clearing PostCommandText (confirmed empty in the field and in the job document), re-running the identical job and observing a second completed job-history row with zero files transferred while the marker stayed byte-identical and its modification time unchanged to the millisecond, the two runs 111.6 seconds apart. An earlier round on the same build produced the same identity through the same path with a differently named administrator, a differently named job and an independently named marker, also 21 bytes, so the result is not a property of one account or job name. Two states were supplied by our own harness rather than by the product and both are stated here: the first-run window was re-opened by an authorised local database reset, deleting the administrator rows from the administration database and restarting the service to return a previously configured host to its shipped zero-administrator state, because an earlier pass had completed setup - a never-configured installation lands in that state by construction and the product's predicate reads only the row count, but this chain was not observed on a machine that had never been touched; and verification ran from loopback on the target host with inbound firewall rules blocking remote TCP and UDP on 47443 and 37443 for the duration of the run, so "reachable from an untrusted network" is a reading of the shipped 0.0.0.0 binding and the disabled IP restriction, both artifact facts, rather than a measurement of an attack crossing a network boundary. Exactly one payload shape was executed - a single whoami redirect proving identity - so nothing destructive, no persistence, no lateral movement, no domain compromise and no partner-system access was performed or measured. Further limits: 26.0.0.348 shares the affected listing with its first-run half confirmed dynamically and its injection tail argued from a shared code base rather than replayed, and no separate score is published for it; the pre-transfer command sibling was not executed and its exploitability is inference; the failing-transfer-with-payload combination was not executed; no cloud-shaped installation was sampled, so the CreateAdminAtConsole branch and its interaction with the legacy creation route are uncharacterised rather than exempt; no post-setup HTTP status sample was taken from the login route, the post-window evidence being that the setup screen is no longer rendered, that a credential form is rendered instead and that the created administrator's credentials were accepted through it; the window's duration in real deployments was not measured and is not estimated; and no other version, edition or family product is claimed, including Titan FTP Server, WebDrive and the older 2.x generation. The shipped proof of concept drives the product's own interface through a real browser, takes every target-specific value as an argument with no address baked in, uses the Python standard library plus a browser, and offers a read-only gate mode that performs the authoritative window check and exits non-zero when the installation is not inside the first-run window.
Proof-of-concept and full analysis:
https://0day-rubbish.com/blog/southriver-titan-mft-firstrun-admin-setup-job-command-injection
PoC location in the public repository: product/southriver/titan-mft-26.0.0/firstrun-admin-setup-job-command-injection-system-rce/exploit/southriver_titan_mft_firstrun_admin_setup_job_command_injection_system_rce.py
Repository: https://github.com/Exploit-Garbage/0day-Rubbish
--
0day Rubbish Research Team
disclosure@0day-rubbish.com
https://0day-rubbish.com