CVE-2022-30781:一条普通的Git命令导致的Gitea RCE

CVE-2022-30781:一条普通的Git命令导致的Gitea RCE

今年年初的时候,我挖掘到了一枚 Gogs 中因为未对用户可控的目录路径进行检测,从而导致后续路径拼接可以导致目录穿越的漏洞(CVE-2022-0415).

攻击者能上传覆盖环境中的任意文件,在覆盖任意文件后,我使用的是之前 CVE-2019-11229 中提到的方法,覆盖一个 Git 仓库中 .git/config 文件,设置 core.sshCommand 参数从而达到远程任意命令执行。

一直以来我都十分欣赏这个漏洞,因为它给人畜无害的 Git 传入了恶意的配置,就能导致命令执行。类似的还有 curl,前阵子做到过一道 CTF 题,在环境变量可控的情况下,可以使用 curl 来覆盖文件,同样也十分精彩。

那么,既然 Gogs 被我们 RCE 了,那基于 Gogs 代码分叉出去的 Gitea,是否也存在调用 Git 时,传入恶意参数导致命令执行的问题呢?这,就是这篇文章要讲述的。

寻找攻击点

CVE-2022-30781:一条普通的Git命令导致的Gitea RCE

Gitea 是一个前后端不分离的项目,很多操作还是通过 POST 表单提交。我刚开始审计 Gitea 项目时,打算先集中看一遍它的输入,因此选择先从 Gitea API 入手。通过点击 Gitea 页面右下角的 「API」即可看到一个用 Swagger 搭建的 API 文档。网页上通过表单提交的操作,在这里基本可以找到与之对应的 RESTful API。

第一个 admin 是管理员的操作,肯定有个中间件鉴权,纵使后面有洞也会被前面的中间件给拦了,优先级靠后,先跳过。第二个 miscellaneous 是一对杂项功能,基本不涉及啥复杂的交互,也先跳过…… 之后一连串的看下去,都是些简单的 CRUD 操作,寻思也写不出啥洞,我也懒得去看。😅
而后当点开 repository 选项卡,第一个接口是:

POST /repos/migrate Migrate a remote git repository

诶~ 这个好像有点意思,迁移远端的仓库过来,那肯定是要请求给定的远端仓库 URL,说不定保底就是个 SSRF。展开看接口传入的 JSON 内容,其中包含远端仓库的 URL、是否迁移 Issues、Pull Request、Releases、LFS 等数据。联想到我之前挖的 Gitea 任意文件删除漏洞就是在处理 LFS 文件这里,说不定这里从远端迁移 LFS 文件也会存在类似路径穿越的问题?
带着这个猜想,我去看了 Gitea Migration 部分的代码,不看不知道,一看才发现这功能是个筛子。

Gitea Migration

Gitea 的 Migration 迁移功能由两部分组成,Downloader 与 Uploader,对应到代码中分别是 migration.Downloader 与 migration.Uploader 两个接口。前者负责从远端的仓库服务下载仓库信息,后者负责将信息打入到 Gitea 中。
目前 Downloader 支持从 GitHub、Gitlab、GitBucket、Gogs、Gitea 等服务导入代码,你可以在 services/migrations 目录下看到对这些平台的 Downloader 接口实现。一般都是调这些服务的 API 来获取托管在其上面仓库的 Issue、Pull Request、Releases 等信息。而 Uploader 的实现只有一个,那就是 Gitea,因为我们最终只会将远端仓库迁移至本 Gitea 实例中。

在 services/migrations/migrate.go#migrateRepository 是迁移一个远端仓库所要进行的步骤。在给函数传入了对应的 Downloader 和 Uploader 后,它将依次做如下操作:

调用的接口方法说明
downloader.GetRepoInfo获取远端仓库基本信息
downloader.FormatCloneURL获取远端仓库 Git Clone 地址
uploader.CreateRepo创建本地仓库
downloader.GetTopics uploader.CreateTopics获取远端仓库 Topic + 创建本地仓库 Topic
downloader.GetMilestones uploader.CreateMilestones获取远端仓库里程碑 + 创建本地仓库里程碑
downloader.GetLabels uploader.CreateLabels获取远端仓库标签 + 创建本地仓库标签
downloader.GetReleases uploader.CreateReleases获取远端仓库 Release 版本 + 创建本地仓库 Release 版本
downloader.GetIssues uploader.CreateIssues获取远端仓库 Issue + 创建本地仓库 Issue
downloader.GetComments uploader.CreateComments获取远端仓库评论 + 创建本地仓库评论
downloader.GetPullRequests uploader.CreatePullRequests获取远端仓库 Pull Request + 创建本地仓库 Pull Request
downloader.GetReviews uploader.CreateReviews获取远端仓库 Code Review + 创建本地仓库 Code Review

