Martin's Blog

CVE-2026-88772: Following a NetScaler DTLS Overflow Across Two Builds

CVE-2026-88772 is a pre-authentication memory-overflow vulnerability in NetScaler ADC and NetScaler Gateway. Citrix says that it can lead to remote code execution or denial of service, requires DTLS to be enabled, and has been exploited in the wild.

The public advisory tells operators what to do: upgrade to a fixed build. It does not show the vulnerable copy or explain what changed. A watchTowr analysis fills in that gap with a detailed account of DTLS record processing, a patched binary comparison, debugger observations, and a working exploit chain.

Comparing the local NetScaler 14.1 builds independently confirms the central root-cause claim. In build 73.30, the packet engine appends a linked chain of DTLS buffers to a fixed 0x8c00-byte global scratch area without checking the cumulative length. In build 73.37, the corresponding function tracks the remaining capacity and rejects an element before the copy that would exceed it.

That is strong static confirmation of the memory-corruption primitive and of the repair. It is not a local reproduction of a crash or remote code execution. No malicious packet was sent, DTLS was not enabled in the lab, and the public exploit was not run.

What Citrix confirms, and what the binaries add

Citrix bulletin CTX697096 describes CVE-2026-88772 as a memory overflow that permits unauthenticated remote code execution and denial of service. For NetScaler 14.1, the bulletin lists builds before 14.1-73.37 as affected. It also states that DTLS must be enabled and that DTLS is enabled by default on a VPN virtual server unless it has been configured off.

The local comparison answers a narrower set of questions:

QuestionResult
Does 14.1-73.30 contain the disclosed unchecked cumulative copy?Yes
Is the copy associated with DTLS dispatch?Yes, through matching method tables and DTLS version values
Does 14.1-73.37 repair the same copy operation?Yes
Does the repair reject excess data before copying it?Yes
Was an appliance crash reproduced?No
Was instruction-pointer control or code execution reproduced?No
Is the vulnerable DTLS path exposed in the current lab configuration?No

Only builds 14.1-73.30 and 14.1-73.37 were independently inspected. Citrix’s broader affected-version range remains the authoritative product guidance; the local work does not establish when the defective logic first appeared.

The exact vendor artifacts were:

build-14.1-73.30_nc_64.tgz
  SHA-256 acd260b4382a33e8c05ba604b5b9a80bb2c38c0bf161d8ba653aa4b8118c22d6

14.1-73.30 /netscaler/nsppe
  SHA-256 6ce73d12c24d2c23e77fbc8234091dd069b610b658455a7dc18b087ee8890f85

build-14.1-73.37_nc_64.tgz
  SHA-256 e0151fd7c44c6d1fa99872a1e01912e92a0df1b611cf9c3c91fc854701293a06

14.1-73.37 /netscaler/nsppe
  SHA-256 1a667ad8cf148571a2ffe1e8b368f5017e8375ac979ed76a835075c39d441c01

Publishing the hashes matters in binary research. Function addresses and pseudocode are useful only when readers can identify the exact files from which they came.

Getting to the packet engine

The NetScaler update archives do not present nsppe as a convenient standalone file. Each archive contains a compressed system image. That system image is an ELF file with an embedded mfs section, and that section contains a UFS2 filesystem.

The extraction path was therefore:

NetScaler update archive
        |
        v
compressed ns-14.1-73.xx system image
        |
        v
ELF mfs section
        |
        v
UFS2 /netscaler/nsppe

The archives were hashed before extraction and left unchanged. The embedded UFS2 filesystems were parsed from disposable copies, and the recovered 73.30 nsppe was byte-for-byte identical to the copy already used elsewhere in the assessment.

IDA Pro 9.4 decompiled the relevant functions. The 73.30 target is at 0x1434a80; the corresponding 73.37 target is at 0x1436420. The decisive operations were then checked against objdump output rather than accepted from the decompiler alone.

That distinction is important. IDA’s generated function names, local variable names, inferred types, and structured control flow are approximations. A length load, destination update, comparison, conditional branch, and call order in the machine instructions are considerably firmer evidence.

Associating the function with DTLS

Neither target has a useful source-level name, and IDA reports no direct callers. That could be a dead end if the analysis depended on a guessed function role.

It does not. The 73.30 function pointer occurs in four method-table entries at file offsets 0x2718f30, 0x2719058, 0x2719210, and 0x27193c8. The 73.37 pointer occurs in the four corresponding entries at 0x271af30, 0x271b058, 0x271b210, and 0x271b3c8. Nearby fields contain 0xfeff and 0xfefd, the DTLS version values.

