Contents

Golang安全问题分享

一些Golang的安全问题。

前言

Golang 是一门强类型的语言,在代码中不使用 cgo 的情况下默认使用静态编译,在编译过程中就能杜绝许多安全问题,因此 Golang 会出现的一些安全问题经常是因为依赖库中的issues和开发者自身操作不当。本文将分享几个 Golang 开发中容易忽视的安全问题。

整数溢出

无符号整型的溢出:

1
2
3
4
5
6
7
8
9
func main() {
	var a1 uint16 = 65535
	var b1 uint16 = 1
	var sum1 = a1 + b1
	fmt.Println(reflect.TypeOf(sum1))
	fmt.Println(sum1)
	//uint16
	//0
}

如果开发者定义时直接赋值一个大小已经溢出的数,编译器会编译不通过

https://leonsec.gitee.io/images/image-20210901172926828.png

有符号数的溢出也是一样:

1
2
3
4
5
6
7
8
9
func main() {
	var a2 int8 = 127
	var b2 int8 = 1
	var sum2 int8 = a2 + b2
	fmt.Println(reflect.TypeOf(sum2))
	fmt.Println(sum2)
	//int8
	//-128
}

可以看到对127加1超出了int8的范围,翻转为了-128

$GOROOT/src/math/const.go文件可以看到对范围的定义

https://leonsec.gitee.io/images/image-20210901174316176.png

在类型转换中,也会出现较大整型向较小整型转换的截断问题,Golang 是一种强类型语言,但是 Golang 提供了类型强制转换的功能,来跳过该限制

如:

1
2
3
4
5
6
func main() {
	var a3 int16 = 256
	var b3 = int8(a3)
	fmt.Println(b3)
	//0
}

旧版本的kubectl命令行中出现了一个strconv.Atoi导致的截断问题。当我们传入port参数的对应字符串后,容器启动的端口这一参数会将经Atoi处理后的字符串进行int32的类型转换。由于64位系统的int是int64类型。转int32的话会出现明显截断:

1
2
3
4
v , _ := strconv.Atoi("4294967377")
s  := int32(v)
fmt.Println(s)
// 81

这样就有可能导致81端口的服务启动,或者被停止。

小结:

溢出问题一般会出现在开发者对数进行加减乘除操作等其他转换操作,如果开发者没有注意该整数定义的类型,对其操作超过了范围,就会出现无法预料的bug或者是安全漏洞

伪随机数

伪随机数,是使用一个确定性的算法计算出来的似乎是随机的数序,因此伪随机数实际上并不随机,只要用于计算的种子值相同,计算出来的伪随机数就相同,所以正常使用中开发者一般会使用当前时间作为种子值,但是仍然存在被猜测爆破的风险

1
2
3
4
5
6
7
func main() {
	fmt.Println(rand.Int())
}

//5577006791947779410

//https://play.golang.org/ => 5577006791947779410

我们在https://golang.google.cn/pkg/math/rand/#Seed可以看到,Golang 官方库math/rand生成种子值默认为1,如果用户不指定种子值,则默认使用1作为种子

https://leonsec.gitee.io/images/image-20210902103330673.png

如果开发者使用的种子值信息泄漏了,也会被恶意攻击者利用导致一些安全问题,例如gin框架中开发者使用伪随机数作为cookie的key值,如果种子值被泄漏,则会导致攻击者本地生成cookie,越权攻击

小结:

在开发中使用伪随机数时应注意种子值的修改,如果我们的应用对安全性要求比较高,需要使用真随机数的话,可以使用crypto/rand 包中的方法

Windows 非可信目录路径查找

CVE-2021-3115

该漏洞是由日本安全研究人员RyotaK发现的,属于命令注入漏洞。漏洞产生的根本原因是用户运行go get命令来提取库时编译过程的工作原理造成的。

在Windows 命令窗口输入netstat,Windows就会在当前目录中搜索netstat.exe、netstat.bat或其他netstat.*可执行文件,这些搜索到的文件会优先执行,如果当前目录中不存在 netstat 相关的可执行文件,那么Windows shell就会查找 netstat 系统工具,该系统工具位于 Windows %PATH% 变量中。

为了保持一致性,Golang 二进制文件在Unix 系统中效仿了Unix 规则,在Windows 系统中效仿了Windows 规则。也就是说,运行下面的命令在Unix系统和Windows 系统中会产生不同的行为:

1
out, err := exec.Command("go", "version").CombinedOutput() 

在Windows 系统中,本地的Go二进制文件优先级高,而Unix 系统中会首先搜索$PATH变量来确认其可信位置中是否存在 Go 二进制文件。

这种优先本地、非可信目录的方法也在Go helper和编译器中实现了,比如为调用C语言代码生成Go包的工具:cgo。

Cgo在Windows系统中编译C代码时,Golang可执行文件首先会在非可信的本地目录中搜索GCC编译器。Cgo运行的go命令会包含包资源。因此,攻击者可以利用cgo来启动恶意的gcc.exe程序。

SSTI

Golang的html/template包提供模板渲染的功能,当源码中出现用户可控且直接拼接渲染时会出现模板注入的漏洞,轻则造成信息泄漏,严重的可能会出现rce等

一个demo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package main

import (
	"encoding/base64"
	"fmt"
	"html/template"
	"io/ioutil"
	"net/http"
)

type User struct {
	ID       int
	Email    string
	Password string
	Image    string
}

func ShowImg(img string) string {
	b, err := ioutil.ReadFile(img)
	if err != nil {
		fmt.Print(err)
	}
	imgb64 := base64.StdEncoding.EncodeToString(b)
	return imgb64
}

func main() {
	var u = &User{1, "test@gmail.com", "test#123", "test.jpg"}

	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		var tmpl = fmt.Sprintf(`
<html>
<head>
<title>SSTI</title>
</head>
<h1>SSTI Research</h1>
<h2>No search results for %s</h2>
<h2> Hi {{ .Email }}<h2>
<img src="data:image/png;base64,{{showimg .Image}}"/>
</html>`, r.URL.Query()["q"][0])

		t, err := template.New("page").Funcs(template.FuncMap{"showimg": ShowImg}).Parse(tmpl)

		if err != nil {
			fmt.Println(err)
		}

		t.Execute(w, &u)
	})
	http.ListenAndServe(":10000", nil)
}

这里我实现了一个函数func ShowImg(img string)用于动态展示图片,并且模板渲染的参数直接拼接且用户可控,所以这里会造成任意文件读取漏洞,所以开发时要注意自定义的渲染函数可能存在被利用的情况

https://leonsec.gitee.io/images/image-20210923120912802.png

我们还可以利用{{.}}来泄漏信息:

https://leonsec.gitee.io/images/image-20210923120851226.png

除了这些,当存在SSTI时,我们还可以进行XSS:

参考:https://blog.takemyhand.xyz/2020/05/ssti-breaking-gos-template-engine-to.html

1
{{define "T1"}}<script>alert(1)</script>{{end}} {{template "T1"}}

https://leonsec.gitee.io/images/image-20210923121521542.png

net

在官方issues中,net相关库的安全问题比较多,这里找几个典型的在开发中容易忽视的安全问题介绍

CRLF注入

CVE-2019-9741

issues: https://github.com/golang/go/issues/30794

测试代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//go version go1.11.5
package main

import (
        "fmt"
        "net/http"
)

func main() {
        client := &http.Client{}
        host := "172.17.0.1:20000?a=1 HTTP/1.1\r\nX-injected: header\r\nTEST: 123"
        // host := "127.0.0.1"
        url := "http://" + host + ":8080/test/?test=a"
        request, err := http.NewRequest("GET", url, nil)
        if err != nil {
                fmt.Println(err.Error())
        }
        resp, err := client.Do(request)
        if err != nil {
                fmt.Println(err.Error())
        }
        resp.Body.Close()
}

监听接收结果:

