Blog / CVE explicados

CVE-2026-104935: Ivanti confía en la cabecera, no en la URL

CVE-2026-104935 encadena una cabecera de enrutamiento y una carrera en la subida de archivos hasta lograr ejecución de código en Ivanti Connect Secure. El root completo necesita un tercer fallo, todavía sin parchear.

Estudios de doctorado en Seguridad de la Información completadosDefensa universitaria superada · Defensa final de la tesis pendiente

Un dispositivo VPN totalmente parcheado, y aun así root

Este es todo el primer movimiento: un POST a la página pública de inicio de sesión de Ivanti Connect Secure, welcome.cgi, que no necesita ninguna sesión, con una cabecera adicional, X-DSCGI-PATH, apuntando a la ruta de otro archivo CGI que normalmente requiere autenticación. La comprobación de autenticación lee la URL. La cabecera decide a dónde va realmente la solicitud. [1]

Volexity rastrea al actor detrás de esto como UTA0576, y cuando su análisis técnico se publicó el 13 de octubre de 2026, el grupo ya había usado ese truco de cabecera, encadenado con un segundo fallo, para conseguir ejecución de código sin autenticación como el usuario sin privilegios cgi-server del dispositivo, en una compilación con todos los parches disponibles aplicados, 22.8R2.4. [1] Estos dos fallos se registran juntos como CVE-2026-104935: CVSS 8,6, CWE-290 (bypass de autenticación por suplantación), afecta a Ivanti Connect Secure 25.1.0.0 anterior a 25.1.1.2, 25.1.2.0 anterior a 25.1.3.0 y todas las compilaciones 22.8R2 hasta 22.8R2.4, corregido en 25.1.1.2, 25.1.3.0, 25.1.3.1 y 22.8R2.5 el 13 de octubre de 2026. [2] CISA lo añadió el mismo día al catálogo de vulnerabilidades explotadas conocidas, con un plazo de tres días para las agencias federales que ya ha vencido. [2][3]

La ejecución de código como cgi-server no es el final de la historia. Volexity también reconstruyó una tercera etapa, todavía sin número de CVE ni parche, que lleva de ahí hasta root, sin puerta trasera ni webshell. No he ejecutado nada de esto por mi cuenta; lo que sigue se basa en el análisis técnico de Volexity y en los registros de CVE y KEV, consultados el 18 de octubre de 2026. [1][2][3] Lo que merece su atención no es que un dispositivo VPN haya caído. Es que dos de los tres fallos son exactamente el tipo de cosa que detecta un revisor de código, no el tipo que descubre un respondedor de incidentes después del hecho.

POST /dana-na/auth/url_default/welcome.cgi HTTP/1.1
Host: vpn.example.com
X-DSCGI-PATH: /home/webserver/htdocs/dana/fb/smb/wu.cgi
Content-Type: application/x-www-form-urlencoded

(welcome.cgi needs no session at all)

// Host and paths are illustrative, not turnkey.
// The URL says "public login page" -> the auth check reads the URL.
// The header says "run wu.cgi instead" -> routing reads the header.
// Nothing checks that the two agree.

La página de inicio de sesión comprueba la URL. Una cabecera decide qué script se ejecuta

Todo despachador como este responde a dos preguntas distintas para cada solicitud: si a quien llama se le permite estar ahí, y qué archivo la gestiona realmente. ICS responde a la primera comprobando la ruta de la URL, es decir, si se trata de uno de los endpoints públicos sin autenticación, y a la segunda comprobando una cabecera de enrutamiento, X-DSCGI-PATH, que nombra el script CGI a invocar. [1] Dividir esto en dos preguntas es una ingeniería razonable. Nada en ese diseño es, por sí solo, descuidado.

Solo se rompe porque nada comprueba que las dos respuestas coincidan. El despachador asume que una solicitud que llega al script público de inicio de sesión es, por definición, una solicitud pública. El atacante controla una cabecera que sobrescribe en silencio qué script la gestiona realmente. Apunte esa cabecera a wu.cgi, un manejador de subidas protegido, y una solicitud con forma de acceso a la página de inicio de sesión se convierte en realidad en una llamada a territorio autenticado, ejecutándose con la confianza que el despachador concede a todo aquello que cree que ya ha pasado la puerta.

El manejador de subidas confía en el reloj, no en el contenido

Que le enruten a wu.cgi es el primer paso. El segundo convierte ese enrutamiento en ejecución de código, y es un fallo de tiempos, no de contenido. wu.cgi escribe un archivo subido en /home/runtime/tmp/<MD5 del nombre de archivo que usted eligió>, y solo después de que esté en disco comprueba el manejador si el contenido es realmente un archivo ZIP válido. Si no lo es, elimina el archivo. [1] Nombre un script de Perl con extensión .zip, y durante una breve ventana, un script dispuesto a ejecutarse de buena gana reposa en una ruta que usted ya conoce, porque eligió el nombre del archivo y un hash predecible lo convirtió en esa ruta.

