X-Ops

Gitea CVE-2026-60004: RCE crítico a través de hooks de Git ya en explotación

CVE-2026-60004 ha convertido a Gitea en el centro de todas las miradas durante la última semana. El 27 de julio de 2026, los mantenedores publicaban la versión 1.27.1 con un parche para una vulnerabilidad de inyección de código en el endpoint `diffpatch` de la API REST. Tres días después llegaba el advisory oficial con prueba de concepto incluida. El 25 de agosto, la CISA lo añadía a su catálogo Known Exploited Vulnerabilities (KEV) bajo la Binding Operational Directive 26-04, dando a las agencias federales de Estados Unidos un plazo de 72 horas para remediarlo. A día de hoy, la organización Shadowserver reporta más de 8.393 direcciones IP con instancias de Gitea todavía vulnerables, y se ha documentado al menos una intrusión real que terminó desplegando un dropper de criptominado. Si tienes un servidor de Gitea expuesto a internet, este artículo es para ti.

Qué ha pasado exactamente

El aviso es directo. Gitea describe el fallo así: "el endpoint diffpatch puede abusarse para instalar y ejecutar un Git hook a partir de contenido controlado por el repositorio. Un atacante con acceso de escritura ordinario puede ejecutar comandos shell arbitrarios como el usuario del sistema que corre Gitea". Traducido: alguien con permiso para subir cambios a un repositorio consigue plantar un hook malicioso y forzar a Git a ejecutarlo, con todos los privilegios del proceso de Gitea. El CVE-2026-60004 recibe una puntuación CVSS 3.1 de 9.8, la máxima franja crítica.

El rango afectado es amplio: todas las versiones desde la 1.17 hasta la 1.27.0, sin excepción. Gitea corrigió el problema en 1.27.1, publicada el 27 de julio. Una semana después, el 27 de agosto, los responsables liberaron 1.27.2 con correcciones adicionales; ambas versiones cierran la puerta. El investigador Shai Rod (NightRang3r), de Salesforce, está acreditado como descubridor. El advisory público, registrado como GHSA-rcr6-4jqh-j84m, incluye el código del exploit de prueba, así que la barrera de entrada para reproducir el ataque es prácticamente nula.

El detalle que convierte un CVE "alto" en "urgente" es la configuración por defecto. Gitea activa el registro abierto de usuarios nada más instalarse: no exige confirmación por correo, no requiere aprobación manual, no marca cuentas nuevas como restringidas y no impone un límite de creación de repositorios. Cualquier visitante anónimo puede crearse una cuenta, crear un repositorio y obtener los permisos de escritura necesarios para llegar al endpoint vulnerable. En otras palabras, el prerrequisito de "tener acceso de escritura" se convierte en una formalidad trivial en cualquier instancia sin personalizar.

Anatomía técnica del exploit

El endpoint afectado responde a `POST /api/v1/repos/{owner}/{repo}/diffpatch`. Su trabajo es recibir un parche en formato unified diff y aplicarlo sobre un clon temporal del repositorio. El código vulnerable prepara ese clon como un repositorio bare de Git y lanza `git apply` con una combinación concreta de flags: `--index`, `--recount`, `--cached`, `--binary`, y `-3` cuando el binario de Git del servidor es 2.32 o superior.

Ahí está la grieta. En un repositorio bare no existe directorio de trabajo; el propio directorio del repositorio es el directorio interno de Git. Eso significa que cualquier ruta resuelta por Git al procesar el parche puede coincidir con rutas estructurales como `hooks/`. Cuando el atacante envía el mismo parche dos veces seguidas, el modo `-3` fuerza la resolución de un conflicto add/add. El fallback de tres vías escribe el archivo resultante en disco para resolver la colisión, y si esa ruta está bajo `hooks/post-index-change`, Git la interpreta como un hook de cambio de índice y la ejecuta automáticamente la próxima vez que actualiza el índice. El contenido del archivo atacante corre como shell script con los privilegios del usuario que ejecuta Gitea.

La PoC pública, autoría del investigador imbas007, automatiza este flujo. El atacante autenticado envía el parche malicioso al endpoint; Gitea aplica el parche en su clon bare temporal; Git materializa el hook por la resolución del conflicto; el hook se ejecuta en el siguiente ciclo de `git update-index`. La cadena entera, desde el primer POST hasta la ejecución del comando, se completa en segundos. El análisis publicado por SOC Prime confirma que el arreglo en 1.27.1 sustituye el clon temporal bare por uno no bare con working tree, lo que separa físicamente la ruta `hooks/` del contenido del parche y cierra el camino.

Por qué importa en una instancia self-hosted

