目录
回合一:千变万化的内网地址
回合二:URL跳转
回合三:DNS Rebinding
首页 后端开发 Golang Go中的SSRF攻防战

Go中的SSRF攻防战

Jul 24, 2023 pm 02:05 PM
go ssrf

什么是SSRF

SSRF英文全拼为Server Side Request Forgery,翻译为服务端请求伪造。攻击者在未能取得服务器权限时,利用服务器漏洞以服务器的身份发送一条构造好的请求给服务器所在内网。关于内网资源的访问控制,想必大家心里都有数。

Go中的SSRF攻防战

上面这个说法如果不好懂,那老许就直接举一个实际例子。现在很多写作平台都支持通过URL的方式上传图片,如果服务器对URL校验不严格,此时就为恶意攻击者提供了访问内网资源的可能。

“千里之堤,溃于蚁穴”,任何可能造成风险的漏洞我们程序员都不应忽视,而且这类漏洞很有可能会成为别人绩效的垫脚石。为了不成为垫脚石,下面老许就和各位读者一起看一下SSRF的攻防回合。

回合一:千变万化的内网地址

为什么用“千变万化”这个词?老许先不回答,请各位读者耐心往下看。下面,老许用182.61.200.7(www.baidu.com的一个IP地址)这个IP和各位读者一起复习一下IPv4的不同表示方式。

Go中的SSRF攻防战

注意⚠️:点分混合制中,以点分割地每一部分均可以写作不同的进制(仅限于十、八和十六进制)。

上面仅是IPv4的不同表现方式,IPv6的地址也有三种不同表示方式。而这三种表现方式又可以有不同的写法。下面以IPv6中的回环地址0:0:0:0:0:0:0:1为例。

Go中的SSRF攻防战

注意⚠️:冒分十六进制表示法中每个X的前导0是可以省略的,那么我可以部分省略,部分不省略,从而将一个IPv6地址写出不同的表现形式。0位压缩表示法和内嵌IPv4地址表示法同理也可以将一个IPv6地址写出不同的表现形式。

讲了这么多,老许已经无法统计一个IP可以有多少种不同的写法,麻烦数学好的算一下。

内网IP你以为到这儿就完了嘛?当然不!不知道各位读者有没有听过xip.io这个域名。xip可以帮你做自定义的DNS解析,并且可以解析到任意IP地址(包括内网)。

Go中的SSRF攻防战

我们通过xip提供的域名解析,还可以将内网IP通过域名的方式进行访问。

关于内网IP的访问到这儿仍将继续!搞过Basic验证的应该都知道,可以通过http://user:passwd@hostname/进行资源访问。如果攻击者换一种写法或许可以绕过部分不够严谨的逻辑,如下所示。

Go中的SSRF攻防战

关于内网地址,老许掏空了所有的知识储备总结出上述内容,因此老许说一句千变万化的内网地址不过分吧!

此时此刻,老许只想问一句,当恶意攻击者用这些不同表现形式的内网地址进行图片上传时,你怎么将其识别出来并拒绝访问。不会真的有大佬用正则表达式完成上述过滤吧,如果有请留言告诉我让小弟学习一下。

花样百出的内网地址我们已经基本了解,那么现在的问题是怎么将其转为一个我们可以进行判断的IP。总结上面的内网地址可分为三类:一、本身就是IP地址,仅表现形式不统一;二、一个指向内网IP的域名;三、一个包含Basic验证信息和内网IP的地址。根据这三类特征,在发起请求之前按照如下步骤可以识别内网地址并拒绝访问。

  1. 解析出地址中的HostName。

  2. 发起DNS解析,获得IP。

  3. 判断IP是否是内网地址。

上述步骤中关于内网地址的判断,请不要忽略IPv6的回环地址和IPv6的唯一本地地址。下面是老许判断IP是否为内网IP的逻辑。