This explains the missing direct call references: the function is reached through indirect method dispatch. It also provides local component evidence that does not depend solely on the public write-up’s naming.

Method-table membership is not, on its own, proof that an unauthenticated packet reaches the vulnerable operation. The network entry and crafted-record path are established by Citrix and watchTowr’s reported runtime analysis. The method tables connect that account to the exact local binaries.

The 73.30 copy has no cumulative bound

The vulnerable function first copies a short header into the global scratch area. It then places a destination cursor immediately after the header and walks a linked buffer chain.

IDA renders the central 73.30 loop as:

sub_1B05B70(/*a1=*/ &qword_355BBC0, /*a2=*/ a1 + 1664, /*a3=*/ *(unsigned __int8 *)(a1 + 1677));
v8 = (char *)&qword_355BBC0 + *(unsigned __int8 *)(a1 + 1677);
do
{
  *(_DWORD *)(v6 + 248) = 0;
  sub_1B05B70(/*a1=*/ v8, /*a2=*/ *(_QWORD *)(v6 + 232), /*a3=*/ *(unsigned __int16 *)(v6 + 242));
  v8 += *(unsigned __int16 *)(v6 + 242);
  v6 = *(_QWORD *)(v6 + 96);
}
while ( v6 != 0 );

The copy helper is an indirect CPU-dispatched implementation with memcpy semantics. The machine code makes the relevant data flow explicit:

1434b1b  movzwl 0xf2(%r13),%edx   ; element length
1434b23  mov    0xe8(%r13),%rsi   ; element data
1434b2a  mov    %rbx,%rdi         ; destination cursor
1434b2d  call   0x1b05b70         ; copy
1434b32  movzwl 0xf2(%r13),%eax
1434b3a  add    %rax,%rbx         ; advance destination
1434b3d  mov    0x60(%r13),%r13   ; next element
1434b44  jne    0x1434b10         ; repeat

There is no comparison between the accumulated destination position and a capacity before the loop repeats. Every individual element can be a valid 16-bit length while the sum of many elements exceeds the destination.

This is the security failure. The code should enforce:

header length + sum of all linked element lengths <= destination capacity

Build 73.30 does not enforce that invariant in this path.

The 73.37 repair

The fixed function preserves the same basic operation but adds capacity accounting around it. IDA renders the first fixed loop as:

sub_1B075E0(/*a1=*/ &qword_355DBC0, /*a2=*/ a1 + 1664, /*a3=*/ *(unsigned __int8 *)(a1 + 1677));
v7 = *(unsigned __int8 *)(a1 + 1677);
v8 = 35840 - v7;
v9 = (char *)&qword_355DBC0 + v7;
while ( 1 )
{
  a3 = *(unsigned __int16 *)(v5 + 242);
  if ( v8 < (unsigned int)a3 )
    goto LABEL_1080;
  *(_DWORD *)(v5 + 248) = 0;
  sub_1B075E0(/*a1=*/ v9, /*a2=*/ *(_QWORD *)(v5 + 232), a3);
  v10 = *(unsigned __int16 *)(v5 + 242);
  v9 += v10;
  v8 -= v10;
  v5 = *(_QWORD *)(v5 + 96);
  if ( v5 == 0 )
    goto LABEL_7;
}

The corresponding instructions are even clearer:

143649d  mov    $0x8c00,%r15d     ; destination capacity
14364a3  sub    %eax,%r15d        ; account for header
14364b0  movzwl 0xf2(%r14),%edx   ; next element length
14364b8  cmp    %edx,%r15d        ; compare with remaining
14364bb  jb     0x143b953         ; reject when length is larger
14364d6  call   0x1b075e0         ; copy only after the check
14364e3  add    %rax,%rbx         ; advance destination
14364e6  sub    %eax,%r15d        ; reduce remaining capacity

The rejection block at 0x143b953 sets error value 0x32, increments error counters, and follows the function’s failure handling. The important ordering is comparison, branch, then copy. This is a repair at the same sink rather than an unrelated behavioural difference between two large binaries.

The patch can be summarised without decompiler syntax:

Stage14.1-73.3014.1-73.37
Copy initial headerYesYes
Account for header sizeDestination cursor onlySubtract from 0x8c00 remaining capacity
Load each element lengthYesYes
Check cumulative capacity before copyNoYes
Reject an element larger than the remaining capacityNoYes
Subtract each successful copyNoYes

Reviewing the watchTowr analysis

