~/investigacion $ cat caso-002.md
Gitea diffpatch: ejecución de código desde una cuenta sin permisos de administrador
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 esgit write-tree, que corre/bin/sh hooks/post-index-changeconuid=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.0sin 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.inilegible, 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 cadenaconflict detected at: git ls-files -u -z: %w, el detector de colisiones de un merge three-way, que confirma el mecanismo add/add descrito.
tú@nullsector:~$ mail miguel
Colaboraciones, charlas, prensa o investigación: escríbeme.
¿Necesitas un pentest o un análisis de vulnerabilidades? Pídelo en Xpectra.ai ↗