// IsLocalIP 判断是否是内网ip
func IsLocalIP(ip net.IP) bool {
if ip == nil {
return false
	}
// 判断是否是回环地址, ipv4时是127.0.0.1;ipv6时是::1
if ip.IsLoopback() {
return true
	}
// 判断ipv4是否是内网
if ip4 := ip.To4(); ip4 != nil {
return ip4[0] == 10 || // 10.0.0.0/8
			(ip4[0] == 172 && ip4[1] >= 16 && ip4[1] <= 31) || // 172.16.0.0/12
			(ip4[0] == 192 && ip4[1] == 168) // 192.168.0.0/16
	}
// 判断ipv6是否是内网
if ip16 := ip.To16(); ip16 != nil {
// 参考 https://tools.ietf.org/html/rfc4193#section-3
// 参考 https://en.wikipedia.org/wiki/Private_network#Private_IPv6_addresses
// 判断ipv6唯一本地地址
return 0xfd == ip16[0]
	}
// 不是ip直接返回false
return false
}
登录后复制

下图为按照上述步骤检测请求是否是内网请求的结果。

Go中的SSRF攻防战

小结:URL形式多样,可以使用DNS解析获取规范的IP,从而判断是否是内网资源。

回合二:URL跳转

如果恶意攻击者仅通过IP的不同写法进行攻击,那我们自然可以高枕无忧,然而这场矛与盾的较量才刚刚开局。

我们回顾一下回合一的防御策略,检测请求是否是内网资源是在正式发起请求之前,如果攻击者在请求过程中通过URL跳转进行内网资源访问则完全可以绕过回合一中的防御策略。具体攻击流程如下。

Go中的SSRF攻防战

如图所示,通过URL跳转攻击者可获得内网资源。在介绍如何防御URL跳转攻击之前,老许和各位读者先一起复习一下HTTP重定向状态码——3xx。

根据维基百科的资料,3xx重定向码范围从300到308共9个。老许特意瞧了一眼go的源码,发现官方的http.Client发出的请求仅支持如下几个重定向码。

301:请求的资源已永久移动到新位置;该响应可缓存;重定向请求一定是GET请求。

302:要求客户端执行临时重定向;只有在Cache-Control或Expires中进行指定的情况下,这个响应才是可缓存的;重定向请求一定是GET请求。

303:当POST(或PUT / DELETE)请求的响应在另一个URI能被找到时可用此code,这个code存在主要是为了允许由脚本激活的POST请求输出重定向到一个新的资源;303响应禁止被缓存;重定向请求一定是GET请求。

307:临时重定向;不可更改请求方法,如果原请求是POST,则重定向请求也是POST。

308:永久重定向;不可更改请求方法,如果原请求是POST,则重定向请求也是POST。

3xx状态码复习就到这里,我们继续SSRF的攻防回合讨论。既然服务端的URL跳转可能带来风险,那我们只要禁用URL跳转就完全可以规避此类风险。然而我们并不能这么做,这个做法在规避风险的同时也极有可能误伤正常的请求。那到底该如何防范此类攻击手段呢?

看过老许“Go中的HTTP请求之——HTTP1.1请求流程分析”这篇文章的读者应该知道,对于重定向有业务需求时,可以自定义http.Client的CheckRedirect。下面我们先看一下CheckRedirect的定义。

CheckRedirect func(req *Request, via []*Request) error
登录后复制

这里特别说明一下,req是即将发出的请求且请求中包含前一次请求的响应,via是已经发出的请求。在知晓这些条件后,防御URL跳转攻击就变得十分容易了。

  1. 根据前一次请求的响应直接拒绝307308的跳转(此类跳转可以是POST请求,风险极高)。

  2. 解析出请求的IP,并判断是否是内网IP。

根据上述步骤,可如下定义http.Client

client := &http.Client{
	CheckRedirect: func(req *http.Request, via []*http.Request) error {
// 跳转超过10次,也拒绝继续跳转
if len(via) >= 10 {
return fmt.Errorf("redirect too much")
		}
		statusCode := req.Response.StatusCode
if statusCode == 307 || statusCode == 308 {
// 拒绝跳转访问
return fmt.Errorf("unsupport redirect method")
		}
// 判断ip
		ips, err := net.LookupIP(req.URL.Host)
if err != nil {
return err
		}
for _, ip := range ips {
if IsLocalIP(ip) {
return fmt.Errorf("have local ip")
			}
			fmt.Printf("%s -> %s is localip?: %v\n", req.URL, ip.String(), IsLocalIP(ip))
		}
return nil
	},
}
登录后复制