The local binaries match the most important technical details in watchTowr’s post: the two function addresses, the 0x8c00 scratch size, the missing 73.30 accounting, and the 73.37 pre-copy check.

The arithmetic in the disclosed test case is also internally consistent. The described linked data totals 173,652 bytes. Adding the 13-byte header produces 173,665 bytes, exceeding a 35,840-byte destination by 137,825 bytes.

There are several reasons to treat the post as strong research rather than a generic vulnerability announcement:

There are also useful evidentiary limits. The post does not publish hashes for the exact binaries behind its screenshots and snippets. The local comparison supplies hashes for the inspected release artifacts, but did not reproduce its debugger session.

The linked watchTowr repository deserves a separate warning. Although it is described as a detection artifact generator, the source contains 73.30-specific addresses, a return-oriented programming chain, a memory-permission change, executable payload bytes, and arbitrary-file-write behaviour. It is a weaponised, build-specific exploit, not a passive scanner.

It was inspected but not executed. It should not be used for routine exposure checking, against production appliances, or outside an explicitly authorised disposable lab.

What the evidence proves

The result is easiest to understand as three separate evidence layers.

First, the local static comparison proves that the exact 73.30 binary contains an unchecked cumulative copy capable of writing beyond the fixed destination when presented with an excessive chain. It also proves that 73.37 adds the missing check at that copy.

Second, Citrix supplies the product-level security claim: the path is pre-authentication, requires DTLS, permits RCE or denial of service, affects 14.1 builds before 73.37, and has been exploited.

Third, watchTowr supplies reported runtime evidence beyond the local work: controlled registers, instruction-pointer control, and code execution. That makes the stronger impact credible. It does not turn an unexecuted public exploit into a local reproduction.

The distinction matters because memory corruption, process crash, controlled instruction pointer, and reliable remote code execution are not interchangeable claims. Each requires additional evidence.

The local exposure question

The assessed appliance is isolated on a VirtualBox host-only network. It has a management address, but no VIP, SNIP, backend, Gateway/VPN virtual server, or application virtual server. SSL VPN is disabled.

The vulnerable code exists in the 73.30 binary, but the documented appliance configuration does not expose the required DTLS path. That is not a mitigation claim for deployed systems. It is simply the difference between binary presence and runtime reachability in this particular lab.

For operators, the practical questions are:

  1. Is the appliance running a version Citrix lists as affected?
  2. Does it have a reachable VPN virtual server?
  3. Is DTLS enabled for that virtual server?
  4. Has it been upgraded to the fixed build recommended by Citrix?

Configuration review can answer the exposure questions. It cannot safely confirm the absence of compromise, and a weaponised exploit is the wrong tool for routine validation.

Why the fixed build is the useful control

Large proprietary binaries contain countless differences between releases. A string, a crash-related diagnostic, or a new comparison near a suspected function is not enough to confirm a vulnerability.

The valuable feature of this comparison is that 73.37 changes the exact operation alleged to be unsafe. The destination, header copy, linked-element length, source pointer, next pointer, and copy helper all remain recognisable. The added state is a capacity counter, and the added branch sits immediately before the copy.

That makes the patched build a negative control. It rules out the simpler explanation that the observed difference is unrelated protocol refactoring. It also gives a precise regression property: an aggregate chain larger than 0x8c00 must be rejected before any out-of-range copy occurs.

Useful regression cases would include totals just below the limit, exactly at the limit, one byte above it, and many individually valid elements whose sum is too large. Those tests were not run here; they describe what a dynamic verification should establish on authorised disposable targets.

Conclusion

NetScaler 14.1-73.30 contains the DTLS record-chain copy described for CVE-2026-88772. It copies a header and a linked sequence of attacker-influenced buffers into a fixed 0x8c00 global area while advancing the destination without a cumulative bounds check.

NetScaler 14.1-73.37 repairs that root cause. It subtracts the header from the available capacity, checks every element before copying it, and rejects input that would exceed the remaining space.

That is enough to confirm the vulnerable primitive and the sink-level fix in the two inspected builds. It is not a local RCE demonstration, and it does not show that 73.37 is free from unrelated DTLS defects. Citrix’s bulletin remains the authority for the affected product range and upgrade guidance; watchTowr’s debugger work supplies the runtime exploitation evidence.

The practical conclusion is less subtle than the evidentiary one: appliances meeting the DTLS prerequisite should be upgraded according to CTX697096. The public exploit is not required to reach that decision, and running it is a poor substitute for version and configuration review.

#NetScaler #CVE-2026-88772 #DTLS #Memory Corruption #Reverse Engineering #Incident Response