{"schema":"threatline.record/2","record":{"id":"GHSA-qq23-mqmv-pc65","aliases":["GHSA-qq23-mqmv-pc65"],"summary":"FreeRDP client accepts a negotiated security protocol outside its enabled set (PROTOCOL_RDSTLS, PROTOCOL_HYBRID_EX)","details":"This issue was found by Anthropic using Claude to study the security of open-source software, validated manually by me from\r\nAda Logics.\r\n\r\nCWE-757 (Selection of Less-Secure Algorithm During Negotiation), contributing CWE-522\r\n(Insufficiently Protected Credentials) and CWE-1390 (Weak Authentication).\r\n\r\n### Relationship to GHSA-x7v6-xfx3-52j6 (distinct; not fixed by it)\r\n\r\nThis is not the \"unintended RDSTLS\" issue fixed in 3.31.0. GHSA-x7v6-xfx3-52j6 is a **server-side**\r\nbug in `rdp_server_accept_nego()`: on a negotiation failure the server stores\r\n`PROTOCOL_FAILED_NEGO (0x80000000) | HYBRID_REQUIRED_BY_SERVER (0x5)` in `SelectedProtocol`, and the\r\nvalue `0x80000005` happens to contain the `PROTOCOL_RDSTLS` bit (0x4), so a failure was dispatched as\r\nan RDSTLS selection. Its fix adds a terminal-failure guard\r\n(`if (SelectedProtocol & PROTOCOL_FAILED_NEGO) return FALSE`) — it does **not** add any check that a\r\nnegotiated protocol is in the client's enabled set.\r\n\r\nThis finding is on the **client** side and is a different root cause: `nego_process_negotiation_response()`\r\naccepts whatever `selectedProtocol` the peer puts in a well-formed `RDP_NEG_RSP` (here `PROTOCOL_RDSTLS`\r\n0x4 or `PROTOCOL_HYBRID_EX` 0x8) without ever comparing it against `nego->EnabledProtocols[]`. Because\r\nthe 3.31.0 fix never touched this path, the downgrade still reproduces at current master (see Evidence):\r\na client started with `/sec:nla` (only `PROTOCOL_HYBRID` enabled) proceeds into the RDSTLS/HYBRID_EX\r\npath a hostile peer selects.\r\n\r\n### Summary\r\n\r\nWhen the client parses the server's X.224 negotiation response, the selected protocol is checked\r\nagainst `nego->EnabledProtocols[]` only for `PROTOCOL_RDSAAD`, `PROTOCOL_HYBRID`, `PROTOCOL_SSL`\r\nand (in the `else` branch) `PROTOCOL_RDP`. `PROTOCOL_RDSTLS` (0x04) and `PROTOCOL_HYBRID_EX` (0x08)\r\nare not compared against the enabled set, and `nego_set_selected_protocol()` does not validate\r\neither, so a peer can select a protocol the client neither requested nor enabled.\r\n\r\nA client started with `/sec:nla` has `EnabledProtocols[] == { PROTOCOL_HYBRID }` while\r\n`NegotiateSecurityLayer` remains TRUE, so the response is still parsed and honoured. A peer that\r\nanswers `selectedProtocol = PROTOCOL_RDSTLS`:\r\n\r\n- passes the ungated branch and stays in `NEGO_STATE_FINAL`;\r\n- has `nego_update_settings_from_state()` (nego.c:1701-1717) rewrite the client's policy to\r\n  `TlsSecurity=TRUE`, `NlaSecurity=FALSE`, `RdstlsSecurity=TRUE`, `EncryptionLevel=NONE` — the\r\n  user's NLA pinning is discarded;\r\n- is reached via `nego_try_connect()` (nego.c:342) → `transport_connect_rdstls()`;\r\n- receives an RDSTLS authentication request that carries the pre-allocated all-zero\r\n  `ServerAutoReconnectCookie` (`rdstls_write_authentication_request_with_cookie`, rdstls.c:449-453,\r\n  `sessionId=0`, 28 zero bytes), so nothing has to be guessed;\r\n- replies `RDSTLS_RESULT_SUCCESS`, which the client accepts (`rdstls.c:1009`);\r\n- then receives the Client Info PDU, which for a TLS-protected non-CredSSP session carries\r\n  `settings->Password` in cleartext (`rdp_write_info_packet`, `libfreerdp/core/info.c`).\r\n\r\nThe same downgrade attempted with `PROTOCOL_SSL` is refused by the client, because that value is one\r\nof the four the block does gate. `PROTOCOL_HYBRID_EX` is the second ungated value; it routes to\r\n`transport_connect_nla(transport, TRUE)` and so remains CredSSP — policy-relevant rather than\r\ncredential-disclosing, but the same missing check.\r\n\r\n### Primitive\r\n\r\nAttacker-controlled input: the 4-byte `selectedProtocol` field of the X.224 Connection Confirm, i.e.\r\nthe first server-to-client packet. It influences a branch (`nego->SelectedProtocol` → security-layer\r\ndispatch) and the client's whole security policy. Result: a client-configured security requirement\r\nis overridden by the peer, and the user's reusable password is disclosed in cleartext to that peer.\r\n\r\n### Preconditions and scope caveats\r\n\r\n- The attacker must be able to answer the client's negotiation: an on-path position, a hostile\r\n  RD Gateway/proxy, or a server the user was induced to connect to.\r\n- The attacker's TLS certificate must be accepted by the client — `/cert:ignore`, a chain the client\r\n  trusts, or a user who clicks through the TOFU / changed-certificate prompt. Certificate validation\r\n  runs before the RDSTLS handshake, so this is the effective barrier.\r\n- Affects clients that pin `/sec:nla` (or otherwise leave a single protocol enabled) without also\r\n  passing `-nego`.\r\n- The RDSTLS authentication request itself only discloses an all-zero cookie; the password is\r\n  disclosed later, in the Client Info PDU.\r\n\r\nWe rate this Medium; the final rating is yours.\r\n\r\n### Evidence (validated run at `7e0a52a` (current master, 2026-09-14))\r\n\r\n```\r\nvictim command line       : sfreerdp /v:127.0.0.1:3389 /cert:ignore /sec:nla /u:admin /p:'SuperSecret123!'\r\nvictim policy             : /sec:nla  => EnabledProtocols[] = { PROTOCOL_HYBRID } only\r\n\r\n[attacker] victim X.224 Connection Request: requestedProtocols=0x00000003\r\n[attacker] answering with selectedProtocol=0x00000004 (RDSTLS)  <-- NOT in the client's enabled set\r\n[attacker] TLS handshake COMPLETED with the victim (client proceeded into the RDSTLS path)\r\n[attacker] victim sent an RDSTLS authentication request (40 bytes):\r\n            version=0x0001 type=0x0002 dataType=0x0002\r\n            RDSTLS_DATA_AUTORECONNECT_COOKIE sessionId=0 cookieLen=28\r\n            cookie=00000000000000000000000000000000000000000000000000000000\r\n[attacker] answered RDSTLS_RESULT_SUCCESS -- the victim now believes it is authenticated\r\n\r\n=========== server-side credential snapshot #2 (server settings after Client Info PDU decode) ===========\r\n  domainName [0] : \"(null)\"\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\nNegative control, same peer answering `selectedProtocol = PROTOCOL_SSL (0x01)`:\r\n\r\n```\r\n[attacker] no TLS handshake from the victim ... client refused the offered protocol before starting TLS\r\n[ERROR][com.freerdp.core.nego] - [nego_connect]: Protocol Security Negotiation Failure\r\n[ERROR][com.freerdp.core] - [rdp_client_connect]: ERRCONNECT_SECURITY_NEGO_CONNECT_FAILED [0x0002000C]\r\n```\r\n\r\nThird case, `selectedProtocol = PROTOCOL_HYBRID_EX (0x08)`:\r\n\r\n```\r\n[attacker] TLS handshake COMPLETED with the victim (client proceeded into the HYBRID_EX path)\r\n[attacker] RESULT: client accepted selectedProtocol=0x08 although it is not in EnabledProtocols[]\r\n```\r\n\r\n### Reproducer archive (attached)\r\n\r\n[ANT-2026-D7RY7FA7-e2e.zip](https://github.com/user-attachments/files/32214377/ANT-2026-D7RY7FA7-e2e.zip)\r\n\r\nAttached as `ANT-2026-D7RY7FA7-e2e.zip`. It contains a self-contained `docker-e2e/` build context\r\n(Ubuntu 24.04, clang-20, FreeRDP built from source at `ARG TARGET_COMMIT`), an external hostile peer\r\nwritten in plain Python (no FreeRDP code), and a credential-printing server that is a consumer of\r\nFreeRDP's public server API (`listener.h`/`peer.h`) configured with `FreeRDP_TlsSecurity=TRUE`.\r\nNo FreeRDP source is patched, stubbed or reimplemented; the victim is FreeRDP's own `sfreerdp`.\r\n\r\n```\r\nunzip ANT-2026-D7RY7FA7-e2e.zip && cd ANT-2026-D7RY7FA7\r\ndocker build -t ant-d7ry7fa7 docker-e2e/\r\ndocker run --rm ant-d7ry7fa7\r\n```\r\n\r\nBuild it at another commit with `--build-arg TARGET_COMMIT=<sha>`. The run takes a few minutes and\r\nprints the three cases above in order. One note on the harness: after the RDSTLS handshake the\r\nattacker relays the victim's stream to the credential-printing server and rewrites\r\n`clientCoreData::serverSelectedProtocol` from 0x04 to 0x01 on the wire, because `gcc.c:1335` makes\r\nthat backend reject a stream whose echoed protocol differs from the one it itself negotiated. That\r\nrewrite is attacker-side bookkeeping in the relay, not a change to FreeRDP.\r\n\r\n### Credit\r\n\r\nFound by Anthropic using Claude to study the security of open-source software, validated manually by\r\nAda Logics. Please credit Claude and Ada Logics in any advisory or release note.\r\n\r\nHandled under Anthropic's coordinated vulnerability disclosure policy\r\n(https://www.anthropic.com/coordinated-vulnerability-disclosure, 90 days). Questions to\r\nsecurity-cvd@anthropic.com.\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-qq23-mqmv-pc65","source_modified_at":"2026-09-29T06:37:35Z","fetched_at":"2026-10-24T20:40:58.529015Z","package":{"ecosystem":"c","name":"FreeRDP"},"range":"<= 3.31.0"}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-qq23-mqmv-pc65","source_modified_at":"2026-09-29T06:37:35Z","fetched_at":"2026-10-24T20:40:58.529015Z","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:35Z","modified_at":"2026-09-29T06:37:35Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-qq23-mqmv-pc65","fetched_at":"2026-10-24T20:40:58.529015Z","source_modified_at":"2026-09-29T06:37:35Z"}],"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:35Z","content_hash":"a4852a445068d37f904c1badfa0f8ee45133c24b299727619e8199aa18b033e4"}}
