FreeRDP: `/restricted-admin` does not suppress credential delegation — the client sends the cleartext password to the RDP server
What fixes it
- 3.32.0FreeRDP, cGitHub
A fixed version on one release line is not the upgrade target for every line. Check your exact version and any vendor backports.
Description
This issue was found by Anthropic using Claude to study the security of open-source software, validated manually by me from Ada Logics.
Summary
/restricted-admin sets FreeRDP_RestrictedAdminModeRequired, which makes the client advertise RESTRICTED_ADMIN_MODE_REQUIRED in the RDP negotiation request. It does not set FreeRDP_DisableCredentialsDelegation. nla_encode_ts_password_credentials() decides whether to emit real credentials from settings->DisableCredentialsDelegation alone, and never consults RestrictedAdminModeRequired, so a client started with /restricted-admin together with /p:<password> serialises the cleartext domainName, userName and password into TSPasswordCreds and delivers them to the server in the CredSSP authInfo.
MS-RDPBCGR 2.2.1.1.1 describes the flag as "the client requires credential-less logon over CredSSP (also known as 'restricted admin mode')". The point of that mode is that the destination host — the party assumed to be compromised in a bastion/jump-host deployment — does not receive a reusable secret. In FreeRDP it receives the administrator's plaintext password.
Root cause
restricted-admin (client/common/cmdline.c:5034) sets only FreeRDP_ConsoleSession and FreeRDP_RestrictedAdminModeRequired. FreeRDP_DisableCredentialsDelegation stays at its settings_new() default of FALSE (libfreerdp/core/settings.c:1024); the only CLI path that ever sets it is the separately-spelled -credentials-delegation option (client/common/cmdline.c:4905).
nla_client_setup_identity() (libfreerdp/core/nla.c:408-426) special-cases restricted admin only for the pass-the-hash input shape; with /p: it takes the usePassword branch and loads the literal password into nla->identity->Password. The encoder then gates on the wrong flag:
/* libfreerdp/core/nla.c:1758 */
if (!settings->DisableCredentialsDelegation && nla->identity)
{
username.len = nla->identity->UserLength * sizeof(WCHAR);
username.data = (BYTE*)nla->identity->User;
domain.len = nla->identity->DomainLength * sizeof(WCHAR);
domain.data = (BYTE*)nla->identity->Domain;
password.len = nla->identity->PasswordLength * sizeof(WCHAR);
password.data = (BYTE*)nla->identity->Password;
}Two related observations from the same run:
- Even with
-credentials-delegation, which does blankTSPasswordCreds, the password still reaches the server in the Client Info PDU:connection.c:477-481enablesAutoLogonEnabledwheneverUsernameandPasswordare set, andinfo.c:992-1006writescbPassword/passwordWinto the PDU. Under/restricted-adminboth channels carry the secret. - The client parses but never enforces the server's
RESTRICTED_ADMIN_MODE_SUPPORTEDresponse flag —RESTRICTED_ADMIN_MODE_SUPPORTEDappears only at the server emit site (nego.c:1583) and in a log-formatting helper (nego.c:1376). A server that does not advertise support still receives the credentials. This second point is from source reading only; it cannot be exhibited against a FreeRDP-based peer, because a FreeRDP server rejects the connection outright when it does not advertise support (nego.c:1297-1309).
Primitive shape
| Aspect | Value | |---|---| | Direction | Outbound disclosure, client to server | | Data | Full Domain / Username / Password UTF-16 strings, unbounded length | | Attacker role | Recipient (the RDP server), not the shaper of the data | | Precondition | Victim runs a FreeRDP client with /restricted-admin and supplies a password (/p: or the interactive prompt) | | Reachability | Shipped client binaries in a default build via a documented option; client/common/cmdline.c is shared by xfreerdp, sfreerdp, SDL, Wayland, macOS and Android clients | | Impact | A compromised or MITM'd destination host obtains a reusable administrative credential that restricted admin mode is intended to withhold |
CWE-522 (Insufficiently Protected Credentials); contributing, the opt-in mitigation is never applied to the code path it is meant to govern (CWE-665-adjacent, incorrect initialisation of a security-relevant setting).
Validated evidence
Run at 7e0a52a12449fc8a6ca1b108c22ffec47cbeba28. Both endpoints are real FreeRDP: the client is FreeRDP's own sfreerdp sample-client binary driven only through its public command line; the server is a small consumer of FreeRDP's public server API that lets unmodified libfreerdp run the entire TLS + CredSSP + TSCredentials exchange and then prints FreeRDP_Domain / FreeRDP_Username / FreeRDP_Password, which is exactly where nla_read_ts_credentials() (libfreerdp/core/nla.c:1426) stores the delegated values. No FreeRDP source is patched, stubbed or reimplemented.
# CASE: VULNERABLE: /restricted-admin together with a plaintext password # client: sfreerdp /v:127.0.0.1:3389 /cert:ignore +auth-only /u:admin /p:SuperSecret123! /restricted-admin [INFO][com.freerdp.core.nego] - [nego_process_negotiation_request]: RDP_NEG_REQ::flags RESTRICTED_ADMIN_MODE_REQUIRED =========== server-side credential snapshot #1 (server settings after CredSSP TSCredentials decode) =========== domainName [0] : "" userName [1] : "admin" password [2] : "SuperSecret123!" RESULT: PLAINTEXT PASSWORD RECEIVED BY THE SERVER ==============================================================
Control, same client and server, delegation explicitly turned off:
# CASE: CONTROL: credential delegation explicitly disabled (-credentials-delegation) # client: sfreerdp /v:127.0.0.1:3389 /cert:ignore +auth-only /u:admin /p:SuperSecret123! /restricted-admin -credentials-delegation =========== server-side credential snapshot #1 (server settings after CredSSP TSCredentials decode) =========== domainName [0] : "" userName [1] : "" password [2] : "" RESULT: password field is empty (credential delegation suppressed)
Snapshot #1 in each case is the decoded TSPasswordCreds. The later snapshots in the full output show the Client Info PDU re-delivering the password in both cases, per observation (1) above.
Reproduction
unzip ANT-2026-0EBDJ2HV-e2e.zip && cd ANT-2026-0EBDJ2HV docker build -t ant-2026-0ebdj2hv docker-e2e/ docker run --rm ant-2026-0ebdj2hv
No special docker run flags are needed. The image builds FreeRDP from source at ARG TARGET_COMMIT=7e0a52a12449fc8a6ca1b108c22ffec47cbeba28; pass --build-arg TARGET_COMMIT=<sha> to build the same reproducer at any other commit.
Reproducer attached as ANT-2026-0EBDJ2HV-e2e.zip.
Scope notes, stated plainly
The attacker must be the destination endpoint: a compromised host, or an on-path attacker with a certificate the client accepts. In the absence of /restricted-admin, delegating credentials over CredSSP is correct RDP behaviour, so the defect is that an opt-in mitigation is silently inert, not an unconditional leak. MS-RDPBCGR words the flag permissively ("it is acceptable for the client to send empty credentials") rather than as a requirement. The test server holds the user's NT hash in its NTLM SAM so that it can complete the handshake; that is a property of NTLM, and it matches reality — a compromised domain-member server receives the NTLM session key from the DC through pass-through authentication, and a Kerberos service holds its own service key, so a real compromised destination can decrypt authInfo the same way.
We rate this Medium; the final severity is yours to set.
Disclosure
Found by Anthropic using Claude to study the security of open-source software, validated manually by Ada Logics. Handled under Anthropic's coordinated vulnerability disclosure policy (https://www.anthropic.com/coordinated-vulnerability-disclosure, 90 days). Please credit Claude and Ada Logics in any advisory.
Text from GitHub Advisory Database
Affected versions 1
- FreeRDP, c<= 3.31.1GitHub