El riesgo real no es el RCE aislado, sino lo que viene después. Un servidor de Gitea en una organización típica tiene visibilidad sobre la red interna, acceso al almacenamiento compartido, proximidad a secretos que mueven la maquinaria diaria: tokens de despliegue, claves SSH de runners, credenciales de registros de contenedores, OAuth contra proveedores cloud, webhooks con tokens de larga duración. El reporte de incidentes publicado por el desarrollador Andrey (@Causelof) en Habr describe exactamente este escenario. Su proveedor de hosting HOSTKEY le avisó de que la máquina virtual llevaba horas con la CPU por encima del 70 por ciento; al investigar, descubrió que el RCE se había utilizado para desplegar un dropper con comportamiento de criptominero. El código atacante había escrito primero una prueba de RCE en una rama de Git, después descargó un shell-loader universal y luego el payload del minero. La parte activa del ataque duró unos once segundos. El contenedor no era privilegiado y el payload no sobrevivió al reinicio, pero eso es cuestión de suerte, no de diseño.

El historial reciente de Gitea tampoco invita a la calma. En julio, CVE-2026-20896 (CVSS 9.8), un bypass de autenticación en imágenes Docker oficiales con reverse proxy auth headers, ya fue objeto de explotación trece días después de su divulgación. En mayo, CVE-2026-27771 expuso registros de contenedores privados en más de 30.000 despliegues. El patrón es claro: cada pocos meses aparece un fallo crítico, y la comunidad tarda en parchear.

Cómo saber si tu instancia ya fue comprometida

Gitea no publicó una guía formal de detección, pero hay señales claras que conviene revisar. Lo primero es el árbol de hooks de cada repositorio. Localiza el directorio de datos, normalmente `/var/lib/gitea/data/gitea-repositories/`, y dentro de cada repositorio busca `hooks/post-index-change` y `hooks/post-checkout`. Cualquier archivo presente en esas rutas que no hayas creado tú de forma explícita es candidato a hook inyectado. También conviene revisar `hooks/pre-receive`, `hooks/post-receive`, `hooks/update` y los symlinks bajo `custom_hooks/`. Los timestamps de modificación que coincidan con respuestas rápidas a `POST /api/v1/repos/.../diffpatch` son la señal más clara.

En los logs, busca llamadas al endpoint diffpatch. Con la ubicación típica en Linux:

``` grep -E '"POST /api/v1/repos/[^/]+/[^/]+/diffpatch' /var/lib/gitea/log/gitea.log ```

Si ves POSTs exitosos desde cuentas recién creadas, o desde cuentas que normalmente no tocan ese repositorio, tienes una pista sólida. Los IDs de usuario sospechosos se identifican con:

``` grep -E 'User created' /var/lib/gitea/log/gitea.log | head -n 50 ```

Gitea separa los logs por modo, así que conviene revisar también `ssh.log` (para detectar accesos por SSH que no correspondan a desarrolladores conocidos), `http.log` (para observar picos anómalos en POSTs de la API REST) y `xorm.log` si tienes la depuración a nivel SQL habilitada. Una secuencia característica de compromiso combina tres eventos en pocos segundos: creación de cuenta, creación de repositorio, llamada a `diffpatch`. Buscar esa cadena explícitamente acelera la investigación:

``` grep -E 'User created|repo created|/diffpatch' /var/lib/gitea/log/gitea.log \ | tail -n 200 ```

A nivel de sistema, busca procesos hijos inesperados. El usuario que corre Gitea (por defecto `git`) no debería lanzar shells ni binarios de minería. Comandos útiles:

``` ps -u git -o pid,ppid,start,cmd --forest ls -la /proc/*/cwd 2>/dev/null | grep -E '/tmp|/dev/shm' find / -user git -type f -newer /var/lib/gitea/data/gitea.db -not -path '/var/lib/gitea/*' 2>/dev/null ```

Los payloads del incidente de Habr dejaron archivos en `/tmp` y `/dev/shm`, intentaron descargar binarios vía `curl` o `wget`, y escribieron un script de minero en una ruta de trabajo del repositorio. Cualquier aparición de `xmrig`, `minerd`, `kdevtmpfsi` o nombres similares en procesos del usuario `git` es alerta roja.

En el lado de red, revisa conexiones salientes desde el host de Gitea. La instancia documentada hablaba con pools de minería y con repositorios públicos tipo GitHub o GitLab para bajar el shell-loader. Con `ss` o `tcpdump`:

``` ss -tnp 'state established' | grep -E ':(3333|4444|5555|7777|8888|14444|14433)' ```

