{"schema":"threatline.record/2","record":{"id":"GHSA-m998-cvfm-9444","aliases":["GHSA-m998-cvfm-9444"],"summary":"FreeRDP shadow accepts host input before Confirm Active and authentication","details":"### Summary\r\n\r\nAn unauthenticated network client can negotiate TLS with `freerdp-shadow-cli`, complete setup through Client Info, receive Demand Active, and send an input PDU without sending Confirm Active. The server dispatches the input before the shadow server reaches its `PostConnect` authentication callback. With the default `mayInteract = TRUE` setting and the X11 backend built with XTEST, the event is injected into the host user's X display.\r\n\r\nThe lab run assessed current `master` at `fe8e4a68` and sent one scancode press and release in this state. `xev` on the shadowed display recorded keycode 38, keysym `a`. A normal client using the same invalid credentials completed the activation sequence and was rejected by `shadow_client_post_connect`. The attack connection never reached that authentication callback.\r\n\r\nThis is not a TLS break or credential theft. The demonstrated impact is unauthenticated host-session input, scored High at 7.5. We did not demonstrate screen disclosure, denial of service, or command execution, so the report does not claim confidentiality or availability impact and does not claim Critical severity or RCE.\r\n\r\n### Affected versions and prerequisites\r\n\r\nThe vulnerable state-machine branch was introduced before FreeRDP 3.0.0. All 48 available stable 3.x tags from 3.0.0 through 3.31.1 contain the same branch and retain the input-dispatch, shadow-authentication, XTEST, and enabled-interaction path. Current `master` is also affected. No fixed release is known.\r\n\r\nRequired conditions are:\r\n\r\n- network access to a FreeRDP shadow listener;\r\n- the server accepts TLS-only RDP negotiation;\r\n- shadow authentication is enabled but occurs in `PostConnect`;\r\n- interaction is enabled, which is the default;\r\n- the X11 shadow backend is built with XTEST for the demonstrated sink.\r\n\r\nThe default server object enables authentication and interaction. Without an explicit bind address, shadow opens the TCP listener on all interfaces. Actual deployment prevalence is unknown.\r\n\r\n### Technical details\r\n\r\nIn `libfreerdp/core/peer.c`, `peer_recv_callback_internal` handles `CONNECTION_STATE_CAPABILITIES_EXCHANGE_CONFIRM_ACTIVE` by passing any received PDU to `peer_recv_pdu`. The comment describes a reactivation exception, but the code does not distinguish reactivation from the initial unauthenticated connection:\r\n\r\n```c\r\ncase CONNECTION_STATE_CAPABILITIES_EXCHANGE_CONFIRM_ACTIVE:\r\n\t/**\r\n\t * During reactivation sequence the client might sent some input or channel data\r\n\t * before receiving the Deactivate All PDU. We need to process them as usual.\r\n\t */\r\n\tret = peer_recv_pdu(client, s);\r\n\tbreak;\r\n```\r\n\r\n`peer_recv_data_pdu` then dispatches `DATA_PDU_TYPE_INPUT` without an authentication or activation check:\r\n\r\n```c\r\ncase DATA_PDU_TYPE_INPUT:\r\n\tif (!input_recv(rdp->input, s))\r\n\t\treturn STATE_RUN_FAILED;\r\n\tbreak;\r\n```\r\n\r\n`server/shadow/shadow_input.c` checks only `client->mayInteract` before invoking the subsystem keyboard callback. The X11 implementation in `server/shadow/X11/x11_shadow.c` calls `XTestFakeKeyEvent` and flushes the display.\r\n\r\nAuthentication occurs later. `rdp_peer_handle_state_active` calls the peer's `PostConnect` callback only after the protocol state reaches `CONNECTION_STATE_ACTIVE`. `shadow_client_post_connect` then calls the subsystem authentication function and closes the peer if it fails. An initial-connection input PDU therefore reaches the X11 sink before invalid TLS-only credentials can be rejected.\r\n\r\nThe decisive Confirm Active branch entered the tree in the October 2022 state-machine rewrite, commit `33827cb9`. No current source change gates this initial input on successful shadow authentication.\r\n\r\n### Reproduction and observed result\r\n\r\nThe authorized current-`master` run used an authentication-enabled shadow server on a disposable X11 display. The client selected TLS while the server offered RDP, TLS, NLA, and EXT. The replay completed MCS and Client Info, received Demand Active, deliberately omitted Confirm Active, and sent this input packet:\r\n\r\n```text\r\n0300003102f08064000803eb70802222001700f103f1030100000110001c0000000100000000000000040000001e000000\r\n```\r\n\r\nObserved replay output:\r\n\r\n```text\r\nDemand Active; shareId=0x000103f1\r\ngot_da=True\r\nINPUT PDU sent (49 bytes): 0300003102f08064000803eb70802222001700f103f1030100000110001c0000000100000000000000040000001e000000\r\npost-input recv: The read operation timed out\r\n```\r\n\r\nObserved X11 output:\r\n\r\n```text\r\nKeyPress event, serial 14, synthetic NO, window 0x42,\r\n    state 0x0, keycode 38 (keysym 0x61, a), same_screen YES,\r\n    XLookupString gives 1 bytes: (61) \"a\"\r\nKeyRelease event, serial 17, synthetic NO, window 0x42,\r\n    state 0x0, keycode 38 (keysym 0x61, a), same_screen YES,\r\n    XLookupString gives 1 bytes: (61) \"a\"\r\n```\r\n\r\nFor the normal control, the client sent Confirm Active and completed finalization. The same invalid credentials then reached `shadow_client_post_connect`, which logged `client authentication failure: -1`, and `rdp_peer_handle_state_active` reported PostConnect failure. This establishes that the credentials were not valid and that the attack reached the host-input sink earlier than their rejection.\r\n\r\nThe sibling verifier is network-free and safe to run:\r\n\r\n```sh\r\npython3 poc/verify_evidence.py\r\n```\r\n\r\nIt parses the exact retained packet and checks the minimal negotiation, X11, and normal-authentication records. It verifies the preserved evidence; it does not recreate the live X11 run.\r\n\r\n<!-- POC_FILES_BEGIN -->\r\n### `poc/verify_evidence.py`\r\n\r\n<!-- BEGIN FILE: poc/verify_evidence.py -->\r\n```python\r\n#!/usr/bin/env python3\r\n\"\"\"Offline verifier for the preserved FreeRDP shadow input-injection evidence.\r\n\r\nThis script performs no network or X11 operations. It checks the exact input\r\npacket and the minimal execution records retained from the authorized lab run.\r\n\"\"\"\r\n\r\nimport struct\r\n\r\n\r\nPACKET_HEX = (\r\n    \"0300003102f08064000803eb70802222001700f103f1030100000110001c000000\"\r\n    \"0100000000000000040000001e000000\"\r\n)\r\n\r\nATTACK_TRANSCRIPT = \"\"\"\\\r\nDemand Active; shareId=0x000103f1\r\ngot_da=True\r\nINPUT PDU sent (49 bytes): 0300003102f08064000803eb70802222001700f103f1030100000110001c0000000100000000000000040000001e000000\r\npost-input recv: The read operation timed out\r\n\"\"\"\r\n\r\nNEGOTIATION_RECORD = \"\"\"\\\r\nClient Security: RDSTLS:0 NLA:0 EXT:0 TLS:1 RDP:0\r\nServer Security: RDSTLS:0 NLA:1 EXT:1 TLS:1 RDP:1\r\nNegotiated Security: RDSTLS:0 NLA:0 EXT:0 TLS:1 RDP:0\r\n\"\"\"\r\n\r\nXEV_RECORD = \"\"\"\\\r\nKeyPress event, serial 14, synthetic NO, window 0x42,\r\n    state 0x0, keycode 38 (keysym 0x61, a), same_screen YES,\r\n    XLookupString gives 1 bytes: (61) \"a\"\r\nKeyRelease event, serial 17, synthetic NO, window 0x42,\r\n    state 0x0, keycode 38 (keysym 0x61, a), same_screen YES,\r\n    XLookupString gives 1 bytes: (61) \"a\"\r\n\"\"\"\r\n\r\nNORMAL_AUTH_CONTROL = \"\"\"\\\r\nshadow_client_post_connect: Client from 127.0.0.1 is activated (800x600@32)\r\nshadow_client_post_connect: client authentication failure: -1\r\nrdp_peer_handle_state_active: PostConnect for peer failed\r\n\"\"\"\r\n\r\n# The inspected replay sent these application-level steps in this order. The\r\n# finalization sequence, including Confirm Active, was deliberately omitted.\r\nREPLAY_STEPS = (\r\n    \"X.224 TLS-only negotiation\",\r\n    \"MCS setup and Client Info\",\r\n    \"receive Demand Active\",\r\n    \"send DATA_PDU_TYPE_INPUT\",\r\n)\r\n\r\n\r\ndef require(condition, message):\r\n    if not condition:\r\n        raise AssertionError(message)\r\n\r\n\r\ndef u16le(data, offset):\r\n    return struct.unpack_from(\"<H\", data, offset)[0]\r\n\r\n\r\ndef u32le(data, offset):\r\n    return struct.unpack_from(\"<I\", data, offset)[0]\r\n\r\n\r\ndef main():\r\n    packet = bytes.fromhex(PACKET_HEX)\r\n\r\n    require(packet[:2] == b\"\\x03\\x00\", \"not a TPKT packet\")\r\n    require(struct.unpack_from(\">H\", packet, 2)[0] == len(packet) == 49,\r\n            \"TPKT length mismatch\")\r\n    require(packet[4:7] == b\"\\x02\\xf0\\x80\", \"not X.224 data\")\r\n    require(packet[7] == 0x64, \"not an MCS SendDataRequest\")\r\n    require(struct.unpack_from(\">H\", packet, 10)[0] == 1003,\r\n            \"unexpected MCS channel\")\r\n    require(u16le(packet, 15) == 34, \"unexpected Share Control length\")\r\n    require((u16le(packet, 17) & 0x0F) == 0x07,\r\n            \"not a Share Control Data PDU\")\r\n    require(u32le(packet, 21) == 0x000103F1, \"Share ID mismatch\")\r\n    require(packet[29] == 0x1C, \"not DATA_PDU_TYPE_INPUT\")\r\n    require(u16le(packet, 33) == 1, \"unexpected input-event count\")\r\n    require(u16le(packet, 41) == 0x0004, \"not a scancode event\")\r\n    require(u16le(packet, 43) == 0, \"unexpected keyboard flags\")\r\n    require(u16le(packet, 45) == 0x001E, \"unexpected scancode\")\r\n    require(\"got_da=True\" in ATTACK_TRANSCRIPT, \"Demand Active was not recorded\")\r\n    require(\"Confirm Active\" not in REPLAY_STEPS,\r\n            \"the replay sequence unexpectedly contains Confirm Active\")\r\n\r\n    require(\"Server Security: RDSTLS:0 NLA:1 EXT:1 TLS:1 RDP:1\"\r\n            in NEGOTIATION_RECORD, \"server defaults are missing\")\r\n    require(\"Negotiated Security: RDSTLS:0 NLA:0 EXT:0 TLS:1 RDP:0\"\r\n            in NEGOTIATION_RECORD, \"TLS-only result is missing\")\r\n    require(\"KeyPress event\" in XEV_RECORD and \"KeyRelease event\" in XEV_RECORD,\r\n            \"X11 press/release record is incomplete\")\r\n    require(\"keycode 38 (keysym 0x61, a)\" in XEV_RECORD,\r\n            \"X11 key identity is missing\")\r\n    require(\"client authentication failure: -1\" in NORMAL_AUTH_CONTROL,\r\n            \"normal authentication control is missing\")\r\n    require(\"PostConnect for peer failed\" in NORMAL_AUTH_CONTROL,\r\n            \"normal control did not reach the PostConnect rejection\")\r\n\r\n    print(\"PASS: exact 49-byte packet is a scancode DATA_PDU_TYPE_INPUT PDU\")\r\n    print(\"PASS: replay reached Demand Active without a Confirm Active step\")\r\n    print(\"PASS: server offered NLA/EXT/TLS but the client selected TLS only\")\r\n    print(\"PASS: X11 recorded keycode 38 press and release\")\r\n    print(\"PASS: normal completed handshake reached PostConnect and rejected the credentials\")\r\n    print(\"PASS: preserved evidence supports pre-auth host-session input injection\")\r\n\r\n\r\nif __name__ == \"__main__\":\r\n    main()\r\n```\r\n<!-- END FILE: poc/verify_evidence.py -->\r\n<!-- POC_FILES_END -->\r\n\r\n### Impact\r\n\r\nThe demonstrated impact is unauthenticated modification of the host user's interactive session through synthetic keyboard input. The test proves high integrity impact under the stated configuration. It does not prove that the attacker can observe the desktop, crash the service, or reliably execute a command in every desktop environment.\r\n\r\nCVSS v3.1: `AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N` = 7.5, High.\r\n\r\n### Suggested remediation\r\n\r\nReject input during the initial `CONNECTION_STATE_CAPABILITIES_EXCHANGE_CONFIRM_ACTIVE` state. The existing reactivation exception should apply only when the same peer is already connected and authenticated. Add a second shadow-layer guard that discards every input callback until authentication succeeds.\r\n\r\nCover slow-path and fast-path keyboard, mouse, extended mouse, Unicode keyboard, and synchronize input. Regression tests should show that input before Confirm Active and before successful PostConnect produces no subsystem callback, while input after a normal authenticated activation and during authenticated reactivation still works.\r\n\r\nUntil a fix is available, operators can disable interaction, restrict the listener to trusted interfaces and clients, or require a security mode that authenticates before MCS setup.\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-m998-cvfm-9444","source_modified_at":"2026-09-30T21:44:59Z","fetched_at":"2026-10-27T04:51:10.072820Z","package":{"ecosystem":"c","name":"FreeRDP"},"range":">= 3.0.0, <= 3.31.1"}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-m998-cvfm-9444","source_modified_at":"2026-09-30T21:44:59Z","fetched_at":"2026-10-27T04:51:10.072820Z","package":{"ecosystem":"c","name":"FreeRDP"},"version":"3.32.0"}],"cvss":7.5,"severity":"High","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-09-30T21:44:59Z","modified_at":"2026-09-30T21:44:59Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-m998-cvfm-9444","fetched_at":"2026-10-27T04:51:10.072820Z","source_modified_at":"2026-09-30T21:44:59Z"}],"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-30T21:44:59Z","content_hash":"f44398303d7b829bf5f534a72874679181b75d23fe77c62ad1acc314c1ef43b1"}}
