Rapid7 Analysis: Unauthenticated File Upload to RCE in BeyondTrust Remote Support (CVE-2026-102764)

Source: Rapid7•

Rapid7 Analysis: Unauthenticated File Upload to RCE in BeyondTrust Remote Support (CVE-2026-102764)

Overview An origin-validation flaw in BeyondTrust Remote Support lets an unauthenticated attacker write files on the appliance and execute code as the site's operating-system user. The entry point is the public customer connection used to start a support session. No account credentials or user…

Overview

An origin-validation flaw in BeyondTrust Remote Support lets an unauthenticated attacker write files on the appliance and execute code as the site's operating-system user. The entry point is the public customer connection used to start a support session. No account credentials or user interaction are required.

The flaw sits between the internal message router, brain, and an appliance-side customer worker, thin-scc. An attacker can put a reserved local router ID into a message's source address. The router forwards the message, and the worker accepts that address as proof that the request came from a local component. This gives the attacker access to an internal file-transfer service, including its native filesystem provider.

We first confirmed the file write then used the primitive to upload a PHP file to the site's public directory. Requesting that file gave us code execution as the site user. A Metasploit module now implements the chain with PHP and Unix Command targets.

We tested Remote Support 24.1.2, build 1524046, revision 4208-ddcf90715106aa0c0e19c103f8d2c412af4beff3. The test appliance had public issue-menu session starts and thin chat enabled (default). Other affected releases and fixed builds have not been established. The VM's base appliance version of Secure Remote Access, 7.1.0, is separate from the Remote Support application version.

Impact

The attacker controls both the contents of an uploaded file and its destination path, subject to the worker's filesystem permissions. The same origin forgery also reaches the file-send operation. We used it to retrieve /proc/self/environ to determine the base installation path of the Remote Support before dropping the payload

On the tested appliance, the native file service could write into the PHP document root. The Metasploit module returns code execution in the context of msfuser, UID 1000, which establishes unauthenticated remote code execution as the site user.

Technical Analysis

We will demonstrate how an attacker can start an anonymous session and then forge a message that the appliance will accept as local. Then outline how we can reach the native file transfer service, choose the uploaded file’s contents and destination and turn the file write into PHP execution.

Addresses use Ghidra image base 0x100000. Subtract that value for the raw ELF virtual address. Function names beginning with FUN_ are Ghidra labels; descriptive names are taken from diagnostic strings where available.

Starting an anonymous customer session

We begin by obtaining two customer keys from POST /api/start_session. These are fresh public guest sessions. The keys let the client use the customer transport, but do not identify a logged-in account. Each guest connects through the /nw WebSocket endpoint, using either the native or thin customer protocol.

source/app/write_app_chooser_config:

app/write_app_chooser_config shows where the two customer protocols go. The native protocol reaches the brain socket at [A1]; the thin protocol launches a wrapper at [A2].

source/app/thin-scc-wrapper:

app/thin-scc-wrapper checks that the thin customer's key belongs to an unexpired sdcust session at [A3]. A key obtained through the public session-start endpoint passes this check.

app/thin-scc-wrapper, after the session check:

The same wrapper then prepares a working directory and starts thin-scc at [A4]. This process runs on the appliance, even though it is handling an anonymous customer's thin session.

The native connection gives us a client for the internal message-routing system, referred to here as MRS. The thin connection gives us an appliance-side worker to address. Neither step requires a representative to accept the session.

A network message can claim to be local

MRS addresses have three parts: node:router:handler. Each message carries source and destination addresses in a 36-byte header made up of nine little-endian 32-bit fields.

Header offset

Field

Value used by the exploit

0x04

Source node

The customer's normal node

0x08

Source router

Changed to the reserved value 1

0x0c

Destination node

The worker's node

0x10

Destination router

The worker's router

0x14

Destination handler

Main dispatcher, 2048, on the tested build

0x24

Payload

Service name, newline, then packed fields

app/brain — ingress check:

app/brain has an anti-spoofing check in FUN_01566ae0, identified by its handleMessageFromOtherRouter diagnostic. At [B1], it rejects a network packet whose source router matches the receiving router's actual ID.

Here, param_1 is the receiving router, param_2 is the packet, and param_3 is the incoming connection. Because param_2 points to eight-byte elements, *(int *)(param_2 + 1) reads the source router at byte offset 0x08.

That leaves a gap: the check compares against the actual router ID, while downstream code also recognises a reserved local alias. On this appliance, the alias is 1.

The forwarding path in the same app/brain function passes the packet onward at [B2]. It does not remove the source claim that matters to the worker's local-origin check.

app/brain — local-address predicate:

The local-address predicate makes the mismatch clear. In app/brain, FUN_0155ad40 first checks the node at [C1]. If that matches, router 1 is enough to return true at [C2]; the comparison with the actual local router is skipped.