puertos típicos de pools de minería. Si tu contenedor está en Docker, el equivalente es `docker exec gitea ss -tnp` desde el host.

Procedimiento de actualización

Para una instalación Docker con la imagen oficial, lo correcto es fijar la versión 1.27.2 o superior y forzar un pull fresco. Edita el `docker-compose.yml` y reemplaza la línea de imagen:

``` image: gitea/gitea:1.27.2 ```

Después:

``` docker compose pull gitea docker compose up -d gitea docker compose exec gitea gitea --version ```

Para una instalación con binario en `systemd`, descarga el tarball desde el mirror oficial, sustituye el binario y reinicia:

``` sudo systemctl stop gitea sudo mv /usr/local/bin/gitea /usr/local/bin/gitea.bak sudo curl -L -o /usr/local/bin/gitea https://dl.gitea.com/gitea/1.27.2/gitea-1.27.2-linux-amd64 sudo chmod +x /usr/local/bin/gitea sudo systemctl start gitea gitea --version ```

Antes de levantar nada, captura una imagen forense del estado actual. Aunque no sospeches compromiso, este snapshot es tu punto de retorno:

``` docker commit gitea gitea-pre-upgrade-2026-08-30 # o, para binario nativo sudo tar czf /var/backups/gitea-data-2026-08-30.tgz /var/lib/gitea ```

Si tu instancia estuvo expuesta a internet con registro abierto antes del parche, trátala como potencialmente comprometida. Rota tokens de API, secretos de base de datos, claves SSH de runners, webhooks y credenciales OAuth. La rotación no es opcional; el incidente de Habr documentó persistencia limitada precisamente porque los secretos se rotaron a tiempo.

Una checklist operativa mínima para esa rotación incluye: claves SSH desplegadas desde los repos (deploy keys, una por servicio), tokens de acceso al registro de contenedores si Gitea hace de registry, secretos cifrados en Actions de Gitea, webhooks salientes con tokens Bearer, credenciales OAuth contra GitHub o GitLab si usas mirror, y la contraseña de la base de datos interna. La contraseña de admin de Gitea también entra en esta rotación si tu instalación usa autenticación local. Auditar el log de Actions es especialmente importante: un atacante con RCE puede crear un workflow nuevo que se ejecute automáticamente y ejecute código en runners.

Endurecimiento recomendado

El parche cierra el vector actual, pero la superficie de Gitea merece atención sostenida. Cuatro controles marcan la diferencia.

Primero, desactiva el registro abierto. En `app.ini`, dentro de `[service]`, pon `DISABLE_REGISTRATION = true`. Si necesitas permitir registros, configura `ALLOW_ONLY_INTERNAL_REGISTRATION = true` y exige confirmación por correo con `ENABLE_NOTIFY_MAIL = true`. Así eliminas el camino "visitante anónimo crea cuenta y repositorio".

Segundo, restringe los hooks. En `[repository]`, `DISABLE_HOOKS = true` si tu flujo no depende de webhooks de Git. Si los necesitas, configura `DISABLE_GIT_HOOKS = true` para que el panel web no permita instalar hooks personalizados; deja que la administración de hooks pase por configuración centralizada en el servidor.

Tercero, segmenta la red. Gitea no necesita hablar con internet para hacer su trabajo. En Docker, una red interna sin egress:

``` networks: internal: internal: true ```

Si necesitas clones de repos remotos, haz proxy mediante un runner dedicado con egress controlado. Bloquear el acceso saliente directo del contenedor de Gitea cierra la mayoría de cadenas de RCE-a-minero, porque el atacante pierde la ruta de descarga del payload.

Cuarto, vigila la exposición. Shadowserver lleva publicando diariamente el número de IPs con instancias vulnerables. Apunta el dato y compáralo con tu propia exposición. Una instancia de Gitea solo debería ser accesible desde tu VPN corporativa o desde un reverse proxy autenticado, nunca directamente en el 3000 público. Configura fail2ban o equivalente contra los endpoints de login y registro.

El aprendizaje de fondo

CVE-2026-60004 no es un bug exótico. Es un recordatorio de cómo se rompe la cadena de confianza cuando una API con permisos altos acepta entrada no saneada de un usuario con permisos bajos. El patrón "endpoint que materializa archivos en rutas estructurales de Git con un modo de aplicación que permite escapes de path" lleva años documentado en la literatura de seguridad de Git. La PoC es pública, el rango de versiones afectadas es enorme, y la configuración por defecto multiplica el riesgo. Si tu organización usa Gitea, hoy mismo es un buen día para comprobar la versión, auditar hooks, rotar secretos y bloquear el registro abierto.