Volexity describe cómo el atacante lanza solicitudes paralelas en carrera exactamente durante esa ventana: suficientes solicitudes, enrutadas mediante el truco de cabecera de la primera etapa, dirigidas a esa ruta temporal, para capturar el script de Perl antes de que la eliminación gane la carrera. Basta con que una solicitud llegue a tiempo. [1]

La carga útil que Volexity recuperó no se detuvo en Perl. Encadenaba hacia un script de Python, entregado comprimido con Base64 y zlib y ejecutado en memoria con exec() en lugar de escribirse como un archivo propio. [1] El objetivo en todo momento: dejar lo menos posible en disco para que alguien lo encuentre después.

// wu.cgi's order of operations, reconstructed from Volexity's writeup.
// This is the sequence, not a working exploit.

1. Attacker uploads "payload.zip" containing a Perl script, not a real ZIP.
2. wu.cgi writes it to /home/runtime/tmp/<MD5("payload.zip")> immediately.
3. wu.cgi then checks: is this actually a valid ZIP archive?
4. Check fails -> wu.cgi deletes the file.

// The gap between step 2 and step 4 is the whole bug.
// The attacker fires repeated requests (via the stage-one header trick)
// at the path from step 2, racing to execute it before step 4 wins.
// One request landing inside that window is enough.

Dos etapas corregidas. La tercera no, y es la interesante

La corrección de Ivanti para CVE-2026-104935, publicada el 13 de octubre de 2026, cierra las etapas uno y dos: 25.1.1.2, 25.1.3.0, 25.1.3.1 y 22.8R2.5 son las versiones en las que esta cadena concreta deja de funcionar. [2] Si usted ejecuta una compilación anterior en cualquiera de esas tres líneas, esto no es algo que se pueda «dejar para el próximo sprint». Los propios datos de explotación del registro CVE ya lo califican como «activo», con impacto técnico «total», y el plazo federal de tres días de CISA ya ha pasado. [2][3]

En la tercera etapa es donde terminan las notas del parche. Una vez que un atacante tiene ejecución de código como el usuario sin privilegios cgi-server, Volexity descubrió que UTA0576 redirigió dsinvoked, el propio proceso de mantenimiento con privilegios de root de ICS, para ejecutar código elegido por el atacante como root, redirigiendo hacia dónde apunta una tarea ya existente, todo dentro de la memoria de un proceso que ya estaba en ejecución. [1] No voy a detallar la biblioteca concreta ni los detalles de configuración que nombró Volexity. El mecanismo es público; un parche para él no lo es, y repetir aquí detalles a nivel de exploit para una escalada todavía abierta no es algo que haga, independientemente de quién ya lo haya publicado.

Lo que sí importa, al margen del estado del parche: la escalada se revierte a sí misma. Restaura la entrada de configuración y los bytes de la biblioteca en memoria una vez que confirma el acceso root, comprobando si /etc/shadow es legible, de modo que el dispositivo parece intacto después. Volexity afirma claramente que la propia Integrity Checker Tool de Ivanti no detecta esto, porque ICT comprueba lo que hay en disco, y esto nunca toca el disco. [1] Una ejecución limpia de ICT hoy no dice nada sobre si la tercera etapa ha afectado a un dispositivo, ni sobre si eso ocurrió antes del 13 de octubre o podría seguir ocurriendo ahora, ya que todavía no existe corrección para ella.

Cómo buscaría esto en código que no tiene nada que ver con Ivanti

Las etapas uno y dos no son errores específicos de ICS. Son dos patrones que compruebo en cualquier proxy inverso, puerta de enlace de API o despachador tipo CGI, en cualquier stack. El primero: ¿el componente que decide si quien llama puede entrar lee el mismo campo que el componente que decide qué manejador se ejecuta? Si la autorización depende de la URL y el enrutamiento depende de una cabecera, una cookie o un parámetro de consulta que la capa de autenticación nunca mira, ese mismo fallo existe también aquí, solo que con nombres de campo distintos.

El segundo: ¿existe algo que su servicio escribe en una ruta referenciable, aunque sea brevemente, antes de haberlo validado? Los manejadores de subidas son el hogar clásico de este problema, pero aparece en cualquier lugar donde un servicio persiste primero y comprueba después: una escritura en caché, un archivo temporal, una entrada en una cola que un worker puede recoger antes de que se ejecute un validador. Las dos preguntas siguientes son las que realmente me haría al leer por primera vez un manejador como wu.cgi, y no necesitan el código fuente de Ivanti para aplicarse.

// Two questions I ask reading any dispatcher or upload handler,
// independent of what language or framework it's written in.

// 1. Auth-vs-routing mismatch
//    - What field decides "is this caller allowed in"? (often the URL)
//    - What field decides "which handler actually runs"? (sometimes a
//      header, a cookie, a query parameter)
//    - If those are two different fields, does anything enforce that
//      they agree?

