{"schema":"threatline.record/2","record":{"id":"GHSA-jhxw-3hj9-9hqh","aliases":["GHSA-jhxw-3hj9-9hqh"],"summary":" `/restricted-admin` does not suppress credential delegation — the client sends the cleartext password to the RDP server","details":"This issue was found by Anthropic using Claude to study the security of open-source software, validated manually by me from Ada Logics. \r\n\r\n### Summary\r\n\r\n`/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`.\r\n\r\nMS-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.\r\n\r\n### Root cause\r\n\r\n`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`).\r\n\r\n`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:\r\n\r\n```c\r\n/* libfreerdp/core/nla.c:1758 */\r\nif (!settings->DisableCredentialsDelegation && nla->identity)\r\n{\r\n    username.len  = nla->identity->UserLength * sizeof(WCHAR);\r\n    username.data = (BYTE*)nla->identity->User;\r\n    domain.len    = nla->identity->DomainLength * sizeof(WCHAR);\r\n    domain.data   = (BYTE*)nla->identity->Domain;\r\n    password.len  = nla->identity->PasswordLength * sizeof(WCHAR);\r\n    password.data = (BYTE*)nla->identity->Password;\r\n}\r\n```\r\n\r\nTwo related observations from the same run:\r\n\r\n1. Even with `-credentials-delegation`, which does blank `TSPasswordCreds`, the password still reaches the server in the Client Info PDU: `connection.c:477-481` enables `AutoLogonEnabled` whenever `Username` and `Password` are set, and `info.c:992-1006` writes `cbPassword`/`passwordW` into the PDU. Under `/restricted-admin` both channels carry the secret.\r\n2. The client parses but never enforces the server's `RESTRICTED_ADMIN_MODE_SUPPORTED` response flag — `RESTRICTED_ADMIN_MODE_SUPPORTED` appears 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`).\r\n\r\n### Primitive shape\r\n\r\n| Aspect | Value |\r\n|---|---|\r\n| Direction | Outbound disclosure, client to server |\r\n| Data | Full `Domain` / `Username` / `Password` UTF-16 strings, unbounded length |\r\n| Attacker role | Recipient (the RDP server), not the shaper of the data |\r\n| Precondition | Victim runs a FreeRDP client with `/restricted-admin` and supplies a password (`/p:` or the interactive prompt) |\r\n| 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 |\r\n| Impact | A compromised or MITM'd destination host obtains a reusable administrative credential that restricted admin mode is intended to withhold |\r\n\r\nCWE-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).\r\n\r\n### Validated evidence\r\n\r\nRun 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.\r\n\r\n```\r\n# CASE: VULNERABLE: /restricted-admin together with a plaintext password\r\n# client: sfreerdp /v:127.0.0.1:3389 /cert:ignore +auth-only /u:admin /p:SuperSecret123! /restricted-admin\r\n[INFO][com.freerdp.core.nego] - [nego_process_negotiation_request]: RDP_NEG_REQ::flags RESTRICTED_ADMIN_MODE_REQUIRED\r\n\r\n=========== server-side credential snapshot #1 (server settings after CredSSP TSCredentials decode) ===========\r\n  domainName [0] : \"\"\r\n  userName   [1] : \"admin\"\r\n  password   [2] : \"SuperSecret123!\"\r\n  RESULT: PLAINTEXT PASSWORD RECEIVED BY THE SERVER\r\n==============================================================\r\n```\r\n\r\nControl, same client and server, delegation explicitly turned off:\r\n\r\n```\r\n# CASE: CONTROL: credential delegation explicitly disabled (-credentials-delegation)\r\n# client: sfreerdp /v:127.0.0.1:3389 /cert:ignore +auth-only /u:admin /p:SuperSecret123! /restricted-admin -credentials-delegation\r\n\r\n=========== server-side credential snapshot #1 (server settings after CredSSP TSCredentials decode) ===========\r\n  domainName [0] : \"\"\r\n  userName   [1] : \"\"\r\n  password   [2] : \"\"\r\n  RESULT: password field is empty (credential delegation suppressed)\r\n```\r\n\r\nSnapshot `#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.\r\n\r\n### Reproduction\r\n\r\n[ANT-2026-0EBDJ2HV-e2e.zip](https://github.com/user-attachments/files/32213839/ANT-2026-0EBDJ2HV-e2e.zip)\r\n\r\n```\r\nunzip ANT-2026-0EBDJ2HV-e2e.zip && cd ANT-2026-0EBDJ2HV\r\ndocker build -t ant-2026-0ebdj2hv docker-e2e/\r\ndocker run --rm ant-2026-0ebdj2hv\r\n```\r\n\r\nNo special `docker run` flags are needed. The image builds FreeRDP from source at\r\n`ARG TARGET_COMMIT=7e0a52a12449fc8a6ca1b108c22ffec47cbeba28`; pass\r\n`--build-arg TARGET_COMMIT=<sha>` to build the same reproducer at any other commit.\r\n\r\nReproducer attached as `ANT-2026-0EBDJ2HV-e2e.zip`.\r\n\r\n### Scope notes, stated plainly\r\n\r\nThe 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.\r\n\r\nWe rate this Medium; the final severity is yours to set.\r\n\r\n### Disclosure\r\n\r\nFound 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.\r\n","categories":["Unclassified"],"product":"FreeRDP","vendor":"","ecosystem":null,"affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-jhxw-3hj9-9hqh","source_modified_at":"2026-09-29T06:37:25Z","fetched_at":"2026-10-24T18:03:20.872036Z","package":{"ecosystem":"c","name":"FreeRDP"},"range":"<= 3.31.1"}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-jhxw-3hj9-9hqh","source_modified_at":"2026-09-29T06:37:25Z","fetched_at":"2026-10-24T18:03:20.872036Z","package":{"ecosystem":"c","name":"FreeRDP"},"version":"3.32.0"}],"cvss":null,"severity":"Medium","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-09-29T06:37:25Z","modified_at":"2026-09-29T06:37:25Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-jhxw-3hj9-9hqh","fetched_at":"2026-10-24T18:03:20.872036Z","source_modified_at":"2026-09-29T06:37:25Z"}],"sources":["GitHub Advisory Database"],"references":[],"locations":[],"motoko_assessment":{"status":"requires_architecture_review","automatic_remediation":false},"classification":{"method":"product-first software-layer rules","rules_version":"2.1","matched_on":null,"confidence":"unknown","reviewed":false},"change_timestamp":"2026-09-29T06:37:25Z","content_hash":"e96c136bd89df68904a435d6a6e2a36ff8b24c0e48a713fe144ec4dd3c6d0a02"}}
