~/investigacion $ cat caso-002.md

Gitea diffpatch: ejecución de código desde una cuenta sin permisos de administrador

CVSS 9.8 · crítico CVE-2026-60004 Gitea Code injection CWE-94 CISA KEV

Reconstrucción de la inyección de código por la ruta diffpatch de Gitea, divulgada por el fabricante con prueba de concepto funcional. El caso prueba a nivel de proceso quién ejecuta realmente, mide la latencia desde una cuenta sin permisos de administrador y publica el falso positivo que midió en sus propias reglas.

Qué se publicó y qué hace este caso

Gitea publicó el 28 de julio de 2026 un aviso GHSA-rcr6-4jqh-j84m con una prueba de concepto que funciona, y el commit de arreglo d7bc52be es del 26 de julio, embarcado en Gitea 1.27.1. NVD abrió CVE-2026-60004 el 26 de agosto con CWE-94 y un CVSS 3.1 de 9.8 que la propia NVD marca como Secondary. En el feed oficial de CISA KEV la entrada tiene dateAdded 2026-08-25 y dueDate 2026-08-28, un día antes de que NVD publicara la CVE.

El caso no descubre nada: toma esa divulgación y la reconstruye en laboratorio propio para que un defensor vea la cadena, el cierre contra la versión arreglada y las reglas con sus falsos positivos medidos. Es el primer caso producido de principio a fin por el pipeline v2 del laboratorio, donde cada gate de fase es un script contado: el verificador de números publicados detectó 26 corrupciones inyectadas en 26 intentos.

La cadena, paso a paso

El punto de entrada es la ruta diffpatch. Un diff con forma de parche llega a un clon temporal del repositorio; en un merge add/add con conflicto, Git materializa el contenido y, si el sistema de ficheros temporal es escribible y ejecutable, un hook cae en el sitio donde Git lo va a invocar. La cadena real de ejecución medida en el caso no es la que sugiere la salida: el padre auténtico es git write-tree, que ejecuta /bin/sh hooks/post-index-change como uid=1000(git).

Lo que hace incómoda la vulnerabilidad es su silencio. Las dos peticiones responden HTTP 201 y el fallo del hook no se propaga a la respuesta, así que el operador no ve nada raro. El log de la aplicación tampoco ayuda: en las corridas del caso no hay ni una línea que nombre la ejecución del hook.

Y no hace falta ser administrador. La cuenta con la que se corrió todo se creó por registro abierto, sin token de API. Entre esa cuenta y la ejecución de código pasaron entre 0,82 y 0,89 segundos medidos en tres corridas.

Qué hizo el agente y qué midió

Primero canario, siempre: antes de cualquier demostración de impacto se valida la ejecución con un canario reversible, y la demostración pasa por un gate humano explícito. Después, verificación del arreglo por A/B con testigo: 1.27.1 rechaza el payload byte idéntico en 22 de 22 intentos, con el monitor testificando que llegó a disco y la rama vulnerable ejecutándolo ese mismo día. Sin control positivo vivo, un negativo no significa nada.

Con la misma primitiva se midió el radio de impacto: app.ini legible, credenciales plantadas leídas de vuelta y escritura arbitraria dentro de /data/gitea. Lo que el caso no hizo fue conectarse a ninguna base de datos ni quedarse con una credencial de base de datos de un sistema real.

La contribución propia, independiente del aviso, es estática: en el binario 1.27.0 aparece la cadena conflict detected at: git ls-files -u -z: %w. git ls-files -u lista las entradas sin mergear de un índice, o sea que es el detector de colisiones de un merge three-way. Su presencia en el handler de diffpatch confirma el mecanismo add/add descrito en el aviso sin depender de él.

Dónde se detuvo el agente

Este caso publica sus mediciones malas. designs/FACTS.md tiene una sección de intentos descartados y mediciones inválidas: el primer grep -c diffpatch sobre /usr/local/bin/gitea devolvió 0 porque ese fichero es un wrapper bash de 581 bytes; la marca de tiempo del canario salió truncada porque busybox no expande %N; y un total_repos=0 era un bug de comillas del propio hook, así que la cifra de repositorios no está establecida.

La sonda ab-structural.sh se declaró no fiable y se retiró como prueba: el repositorio temporal vive milisegundos y las comprobaciones atómicas se perdían en una carrera. Y el primer pcap sencillamente no existía, porque tcpdump sin sudo no captura en el puente; se recapturó y se dejó escrito porque ese primer pcap habría sido evidencia falsa.

En detección, el caso midió su propio falso positivo: la sid:202660041 disparó en los tres ciclos de un control benigno y se publica con confianza baja. Y hay una limitación que no se arregla con tuning: el arreglo conserva git apply --index --cached -3, así que detectar por contenido del diff es frágil por construcción. Queda sin establecer la prevalencia de instalaciones con las tres precondiciones expuestas, la explotación real y si otras mitigaciones rompen la cadena.

Detección y remediación

Actualizar a 1.27.1 o superior. Auditoría rápida sin tocar el servicio: comprobar la versión (de 1.17 hasta antes de 1.27.1), comprobar que la ruta diffpatch existe leyendo el binario y no los documentos, y ver desde dentro del contenedor si el sistema de ficheros temporal es escribible y ejecutable. Quitar el exec de ese montaje o deshabilitar la ruta rompe la cadena, aunque el caso probó solo el defecto del fabricante.

Las reglas publicadas cubren tres capas. En la web: un parche de hook ejecutable llegando a diffpatch, el mismo parche dos veces desde el mismo origen (la colisión add/add) y un parche aceptado con 201, que es el éxito silencioso. En el host: git write-tree engendrando un shell hook y un fichero de hook materializándose dentro del clon temporal de Gitea. En la red y en artefactos: Suricata y YARA.

Las detecciones disparan por reproducción offline de los pcaps del caso, nunca contra una interfaz viva, y antes de llevarlas a producción hay que ajustarlas contra la telemetría propia: el caso lo dice literalmente y el falso positivo medido es la razón concreta.

Resultado

  • La ruta diffpatch ejecuta código como el servicio git: el padre real es git write-tree, que corre /bin/sh hooks/post-index-change con uid=1000(git). Probado a nivel de proceso, no por texto de salida.
  • Las tres precondiciones del aviso (Git >= 2.32, ruta diffpatch habilitada y sistema de ficheros temporal escribible y ejecutable) se cumplen por defecto en la imagen oficial gitea/gitea:1.27.0 sin tocar la configuración.
  • Latencia medida de entrega: 0,82 a 0,89 segundos en tres corridas, con las dos peticiones respondiendo HTTP 201 (éxito silencioso: el fallo del hook no se propaga a la respuesta) desde una cuenta sin permisos de administrador creada por registro abierto.
  • El arreglo 1.27.1 rechaza el payload byte idéntico: 22 de 22 intentos sin ejecución y el segundo diffpatch respondiendo 500, con el monitor testificando que el payload llegó a disco y la rama vulnerable ejecutando el mismo día.
  • Radio de impacto medido por la misma primitiva: app.ini legible, credenciales plantadas leídas y escritura arbitraria dentro de /data/gitea. Hallazgo estático propio, independiente del aviso: en el binario 1.27.0 aparece la cadena conflict detected at: git ls-files -u -z: %w, el detector de colisiones de un merge three-way, que confirma el mecanismo add/add descrito.
~/contactorespuesta < 24 h hábiles · es / en

tú@nullsector:~$ mail miguel

[email protected]

Colaboraciones, charlas, prensa o investigación: escríbeme.

¿Necesitas un pentest o un análisis de vulnerabilidades? Pídelo en Xpectra.ai ↗