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-88773 — Technical analysis of a NetScaler nsppe HTTP/1.1 chunk-size integer-wrap request smuggling flaw, with disassembly, wire-level PoC flows, and firmware patch diff. | Kitploit
Tools/GitHubGitHub/scyrix-llc/cve-2026-88773
Vulnerability AnalysisExploitationReverse EngineeringWeb Application ExploitationWeb SecurityBinary AnalysisPapers & ResearchFirmware Analysis
GitHubscyrix-llc/cve-2026-88773

CVE-2026-88773

Technical analysis of a NetScaler nsppe HTTP/1.1 chunk-size integer-wrap request smuggling flaw, with disassembly, wire-level PoC flows, and firmware patch diff.

111 month agoNot yet reviewed
View Repository
Website

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-88773: NetScaler nsppe (HTTP Smuggle)

32-bit chunk-size accumulator wrap in the HTTP/1.1 parser

TL;DR

The nsppe HTTP/1.1 chunked-body parser reads the chunk-size field into a 32-bit register and never checks for overflow. Nine or more hex digits and the value silently wraps: 100000005 (2^32 + 5) is consumed as a chunk size of 5.

That's a funny parser bug until you remember what NetScaler does with the body: it forwards the client's chunked framing to the backend exactly as received, but it decides where the request ends using its own wrapped value. So the proxy believes request #1 is complete after five body bytes, while a correct backend reading the same bytes computes 4,294,967,301 and settles in for a 4 GB body that will never arrive. Two machines, one byte stream, two different opinions about where a request ends.

Everything after NetScaler's version of "the end" gets reclassified as a new pipelined request on the same client connection, then forwarded independently on the shared proxy-to-backend connection.

What you end up with depends on the backend's own parser:

  • Backend truncates to 32 bits too: the smuggled bytes execute as an independent request. Classic prefix-hijack.
  • Backend parses correctly: hard desync, connection wedged on a 4 GB body that never arrives. Connection-pool exhaustion DoS.

One unauthenticated request. Default configuration. No preconditions beyond reaching an HTTP vserver.


Root cause

The chunk-size hex parser

The parser's job is simple: turn a chunk-size line like 1a3f\r\n into a number. It takes the buffer in rdi with its length in esi, accumulates the hex value into the 32-bit slot at *rdx (in and out, so a size split across two packets resumes cleanly), skips whitespace, and requires the trailing \r\n. It answers with bytes consumed, 0 for "need more data", or -1 for malformed.

The whole loop, small enough to see the bug with the naked eye:

20af5e0: movzx  eax, BYTE PTR [rbx]
20af5e3: test   BYTE PTR [rax+rax*1+0x2746c80], 0x10   ; ctype: hex digit?
20af5eb: je     20af60b
20af5ed: mov    ecx, r11d
20af5f0: shl    ecx, 0x4                               ; value <<= 4
20af5f3: movzx  r11d, BYTE PTR [rax+0x2746b80]         ; hexval(digit)
20af5fb: add    r11d, ecx                              ; value = value*16 + d
20af5fe: inc    rbx
20af601: dec    r8d
20af604: jne    20af5e0                                ; next digit

r11d is the accumulator, and it is 32 bits wide. Every digit does value <<= 4; value += digit. No digit counter, no magnitude cap, no overflow check, no jo anywhere in the loop. The ninth digit doesn't error out; it shifts the top of the number off the end of the register and nobody notices. 0x100000005 & 0xffffffff = 5, and the parser is perfectly satisfied with that.

The only sanity check rejects the wrong half

There is exactly one sanity check downstream, and it's wrong in an almost poetic way. The dechunking driver (sub_20af6b0, call sites 0x20af719 / 0x20af9fb, size stored at [r12+0x344]) applies a sign test to the accumulated value. Sit with that for a second: the only wraps that get caught are the ones landing in the negative half, [0x80000000, 0xffffffff]. A wrapped size whose low 32 bits come out below 0x80000000 sails through as a completely normal chunk size. 0x100000005 wrapping to 5 is the canonical case: wrapped, unnoticed, and devastating.


Why it smuggles

The packet turns into a smuggle because of one design decision in how NetScaler treats bodies: it forwards the client's chunked framing to the backend byte for byte, untouched, while using its own (wrapped) understanding of that framing to decide where the client's request ends.

So NetScaler ends request #1 after five body bytes and queues the trailing 37 as a second, pipelined request. The backend, doing the arithmetic correctly, reads 100000005 as 4,294,967,301 and keeps waiting for body on request #1. From that point on, both boxes are reading the same stream and living in different worlds.

Which world you end up in depends on the backend.

Flow A: correct 64-bit backend. The desync wedge.

Attacker                           NetScaler (<=73.30)                            Backend (RFC 64-bit)
   |                                        |                                               |
   |  [1] one TCP segment, 122 bytes:       |                                               |
   |    POST /wrap HTTP/1.1                 |                                               |
   |    Host: poc                           |                                               |
   |    Transfer-Encoding: chunked          |                                               |
   |    100000005\r\nAAAAA\r\n0\r\n\r\n     |                                               |
   |    GET /SMUGGLED HTTP/1.1              |                                               |
   |    Host: poc\r\n\r\n                   |                                               |
   |--[1]---------------------------------->|                                               |
   |                                        | [2] parses "100000005" into a u32             |
   |                                        |     wraps to 5; consumes AAAAA;               |
   |                                        |     sees terminating 0-chunk                  |
   |                                        |     ==> request #1 COMPLETE                   |
   |                                        |     trailing 37 bytes held as a               |
   |                                        |     pending pipelined request #2              |
   |                                        |                                               |
   |                                        |--[3] 85 bytes, framing VERBATIM-------------->|
   |                                        |   POST /wrap HTTP/1.1                         |
   |                                        |   Transfer-Encoding: chunked                  |
   |                                        |   100000005\r\nAAAAA\r\n0\r\n\r\n             |
   |                                        |                                               |
   |                                        |                                               | [4] parses 0x100000005 (64-bit)
   |                                        |                                               |     = 4,294,967,301 bytes
   |                                        |                                               |     waiting for a body that never co
   |                                        |                                               |     [WEDGED: no response, ever]
   |                                        |                                               |
   |                                        | GET /SMUGGLED waits in NetScaler's            |
   |                                        | pipeline queue forever: response #1           |
   |                                        | never arrives                                 |
   |                                        |                                               |

Flow B: backend that truncates to 32 bits like NetScaler. The completed smuggle.

Download Tool