
Non-destructive detector for unauthenticated RCE in BeyondTrust Remote Support and PRA, chaining argument injection and PostgreSQL escape bypass to verify exploitability via time-based differential.
Ethical Warning: Research, educational, and authorized testing purposes only. Please do not use against systems without permission.
BeyondTrust Remote Support (RS) and Privileged Remote Access (PRA) are appliances that broker privileged remote sessions into an organization's internal systems, so they are frequently exposed to the internet and hold a trusted position on the network.
An unauthenticated attacker who can reach the appliance's WebSocket endpoint can execute operating-system commands as the site user by chaining two flaws: an argument injection in the appliance's thin-scc-wrapper script (CVE-2024-12356) and a PostgreSQL string-escape bypass that turns an invalid UTF-8 byte into an early SQL-literal break (CVE-2025-1094). The vulnerable path is reached before any authentication.
bt_rs_rce_check.py in this repository is a non-destructive detector for that chain. It drives the exact unauthenticated code path the real exploit uses but stops at proof-of-reachability: the only thing it ever asks the target to run is a benign sleep (a time-based differential) or a benign hostname/HTTP lookup to an out-of-band collector you control. It never opens a shell, writes files, reads data, persists, or moves laterally.
psql invalid-UTF-8 SQL-injection flaw, chained to complete the RCEBT24-10-ONPREM1 / BT24-10-ONPREM2 for self-hosted appliances; cloud instances were patched by BeyondTrust).CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:HThe chain yields unauthenticated remote code execution as the site user on an appliance whose entire purpose is to broker privileged access into internal systems, so a single anonymous request converts an exposed RS or PRA host into a foothold and a pivot toward every downstream system the appliance mediates.
It requires no credentials, no user interaction, and no prior access: the attacker is external and anonymous, and the only prerequisite is network reachability to the appliance's WebSocket endpoint.
The vulnerability was disclosed after it was found being exploited in the wild, was added to the CISA Known Exploited Vulnerabilities catalog on 19 December 2024, and has been publicly linked to the December 2024 breach of the U.S. Department of the Treasury attributed to a state-sponsored actor.
The vulnerable logic lives in the appliance's thin-scc-wrapper shell script, which reads an attacker-controlled session key (gskey) from an unauthenticated WebSocket frame and passes it toward the database. Full RCE is a chain of three distinct problems.
1. Argument injection in thin-scc-wrapper (CVE-2024-12356). The wrapper passes the untrusted gskey to a helper unquoted, so the shell word-splits it into arguments before echo runs:
quoted=$(export PHPRC="$BG_app_root/config/php-cli.ini"; echo $gskey | $ingrediRoot/app/dbquote)
A key that begins with -e turns echo into echo -e, which interprets backslash escapes and lets the attacker emit arbitrary raw bytes — most importantly \xC0 — into the data flowing to the next stage. The attacker controls a flag of an existing command rather than injecting a new command, which is why this is argument injection (CWE-88) rather than classic command injection.
2. PostgreSQL escape bypass via invalid UTF-8 (CVE-2025-1094). dbquote escapes the value with PHP's pg_escape_string(), which calls libpq's PQescapeStringInternal(). For a multibyte character it trusts the length reported by pg_encoding_mblen() and copies the character's bytes verbatim, without validating them:
len = pg_encoding_mblen(encoding, source);
for (i = 0; i < len; i++) /* copy the character */
*target++ = *source++;
A byte like 0xC0 announces a 2-byte character, so the escaper swallows the next byte as its "continuation" — even when that byte is a single quote (0x27). The sequence 0xC0 0x27 therefore passes through with an unescaped single-quote byte still embedded in the "escaped" string.
3. psql breaks out of the literal and runs \! (CVE-2025-1094). The "escaped" string is piped to psql, which does not treat 0xC0 0x27 as one character, so the 0x27 closes the SQL string literal early and everything after it is parsed as new input. The attacker uses that to reach psql's \! meta-command, which runs a shell command as the site user:
<0xC0>'; \! <cmd> #
Rapid7 found that RCE in practice always turns on CVE-2025-1094, and that it suffices on its own: delivering the raw 0xC0 byte inside a binary WebSocket frame (rather than a text frame, where UTF-8 validation would strip it) reaches gskey unmodified and triggers the SQL breakout with no echo -e step at all. This detector uses that binary-frame approach.
The entire chain is reachable pre-authentication over a single WebSocket:
/nw (a Tornado handler on the appliance).Sec-WebSocket-Protocol: ingredi support desk customer thin header, sent URL-encoded as ingredi%20support%20desk%20customer%20thin.Host header (mapped to an installed company) or, as a fallback, the X-Ns-Company header. If neither maps to an installed company the appliance closes the socket before gskey is processed.1, a thinMint UUID, authType 0 (gskey auth), then the malicious gskey (version 1 means there is no locale_code line):1
<thinMint UUID>
0
<0xC0>'; \! <cmd> #
BT24-10 adds an input-validation gate that rejects any non-alphanumeric gskey before it reaches dbquote/psql:
elif [[ ! "$gskey" =~ ^[a-zA-Z0-9]{32}$ ]]; then
blog "bad session key given"
...
exit 1
Any key containing 0xC0, a quote, or a shell metacharacter is rejected and the script exits, so a patched appliance produces no 1 failure reply and no command execution. The detector turns that behavioral difference into an egress-independent time-based differential that needs no outbound connectivity from the target: