Hola a todos!
Nos hacemos eco de un artículo del blog de seguridad de Microsoft del día 2 de septiembre de este año.
Hace varias semanas, Microsoft detectó un exploit de ejecución remota de código de 0 días que se usaba para atacar el software FTP SolarWinds Serv-U en ataques limitados y dirigidos. El Centro de Inteligencia de Amenazas de Microsoft (MSTIC) atribuyó el ataque con mucha confianza a DEV-0322, un grupo que opera fuera de China, según la victimología, las tácticas y los procedimientos observados. En este blog, compartimos información técnica sobre la vulnerabilidad, rastreada como CVE-2021-35211, que compartimos con SolarWinds, quien lanzó rápidamente actualizaciones de seguridad para corregir la vulnerabilidad y mitigar los ataques.
Este análisis fue realizado por el equipo de Ingeniería de Seguridad e Investigación Ofensiva de Microsoft, un grupo enfocado encargado de apoyar a equipos como MSTIC con experiencia en desarrollo de exploits. El mandato de nuestro equipo es hacer que la informática sea más segura. Hacemos esto aprovechando nuestro conocimiento de las técnicas y procesos de los atacantes para crear y mejorar las protecciones en Windows y Azure a través de la ingeniería inversa, la creación y replicación de ataques, la investigación de vulnerabilidades y el intercambio de inteligencia.
A principios de julio, MSTIC proporcionó a nuestro equipo datos que parecían indicar un comportamiento de explotación contra una vulnerabilidad recién descubierta en el componente SSH del servidor FTP de SolarWinds Serv-U. Aunque la información contenía indicadores útiles, carecía del exploit en cuestión, por lo que nuestro equipo se dispuso a reconstruir el exploit, lo que requería encontrar y comprender primero la nueva vulnerabilidad en el código relacionado con Serv-U SSH.
Como sabíamos que se trataba de una vulnerabilidad previa a la autenticación remota, construimos rápidamente un fuzzer centrado en las partes previas a la autenticación del protocolo de enlace SSH y notamos que el servicio capturó y aprobó todas las infracciones de acceso sin finalizar el proceso. Inmediatamente se hizo evidente que el proceso Serv-U haría que los intentos de explotación sigilosos y confiables fueran fáciles de lograr. Concluimos que la vulnerabilidad explotada fue causada por la forma en que Serv-U creó inicialmente un contexto OpenSSL AES128-CTR. Esto, a su vez, podría permitir el uso de datos no inicializados como puntero de función durante el descifrado de mensajes SSH sucesivos. Por lo tanto, un atacante podría aprovechar esta vulnerabilidad conectándose al puerto SSH abierto y enviando una solicitud de conexión de autorización previa con formato incorrecto. También descubrimos que los atacantes probablemente usaban archivos DLL compilados sin asignación aleatoria del diseño del espacio de direcciones (ASLR) cargado por el proceso Serv-U para facilitar la explotación.
Compartimos estos hallazgos, así como el fuzzer que creamos, con SolarWinds a través de la Divulgación coordinada de vulnerabilidades (CVD) a través de Microsoft Security Vulnerability Research (MSVR), y trabajamos con ellos para solucionar el problema. Este es un ejemplo de intercambio de inteligencia y colaboración de la industria que da como resultado una protección integral para la comunidad en general a través de la detección de ataques a través de productos y la reparación de vulnerabilidades a través de actualizaciones de seguridad.
Vulnerabilidad en la implementación de SSH de Serv-U:
Secure Shell (SSH) es un protocolo ampliamente adoptado para comunicaciones seguras a través de una red que no es de confianza. El comportamiento del protocolo se define en múltiples solicitudes de comentarios (RFC), y las implementaciones existentes están disponibles en código fuente abierto; utilizamos principalmente RFC 4253, RFC 4252 y libssh como referencias para este análisis.
La implementación de SSH en Serv-U se encontró enumerando referencias a la cadena «SSH-«, que debe estar presente en los primeros datos enviados al servidor. La instancia más probable de tal código fue la siguiente:
Poner un punto de interrupción en el código anterior e intentar conectarse a Serv-U con un cliente SSH confirmó nuestra hipótesis y dio como resultado que el punto de interrupción se alcanzara con la siguiente pila de llamadas:
En este punto, notamos que Serv-U.dll y RhinoNET.dll tienen la compatibilidad con ASLR deshabilitada, lo que los convierte en ubicaciones privilegiadas para los dispositivos ROP, ya que cualquier dirección dentro de ellos será constante en cualquier instancia de servidor que se ejecute en Internet para un Serv determinado. -U versión.
Después de revertir el código relacionado en las DLL de RhinoNET y Serv-U, pudimos rastrear las rutas de los mensajes SSH a medida que Serv-U los procesa. Para manejar una conexión SSH entrante, Serv-U.dll crea un objeto CSUSSHSocket, que se deriva de la clase RhinoNET!CRhinoSocket. La vida útil del objeto CSUSSHSocket es la duración de la conexión TCP; persiste posiblemente en muchos paquetes TCP individuales. El CRhinoSocket subyacente proporciona una interfaz almacenada en búfer al socket de modo que un solo paquete TCP puede contener cualquier número de bytes. Esto implica que un solo paquete puede incluir cualquier cantidad de mensajes SSH (siempre que se ajusten al tamaño máximo del búfer), así como mensajes SSH parciales. La función CSUSSHSocket::ProcessRecvBuffer es entonces responsable de analizar los mensajes SSH de los datos del socket almacenado en búfer.
CSUSSHSocket::ProcessRecvBuffer comienza comprobando la versión de SSH con ParseBanner. Si ParseBanner analiza con éxito la versión SSH del banner, ProcessRecvBuffer luego recorre ParseMessage, que obtiene un puntero al mensaje actual en los datos del socket y extrae los campos msg_id y length del mensaje (más información sobre la función ParseMessage más adelante).
Los datos del socket que se iteran son conceptualmente una matriz de la estructura pseudo-C ssh_msg_t, como se ve a continuación. Los datos del mensaje están contenidos dentro del búfer de carga útil, cuyo primer byte se considera msg_id:
ProcessRecvBuffer luego envía el manejo del mensaje basado en msg_id. Algunos mensajes se manejan directamente desde el bucle de análisis de mensajes, mientras que otros pasan a ssh_pkt_others, que envía el mensaje a una cola para que otro hilo lo recoja y lo procese.
Si msg_id se transfiere al subproceso alternativo, CSSHSession::OnSSHMessage lo procesa. Esta función se ocupa principalmente de los mensajes que deben interactuar con los datos del perfil de usuario administrado por Serv-U (p. ej., autenticación con las credenciales de cada usuario) y las actualizaciones de la interfaz de usuario. CSSHSession::OnSSHMessage resultó no ser interesante en términos de búsqueda de vulnerabilidades, ya que la mayoría de los controladores de mensajes dentro de él requieren una autenticación de usuario exitosa (la telemetría inicial indicó que se trataba de una vulnerabilidad de autenticación previa), y no se encontraron vulnerabilidades en los controladores restantes.
Cuando inicialmente ejecutó fuzzers contra Serv-U con un depurador adjunto, era evidente que la aplicación estaba detectando excepciones que normalmente bloquearían un proceso (como violaciones de acceso), registrando el error, modificando el estado lo suficiente para evitar la terminación del proceso, y luego continuar como si no hubiera habido problema. Este comportamiento mejora el tiempo de actividad de la aplicación del servidor de archivos, pero también da como resultado una posible corrupción de la memoria que persiste en el proceso y se acumula con el tiempo. Como atacante, esto otorga oportunidades como direcciones de código o datos de fuerza bruta con direcciones dinámicas.
Esta eliminación de infracciones de acceso ayuda con la explotación, pero para el fuzzing, filtramos las excepciones «poco interesantes» generadas por infracciones de acceso de lectura/escritura y dejamos que el fuzzer se ejecutara hasta encontrar una falla en la que RIP se había dañado. Esto resultó rápidamente en el siguiente contexto de bloqueo:
Como se vio anteriormente, CRYPTO_ctr128_encrypt en libeay32.dll (parte de OpenSSL) intentó llamar a una dirección no válida. La versión de OpenSSL utilizada es 1.0.2u, por lo que obtuvimos las fuentes para leer detenidamente. A continuación se muestra la función OpenSSL relevante:
Mientras tanto, a continuación se muestra la estructura que se pasa:
Se llegó a la función de bloqueo desde el límite de la API de OpenSSL a través de la siguiente ruta: EVP_EncryptUpdate -> evp_EncryptDecryptUpdate -> aes_ctr_cipher -> CRYPTO_ctr128_encrypt.
Mirando más arriba en la pila de llamadas, es evidente que Serv-U llama a EVP_EncryptUpdate desde CSUSSHSocket::ParseMessage, como se ve a continuación:
En este punto, minimizamos manualmente el búfer de paquetes TCP producido por el fuzzer hasta que solo quedaron los mensajes SSH necesarios para desencadenar el bloqueo. En notación como la utilizada en los RFC, los mensajes SSH requeridos fueron:
Tenga en cuenta que la siguiente descripción hace referencia a las funciones de «cifrado» que se llaman cuando la ruta del código que falla claramente intenta descifrar un búfer. Esto no es un error: Serv-U usa la API cifrada de OpenSSL y, aunque no es óptimo para la claridad del código, tiene un comportamiento correcto ya que el Estándar de cifrado avanzado (AES) está operando en modo de contador (CTR).
Después de realizar un seguimiento de depuración de viaje en el tiempo y depurar la secuencia de procesamiento de mensajes, descubrimos que la causa principal del problema era que Serv-U crea inicialmente el contexto OpenSSL AES128-CTR con un código como el siguiente:
Llamar a EVP_EncryptInit_ex con clave NULL y/o IV es válido, y Serv-U lo hace en este caso porque el contexto se crea mientras se maneja el mensaje KEXINIT, que es antes de que el material clave esté listo. Sin embargo, la expansión de la clave AES no se realiza hasta que se configura la clave, y los datos en la estructura ctx->cipher_data permanecen sin inicializar hasta que se realiza la expansión de la clave. Podemos suponer (correctamente) que nuestra secuencia de mensajes para golpear el bloqueo ha provocado que se llame a enc_algo_client_to_server->decrypt antes de inicializar el material clave. El controlador Serv-U KEXINIT crea objetos para todos los parámetros proporcionados en el mensaje. Sin embargo, los objetos correspondientes actualmente activos para la conexión no se reemplazan con los recién creados hasta que se procesa el siguiente mensaje NEWKEYS. El cliente siempre completa el proceso de intercambio de claves en una conexión SSH normal antes de emitir un mensaje NEWKEYS. Serv-U procesó NEWKEYS (estableciendo así el indicador m_bCipherActive y reemplazando los objetos de cifrado) sin importar el estado de conexión o el intercambio de claves. A partir de esto, podemos ver que el último tipo de mensaje en nuestra secuencia fuzzed no importa: solo es necesario que queden algunos datos para procesar en el búfer del socket para activar el descifrado después de que se haya activado el objeto de cifrado AES CTR parcialmente inicializado.
Explotación:
Como la vulnerabilidad permite cargar RIP desde la memoria no inicializada y como hay algunos módulos sin ASLR en el proceso, la explotación no es tan complicada: podemos encontrar una manera de controlar el contenido de la estructura cipher_data no inicializada, apuntar el puntero de la función cipher_data->block en algún dispositivo ROP inicial y comience una cadena ROP. Debido a que el controlador de excepciones hace que se ignore cualquier falla, no necesariamente necesitamos lograr una ejecución de código confiable en el primer paquete. Es posible volver a intentar la explotación hasta que la ejecución del código sea exitosa; sin embargo, esto dejará rastros en los archivos de registro y, como tal, puede valer la pena invertir más esfuerzo en una técnica diferente que evitaría el registro. El primer paso es encontrar el tamaño del asignación de cipher_data, ya que la vía más directa para llenar previamente el búfer es rociar asignaciones del tamaño de asignación de destino y liberarlas antes de intentar reclamar la dirección como cipher_data. ctx->cipher_data se asigna y asigna en EVP_CipherInit_ex con la siguiente línea:
Con un depurador, podemos ver que ctx_size en nuestro caso es 0x108, y que este asignador termina llamando a ucrtbase!_malloc_base. De la inversión anterior, sabemos que los niveles de análisis de paquetes de CRhinoSocket y CSUSSHSocket llaman al operador new[] para asignar espacio para almacenar los paquetes que enviamos. Afortunadamente, eso también termina en ucrtbase!_malloc_base, usando el mismo montón. Por lo tanto, completar previamente la asignación de destino es tan simple como enviar un paquete TCP o un mensaje SSH del tamaño adecuado y luego cerrar la conexión para garantizar que se libere. El uso de esta ruta para rociar no activa otras asignaciones del mismo tamaño, por lo que no tenemos que preocuparnos por contaminar el montón.
Otro valor importante para extraer del depurador/desensamblado es offsetof(EVP_AES_KEY, bloque), ya que ese desplazamiento en los datos rociados debe establecerse en el dispositivo ROP inicial. Este valor es 0xf8. Convenientemente, la mayor parte del resto de la estructura EVP_AES_KEY se puede usar para el contenido de la cadena ROP en sí, y existe un puntero a la base de esta estructura en los registros rbx, r8 y r10 en el momento de la llamada del puntero de función controlada.
Como una simple prueba de concepto, considere el siguiente código de python:
Lo anterior da como resultado el siguiente contexto en el depurador:
Conclusión: la divulgación responsable y la colaboración de la industria mejoran la seguridad para todos
Nuestra investigación muestra que el servidor Serv-U SSH está sujeto a una vulnerabilidad de ejecución remota de código previa a la autenticación que se puede explotar de manera fácil y confiable en la configuración predeterminada. Un atacante puede aprovechar esta vulnerabilidad conectándose al puerto SSH abierto y enviando una solicitud de conexión de autenticación previa con formato incorrecto. Cuando se explota con éxito, la vulnerabilidad podría permitir que el atacante instale o ejecute programas, como en el caso del ataque dirigido que informamos anteriormente.
Compartimos nuestros hallazgos con SolarWinds a través de la Divulgación de vulnerabilidad coordinada (CVD). También compartimos el fuzzer que creamos. SolarWinds lanzó un parche de aviso y seguridad, que recomendamos encarecidamente a los clientes que apliquen. Si no está seguro de si su sistema está afectado, abra un caso de soporte en el Portal del cliente de SolarWinds.
Además de compartir detalles de vulnerabilidades y herramientas de fuzzing con SolarWinds, también recomendamos habilitar la compatibilidad con ASLR para todos los archivos binarios cargados en el proceso Serv-U. Habilitar ASLR es un indicador de tiempo de compilación simple que está habilitado de forma predeterminada y ha estado disponible desde Windows Vista. ASLR es una mitigación de seguridad crítica para los servicios que están expuestos a entradas remotas que no son de confianza y requiere que todos los archivos binarios en el proceso sean compatibles para ser efectivos en la prevención de que los atacantes usen direcciones codificadas en sus exploits, como fue posible en Serv-U.
Nos gustaría agradecer a SolarWinds por su pronta respuesta. Este caso subraya aún más la necesidad de una colaboración constante entre los proveedores de software, los investigadores de seguridad y otros actores para garantizar la seguridad de la experiencia informática de los usuarios.