如上自定义CheckRedirect可以防范URL跳转攻击,但此方式会进行多次DNS解析,效率不佳。后文会结合其他攻击方式介绍更加有效率的防御措施。

小结:通过自定义http.ClientCheckRedirect可以防范URL跳转攻击。

回合三:DNS Rebinding

众所周知,发起一次HTTP请求需要先请求DNS服务获取域名对应的IP地址。如果攻击者有可控的DNS服务,就可以通过DNS重绑定绕过前面的防御策略进行攻击。

具体流程如下图所示。

Go中的SSRF攻防战

验证资源是是否合法时,服务器进行了第一次DNS解析,获得了一个非内网的IP且TTL为0。对解析的IP进行判断,发现非内网IP可以后续请求。由于攻击者的DNS Server将TTL设置为0,所以正式发起请求时需要再次进行DNS解析。此时DNS Server返回内网地址,由于已经进入请求资源阶段再无防御措施,所以攻击者可获得内网资源。

额外提一嘴,老许特意看了Go中DNS解析的部分源码,发现Go并没有对DNS的结果作缓存,所以即使TTL不为0也存在DNS重绑定的风险。

在发起请求的过程中有DNS解析才让攻击者有机可乘。如果我们能对该过程进行控制,就可以避免DNS重绑定的风险。对HTTP请求控制可以通过自定义http.Transport来实现,而自定义http.Transport也有两个方案。

方案一

dialer := &net.Dialer{}
transport := http.DefaultTransport.(*http.Transport).Clone()
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
	host, port, err := net.SplitHostPort(addr)
// 解析host和 端口
if err != nil {
return nil, err
	}
// dns解析域名
	ips, err := net.LookupIP(host)
if err != nil {
return nil, err
	}
// 对所有的ip串行发起请求
for _, ip := range ips {
		fmt.Printf("%v -> %v is localip?: %v\n", addr, ip.String(), IsLocalIP(ip))
if IsLocalIP(ip) {
continue
		}
// 非内网IP可继续访问
// 拼接地址
		addr := net.JoinHostPort(ip.String(), port)
// 此时的addr仅包含IP和端口信息
		con, err := dialer.DialContext(ctx, network, addr)
if err == nil {
return con, nil
		}
		fmt.Println(err)
	}

return nil, fmt.Errorf("connect failed")
}
// 使用此client请求,可避免DNS重绑定风险
client := &http.Client{
	Transport: transport,
}
登录后复制

transport.DialContext的作用是创建未加密的TCP连接,我们通过自定义此函数可规避DNS重绑定风险。另外特别说明一下,如果传递给dialer.DialContext方法的地址是常规IP格式则可使用net包中的parseIPZone函数直接解析成功,否则会继续发起DNS解析请求。

方案二

dialer := &net.Dialer{}
dialer.Control = func(network, address string, c syscall.RawConn) error {
// address 已经是ip:port的格式
	host, _, err := net.SplitHostPort(address)
if err != nil {
return err
	}
	fmt.Printf("%v is localip?: %v\n", address, IsLocalIP(net.ParseIP(host)))
return nil
}
transport := http.DefaultTransport.(*http.Transport).Clone()
// 使用官方库的实现创建TCP连接
transport.DialContext = dialer.DialContext
// 使用此client请求,可避免DNS重绑定风险
client := &http.Client{
	Transport: transport,
}
登录后复制

dialer.Control在创建网络连接之后实际拨号之前调用,且仅在go版本大于等于1.11时可用,其具体调用位置在sock_posix.go中的(*netFD).dial方法里。

Go中的SSRF攻防战