1
2
3
4
5
6
7
8
9
$ nc -lvnp 20000
Listening on 0.0.0.0 20000
Connection received on 172.17.0.2 48140
GET /?a=1 HTTP/1.1
X-injected: header
TEST: 123:8080/test/?test=a HTTP/1.1
Host: 172.17.0.1:20000
User-Agent: Go-http-client/1.1
Accept-Encoding: gzip

可以发现我们利用\r\n换行成功控制注入到header字段,我们可以利用此攻击内网中的redis、memcached等应用

这个漏洞只在特定版本存在,开发时注意版本即可

header CRLF注入

issues: https://github.com/golang/go/issues/47711

碰巧注意到最近有人在issues提交了header中允许换行符的安全问题,golang官方给其添加上了security标签,但是实际上我觉得在实战中几乎不会出现header头用户可控的情况,来看看demo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
func main() {
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Foo", "Bar")
		w.Header().Set("Xxx: xxxx\nSmuggle", "smuggleval")
	}))
	res, err := http.Get(ts.URL)
	if err != nil {
		log.Fatal(err)
	}
	res.Write(os.Stdout)
	fmt.Printf("%v", res.Header)
}

// HTTP/1.1 200 OK
// Date: Thu, 02 Sep 2021 09:07:13 GMT
// Foo: Bar
// Smuggle: smuggleval
// Xxx: xxxx
// Content-Length: 0

// map[Content-Length:[0] Date:[Thu, 02 Sep 2021 09:07:13 GMT] Foo:[Bar] Smuggle:[smuggleval] Xxx:[xxxx]]

提交者给了一个示例,使用两个换行导致其余的header变成body部分,再加上构造的xss payload,可以逃逸header到body触发xss

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		headerName := r.URL.Query().Get("name")
		headerValue := r.URL.Query().Get("value")
		w.Header().Set(headerName, headerValue)
		fmt.Fprintf(w, "Hello World")
	})

	fmt.Printf("Starting server at port 3000\n")
	if err := http.ListenAndServe(":3000", nil); err != nil {
		log.Fatal(err)
	}
}

//http://localhost:3000/?name=%0a%0a%3Chtml%3E%3Cscript%3Ealert(%27have%20fun%27)%3C/script%3Easd&value=a%0aasd

官方的修复方案是给header加了过滤:

https://leonsec.gitee.io/images/image-20210914095103952.png

ip前导0解析问题

CVE-2021-29923

issues: https://github.com/golang/go/issues/30999

影响版本:Golang < 1.17

近日,研究人员在DEF CON大会介绍了Go中的net模块安全漏洞。漏洞CVE编号为CVE-2021-29923,漏洞产生的原因是net处理混合格式的IP地址方式上存在问题,即当数字IPv4地址中以0开头时会触发漏洞。

在net库中,所有IP地址开头的0都会被移除和丢弃。根据IETF的原始说明,如果IPv4地址的前缀有0,那么可以理解为是八进制。但是Golang的net模块忽略了这一点,并将其作为十进制数来处理。

因此,如果开发者使用net库来验证IP地址是否属于某个特定的范围,比如访问控制列表ACL中的IP列表,结果可能就会出现错误。

我们先来看看ping程序对含有前导0的ip解析:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ ping -c 4 0177.0.0.1
PING 0177.0.0.1 (127.0.0.1): 56 data bytes
64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.084 ms
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.106 ms
64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.158 ms
64 bytes from 127.0.0.1: icmp_seq=3 ttl=64 time=0.081 ms

--- 0177.0.0.1 ping statistics ---
4 packets transmitted, 4 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 0.081/0.107/0.158/0.031 ms

可以发现,ping程序将0177作为八进制,转换为十进制后就是127,所以最后解析为127.0.0.1

再来看 Golang 对含有前导0的处理demo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func main() {
	var input = "0127.0.0.1"
	var addr = net.ParseIP(input)
	fmt.Println("input: ", input, " result: ", addr)

	client := &http.Client{
		Timeout: 2 * time.Second,
	}

	req, err := http.NewRequest("GET", "http://"+input, nil)
	if err != nil {
		fmt.Println(err)
	}
	resp, err := client.Do(req)
	if err != nil {
		fmt.Println(err.Error())
	}
	defer resp.Body.Close()
}

