Skip to content
KitploitKITPLOIT
ToolsExploitsBlog
Log in
Submit
ToolsExploitsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

FeedsContactPrivacy© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2026-88776 — Technical write-up of CVE-2026-88776, a pre-auth integer underflow in NetScaler nsppe's Oracle TNS record parser causing an out-of-bounds write and packet engine crash. | Kitploit
Tools/GitHubGitHub/scyrix-llc/cve-2026-88776
Memory ForensicsVulnerability AnalysisExploitationReverse EngineeringNetwork SecurityBinary AnalysisPapers & Research
GitHubscyrix-llc/cve-2026-88776

CVE-2026-88776

Technical write-up of CVE-2026-88776, a pre-auth integer underflow in NetScaler nsppe's Oracle TNS record parser causing an out-of-bounds write and packet engine crash.

View Repository
21127 days agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

CVE-2026-88776: NetScaler nsppe (Pre-Auth OOB Write)

Integer underflow in the DataStream / Oracle TNS record parser

Summary

NetScaler can load-balance Oracle databases. Point an ORACLE-type vserver at it and the packet engine starts speaking the Oracle TNS protocol itself, parsing client traffic with a built-in handler. That handler keeps a small state machine per connection so it remembers where it is inside a TNS record.

One of those states means "I'm in the middle of a record, more bytes are coming." When the next segment arrives, the handler reads its length and subtracts 10, because 10 bytes of every segment are header.

Nobody ever asked what happens if the segment is shorter than 10 bytes.

Send it one. A 1-to-9-byte runt segment walks into that state, the length goes negative, and because the length lives in an unsigned 16-bit register it doesn't go negative. It wraps to something enormous, up to roughly 64 KB. The handler then happily memcpy's that much data out of your packet into a fixed 0x748-byte scratch buffer in .bss. The engine isn't PIE, so the address being trampled never moves.

You don't have to log in. You don't even need a database behind the vserver, because the appliance answers the TNS handshake itself. The whole attack is one TCP conversation.

Both halves were demonstrated live:

  • Fire the wrapped length and the packet engine dies with SIGSEGV, taking the appliance's dataplane down for about a minute. Deterministic, repeatable, loopable.
  • Tune the length and you get the quiet version. Attacker bytes land over the globals sitting next to the scratch buffer and the engine keeps running. That is a write primitive, not just a crash.

Root cause

The state machine, and how you steer it

The handler stores its per-connection state at [pcb+0x15a]. Most states are boring. The one that matters here is 0x1F4, which means "mid-record, awaiting continuation," and you steer a connection into it using nothing but ordinary protocol bytes:

record type 6 -> deep-parser state 6 -> byte 0x03 (TTIFUN)
  -> state 0x126 -> TTC function byte 0x5e (OALL8)
  -> jump table 0x23589e0, index 0x59 -> writer at bfe27b:
       [pcb+0x15a] = 0x1F4

The next packet on that connection then dispatches on 0x1F4, through bd49a7 into bd5362, and lands here:

bd5432: memset(0x30a7e60, 0, 0x748)    ; scratch region (ends 0x30a85a8)
bd5445: mov   rcx,[rbp-0x208]          ; inbound packet buffer
bd544c: movzx eax,WORD [rcx+0xf2]      ; u16 wire length of the segment
bd5453: add   eax,0xfffffff6           ; len = len - 10   <== THE UNDERFLOW
bd5456: mov   WORD [0x30a8476],ax      ; wrapped length PERSISTED to .bss
bd545d: movzx edx,ax                   ; memcpy length (1..9 -> 0xfff7..0xffff)
bd5460: mov   rsi,[rcx+0xe8]
bd5467: add   rsi,0xa                  ; src = payload + 10
bd546b: mov   edi,0x30a8075            ; dst = FIXED .bss global
bd5470: call  1b075e0                  ; memcpy: up to ~64 KB OOB write
bd5475: mov   BYTE [0x30a8478],1

Read it like a sentence. Take the segment's length. Subtract 10. Store the result, hand it to memcpy, done. At no point does anything ask whether "length minus 10" is still a number that makes sense.

The guard that isn't there

We counted every reference to that length field in this function. Thirty of them. Exactly one is a check, at bd4083, and all it does is compare against zero. A zero-length segment gets rejected. Every other length, including 1 through 9, walks straight into the subtraction.

The code clearly believes the state machine will have delivered a complete-enough record by the time state 0x1F4 runs. The state machine never promised any such thing. A runt segment arriving as its own packet satisfies every dispatch condition and shows up five bytes short.

The truly aggravating part is that the vendor knows this bug class. Elsewhere in the very same binary, sub_119c940 does the identical length-minus-constant arithmetic and gates it with cmp eax,0x3f9 / ja. The safe pattern was sitting right there. This site just didn't use it.

What you hit when it wraps

The destination, 0x30a8075, lives inside a 0x748-byte scratch region with only 0x533 bytes of legitimate space in front of it. The wrapped copy blows past that into the DataStream and negotiation globals next door, with your bytes. And the wrapped length itself is persisted to 0x30a8476, where later code reads it back and trusts it. That is a second corruption lane, one step removed from the memcpy.


The trigger

The setup is one line of configuration, a standard DataStream deployment:

add lb vserver v ORACLE <ip> 1521

No backend needed. The appliance terminates TNS itself and builds its own Accept packet, "Netscaler" vendor string and all.

The conversation runs four steps:

1. TNS Connect (version 0x13A)
     <- the appliance answers Resend(11), then Accept(2), 32 bytes, v0x139

2. one complete, benign TTC round-trip
     (some builds like to glue your runt onto the previous segment;
      this resets the segmentation so the runt lands alone)

3. the arming segment: a TNS Data packet whose declared length is
     actual + 32, carrying TTC bytes {03 5E} (TTIFUN / OALL8)
     -> the state machine latches [pcb+0x15a] = 0x1F4, mid-record

4. the runt: 1-9 bytes, sent as its own TCP segment
     -> passes the zero check, dispatches on 0x1F4
     -> length - 10 wraps
     -> memcpy(0x30a8075, payload+10, ~0xFFF6)
     -> the packet engine dies

Steps 1 through 3 are pure conversation. Step 4 is five bytes.


Observed crash behavior

This has been fired twice on a 14.1-73.30 appliance, same box, same build, two independent runs. Both times the packet engine took SIGSEGV with a matching core dump, ns.log recorded signal 11 inside the window, and the dataplane sat down for the ~60-second warm restart.

The negative control passed both runs. Run the identical conversation but make the final segment a coherent 10 bytes and nothing happens. The engine processes it and carries on. The runt length is the only variable that matters.

Download Tool