可以看到,仓库迁移的操作就是把信息使用 Downloader 下载回来,然后 Uploader 给存储到本地,这样成对的一来一回。
由于 GitHub、Gitlab、GitBucket 这些属于第三方的 SaaS,我们对其 API 返回的内容并不是完全可控的,因此我将目光瞄准了从 Gogs 和 Gitea 迁移。而 Gitea 的 Downloader 的功能相比 Gogs 的多,当 Gitea 要从另一个 Gitea 实例迁移仓库时,它将请求远端 Gitea 实例的 API,来得知该仓库的名称、Issue、Pull Request、Releases 文件等。
我们试想是否可以伪造一个 Gitea 实例,说白了就是伪造这么一套 Gitea API,让当前 Gitea 实例在迁移仓库时去请求我们伪造的 Gitea API 服务,从中传入一些恶意参数看看能不能搞事情。

经过一个通宵的审计加 @Li4n0 的协助,我们终于发现了一枚远程命令执行漏洞。它从恶意的 Gitea 实例读取精心构造的参数后,拼接进正常的 Git 命令,从而导致了远程命令执行。我们形象地将其称之为:Git 投毒(Git Poison)。

Git 投毒

漏洞点出现在对 Pull Request 的数据迁移上,调用链如下:

services/migrations/migrate.go:L376#uploader.CreatePullRequests
services/migrations/gitea_uploader.go:L466#g.newPullRequest
services/migrations/gitea_uploader.go:L602#g.updateGitForPullRequest

出现漏洞的代码块在 services/migrations/gitea_uploader.go:L531-L567 处,精简后的代码如下:

if pr.IsForkPullRequest() && pr.State != "closed" {
        if pr.Head.OwnerName != "" {
            remote := pr.Head.OwnerName
            _, ok := g.prHeadCache[remote]
            if !ok {
                err := g.gitRepo.AddRemote(remote, pr.Head.CloneURL, true)
                if err != nil {
                    ...
                } else {
                    ok = true
                }
            }

            if ok {
                _, err = git.NewCommand(g.ctx, "fetch", remote, pr.Head.Ref).RunInDir(g.repo.RepoPath())
                ...
            }
        }
} 

当远端存在来自 Fork 仓库提交的 Pull Request 请求,且该 PR 状态不为 Close 时,会进入该分支。
这里有一个 Map g.prHeadCache 作为临时缓存。第一次进入时该缓存为空,检测到 remote 的值不在 g.prHeadCache 中,调用 g.gitRepo.AddRemote 方法,该方法执行命令:

git remote add -f <remote> <pr.Head.CloneURL>

该命令正常执行,无错误抛出后,便将ok 设置成 true。到下方执行命令:

git fetch <remote> <pr.Head.Ref>

当我们选择从远端 Gitea 实例执行迁移时,上述 remote pr.Head.CloneURL pr.Head.Ref 参数均取自远端 Gitea Web API 响应中,均是可控的。因此只需要构造一个 HTTP 服务模拟 Gitea Web API 返回响应,以上的三个参数将从响应中获取。

Git --upload-pack 参数

虽然上述两个命令中的三个参数都可控,但情况并不乐观:

  1. 两条指令分别是 git remote add 和 git fetch,我们仅能控制其参数。
  2. 第二条命令执行的条件是需要保证第一条命令执行成功。

第一个限制,也是这个漏洞的难点所在。在翻阅了 Git 文档后,Li4n0 发现 Git 的 fetch 子命令中存在 --upload-pack 这个参数。根据官方文档,当 --upload-pack 被指定时,其仓库拉取操作将使用 git fetch-pack --exec=<upload-pack> 替代。而 git fetch-pack 中的 --exec 参数同 --upload-pack 参数,用于指定远端 git-upload-pack 命令执行的路径。

