0

0

Go中的SSRF攻防战

Go语言进阶学习

Go语言进阶学习

发布时间:2023-07-24 14:05:16

|

971人浏览过

|

来源于Go语言进阶学习

转载

什么是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。

    迅易年度企业管理系统开源完整版
    迅易年度企业管理系统开源完整版

    系统功能强大、操作便捷并具有高度延续开发的内容与知识管理系统,并可集合系统强大的新闻、产品、下载、人才、留言、搜索引擎优化、等功能模块,为企业部门提供一个简单、易用、开放、可扩展的企业信息门户平台或电子商务运行平台。开发人员为脆弱页面专门设计了防刷新系统,自动阻止恶意访问和攻击;安全检查应用于每一处代码中,每个提交到系统查询语句中的变量都经过过滤,可自动屏蔽恶意攻击代码,从而全面防止SQL注入攻击

    下载

根据上述步骤,可如下定义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重绑定攻击。

相关专题

更多
js正则表达式
js正则表达式

php中文网为大家提供各种js正则表达式语法大全以及各种js正则表达式使用的方法,还有更多js正则表达式的相关文章、相关下载、相关课程,供大家免费下载体验。

510

2023.06.20

正则表达式不包含
正则表达式不包含

正则表达式,又称规则表达式,,是一种文本模式,包括普通字符和特殊字符,是计算机科学的一个概念。正则表达式使用单个字符串来描述、匹配一系列匹配某个句法规则的字符串,通常被用来检索、替换那些符合某个模式的文本。php中文网给大家带来了有关正则表达式的相关教程以及文章,希望对大家能有所帮助。

249

2023.07.05

java正则表达式语法
java正则表达式语法

java正则表达式语法是一种模式匹配工具,它非常有用,可以在处理文本和字符串时快速地查找、替换、验证和提取特定的模式和数据。本专题提供java正则表达式语法的相关文章、下载和专题,供大家免费下载体验。

742

2023.07.05

java正则表达式匹配字符串
java正则表达式匹配字符串

在Java中,我们可以使用正则表达式来匹配字符串。本专题为大家带来java正则表达式匹配字符串的相关内容,帮助大家解决问题。

213

2023.08.11

正则表达式空格
正则表达式空格

正则表达式空格可以用“s”来表示,它是一个特殊的元字符,用于匹配任意空白字符,包括空格、制表符、换行符等。本专题为大家提供正则表达式相关的文章、下载、课程内容,供大家免费下载体验。

351

2023.08.31

Python爬虫获取数据的方法
Python爬虫获取数据的方法

Python爬虫可以通过请求库发送HTTP请求、解析库解析HTML、正则表达式提取数据,或使用数据抓取框架来获取数据。更多关于Python爬虫相关知识。详情阅读本专题下面的文章。php中文网欢迎大家前来学习。

293

2023.11.13

正则表达式空格如何表示
正则表达式空格如何表示

正则表达式空格可以用“s”来表示,它是一个特殊的元字符,用于匹配任意空白字符,包括空格、制表符、换行符等。想了解更多正则表达式空格怎么表示的内容,可以访问下面的文章。

234

2023.11.17

正则表达式中如何匹配数字
正则表达式中如何匹配数字

正则表达式中可以通过匹配单个数字、匹配多个数字、匹配固定长度的数字、匹配整数和小数、匹配负数和匹配科学计数法表示的数字的方法匹配数字。更多关于正则表达式的相关知识详情请看本专题下面的文章。php中文网欢迎大家前来学习。

528

2023.12.06

Golang 性能分析与pprof调优实战
Golang 性能分析与pprof调优实战

本专题系统讲解 Golang 应用的性能分析与调优方法,重点覆盖 pprof 的使用方式,包括 CPU、内存、阻塞与 goroutine 分析,火焰图解读,常见性能瓶颈定位思路,以及在真实项目中进行针对性优化的实践技巧。通过案例讲解,帮助开发者掌握 用数据驱动的方式持续提升 Go 程序性能与稳定性。

8

2026.01.22

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Go 教程
Go 教程

共32课时 | 4万人学习

Go语言实战之 GraphQL
Go语言实战之 GraphQL

共10课时 | 0.8万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号