// input:  0127.0.0.1  result:  127.0.0.1

// Get "http://0127.0.0.1": dial tcp 127.0.0.1:80: connect: connection refused

Goalng net/http库中对于ip解析或请求的函数都受其影响

可以看到,Golang对前导0直接采用忽略的方式,而ping将其当作八进制处理,这样的差异可能会导致一些安全问题

FileServer对Range头错误解析

issues: https://github.com/golang/go/issues/40940

影响版本:Golang < 1.16

net/http包中提供了展示静态资源的文件服务器FileServerRange头经常用于断点续传技术中,但是Range是有一定范围和规范的,如果未对传入的Range进行验证,可能会导致不可预料的安全问题

在 Golang 中启动一个静态文件服务器的demo:

1
2
3
4
5
6
7
8
package main

import "net/http"

func main() {
  http.Handle("/", http.FileServer(http.Dir("/tmp")))
  http.ListenAndServe(":3000", nil)
}

加上不符合规则的Range头后请求:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
$ curl -v -H 'Range: bytes=--2' localhost:8081/go          
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8081 (#0)
> GET /go HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/7.64.1
> Accept: */*
> Range: bytes=--2
> 
< HTTP/1.1 206 Partial Content
< Accept-Ranges: bytes
< Content-Range: bytes 475-472/473
< Content-Type: text/plain; charset=utf-8
< Last-Modified: Wed, 22 Sep 2021 07:01:01 GMT
< Date: Wed, 22 Sep 2021 07:22:05 GMT
< Transfer-Encoding: chunked
< 
* Connection #0 to host localhost left intact
* Closing connection 0

可以看到服务端返回了正常响应,错误的解析了Range头Content-Range: bytes 475-472/473,并没有报任何错误,而正常来说对于错误的Range头返回应该是这样的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
$ curl -v -H 'Range: bytes=a' localhost:8081/go            
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8081 (#0)
> GET /go HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/7.64.1
> Accept: */*
> Range: bytes=a
> 
< HTTP/1.1 416 Requested Range Not Satisfiable
< Content-Type: text/plain; charset=utf-8
< Last-Modified: Wed, 22 Sep 2021 07:01:01 GMT
< X-Content-Type-Options: nosniff
< Date: Wed, 22 Sep 2021 07:38:11 GMT
< Content-Length: 14
< 
invalid range
* Connection #0 to host localhost left intact
* Closing connection 0

解析Range头的实现在$GOROOT/src/net/http/fs.goparseRange函数

parseRange函数获取Range标题,从中删除bytes=前缀并将其用","字符拆分。稍后,对于每个拆分字符串或更确切地说是“范围”,它会将其进一步对"-"进行拆分,然后检索startend值。

当只有一个值时,start是一个空字符串。然后程序转到第一个分支并end通过ParseInt(end, 10, 64)调用解析值。

举个例子,如果我们传递bytes=--2Range 标头,我们最终会得到start="", end="-2"字符串,然后end通过ParseInt(end, 10, 64)调用-2int64 值来解析。

官方的修复是在调用ParseInt函数前加上判断是否为负数,不符合规则就报错

https://leonsec.gitee.io/images/image-20210922160221383.png

由此issue还衍生出了一个题目:https://ctftime.org/task/14632

题目实现了一个静态资源服务,利用go-FileServer对Range头的解析差异进行攻击。

HTTP/2拒绝服务漏洞

CVE-2019-9512 && CVE-2019-9514

issues: https://github.com/golang/go/issues/33606

net/httpgolang.org/x/net/http2接受来自不可信的客户端直接连接的服务器可以远程作出分配的内存无限量,直到程序崩溃。

这是一个HTTP/2的自身漏洞,攻击者可能会导致服务器排队无限数量的 PING、ACK 或 RST_STREAM 帧通过请求它们而不读取它们,直到程序内存不足导致拒绝服务,在Golang的某些特定版本中会出现。

HTTP_PROXY安全问题

CVE-2016-5386

httpoxy是一组影响在 CGI 或类 CGI 环境中运行的应用程序代码的漏洞。

详细参考:https://httpoxy.org/

漏洞成因:

  • CGI 规范定义传入请求标头Foo: Bar映射到环境变量 HTTP_FOO == "Bar"。(见 RFC 3875 4.1.18)
  • HTTP_PROXY 环境变量通常用于为 HTTP 客户端配置 HTTP 代理(Go 的 net/http.Client 和 Transport 默认遵守)

这意味着在 CGI 环境中运行的 Go 程序(作为 CGI 主机下的子进程)容易受到包含Proxy:attacker.com:1234的传入请求的攻击,设置 HTTP_PROXY,并更改默认情况下 Go 代理所有出站 HTTP 请求的代理。

官方修复是直接弃用了CGI模式下的HTTP_PROXY标头的设置:

https://leonsec.gitee.io/images/image-20210922170545971.png

官方的poc:https://github.com/httpoxy/go-httpoxy-poc

SMTP注入

issues: https://github.com/golang/go/issues/21159

影响版本:Golang < 1.10

在受影响的版本中,net/smtp 库中的一些函数对接收者邮件地址未做过滤处理,导致可以利用CRLF注入进行修改邮件正文,一个简单的demo如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package main

import (
	"log"
	"net/smtp"
)

func main() {
	to := []string{"recipient@example.net>\r\nDATA\r\nInjected message body\r\n.\r\nQUIT\r\n"}
	msg := []byte("To: recipient@example.net\r\n" +
		"Subject: discount Gophers!\r\n" +
		"\r\n" +
		"Real message body\r\n")
	err := smtp.SendMail("localhost:25", nil, "sender@example.org", to, msg)
	if err != nil {
		log.Fatal(err)
	}
}

官方的修复是对SendMailRcptMail等受影响的函数增加了换行符过滤:

https://leonsec.gitee.io/images/image-20210922185131263.png

ReadRequest栈溢出

CVE-2021-31525

issues: https://github.com/golang/go/issues/45710

ReadRequest 和 ReadResponse 在读取非常大的标头(在 64 位架构上超过 7MB,或在 32 位架构上超过 4MB)时会遇到不可恢复的panic。

服务默认情况下不易受到攻击,但如果通过设置Server.MaxHeaderBytes为更高的值来覆盖默认的最大标头 1MB,则会受到堆栈溢出导致崩溃或拒绝服务。

CGI/FCGI XSS漏洞

CVE-2020-24553

issues: https://github.com/golang/go/issues/40928

当处理程序没有明确设置 Content-Type 标头时,CGI 和 FCGI 实现都默认为text/html

如果攻击者可以让服务器在他们的控制下生成内容(例如包含用户数据的 JSON 或上传的图像文件),这可能会被服务器错误地返回为text/html。如果受害者访问这样的页面,他们可能会在服务器源的上下文中执行攻击者的恶意JavaScript代码。

利用参考:https://www.redteam-pentesting.de/en/advisories/rt-sa-2020-004/-inconsistent-behavior-of-gos-cgi-and-fastcgi-transport-may-lead-to-cross-site-scripting

https://leonsec.gitee.io/images/image-20210923110743421.png

Reference

https://github.com/golang/go/issues/34540

https://github.com/golang/go/issues/40928

https://github.com/golang/go/issues/43783

https://github.com/golang/go/issues/12741

https://github.com/golang/go/issues/47711

https://github.com/golang/go/issues/30794

https://github.com/golang/go/issues/30999

https://github.com/golang/go/issues/40940

https://github.com/golang/go/issues/33606

https://github.com/golang/go/issues/16405

https://github.com/golang/go/issues/21159

https://github.com/golang/go/issues/45710

https://httpoxy.org/

https://www.bleepingcomputer.com/news/security/google-fixes-severe-golang-windows-rce-vulnerability/