而如果我们设置远端 Git 仓库的路径为一个本地的仓库,则对于这个仓库来说,客户端是当前 Gitea 实例,远端服务端也是当前 Gitea 实例机器上的一个目录。因此便会在当前 Gitea 实例所在的机器上执行命令。

因此, git remote add 中<pr.Head.CloneURL> 需填入一个本地的 Git 仓库地址。根据 Git 官方文档的描述,Git 支持 file ssh http 三种协议来获取 Git 仓库,本地仓库选择 file 协议。经过测试,如果使用 file://<path> 这种方式,需传入仓库完整的绝对路径。而我们无法得知线上 Gitea 实例的部署情况,自然不知道其绝对路径。同样在查看 Git 官方文档并测试后,我们发现这里不使用 file 协议头,直接输入仓库的相对路径也是可行的。当前两条git命令就是在一个 Git 仓库下执行的,因此直接传入./ 即可。(也可以使用 file 协议头传入绝对路径 /proc/self/cwd/ 来软链接指向当前 Git 命令的运行目录)

对于第二个限制,可以注意到两行命令均用到了 <remote> 变量。 若将 <remote> 变量设置成 --upload-pack 参数,因为 git remote 命令中无该参数,第一条命令会执行失败,第二条命令便不再会被执行。因此要将第二行命令中的 <pr.Head.Ref> 设置成 --upload-pack 参数,<remote> 设置成任意合法的名称,如 origin

即最终执行的两条命令就是:

git remote add -f origin ./

git fetch origin --upload-pack=bash -c '<cmd>'

综上,搭建一个 HTTP 服务并配置以下路由,来伪装成一个 Gitea 实例,响应体可以从一个正常 Gitea 的 API 中截取。

/api/v1/version
/api/v1/settings/api
/api/v1/repos/<owner>/<repo>/
/api/v1/repos/<owner>/<repo>/topics
/api/v1/repos/<owner>/<repo>/pulls
/api/v1/repos/<owner>/<repo>/issues/1/reactions
/api/v1/repos/<owner>/<repo>/pulls/2/reviews

在 /api/v1/repos/<owner>/<repo>/pulls/2/reviews 路由的响应 JSON 中,修改对应字段控制上文提到了三个字段的值,其中 <cmd> 为执行的命令:

[0].head.ref: --upload-pack=bash -c '<cmd>'
[0].head.repo.clone_url: ./
[0].head.owner.login: <username>

登录 Gitea 实例,右上角点击「+」-> 「迁移外部仓库」->「Gitea」,在 「从 URL 迁移/克隆」 中填入上文搭建的伪装 Gitea 实例地址,执行迁移操作,代码便会被执行。

具体的 Exploit 见:https://github.com/wuhan005/CVE-2022-30781

最后聊几句

其实上面提到的这个只是 Gitea Migration 里杀伤力最大的一个漏洞,比这影响范围小的漏洞还有几个,这些大家可以自己去发掘下。
这个漏洞也正是我在文章开头提到的,给我们日常使用的程序传入恶意的配置或子命令,从而导致任意命令执行。如果开发人员不了解相关的 Trick,那么在调用第三方程序时就会很容易写出类似的漏洞,可谓防不胜防。

时间线

  • 2022-04-16 发现漏洞
  • 2022-04-18 完成 Exploit 编写
  • 2022-04-25 向 Gitea 官方上报漏洞信息
  • 2022-04-26 Gitea 官方回复漏洞已确认,将在 v1.16.7 版本中修复
  • 2022-05-02 Gitea 发布 v1.16.7 版本,漏洞被修复
  • 2022-05-16 下发 CVE 编号:CVE-2022-30781

CVE-2022-30781 exp

Gitea 存储库迁移远程命令执行漏洞。

https://github.com/wuhan005/CVE-2022-30781

exp使用方法:

  1. 使用此存储库中的文件运行 HTTP 文件系统服务器。
  2. 编辑要在api/v1/repos/e99/exp/pulls/1/index.html L96中执行的命令。
  3. http://<your_host>/e99/exp使用Gitea 实例上的URL 迁移远程存储库。

exp下载地址:

https://www.yunzhongzhuan.com/#sharefile=SqYGLMAN_53701
解压密码:www.ddosi.org

from

转载请注明出处及链接

Leave a Reply

您的电子邮箱地址不会被公开。 必填项已用*标注