
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:
A × 32). On both patched and unpatched builds this reaches the DB lookup and returns ['2\n', '1 failure\n'] quickly.<0xC0>'; \! sleep <delay> #. On a vulnerable host psql runs the injected sleep, so the 1 failure reply is delayed by roughly <delay> seconds. On a patched host the gskey regex rejects the key, the script exits, and the socket closes fast with no reply.Because the verdict rests on the timing of the appliance's own reply, it holds even when the target blocks outbound DNS/HTTP — a case where an OAST callback alone would false-negative. The optional callback mode is corroboration only, for hosts that do allow egress.
Install the one dependency:
pip install -r requirements.txt
Differential mode (recommended, egress-independent):
python3 bt_rs_rce_check.py --target rs.example.com --mode differential
Differential with OAST corroboration:
python3 bt_rs_rce_check.py --target rs.example.com \
--oast <your-collector>.oast.pro --mode both
When the Host header does not resolve to a tenant, provide an explicit slug, or a file of candidate slugs (one per line) tried in turn via the X-Ns-Company header:
python3 bt_rs_rce_check.py --target rs.example.com --company acme
python3 bt_rs_rce_check.py --target rs.example.com --company-list slugs.txt
Key options:
| Option | Meaning |
|---|---|
--target <host> | Appliance virtual host / FQDN (required). |
--company <slug> | Explicit tenant slug, sent as X-Ns-Company. |
--company-list <file> | File of candidate slugs, tried until one resolves. |
--oast <domain> | Out-of-band collector domain for callback corroboration. |
--mode {differential,callback,both} | Detection strategy (default differential). |
--delay <seconds> | sleep duration for the time-based test (default 9). |
--timeout <seconds> | Per-probe socket timeout (default 25). |
--header 'H: v' | Extra raw request header; repeatable. |
Reading the outcome:
VULNERABLE — the injected 1 failure reply was delayed by roughly --delay seconds relative to the control. The sleep executed; the unauthenticated RCE chain is exploitable on this host.PATCHED — the control returned 1 failure fast, but the injection was rejected with no reply. The BT24-10 gskey regex is in place and the injection never reaches the psql sink.UNRESOLVED ("company did not resolve") — neither probe produced a 1 failure, so the control never reached the gskey handler either. This is not a patched result; the appliance closed the socket because the Host/company did not map to an installed company. Retry with the correct virtual host (--target) or a valid slug (--company / --company-list).Apply BT24-10 (BT24-10-ONPREM1 / BT24-10-ONPREM2) or upgrade beyond 24.3.1. Because the chain also depends on the PostgreSQL psql flaw (CVE-2025-1094), keep PostgreSQL client tooling patched as well, and restrict network exposure of the appliance's WebSocket endpoint where feasible.
psql invalid-UTF-8 / SQL-injection flaw, chained to complete the RCE.rapid7/metasploit-framework PR #19877, unauthenticated RCE against BeyondTrust RS/PRA.This software is provided for authorized security testing and defensive research only. Run it exclusively against systems you own or are explicitly authorized in writing to test. It is a non-destructive detector that deliberately avoids weaponized command execution, data access, and persistence. You are solely responsible for complying with all applicable laws and for obtaining proper authorization; the authors accept no liability for misuse.