Fix for RDP asyncio timeout#1211
Conversation
|
Sounds good, but isn't that something we should fix in aardwolf? Also, why do we always establish and tear down a connection in each call anyway? Can't we just establish the conn once and terminate it once the protocol finishes? |
I also have a PR for Aardwolf, but we should have this anyway - we should be terminating connections after we are done with them. I have a second PR for exactly what you are suggesting as well, but that requires more work, and I know you prefer smaller PRs that do specific things (I do as well, but I get carried away sometimes lol). This PR is to fix a massive issue that people run into all the time, and then I'll have another PR after this is approved to refactor the RDP connection lifecycle to properly terminate connections via the connection object itself, since the RDP protocol does it a bit janky right now. |
Nice 👍 can you cross ref it?
Okok, appreciate that 😄 fyi, a while ago I added |
|
Nice well done 🫶 |
|
The corresponding aardwolf PR: skelsec/aardwolf#44 |

Description
Fixes an asyncio deadlock in the RDP protocol that causes
nxc rdpto hang indefinitely on hosts with misconfigured RDS Session Host deployments (expired licensing grace period, Connection Broker rejection). When the server sends an unexpected PDU instead ofDEMANDACTIVEPDU, aardwolf's__handle_mandatory_capability_exchangeblocks forever onMCS.out_queue.get(). NXC's outerasyncio.wait_foreventually fires, but cancelling theconnect()coroutine triggers an aardwolf self-deadlock in the__x224_reader_taskcleanup path, preventing the process from exiting. In a multi-host scan, a single bad host blocks every subsequent host on the same thread.This PR fixes the NXC-side cleanup so failed/cancelled connections can always terminate cleanly, regardless of what aardwolf is doing internally:
terminate_conn()helper usesasyncio.wait_for(..., timeout=self.args.rdp_timeout)so cleanup itself can't hang. Callingterminate()explicitly sets aardwolf's__terminate_calledflag beforeasyncio.run()cancels the reader task, breaking the cleanup deadlock chain (reference: aardwolfconnection.py:184-185).connect_rdp_with_cleanup()wrapsconnect_rdp()with afinallyblock that always callsterminate_conn(). Sync login paths (create_conn_obj,plaintext_login,hash_login,kerberos_login) use this so the event loop can cleanly tear down beforeasyncio.run()exits.screen,nla_screen,execute_shell) continue to useconnect_rdp()and handle cleanup in their ownfinallyblocks viaterminate_conn().screen()to always clean up — previously it had nofinallyblock, only returning early on exception.nla_screen()protocol fallback: the original codereturned after the first protocol failure; now itcontinues to try the next protocol, matching the clear intent of thefor proto in self.protoflags_nlaloop.execute_shell()cleanup: the originalawait self.conn.terminate()had no timeout and could hang on the same aardwolf bug.Fixes #1169. A complementary upstream aardwolf PR (skelsec/aardwolf#43) addresses the root cause at the library level (
__handle_mandatory_capability_exchangetimeout +SET_ERROR_INFO_PDUhandling), but this NXC-side change provides defense-in-depth and mitigates the hang even against stock aardwolf.AI assistance disclosure: this PR was produced with Claude Code (Anthropic Claude Opus 4.7) in the following ways: root cause investigation, debugging with network capture and protocol analysis, code authoring, and end-to-end test validation against live lab hosts (Server 2016 / Server 2019 / Server 2022). All changes were manually reviewed by me and verified end-to-end with manual NXC runs before submission.
Type of change
Setup guide for the review
Client: Python 3.13, Kali Linux, aardwolf 0.2.13 (stock upstream).
Reproducing the bug on
main:RDS-RD-Server(Session Host) role installed via Server Manager's RDS Deployment Wizard, OR install the role alone and let the 120-day licensing grace period expire without configuring a license server. Either setup produces a server that accepts TCP/CredSSP but rejects the subsequent session via Connection Broker (ERRINFO_CB_CONNECTION_CANCELLED = 0x1192).nxc rdp <ip> -u <valid_user> -p <valid_pass>— the process hangs indefinitely. Multi-host scans with this host in the target list will stall at this target.Verifying the fix:
nxc rdp <broken_host> -u <user> -p <pass>— should fail within--rdp-timeoutseconds (5 by default) and exit cleanly.nxc rdp <broken_host> <good_host1> <good_host2> -u <user> -p <pass>— the broken host fails fast and the good hosts still succeed withPwn3d!.nxc rdp <good_host> -u <user> -p <pass> --screenshot— still works; PNG saved to~/.nxc/screenshots/.nxc rdp <good_host> -u <user> -p <pass> -x whoami— still works; returns output via clipboard channel.Tested against:
FilterAdministratorToken=1Screenshots (if appropriate):
Before:

After:

Checklist:
tests/e2e_commands.txtfile if necessary (new modules or features are required to be added to the e2e tests)