-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA256 NEFARIOUSPLAN-CANONICAL-V1 {"body_md":"## Digest authentication is per-request by design.\n\nHTTP Digest Access Authentication, as defined in RFC 7616, is structured around one property: the client proves it knows the password on every authenticated request, never just on the first one. The mechanism is the `response` field. When a client wants to call `DESCRIBE rtsp://camera/stream1`, it computes:\n\n```\nHA1 = MD5(username:realm:password)\nHA2 = MD5(method:uri)\nresponse = MD5(HA1:nonce:HA2)\n```\n\nThe `method` and the `uri` are inside the hash. That is not incidental. It is the entire point of the scheme. The same client, with the same credentials, against the same server, calling `TEARDOWN rtsp://camera/stream1` instead of `DESCRIBE`, computes a different `response`. A captured `DESCRIBE` Authorization line cannot be replayed onto a `TEARDOWN` request; the server recomputes against the new method, the hash does not match, the request is rejected.\n\nThat property is the only reason Digest exists. Basic authentication sends the password on every request. Digest replaces the password with a per-request signature so the password never travels and the signature cannot be replayed across methods or URIs. If the server stops verifying the signature, Digest collapses into Basic without the password. What remains is a nonce and a Session ID, both of which travel in cleartext on the wire.\n\n## The camera asks once, then stops asking.\n\nThe PoC's transcript is short. The OPTIONS warmup is unauthenticated:\n\n```\nOPTIONS rtsp://192.168.0.149:554/stream1 RTSP/1.0\nCSeq: 2\n\n→ RTSP/1.0 200 OK\n```\n\nThe first DESCRIBE draws a 401 with a Digest challenge:\n\n```\nDESCRIBE rtsp://192.168.0.149:554/stream1 RTSP/1.0\nCSeq: 3\n\n→ RTSP/1.0 401 Unauthorized\n WWW-Authenticate: Digest realm=\"MERCURY IP-Camera\",\n nonce=\"14ec1bbf397af99e1da88fbd2deb3d54\"\n```\n\nThe client computes the response, retries, and the camera accepts:\n\n```\nDESCRIBE rtsp://192.168.0.149:554/stream1 RTSP/1.0\nCSeq: 4\nAuthorization: Digest username=\"admin\", realm=\"MERCURY IP-Camera\",\n nonce=\"14ec1bbf397af99e1da88fbd2deb3d54\",\n uri=\"rtsp://192.168.0.149:554/stream1\",\n response=\"178011fa106a74cb806124d26be09dd1\"\n\n→ RTSP/1.0 200 OK\n Session: 5579378E\n \n```\n\nSo far ordinary Digest. The server challenged, the client answered with a valid response, the server returned an SDP body and a `Session:` ID. Then the PoC begins sending RTSP control methods with the same nonce, the same Session ID, and an empty `response` field:\n\n```\nSETUP rtsp://192.168.0.149:554/stream1/track1 RTSP/1.0\nCSeq: 5\nAuthorization: Digest username=\"admin\", realm=\"MERCURY IP-Camera\",\n nonce=\"14ec1bbf397af99e1da88fbd2deb3d54\",\n uri=\"rtsp://192.168.0.149:554/stream1\",\n response=\"\"\nSession: 5579378E\n\n→ RTSP/1.0 200 OK\n```\n\n```\nPLAY rtsp://192.168.0.149:554/stream1/ RTSP/1.0\nCSeq: 7\nAuthorization: Digest username=\"admin\", realm=\"MERCURY IP-Camera\",\n nonce=\"14ec1bbf397af99e1da88fbd2deb3d54\",\n uri=\"rtsp://192.168.0.149:554/stream1\",\n response=\"\"\nSession: 5579378E\nRange: npt=0.000-\n\n→ RTSP/1.0 200 OK\n```\n\n```\nTEARDOWN rtsp://192.168.0.149:554/stream1/ RTSP/1.0\nCSeq: 8\nAuthorization: Digest username=\"admin\", ..., response=\"\"\nSession: 5579378E\n\n→ RTSP/1.0 200 OK\n```\n\nThe researcher's PoC marks the transition with one Chinese-language comment, on the line that flips `response` to the empty string:\n\n```python\n# response设置为空,不再认证了\n```\n\n*Response set to empty, no longer authenticates.* The comment is not analysis. It is the literal change being demonstrated and the literal observation about what the camera does next. The Digest hash the spec defines as proof of knowing the password is no longer proof of anything. It is decoration.\n\n## The middleware-outside-the-hijacker pattern, at the protocol level.\n\nThe shape is not new. We have covered it twice this month, in two different network protocols.\n\nThe Go-stdlib instance is in [AdGuard Home](/posts/adguard-home-auth-outside-h2c), which placed its auth middleware on the outside of `h2c.NewHandler`. The HTTP/1.1 upgrade request was authenticated. Every HTTP/2 stream that ran through the hijacked socket afterwards was not. The Node-stdlib instance is in [n8n's chat WebSocket](/posts/n8n-chat-existence-was-the-gate), which registered its upgrade handler directly on the raw HTTP server before Express middleware could see the request. The handshake was gated by existence-of-execution-id. Every WebSocket frame after the upgrade arrived at the inner handler with no re-validation.\n\nBoth are instances of one architectural mistake: request-scoped authentication wrapped around a handler that promotes a single request into a long-lived connection. The handshake is checked. The session that follows is not. The library author named the wrapper \"middleware\" without noticing that the request the middleware sees is the only request inside the wrapper.\n\nCVE-2026-35903 is the same pattern at the protocol level. RTSP does not \"hijack\" a TCP socket the way h2c or WebSocket do, but it does establish a logical session identified by a `Session:` header and a nonce value, and the protocol's authors knew it. The method and the URI go into the Digest hash precisely so that the long-lived session forces per-request re-authentication. MERCURY's implementation collapses that requirement. The session header replaces the response field. The middleware ran on DESCRIBE. Everything after DESCRIBE escaped the middleware in the same way an HTTP/2 frame escapes Express.\n\n## The credential is the Session ID, and the Session ID is on the wire.\n\nOnce `response=\"\"` is accepted, the only fields the camera verifies on subsequent requests are `nonce`, `Session`, and the implicit fact that some authenticated client established this session in the past. None of those are secret. The `Session:` header is sent in cleartext on every RTSP reply. The nonce was sent in cleartext in the 401 challenge. Anything on the LAN with a packet capture knows both values within milliseconds of the legitimate viewer connecting.\n\nThe practical effect is that the camera's authentication boundary is not the password. The password is checked once, on DESCRIBE, by the legitimate viewer. From that point on, the boundary is \"can you see the traffic on the network.\" On a residential Wi-Fi LAN with one camera and one viewer, that boundary includes every device associated with the access point and every device within radio range of it. RTSP control commands an attacker on that boundary can issue without forging Digest:\n\n- `TEARDOWN`: terminate the legitimate viewer's session.\n- `PAUSE` / `PLAY`: control the playback state.\n- `SETUP` with a different `Transport:` header: change where the camera sends its RTP packets.\n\nThe third is the high-impact case. The `Transport:` header in an RTSP `SETUP` request tells the camera where to deliver the RTP video frames. A `SETUP` request the camera accepts without verifying Digest is a `SETUP` request that can rewrite the destination address of the live video feed. The legitimate viewer's RTSP control session keeps appearing valid. The attacker on the LAN starts receiving the camera's frames.\n\n## Two CVSS scores, one mechanism.\n\nTwo CVSS scorings exist for this bug. The researcher's own README declares CVSS 3.1 base 2.3 (`AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:L/A:N`): local network access, high privileges required, low integrity impact, no confidentiality impact, no availability impact. NVD scored the same bug at 9.8 (`AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H`): network reachable, no privileges, full impact on confidentiality, integrity, and availability.\n\nThe two scorings are not arguing about the code. They are arguing about whether seeing one authenticated session on the wire counts as \"no privileges required.\" If the camera is on a residential LAN that also exposes it through a port-forward for remote viewing, which is the use case the product is sold for, NVD's reading is the operative one. The researcher's reading describes the bug on a hardened switched network with no observability. The mechanism is identical in both worlds. The cost is whichever boundary the network supplies, and most Wi-Fi networks supply none.\n\n## A note on what the references contain.\n\nMERCURY has not published a patch. The CVE record references one document: the researcher's PoC. That PoC contains two comments about itself. The English-language header reads \"This code is for authorized security research purposes only.\" The Chinese-language comment in the body reads \"response set to empty, no longer authenticates.\" Both statements are in the same file. Only one of them is technically operative.\n\nPoC: [izxnfirh8148/CVE_REQUESTS_references](https://github.com/izxnfirh8148/CVE_REQUESTS_references/blob/main/MERCURY_MIPC252W/MERCURY_MIPC252W_4th/README.md)","closing_line":"Digest is per-request by design. MERCURY treats it as a handshake.","hook_md":"MERCURY's MIPC252W is a wireless IP camera sold in the Chinese consumer market. It streams video over RTSP and gates the stream behind HTTP Digest authentication. CVE-2026-35903 says the Digest check works the first time and stops working immediately after. The researcher's PoC marks the spot with one Chinese-language comment on the request that follows the authenticated DESCRIBE: `response设置为空,不再认证了`. Translated: *response set to empty, no longer authenticates.* Digest is per-request by design. MERCURY treats it as a handshake.","post_id":323,"slug":"mercury-mipc252w-digest-as-handshake","title":"CVE-2026-35903: MERCURY's RTSP Treats Digest As a Handshake","type":"initial","unreadable_sentence":"Digest is per-request by design. MERCURY treats it as a handshake."} -----BEGIN PGP SIGNATURE----- iHUEARYIAB0WIQRf0htP5+SjynlxywneZjl4jgkQJgUCaviiOQAKCRDeZjl4jgkQ JmwyAQDzaQJTdlFeomwaH45Lj27+p3ekiFvlE1mO271rUsuRAAEA9Ux2WIq0Trmn 64QYiiIe28JxsHSOdAB4K0cr1KYIdAA= =hxiT -----END PGP SIGNATURE-----