上述两个防御方案不仅仅可以防范DNS重绑定攻击,也同样可以防范其他攻击方式。事实上,老许更加推荐方案二,简直一劳永逸!

小结

  1. 攻击者可以通过自己的DNS服务进行DNS重绑定攻击。

  2. 通过自定义http.Transport可以防范DNS重绑定攻击。

以上是Go中的SSRF攻防战的详细内容。更多信息请关注PHP中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

Video Face Swap

Video Face Swap

使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

热门话题

Java教程
1657
14
CakePHP 教程
1415
52
Laravel 教程
1309
25
PHP教程
1257
29
C# 教程
1230
24
Go WebSocket 消息如何发送? Go WebSocket 消息如何发送? Jun 03, 2024 pm 04:53 PM

在Go中,可以使用gorilla/websocket包发送WebSocket消息。具体步骤:建立WebSocket连接。发送文本消息:调用WriteMessage(websocket.TextMessage,[]byte("消息"))。发送二进制消息:调用WriteMessage(websocket.BinaryMessage,[]byte{1,2,3})。

Golang 与 Go 语言的区别 Golang 与 Go 语言的区别 May 31, 2024 pm 08:10 PM

Go和Go语言是不同的实体,具有不同的特性。Go(又称Golang)以其并发性、编译速度快、内存管理和跨平台优点而闻名。Go语言的缺点包括生态系统不如其他语言丰富、语法更严格以及缺乏动态类型。

如何在 Go 中使用正则表达式匹配时间戳? 如何在 Go 中使用正则表达式匹配时间戳? Jun 02, 2024 am 09:00 AM

在Go中,可以使用正则表达式匹配时间戳:编译正则表达式字符串,例如用于匹配ISO8601时间戳的表达式:^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-][0-9]{2}:[0-9]{2})$。使用regexp.MatchString函数检查字符串是否与正则表达式匹配。

Golang 技术性能优化中如何避免内存泄漏? Golang 技术性能优化中如何避免内存泄漏? Jun 04, 2024 pm 12:27 PM

内存泄漏会导致Go程序内存不断增加,可通过:关闭不再使用的资源,如文件、网络连接和数据库连接。使用弱引用防止内存泄漏,当对象不再被强引用时将其作为垃圾回收目标。利用go协程,协程栈内存会在退出时自动释放,避免内存泄漏。

Golang 函数接收 map 参数时的注意事项 Golang 函数接收 map 参数时的注意事项 Jun 04, 2024 am 10:31 AM

在Go中传递map给函数时,默认会创建副本,对副本的修改不影响原map。如果需要修改原始map,可通过指针传递。空map需小心处理,因为技术上是nil指针,传递空map给期望非空map的函数会发生错误。

如何使用 Golang 的错误包装器? 如何使用 Golang 的错误包装器? Jun 03, 2024 pm 04:08 PM

在Golang中,错误包装器允许你在原始错误上追加上下文信息,从而创建新错误。这可用于统一不同库或组件抛出的错误类型,简化调试和错误处理。步骤如下:使用errors.Wrap函数将原有错误包装成新错误。新错误包含原始错误的上下文信息。使用fmt.Printf输出包装后的错误,提供更多上下文和可操作性。在处理不同类型的错误时,使用errors.Wrap函数统一错误类型。

如何在 Go 中创建优先级 Goroutine? 如何在 Go 中创建优先级 Goroutine? Jun 04, 2024 pm 12:41 PM

在Go语言中创建优先级Goroutine有两步:注册自定义Goroutine创建函数(步骤1)并指定优先级值(步骤2)。这样,您可以创建不同优先级的Goroutine,优化资源分配并提高执行效率。

Go 并发函数的单元测试指南 Go 并发函数的单元测试指南 May 03, 2024 am 10:54 AM

对并发函数进行单元测试至关重要,因为这有助于确保其在并发环境中的正确行为。测试并发函数时必须考虑互斥、同步和隔离等基本原理。可以通过模拟、测试竞争条件和验证结果等方法对并发函数进行单元测试。

See all articles