
Este repositorio contiene los resultados de mi investigación de agosto de 2020 sobre el firmware IPC/NVR de Tiandy. Encontré dos vulnerabilidades que podrían usarse para recuperar de forma remota la contraseña del administrador y obtener acceso root al dispositivo.
Este repositorio contiene los resultados de mi investigación de agosto de 2020 sobre el firmware de IPC/NVR de Tiandy (estos dispositivos también se venden como OMNY). Esta "investigación" no fue exhaustiva, pero encontré múltiples métodos para recuperar la contraseña del administrador de forma remota, habilitar telnet y cambiar la contraseña de root.
Es difícil decir exactamente qué versiones están afectadas, ya que solo podemos descargar las recientes. Todas estas versiones descargables están afectadas:
DVRS_V9.12.7.20200422
DVRS_V11.7.4.20200721
NVSS_V13.6.1.20200723
NVSS_V22.1.0.20200722
No solo hay diferentes ramas para diferentes dispositivos, sino que algunos componentes se versionan y actualizan por separado, como la API web, donde encontré una omisión de autenticación que solo funciona en versiones lanzadas desde mediados de 2019, independientemente del número de versión del firmware. Si sabes más sobre las versiones afectadas, agradecería tu ayuda.
Estoy haciendo divulgación completa aquí, pero es razonable. Un parche del proveedor (poco probable, ya que no responden) no haría desaparecer el problema, especialmente cuando ningún dispositivo en línea tiene el firmware más reciente (ni siquiera cerca). La vulnerabilidad real es que esos dispositivos están expuestos a internet. Y esto es algo que los usuarios finales deben solucionar, no Tiandy.
Como extra, también incluyo el desempaquetador de firmware y algo de información sobre cómo acceder a los flujos mediante RTSP/RTMP (suerte encontrando eso en el manual).
Primero presento los scripts:
Luego intento explicar brevemente qué hacen estos scripts y por qué. No repito el código, pero intento dar suficiente contexto para que puedas entenderlo:
Finalmente, esto se pone algo más técnico:
Necesitarás Python 3 con PyCrypto.
Primero, prueba recover.py. Esto requiere que el puerto 3001 sea accesible:
python3 recover.py [HOST]
si todo va bien, se deberían imprimir las credenciales del administrador.
Si ese puerto no es accesible, el web podría funcionar. Esto requiere la url:
python3 cgi_recover.py http://123.45.67.89
python3 cgi_recover.py https://123.45.67.89
Si nada de lo anterior funciona, comprueba si telnet está habilitado. Si lo está, puedes obtener root en el dispositivo directamente, solo tienes que crackear este hash:
support:$1$$AErA9BQgLjrxTJB1748k71:501:501:Linux User,,,:/home/support:/bin/sh
(asegúrate de abrir un PR si realmente lo crackeas :D)
En el firmware NVR V7 más antiguo, puedes ejecutar comandos directamente:
python3 ftpupdate.py [host] [adminpass] '[cmd]'
pero no da salida. Para hacer esto más fácil, he incluido esta abreviatura:
python3 ftpupdate.py [host] [adminpass] adduser [username] [password]
Esto añadirá otro usuario con uid 0.
Primero, habilita telnet usando:
python3 telnet.py [host] [adminpw]
o para dispositivos recientes:
python3 cgi_recover.py [host] telnet
Luego puedes sobrescribir /etc/passwd (asumo que sabes cómo funciona esto). Prueba filetransport.py primero (para NVRs):
python3 filetransport.py [host] [adminpass] put /etc/passwd <[source_file]
esto no da retroalimentación, necesitas probarlo intentando iniciar sesión...
Para modelos IPC donde filetransport.py no funciona, prueba upgrade_rw.py:
python3 upgrade_rw.py [url] [adminpass] /etc/passwd <[source_file]
(esto tampoco da retroalimentación)
Finalmente, para dispositivos aún más nuevos, esto también se puede hacer a través de la API web:
python3 cgi_recover.py [url] write /etc/passwd <[source_file]
Si nada de lo anterior funcionó (comprueba si puedes iniciar sesión), reintenta todos esos métodos pero sobrescribiendo /config/etc/passwd en su lugar. En algunas versiones de firmware, /etc/passwd es un enlace simbólico a ese archivo. Finalmente, también puedes intentar sobrescribir /tdfs/etc/passwd, pero después de eso, puede ser necesario reiniciar el dispositivo, así que para reiniciar, usa:
python3 reboot.py [host] [adminpass]
El firmware antiguo V7 (IPC y NVR) no parece estar afectado, pero si tienes la contraseña del administrador, para NVRs hay un RCE autenticado (ftpupdate.py), y para IPCs, el script upgrade-rw.py podría usarse para sobrescribir /etc/passwd.
Las versiones posteriores del firmware NVR (V9 y V11) incluyen una cuenta predeterminada, que combinada con la escalada de privilegios "pasiva" hace posible recuperar la contraseña del administrador. Luego podemos sobrescribir /etc/passwd usando filetransport.py.
Aunque la cuenta predeterminada no está presente en el firmware IPC, aparece otro método de recuperación: el método PSW. Este es un mecanismo de recuperación de contraseña sin seguridad alguna. Está presente en todas las versiones de firmware descargables desde V9. Mientras que filetransport.py solo funciona en NVRs, upgrade_rw.py logra el mismo propósito en IPCs usando el mecanismo de actualización, por lo que aún podemos obtener acceso root.
El firmware de 2019 introduce otro vector de ataque: una omisión de autenticación usando la API web. Al exportar el archivo de configuración sin autenticación, podemos recuperar la contraseña y preparar un paquete de actualización para sobrescribir archivos arbitrarios.
Hablando de vulnerabilidades, hay 4:
Ten en cuenta que no investigué las funciones de "nube", es decir, si es posible enumerar dispositivos y, por lo tanto, conectarse a dispositivos no expuestos a internet (como ocurre con los dispositivos Xiongmai).
En versiones antiguas, telnet está habilitado por defecto y esto es lo que podemos encontrar en el archivo /etc/passwd:
support:$1$$AErA9BQgLjrxTJB1748k71:501:501:Linux User,,,:/home/support:/bin/sh
(la contraseña de root se actualiza dinámicamente, tampoco crackeé este hash, así que los pull requests son más que bienvenidos :D)
El usuario support (de hecho, presente en todas las versiones de firmware) podría parecer sin privilegios, pero por supuesto, este usuario tiene suficientes privilegios para leer la contraseña de Admin y sobrescribir scripts de inicio escribibles por todos en /etc/init.d o incluso crear nuevos :)
Conceptualmente, el método es realmente simple. Solo enviamos un paquete de inicio de sesión y leemos la respuesta. Eso es todo, porque la respuesta de "inicio de sesión exitoso" contiene las credenciales de todos los usuarios, independientemente de nuestros privilegios. Aunque esto ya era así en V7, prácticamente este método se volvió útil solo cuando se introdujo la cuenta predeterminada en el firmware NVR. El "Default" irremovible no tiene privilegios remotos, así que no puedes hacer nada con él. Bueno, quizás excepto leer la contraseña del administrador...
Aunque esto suena trivial, no fue tan trivial de implementar. Se usa un protocolo personalizado para la comunicación, y las contraseñas se cifran con DES pero con los bits invertidos (la parte más difícil fue descubrir eso), con una clave transmitida por el servidor. Como no hay derivación de clave, un espía podría descifrar fácilmente todo. Sin embargo, todavía hay que descubrir que los bits están invertidos, o reimplementar todo desde cero...
Consulta la función recover_with_default en el archivo recover.py para ver la implementación.
Al analizar el binario, es difícil no notar este mecanismo. Su único propósito es... hacer posible la recuperación de contraseña, y de hecho hace bien esa tarea. Demasiado bien, diría...
¿Qué está pasando aquí? Creo que esto estaba destinado a ser un mecanismo de recuperación de contraseña, presumiblemente creado para que los proveedores pudieran ofrecer una forma a los propietarios de dispositivos de recuperar sus contraseñas.
Puedo plantear la hipótesis de que el flujo se suponía que sería así:
Todo eso está bien, excepto que falta una cosa... ¿dónde está la seguridad? En ninguna parte, resulta. No hay nada que nos impida enviar este paquete, derivar el código de seguridad y recuperar la contraseña de cualquier dispositivo accesible.
Me resulta asombroso, porque no es que haya algún fallo en el mecanismo que anule la seguridad. Simplemente no está ahí en absoluto. No hay nada que corregir, pero esto tampoco me parece una puerta trasera obvia. Esto deja rastros en los registros y tiene 3 esquemas de derivación diferentes, cada uno más sofisticado que el anterior. Esto realmente tomó tiempo de implementar...
Volviendo al método, para que esto funcione, el dispositivo necesita tener un teléfono/correo asociado. Mirando la versión anterior, vi que solo el administrador puede hacer esto, pero luego encontré una omisión. Sorprendentemente, en el firmware más reciente, esa omisión ya no es necesaria, ya que es explícitamente posible cambiar el correo del dispositivo sin autenticación. Ahora, aquí es donde creo que se suponía que estaba la puerta trasera :)
En el lado técnico, este mecanismo es en realidad bastante complicado y fue el más difícil de invertir y reimplementar. Hay 3 versiones de este mecanismo, cada una usa un algoritmo diferente para derivar el código de seguridad. Además de DES con bits invertidos, se involucra un cifrado de sustitución personalizado con una clave hardcodeada. Pero todo esto es en vano, porque Tiandy no puede hacer seguro el cifrado simétrico con una clave hardcodeada, por mucho que lo intenten.
Una cosa que observé es que, dado que el código de seguridad cambia cada minuto, existe la posibilidad de que el proceso original falle solo porque el código cambió entre el paquete del paso 3 y el del paso 5, sin importar cuán poco tiempo haya pasado entre el envío de ambos. Tomé esto en cuenta para que mi script reintente el proceso si el código resulta ser inválido.
Todo el proceso, incluido el establecimiento del correo (que no necesitamos poseer), está implementado en el archivo recover.py.
Esta funciona con versiones de firmware más recientes (2019 y posteriores) que tienen la interfaz web "moderna" (esa con el "mapa". Me gusta ese mapa aunque Australia parece un poco distorsionada).
Esta omisión es simple. Mientras que la mayoría de los endpoints de la API están autenticados, hay algunas excepciones. Sin embargo, la verificación de si se debe omitir la autenticación y la coincidencia de qué endpoint activar están implementadas en lugares diferentes. En la mayoría de los casos, la autenticación se omite cuando la ruta de la URL es igual a una cadena dada, lo cual es seguro.
Pero las versiones recientes introducen otra excepción, que se activa cuando las cadenas Record/DownLoad y ID= simplemente existen en algún lugar de la ruta de la URL.
Ahora, para los endpoints donde se compara la ruta completa, esto sigue siendo seguro. Dado que Security/users es uno de esos endpoints, no podemos recuperar la contraseña directamente. Por suerte para nosotros, el endpoint de exportación de configuración se selecciona verificando si la ruta de la URL comienza con una cadena dada (usando strncmp), así que podemos simplemente añadir esas cadenas a la ruta y exportar el archivo de configuración.
La recuperación de la contraseña usando este fallo está implementada en cgi_recover.py.
Teóricamente, el administrador puede actualizar el firmware, y el firmware no está ni firmado ni cifrado. Pero, ¿realmente necesitamos preparar un paquete de firmware personalizado? A veces no. A veces sí, y fui lo bastante loco como para implementarlo...
Si tienes la contraseña, hay una vulnerabilidad de inyección de comandos que puedes usar, el script ftpupdate.py. Lo que sucede allí es que le decimos al dispositivo que descargue una actualización por ftp y se usa ftpget para realizar esa tarea. Como era de esperar, nuestros parámetros fluyen directamente a la función system().
En el firmware NVR, el protocolo binario tiene el comando FILETRANSPORT que hace exactamente lo que dice. En realidad no hay nada más que decir, porque también podrías descargar el SDK y usar el mismo comando. Por supuesto, quería reimplementarlo, así que para ver cómo funciona, mira filetransport.py.
Los modelos IPC no tienen este comando, sin embargo, como dije antes, siempre podemos actualizar el firmware. Si bien preparar todo el flash es poco práctico, los paquetes de actualización de Tiandy nos permiten reemplazar archivos individuales, que es exactamente lo que necesitamos (ver desempaquetado del firmware).
Excepto que no es tan simple. El formato de archivo "box" tiene algunos metadatos, luego una matriz de archivos. El primer archivo debe llamarse ProductModule, y debe contener parámetros de dispositivo coincidentes, de lo contrario la actualización no continuará. Necesitamos no solo los valores de esos parámetros, sino también los propios parámetros. Además, la parte de metadatos (incluida la versión del archivo box) también se verifica.
Ensamblar manualmente todo eso parece poco práctico, pero afortunadamente hay otra forma. Las exportaciones del archivo de configuración usan el mismo formato de archivo "box", con todos los metadatos coincidentes y el archivo ProductModule incluido, salvo el campo de tipo de actualización.
Pude descubrir cómo llenar este campo y, por lo tanto, implementar este proceso. Aunque el mecanismo también está presente en el firmware NVR, no funciona de la misma manera, pero no tiene sentido seguir analizándolo ya que los NVRs tienen el comando FILETRANSPORT descrito anteriormente.
El script upgrade_rw.py hace uso del proceso de actualización. El nombre sugiere algo más, sin embargo... Eso es porque al exportar el archivo de configuración, especificamos qué archivos exportar por nombre y, como era de esperar, cualquier archivo funciona, por lo que podemos descargar el box y luego leer ese archivo usando el mismo código utilizado para extraer el firmware.
Tanto la exportación como la actualización también se pueden hacer a través de la API web. En este caso, no es posible leer archivos arbitrarios. Aun así, quería implementar esto porque la API parece más estable, consulta cgi_recover.py.
En los modelos que soportan FTP, podría ser posible inyectar comandos de shell en la contraseña del usuario (cuando se ejecuta un comando que añade este usuario para que pueda iniciar sesión por FTP).
Los dispositivos Tiandy tienen abierto el puerto 3001. Este es el puerto necesario para que los métodos no web se ejecuten. Los modelos más nuevos tienen RTSP en el puerto 9100 además del 554, y también tienen RTMP en el puerto 1935. Los modelos IPC usan el puerto 8082 para ONVIF. HTTP y HTTPS se ejecutan en sus puertos estándar.
Los dispositivos con la interfaz web antigua (usando nuestra querida tecnología ActiveX) contienen una de las siguientes cadenas en su respuesta HTTP:
<title>Net Video Browser</title>
tdvideo.css
Los dispositivos con la interfaz web más nueva (esta vez usando... Flash) contienen esto en la respuesta:
res/app-0.1.0.css
(la cabecera Last-Modified está encantada de revelarnos la fecha exacta de lanzamiento)
También podemos identificar esos dispositivos por el certificado, aunque HTTPS no siempre está habilitado. Solo hay dos certificados usados para todos los dispositivos y se pueden encontrar en el firmware descargado, lo que hace inútil el pinning.
El más antiguo:
C=CN, ST=Tianjin, O=Tiandy Tech, CN=dvr_ui
El más nuevo:
C=CN, ST=Tianjin, L=Tianjin, O=Tiandy Tech Ltd, CN=NetDevice
Por cierto... El soporte de HTTPS se implementa mediante un proceso stunnel separado. Esto funciona, pero como era de esperar, la dirección IP se pierde en el proceso, por lo que los registros siempre dicen 127.0.0.1.
Tiandy afirma que sus dispositivos soportan RTSP y RTMP. Eso es genial, pero lo que no podemos encontrar en el manual es cómo usar realmente estos protocolos, porque no podemos encontrar las URLs RTSP y RTMP necesarias. Afortunadamente, tengo esta información como subproducto del análisis, así que puedo compartirla.
Para NVR:
Para ver el flujo en vivo del canal C (comenzando en 1) con el tipo de flujo S (1, 2, 3):
rtsp://username:password@host/C/S
Para IPC:
Para ver el flujo en vivo con el tipo de flujo S:
rtsp://username:password@host/S
Las URLs RTMP no son tan simples porque requieren un hash personalizado para que la solicitud pueda autenticarse. Sin embargo, RTMP también nos permite reproducir el contenido grabado.
La url para el flujo en vivo es:
rtmp://host/live/C/S/authstring
donde C es el canal, S es el tipo de flujo.
La url para la reproducción es:
rtmp://host/vod/START-STOP/C/S/authstring
donde tanto START como STOP son marcas de tiempo unix.
authstring se calcula de la siguiente manera:
base64("username:"+md5("username:password")+":unix_timestamp")
La herramienta rtmpauth.py puede generarlo:
python3 rtmpauth.py username password
Dado que esta marca de tiempo se verifica y la diferencia no puede ser mayor de 2 días, hay limitaciones:
Las actualizaciones de firmware están empaquetadas en un formato de archivo "box" propietario que no está ni firmado ni cifrado. Este formato es en realidad muy simple desde la perspectiva del desempaquetador. Hay un encabezado que omitimos y una matriz de archivos para desempaquetar, donde cada archivo tiene un encabezado de tamaño fijo que contiene el nombre del archivo y el tamaño (dos veces), luego siguen los datos.
La herramienta unbox.py desempaqueta el archivo en un directorio con el nombre del archivo box o el directorio especificado:
python3 unbox.py [box_file]
python3 unbox.py [box_file] [target_dir]
Esta herramienta debería ser segura de usar (escribí esta línea, luego revisé la herramienta de nuevo y encontré una vulnerabilidad... ups) porque las rutas absolutas se convierten en relativas, .. se reemplaza con __ y no hay enlaces simbólicos.
A veces necesitarás ejecutar esta herramienta dos veces, ya que notarás que el archivo dentro de un .box es otro .box.