
CVE-2026-88776の技術解説。NetScaler nsppeのOracle TNSレコードパーサーにおける認証前の整数アンダーフローにより、境界外書き込みとパケットエンジンのクラッシュを引き起こす。
NetScalerはOracleデータベースをロードバランシングできる。ORACLEタイプのvserverを向けると、パケットエンジンはOracle TNSプロトコル自体を話し始め、組み込みハンドラでクライアントトラフィックを解析する。そのハンドラは接続ごとに小さなステートマシンを保持し、TNSレコード内のどこにいるかを記憶する。
その状態の1つは「レコードの途中で、さらにバイトが来る」という意味を持つ。次のセグメントが到着すると、ハンドラはその長さを読み取り、10を引く。なぜなら各セグメントの10バイトはヘッダーだからだ。
セグメントが10バイトより短い場合に何が起こるかを、誰も問いかけなかった。
1つ送ってみよう。1〜9バイトの小さなセグメントがその状態に入り、長さが負になるが、長さは符号なし16ビットレジスタに格納されているため負にはならない。それは巨大な値、最大で約64 KBにラップアラウンドする。そしてハンドラはその量のデータをあなたのパケットから.bss内の固定0x748バイトのスクラッチバッファへ意気揚々とmemcpyする。エンジンはPIEではないため、踏みつけられるアドレスは決して移動しない。
ログインする必要はない。vserverの背後にデータベースすら必要ない。アプライアンスがTNSハンドシェイク自体に応答するからだ。攻撃全体は1つのTCP会話で完結する。
両方の側面がライブで実演された:
ハンドラは接続ごとの状態を[pcb+0x15a]に格納する。ほとんどの状態は退屈だ。ここで重要なのは0x1F4で、「レコード途中、継続待ち」を意味し、通常のプロトコルバイトだけで接続をその状態へ誘導できる:
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
その接続上の次のパケットは 0x1F4 でディスパッチされ、bd49a7 を経由して bd5362 に入り、ここに到達します:
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
文のように読んでください。セグメントの長さを取得します。10を引きます。結果を保存し、memcpyに渡して完了です。どこにも「長さマイナス10」がまだ意味のある数値かどうかを問い合わせる箇所はありません。
この関数内でその長さフィールドへの参照をすべて数えました。30箇所あります。チェックはちょうど1つだけ、bd4083 にあり、それはゼロとの比較を行うだけです。長さゼロのセグメントは拒否されます。それ以外の長さ、1から9を含め、すべてがそのまま減算へと進みます。
コードは、ステート 0x1F4 が実行される時点でステートマシンが十分に完全なレコードを渡していると明確に信じています。ステートマシンはそのようなことを一切約束していません。単独のパケットとして到着する小さすぎるセグメントは、すべてのディスパッチ条件を満たし、5バイト不足した状態で現れます。
本当に腹立たしいのは、ベンダーがこのバグクラスを認識していることです。まったく同じバイナリの別の場所では、sub_119c940 が同一の長さマイナス定数の算術を行い、cmp eax,0x3f9 / ja でそれをゲートしています。安全なパターンはすぐそこにありました。この箇所ではただそれを使っていなかっただけです。
宛先 0x30a8075 は 0x748 バイトのスクラッチ領域内にあり、その手前には正当な空間が 0x533 バイトしかありません。ラップしたコピーはそれを越えて隣接する DataStream およびネゴシエーションのグローバルへ、あなたのバイト列で溢れ出します。そしてラップした長さ自体が 0x30a8476 に永続化され、後のコードがそれを読み戻して信頼します。これは memcpy から一段離れた、第二の破壊経路です。
セットアップは1行の設定、標準的な DataStream デプロイメントです:
add lb vserver v ORACLE <ip> 1521
バックエンドは不要です。アプライアンスはTNS自体を終端し、独自のAcceptパケットを構築します。「Netscaler」ベンダー文字列も含めてすべてです。
会話は4つのステップで進行します:
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
ステップ1から3は純粋な会話です。ステップ4は5バイトです。
これは14.1-73.30アプライアンス上で2回実行されており、同じマシン、同じビルドで、2回の独立した実行です。どちらの場合もパケットエンジンがSIGSEGVで落ち、一致するコアダンプが生成され、ns.logにはその時間枠内でシグナル11が記録され、データプレーンは約60秒間のウォームリスタートの間停止しました。
ネガティブコントロールは両方の実行で合格しました。同一の会話を実行しつつ、最終セグメントを整合性のある10バイトにすると、何も起こりません。エンジンはそれを処理してそのまま動作を続けます。問題となるのは、この中途半端な長さだけです。