// 2. Persist-before-validate
//    - Where does a handler write attacker-influenced content to a
//      referenceable path (disk, cache, queue) before fully validating it?
//    - Is there a window, however small, where something worse can
//      reach what was just written before cleanup or validation runs?

¿Es esto su emergencia? Si usted ejecuta Connect Secure, sí

Seamos honestos sobre a quién afecta esto. Si no ejecuta Ivanti Connect Secure, siga adelante; esto no le concierne. Si lo ejecuta, aquí no hay un aterrizaje suave como a veces ocurre con un hallazgo de baja gravedad inflado hasta parecer urgente. CVE-2026-104935 es previo a la autenticación, sus propios datos de explotación ya lo describen como «activo», y la reconstrucción forense de Volexity encontró que afectó a un dispositivo con todos los parches disponibles ya aplicados. [1][2] «Estamos parcheados» era cierto para ese cliente, y no lo salvó.

Tres cosas determinan el nivel de preocupación. Primero, la compilación que realmente se ejecuta en el dispositivo ahora mismo, no la que su registro de cambios dice que pretendía alcanzar: 25.1.1.1 y anteriores, 25.1.2.2 y anteriores, y 22.8R2.4 y anteriores siguen todas abiertas a las etapas uno y dos. [2] Segundo, si el parche de CVE-2026-104935 es el final de su evaluación de riesgo o solo el principio. Cierra la puerta por la que entró UTA0576, no la que Volexity encontró que ya habían usado una vez dentro. Tercero, si un resultado limpio de la Integrity Checker Tool está sustituyendo a una prueba de que no hay compromiso, algo que el propio análisis de Volexity dice que no es cierto para esta cadena. [1]

La puntuación de 8,6 es honesta respecto a las etapas uno y dos. No dice nada sobre la tercera, porque la tercera etapa todavía no tiene puntuación, y un atacante que encadena las tres no deja de preocuparse por ella solo porque CVSS lo haga.

Qué confirmar antes de dar esto por cerrado

Parchear primero, obviamente. Pero parchear después de los hechos responde a «¿estamos a salvo de ahora en adelante?», no a «¿nos afectó ya?», y la ventana de explotación de esta cadena no requería que usted estuviera sin parchear de ninguna forma que hubiera notado en su momento. [1]

Volexity rastrea la infraestructura del atacante hasta el 30 de septiembre de 2026, con explotación observada desde el 3 de octubre. [1] Si el dispositivo ejecutó una compilación expuesta en algún momento desde finales de septiembre de 2026 en adelante, trátelo como posiblemente comprometido y busque evidencias antes de darlo por cerrado. La siguiente lista sigue el orden en el que yo trabajaría.

Before you call this closed:
  [ ] Confirm the build actually RUNNING on the appliance, not the one
      your change log says. 25.1.1.2 / 25.1.3.0 / 25.1.3.1 / 22.8R2.5 or
      later closes CVE-2026-104935 (stages one and two only).
  [ ] Pull logs for requests to welcome.cgi where X-DSCGI-PATH does not
      match the URL path. That mismatch is this chain's fingerprint.
  [ ] Do not close the incident on a clean Integrity Checker Tool run
      alone. ICT checks on-disk state; stage three never touches disk.
  [ ] If the appliance was internet-facing on a vulnerable build any time
      from late September 2026 on, treat it as possibly compromised, not
      just unpatched.
  [ ] Track Ivanti's advisory for the still-missing privilege-escalation
      fix and apply it the day it ships. There is no CVE for it yet.

El CVE es la parte fácil

CVE-2026-104935 en sí misma se desvanecerá pronto del ciclo de noticias. Las dos clases de fallo que hay detrás, no, porque nunca fueron exclusivas de Ivanti. Una comprobación de autorización y una decisión de enrutamiento que confían en silencio en campos distintos de la misma solicitud, y una ruta de escritura que persiste antes de validar: encuentro ambas cosas en bases de código web y API que nunca han tocado un dispositivo VPN, normalmente en lugares que nadie miró dos veces porque la comprobación evidente —¿está autenticado este endpoint?, ¿se comprueba este tipo de subida?— ya había pasado.

Ese es el límite que una revisión de seguridad del código está diseñada para rastrear: no si una solicitud llegó a una función, sino si cada puerta en el camino lee los mismos hechos sobre esa solicitud. Si quiere que alguien lea su capa de autenticación y enrutamiento con esa pregunta en mente antes de que se convierta en el análisis de un día cero de otra persona, es exactamente el trabajo que hago. Cuénteme qué está ejecutando y definiremos el alcance.

Fuentes

¿Necesita que se compruebe esto en su propio producto?

Revisión de seguridad del código

¿Quiere aprender a encontrar vulnerabilidades como esta por su cuenta?

Curso individual de revisión de código seguro
Todos los artículos

08Contacto

¿Va a lanzar algo que trata datos de usuarios?

Envíeme un breve resumen. Respondo en un plazo de tres días con preguntas o un esbozo de propuesta, y el precio se acuerda por escrito antes de empezar.