The rule is effectively node AND (router == 1 OR router == actual local router). An address supplied by a network client is being used to make that decision.

app/thin-scc — message-origin wrapper:

The corresponding message wrapper in app/thin-scc, passes the message's sender address into its router object's local-address predicate at [C3]. Ghidra did not recover a reliable prototype for this indirect call; the caller consumes its result as a boolean.

We checked this behaviour with the same file-service request under two origins. The ordinary customer source produced no file-service connection during a two-second observation. Changing the source router to 1 caused the worker to accept it.

Reaching the native file-transfer service

In app/thin-scc, FUN_0047a850 dispatches file-transfer messages.

app/thin-scc — file-transfer dispatcher:

It calls the origin wrapper at [D1] and discards a message if that wrapper says the sender is nonlocal. Once that check passes, [D2] matches ftsName against the service instance's name.

The same function only validates responseAddress when the field is nonempty. The exploit omits it, taking the branch at [D3].

app/thin-scc, reply-address handling in FUN_0047a850:

The validator called at [D4] compares the reply address against the message's sender address. It does not independently establish where that sender came from, so making this field mandatory would not fix the origin forgery.

app/thin-scc, action handling in FUN_0047a850:

The dispatcher then exposes DownloadAnonymousFiles at [D5] and UploadFiles at [D6].

The action names describe the worker's side of the transfer. DownloadAnonymousFiles receives bytes onto the appliance; UploadFiles sends a file out. To upload our file, we ask the worker to download it from an MRS listener we control.

The important thing to note here is that the worker has more than one filesystem provider. The session service delegates filesystem work to the browser. A successful transfer through that service does not prove a file was written on the appliance. The local service uses native file objects instead.

app/thin-scc — native service construction:

The ServerContext constructor in app/thin-scc, FUN_002ae620, creates that native provider at [E1]. It builds the name local at [E2] and passes both to the file-transfer service constructor at [E3].

The exploit therefore uses ftsName=local. The native file factory analysis follows this provider to the native file and filesystem-info constructors.

Choosing the file contents and destination

app/thin-scc, FUN_00475820 , handles DownloadAnonymousFiles.

app/thin-scc — anonymous-download request:

The function reads the request's transfer token at [F1], MRS server address at [F2], and destination path at [F3]. All three reach the download routine at [F4].

In this excerpt, lVar12 points to the request's field map. param_2 has already been advanced to its map-end sentinel; 0x9c3700 represents an absent value.

app/thin-scc — MRS download connection:

The download routine at 0x004a21c0 opens an MRS stream to our address at [G1]. It sends get anonymous file at [G2] with our token at [G3]. The token can be a 24 character random string because we control the listener.

The same app/thin-scc routine puts the destination path into a receive-files thread at [G4] and starts it at [G5].

app/thin-scc — receive-files loop:

The receive loop at 0x001d6430 consumes the file metadata we send over that stream. It copies dest_filename at [H1]. Depending on the provider's file information, it receives into that path or a temporary path ending in .n$t at [H2].

After the omitted begin file code has executed, the same app/thin-scc receiver opens the file at [H3] and writes incoming data at [H4].

On completion, the receiver closes the file and, when a temporary path is used, renames it to the final destination. The service already accepts an absolute destination path, so no directory traversal is needed. The worker allows the request because it trusts the forged local source address.

From file upload to PHP execution

The next step is to choose a writable location that the web server would execute. The supplied config/apache_vhost.conf sets the document root to:

An absolute upload into that directory succeeded. There is a small catch when requesting the file: a URL ending in .php returns 404, even while the file existed. app/www/public/.htaccess explains why.

source/app/www/public/.htaccess:

The rule at [I1] rejects a direct .php request. The rule at [I2] maps an extensionless URL to an existing PHP file.

Requesting the uploaded basename without .php executed our payload. It returned its absolute filename, EUID 1000, and the output of id.

We also wanted to avoid guessing Remote Support’s working directory or the versioned installation name. The same native service can send us the worker's /proc/self/environ using UploadFiles. On this appliance, it contains:

BG_app_root gives us the installation directory directly. Appending /app/www/public gives the upload destination; the socket path provides another way to identify the same root. There is one protocol wrinkle: procfs reports the environment file's size as zero, but the worker still streams its contents. We have to read through the data stream's EOF instead of stopping at the advertised size.

Exploit

The Metasploit module carries out the chain using two fresh public customer sessions. It discovers the thin worker through Director.GetGatewaySessionValues for its own thin guest, sends the forged LocalFileTransfer request to the worker's main dispatcher, and serves the PHP bytes over its existing MRS connection. A separate upload server is not needed.

The module writes a random PHP filename into the site's public directory, then POSTs an execution token to the extensionless URL. The PHP file checks that token and unlinks itself before executing the payload. Target 0 runs a PHP payload; target 1 wraps a Unix command in PHP.

For a PHP Meterpreter payload, the basic configuration is:

