Back to Feed
VulnerabilitiesMay 8, 2026

CVE-2025-68670: discovering an RCE vulnerability in xrdp

CVE-2025-68670: Pre-auth RCE in xrdp server via buffer overflow in UTF-16 conversion.

Summary

Kaspersky researchers discovered CVE-2025-68670, a pre-authentication remote code execution vulnerability in the xrdp remote desktop server component used by Kaspersky USB Redirector. The vulnerability exists in the UTF-16 to UTF-8 conversion logic handling client credentials during the Secure Settings Exchange phase of RDP connection setup. xrdp maintainers promptly patched the issue in version 0.10.5 and backported fixes to earlier versions.

Full text

Table of Contents Client data transmission via RDPCVE-2025-68670: an RCE vulnerability in xrdpPoCProtection against vulnerability exploitationVulnerability remediation timelineConclusion Authors Denis Skvortsov Dmitry Shmoylov In addition to KasperskyOS-powered solutions, Kaspersky offers various utility software to streamline business operations. For instance, users of Kaspersky Thin Client, an operating system for thin clients, can also purchase Kaspersky USB Redirector, a module that expands the capabilities of the xrdp remote desktop server for Linux. This module enables access to local USB devices, such as flash drives, tokens, smart cards, and printers, within a remote desktop session – all while maintaining connection security. We take the security of our products seriously and regularly conduct security assessments. Kaspersky USB Redirector is no exception. Last year, during a security audit of this tool, we discovered a remote code execution vulnerability in the xrdp server, which was assigned the identifier CVE-2025-68670. We reported our findings to the project maintainers, who responded quickly: they fixed the vulnerability in version 0.10.5, backported the patch to versions 0.9.27 and 0.10.4.1, and issued a security bulletin. This post breaks down the details of CVE-2025-68670 and provides recommendations for staying protected. Client data transmission via RDP Establishing an RDP connection is a complex, multi-stage process where the client and server exchange various settings. In the context of the vulnerability we discovered, we are specifically interested in the Secure Settings Exchange, which occurs immediately before client authentication. At this stage, the client sends protected credentials to the server within a Client Info PDU (protocol data unit with client info): username, password, auto-reconnect cookies, and so on. These data points are bundled into a TS_INFO_PACKET structure and can be represented as Unicode strings up to 512 bytes long, the last of which must be a null terminator. In the xrdp code, this corresponds to the xrdp_client_info structure, which looks as follows: C { [..SNIP..] char username[INFO_CLIENT_MAX_CB_LEN]; char password[INFO_CLIENT_MAX_CB_LEN]; char domain[INFO_CLIENT_MAX_CB_LEN]; char program[INFO_CLIENT_MAX_CB_LEN]; char directory[INFO_CLIENT_MAX_CB_LEN]; [..SNIP..] } 123456789 {[..SNIP..]char username[INFO_CLIENT_MAX_CB_LEN];char password[INFO_CLIENT_MAX_CB_LEN];char domain[INFO_CLIENT_MAX_CB_LEN];char program[INFO_CLIENT_MAX_CB_LEN];char directory[INFO_CLIENT_MAX_CB_LEN];[..SNIP..]} The value of the INFO_CLIENT_MAX_CB_LEN constant corresponds to the maximum string length and is defined as follows: C #define INFO_CLIENT_MAX_CB_LEN 512 1 #define INFO_CLIENT_MAX_CB_LEN 512 When transmitting Unicode data, the client uses the UTF-16 encoding. However, the server converts the data to UTF-8 before saving it. C if (ts_info_utf16_in( // [1] s, len_domain, self->rdp_layer->client_info.domain, sizeof(self->rdp_layer->client_info.domain)) != 0) // [2] { [..SNIP..] } 12345 if (ts_info_utf16_in( // [1] s, len_domain, self->rdp_layer->client_info.domain, sizeof(self->rdp_layer->client_info.domain)) != 0) // [2]{[..SNIP..]} The size of the buffer for unpacking the domain name in UTF-8 [2] is passed to the ts_info_utf16_in function [1], which implements buffer overflow protection [3]. C static int ts_info_utf16_in(struct stream *s, int src_bytes, char *dst, int dst_len) { int rv = 0; LOG_DEVEL(LOG_LEVEL_TRACE, "ts_info_utf16_in: uni_len %d, dst_len %d", src_bytes, dst_len); if (!s_check_rem_and_log(s, src_bytes + 2, "ts_info_utf16_in")) { rv = 1; } else { int term; int num_chars = in_utf16_le_fixed_as_utf8(s, src_bytes / 2, dst, dst_len); if (num_chars > dst_len) // [3] { LOG(LOG_LEVEL_ERROR, "ts_info_utf16_in: output buffer overflow"); rv = 1; } / / String should be null-terminated. We haven't read the terminator yet in_uint16_le(s, term); if (term != 0) { LOG(LOG_LEVEL_ERROR, "ts_info_utf16_in: bad terminator. Expected 0, got %d", term); rv = 1; } } return rv; } 123456789101112131415161718192021222324252627 static int ts_info_utf16_in(struct stream *s, int src_bytes, char *dst, int dst_len){ int rv = 0; LOG_DEVEL(LOG_LEVEL_TRACE, "ts_info_utf16_in: uni_len %d, dst_len %d", src_bytes, dst_len); if (!s_check_rem_and_log(s, src_bytes + 2, "ts_info_utf16_in")) { rv = 1; } else { int term; int num_chars = in_utf16_le_fixed_as_utf8(s, src_bytes / 2, dst, dst_len); if (num_chars > dst_len) // [3] { LOG(LOG_LEVEL_ERROR, "ts_info_utf16_in: output buffer overflow"); rv = 1; } / / String should be null-terminated. We haven't read the terminator yet in_uint16_le(s, term); if (term != 0) { LOG(LOG_LEVEL_ERROR, "ts_info_utf16_in: bad terminator. Expected 0, got %d", term); rv = 1; } } return rv;} Next, the in_utf16_le_fixed_as_utf8_proc function, where the actual data conversion from UTF-16 to UTF-8 takes place, checks the number of bytes written [4] as well as whether the string is null-terminated [5]. C { unsigned int rv = 0; char32_t c32; char u8str[MAXLEN_UTF8_CHAR]; unsigned int u8len; char *saved_s_end = s->end; // Expansion of S_CHECK_REM(s, n*2) using passed-in file and line #ifdef USE_DEVEL_STREAMCHECK parser_stream_overflow_check(s, n * 2, 0, file, line); #endif // Temporarily set the stream end pointer to allow us to use // s_check_rem() when reading in UTF-16 words if (s->end - s->p > (int)(n * 2)) { s->end = s->p + (int)(n * 2); } while (s_check_rem(s, 2)) { c32 = get_c32_from_stream(s); u8len = utf_char32_to_utf8(c32, u8str); if (u8len + 1 <= vn) // [4] { /* Room for this character and a terminator. Add the character */ unsigned int i; for (i = 0 ; i < u8len ; ++i) { v[i] = u8str[i]; } v n -= u8len; v += u8len; } else if (vn > 1) { /* We've skipped a character, but there's more than one byte * remaining in the output buffer. Mark the output buffer as * full so we don't get a smaller character being squeezed into * the remaining space */ vn = 1; } r v += u8len; } // Restore stream to full length s->end = saved_s_end; if (vn > 0) { *v = '\0'; // [5] } + +rv; return rv; } 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152 { unsigned int rv = 0; char32_t c32; char u8str[MAXLEN_UTF8_CHAR]; unsigned int u8len; char *saved_s_end = s->end; // Expansion of S_CHECK_REM(s, n*2) using passed-in file and line #ifdef USE_DEVEL_STREAMCHECK parser_stream_overflow_check(s, n * 2, 0, file, line); #endif // Temporarily set the stream end pointer to allow us to use // s_check_rem() when reading in UTF-16 words if (s->end - s->p > (int)(n * 2)) { s->end = s->p + (int)(n * 2); } while (s_check_rem(s, 2)) { c32 = get_c32_from_stream(s); u8len = utf_char32_to_utf8(c32, u8str); if (u8len + 1 <= vn) // [4] { /* Room for this character and a terminator. Add the character */ unsigned int i; for (i = 0 ; i < u8len ; ++i) { v[i] = u8str[i]; } v n -= u8len; v += u8len; } else if (vn > 1) { /* We've skipped a character, but there's more than one byte * remaining in the output buffer. Mark the output buffer as * full so we don't get a smaller character being squeezed into * the remaining space */ vn = 1; } r v += u8len; } // Restore stream to full length s->end = saved_s_end; if (vn > 0) { *v = '\0'; // [5] } + +rv; return rv;} Consequently, up to 512 bytes of input data in UTF-16 are converted into UTF-8 data, which can also reach a size of up to 512 bytes. CVE-2025-68670: an RCE vulnerability in xrdp The vulnerability exists within the xrdp_wm_parse_domain_information function, which processes the domain name saved on the server in UTF-8. Like the functions described above, this one is called before client authentication, meaning exploitation does not require valid credentials. The call stack below illustrates this. C x rdp_wm_parse_domain_information(char *originalDomainInfo, int comboMax, int decode, char *resultBuffer) xrdp_login_wnd_create(struct xrdp_wm *self) xrdp_wm_init(struct xrdp_wm *self

Indicators of Compromise

  • cve — CVE-2025-68670

Entities

Kaspersky (vendor)Kaspersky USB Redirector (product)xrdp (product)RDP (Remote Desktop Protocol) (technology)