目錄
回合一:千變萬化的內部網路位址
回合二: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

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

<🎜>:泡泡膠模擬器無窮大 - 如何獲取和使用皇家鑰匙
3 週前 By 尊渡假赌尊渡假赌尊渡假赌
北端:融合系統,解釋
3 週前 By 尊渡假赌尊渡假赌尊渡假赌
Mandragora:巫婆樹的耳語 - 如何解鎖抓鉤
3 週前 By 尊渡假赌尊渡假赌尊渡假赌

熱工具

記事本++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教學
1666
14
CakePHP 教程
1425
52
Laravel 教程
1325
25
PHP教程
1273
29
C# 教程
1252
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})。

如何在 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 與 Go 語言的區別 Golang 與 Go 語言的區別 May 31, 2024 pm 08:10 PM

Go和Go語言是不同的實體,具有不同的特性。 Go(又稱Golang)以其並發性、編譯速度快、記憶體管理和跨平台優點而聞名。 Go語言的缺點包括生態系統不如其他語言豐富、文法更嚴格、缺乏動態類型。

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,優化資源分配並提高執行效率。

如何在 Golang 單元測試中使用 gomega 進行斷言? 如何在 Golang 單元測試中使用 gomega 進行斷言? Jun 05, 2024 pm 10:48 PM

如何在Golang單元測試中使用Gomega進行斷言在Golang單元測試中,Gomega是一個流行且功能強大的斷言庫,它提供了豐富的斷言方法,使開發人員可以輕鬆驗證測試結果。安裝Gomegagoget-ugithub.com/onsi/gomega使用Gomega進行斷言以下是使用Gomega進行斷言的一些常用範例:1.相等斷言import"github.com/onsi/gomega"funcTest_MyFunction(t*testing.T){

See all articles