Select target 0 and a compatible command payload, such as php/meterpreter/reverse_tcp. The module documentation covers the directory override and other options.

Figure 1: Exploiting the anonymous file-transfer service with Metasploit.

Testing produced two PHP Meterpreter sessions, a command-wrapped PHP Meterpreter session, and a Bash shell. Both targets also ran with AutoCheck disabled, and immediate repeat exploitation succeeded. The Bash shell returned:

IOCs

The excerpts below cover HTTP requests, native file-transfer activity in thin-scc, and customer connections recorded by brain. They come from our lab runs on 28–29 October 2026. Original timestamps, addresses, and paths are retained; session and transfer tokens are redacted where noted. Unrelated entries are omitted.

The site logs are stored under $BG_app_root/log/, where BG_app_root identifies the Remote Support installation directory:

Log

Evidence shown below

httpd_access.log

Public session creation and requests to uploaded PHP endpoints

thin-scc.log

Native file-transfer stream creation and routing errors

thin-scc-errors.log

Rejected file-transfer requests, including the action and destination path

brain.log

Customer connection records with client addresses and timestamps

Active workers also keep logs under $BG_app_root/data/tmp/sdcust_thin_client/<thinMint>/; the collection details below explain how these reach the site log directory. The installation path varies between appliances.

Requests to an uploaded PHP endpoint

The command-only Metasploit run created two public customer sessions, invoked its uploaded PHP endpoint, and then checked that the endpoint had gone. The relevant access-log lines are:

Here, /qdgusthrmlirprls mapped to qdgusthrmlirprls.php in the public directory. The first POST ran the command and returned 200. The later POST was the module's cleanup request; the GET independently checked the endpoint. Both received 404 after the file had deleted itself.

This gives defenders a sequence to investigate: public session creation followed by a request to a new, unfamiliar endpoint, potentially followed by 404s for the same path. Session creation alone is normal Remote Support activity. A 200 followed by a 404 is also insufficient by itself; correlate the requests with file changes, session records, and any unusual outbound connections.

The URL may not contain .php

The earlier PHP canary produced these lines:

The direct .php request failed, while the extensionless request executed the same file. The final 404 followed self-deletion. Detection that only searches for requests ending in .php would miss the successful request in this example.

The native transfer itself travels inside the customer MRS connection. These access-log entries show the HTTP trigger, not the file contents or the forged router ID. The captured HTTP log does not expose the inner MRS messages, so it cannot establish the origin bypass on its own.

Files and connections

Look for unexpected PHP files under the site's public directory and receive files ending in .php.n$t. An interrupted transfer may leave the temporary file even if the final URL returns 404. The .n$t suffix also belongs to the normal transfer implementation, so its presence needs context.

Native file-transfer traces

thin-scc.log and thin-scc-errors.log contain more direct evidence of the file-service requests. For example, this line from thin-scc-errors.log names the action, native provider, and intended PHP destination. Only ckey and downloadToken have been redacted:

This was our negative control: the request used the customer's ordinary source address and was rejected. It is evidence of an attempted invocation, not a successful upload. The forged local source passes the check that produces this warning, so a successful exploit need not leave a DISCARDING NONLOCAL MESSAGE entry.

The successful PHP module run left these lines in thin-scc.log:

Our requests omit responseAddress. The handler starts the transfer, then tries to send its action response to an empty address. That produces the 0:0:0 error without preventing the file write. The payload callback independently confirms execution at 17:06:37.

Look for this combination in the proc{LocalFileTransfer} context, especially around unfamiliar PHP requests or outbound connections. The 1:1129726787:1031 value identifies an internal MRS mailbox; use the brain connection records below for the client IP. These entries do not identify the written filename or prove execution on their own. The empty-address errors also depend on how the exploit request is constructed; another implementation may leave different traces.

Brain connection records

brain.log helps link the activity to a client address. These two entries preceded the same module run:

These are useful for a timeline, but customer connections are normal activity. We found no UploadFiles or DownloadAnonymousFiles action records in the captured brain logs. Generic EOF, connection-reset, and conference errors also appeared; they are too broad to treat as exploit indicators by themselves.

Where to collect the native logs

The aggregated logs are under $BG_app_root/log/. While a worker is running, its own logs are under $BG_app_root/data/tmp/sdcust_thin_client/<thinMint>/. The wrapper appends those logs to the site's log directory when the worker exits, then clears the per-worker copies. Collect both locations when investigating a live session.

The captured brain.ini and thin-scc.ini both had the debug output disabled with enabled=0. No debug log was captured, and we did not change the logging configuration. Brain's debug configuration also limits MRS_* categories to Info, so enabling its debug output alone would not guarantee detailed message payloads.

Names such as UploadFileStarted and DownloadFileEnded occur in the native protocol's callback code, but we did not find them in the collected logs. They should not be presented as default audit events

What this article says