Rumah >pembangunan bahagian belakang >Golang >Serangan dan pertahanan SSRF di Go
SSRF dieja dalam bahasa Inggeris sebagai Server Side Request Forgery
, yang diterjemahkan sebagai pemalsuan permintaan sebelah pelayan. Apabila penyerang gagal mendapatkan kebenaran pelayan, dia menggunakan kelemahan pelayan untuk menghantar permintaan yang dibina sebagai pelayan ke intranet di mana pelayan berada. Mengenai kawalan capaian sumber intranet, semua orang mesti menyedarinya.
Jika penyataan di atas tidak mudah difahami, maka Lao Xu hanya akan memberikan contoh praktikal. Banyak platform penulisan kini menyokong muat naik imej melalui URL Jika pelayan tidak mengesahkan URL dengan ketat, penyerang berniat jahat mungkin boleh mengakses sumber intranet.
"Daik seribu batu akan runtuh dalam sarang semut". Kami pengaturcara tidak seharusnya mengabaikan sebarang celah yang boleh menyebabkan risiko, dan celah tersebut berkemungkinan menjadi batu loncatan untuk prestasi orang lain. Untuk tidak menjadi batu loncatan, Lao Xu akan melihat pusingan serangan dan pertahanan SSRF dengan semua pembaca.
Mengapa menggunakan perkataan "berubah-ubah"? Lao Xu tidak akan menjawab buat masa ini, tetapi pembaca sila baca dengan sabar. Di bawah, Lao Xu menggunakan 182.61.200.7
(www.baidu.com的一个IP地址)这个IP和各位读者一起复习一下IPv4的不同表示方式。
注意⚠️:点分混合制中,以点分割地每一部分均可以写作不同的进制(仅限于十、八和十六进制)。
上面仅是IPv4的不同表现方式,IPv6的地址也有三种不同表示方式。而这三种表现方式又可以有不同的写法。下面以IPv6中的回环地址0:0:0:0:0:0:0:1
sebagai contoh.
Nota⚠️: 0 pendahuluan setiap borang. Dengan cara yang sama seperti perwakilan termampat 0-bit dan perwakilan alamat IPv4 terbenam, alamat IPv6 juga boleh ditulis dalam perwakilan yang berbeza.
Selepas bercakap banyak, Lao Xu tidak lagi boleh mengira berapa banyak cara yang berbeza untuk IP boleh ditulis. Sila buat beberapa pengiraan jika anda mahir dalam matematik.
IP Dalaman, adakah anda fikir ia sudah sampai di sini? Sudah tentu tidak! Saya tidak tahu sama ada pembaca pernah mendengar tentang xip.io
这个域名。xip
可以帮你做自定义的DNS解析,并且可以解析到任意IP地址(包括内网)。
我们通过xip
提供的域名解析,还可以将内网IP通过域名的方式进行访问。
关于内网IP的访问到这儿仍将继续!搞过Basic验证的应该都知道,可以通过http://user:passwd@hostname/
mengakses sumber. Jika penyerang mengubah cara penulisan, dia mungkin boleh memintas beberapa logik yang kurang ketat, seperti yang ditunjukkan di bawah.
Mengenai alamat intranet, Lao Xu menghabiskan semua rizab pengetahuannya untuk meringkaskan kandungan di atas, jadi Lao Xu berkata bahawa alamat intranet yang sentiasa berubah tidak terlalu banyak!
Pada masa ini, Lao Xu hanya ingin bertanya, apabila penyerang berniat jahat menggunakan pelbagai bentuk alamat intranet ini untuk memuat naik imej, bagaimanakah anda mengenal pasti mereka dan menafikan akses? Tidak akan ada sesiapa yang boleh menggunakan ungkapan biasa untuk melengkapkan penapisan di atas. Jika ya, sila tinggalkan mesej dan beritahu saya supaya saya boleh belajar daripadanya.
Kami pada asasnya telah memahami pelbagai alamat intranet, jadi persoalannya sekarang ialah bagaimana untuk menukarnya menjadi IP yang boleh kami nilai. Untuk meringkaskan, alamat intranet di atas boleh dibahagikan kepada tiga kategori: 1. Ia adalah alamat IP itu sendiri, tetapi ungkapannya tidak seragam 2. Nama domain yang menunjuk ke IP intranet 3. Alamat yang mengandungi pengesahan Asas; maklumat dan IP intranet. Berdasarkan tiga jenis ciri ini, anda boleh mengenal pasti alamat intranet dan menafikan akses dengan mengikuti langkah berikut sebelum memulakan permintaan.
Uraikan Nama Hos dalam alamat.
Mulakan resolusi DNS dan dapatkan IP.
Tentukan sama ada IP ialah alamat intranet.
Apabila menilai alamat intranet dalam langkah di atas, sila jangan abaikan alamat gelung balik IPv6 dan alamat setempat unik IPv6. Berikut ialah logik yang digunakan oleh Lao Xu untuk menentukan sama ada IP itu ialah IP intranet.
// 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 }
Gambar di bawah menunjukkan hasil mengikut langkah di atas untuk mengesan sama ada permintaan itu adalah permintaan intranet.
Ringkasan: URL datang dalam pelbagai bentuk dan anda boleh menggunakan resolusi DNS untuk mendapatkan IP piawai untuk menentukan sama ada ia adalah sumber intranet.
Jika penyerang berniat jahat hanya menyerang melalui cara penulisan IP yang berbeza, maka kita secara semula jadi boleh duduk dan berehat, tetapi pertempuran antara lembing dan perisai ini baru sahaja bermula.
Mari semak strategi pertahanan dalam Pusingan 1. Mengesan sama ada permintaan itu adalah sumber intranet sebelum permintaan dimulakan secara rasmi Jika penyerang mengakses sumber intranet melalui lompatan URL semasa proses permintaan, dia boleh memintas sepenuhnya Pusingan 1. defensif. strategi. Proses serangan khusus adalah seperti berikut.
Seperti yang ditunjukkan dalam rajah, penyerang boleh mendapatkan sumber intranet melalui lompatan URL. Sebelum memperkenalkan cara untuk mempertahankan diri daripada serangan lompat URL, Lao Xu dan pembaca akan menyemak semula kod status pengalihan HTTP-3xx terlebih dahulu.
Menurut Wikipedia, terdapat 9 kod ubah hala 3xx antara 300 hingga 308. Lao Xu melihat khas pada kod sumber go dan mendapati bahawa rasmi http.Client
Permintaan yang dikeluarkan hanya menyokong nilai berikut: Kod orientasi. http.Client
发出的请求仅支持如下几个重定向码。
301
301
: Sumber yang diminta telah dipindahkan secara kekal ke lokasi baharu;
302
:要求客户端执行临时重定向;只有在Cache-Control或Expires中进行指定的情况下,这个响应才是可缓存的;重定向请求一定是GET请求。
303
:当POST(或PUT / DELETE)请求的响应在另一个URI能被找到时可用此code,这个code存在主要是为了允许由脚本激活的POST请求输出重定向到一个新的资源;303响应禁止被缓存;重定向请求一定是GET请求。
307
:临时重定向;不可更改请求方法,如果原请求是POST,则重定向请求也是POST。
308
: Ubah hala kekal; kaedah permintaan tidak boleh diubah Jika permintaan asal adalah POST, permintaan diubah hala juga adalah POST.
Itu sahaja untuk semakan kod status 3xx, mari teruskan perbincangan tentang pusingan serangan dan pertahanan SSRF. Memandangkan lompatan URL pada bahagian pelayan mungkin membawa risiko, kami boleh mengelak sepenuhnya risiko tersebut selagi kami melumpuhkan lompatan URL. Walau bagaimanapun, kami tidak boleh melakukan ini Walaupun pendekatan ini mengelakkan risiko, ia juga berkemungkinan besar merosakkan permintaan biasa secara tidak sengaja. Jadi bagaimana untuk mengelakkan serangan sedemikian?
看过老许“Go中的HTTP请求之——HTTP1.1请求流程分析”这篇文章的读者应该知道,对于重定向有业务需求时,可以自定义http.Client的CheckRedirect
。下面我们先看一下CheckRedirect
的定义。
CheckRedirect func(req *Request, via []*Request) error
这里特别说明一下,req
是即将发出的请求且请求中包含前一次请求的响应,via
是已经发出的请求。在知晓这些条件后,防御URL跳转攻击就变得十分容易了。
根据前一次请求的响应直接拒绝307
和308
的跳转(此类跳转可以是POST请求,风险极高)。
解析出请求的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解析,效率不佳。后文会结合其他攻击方式介绍更加有效率的防御措施。
Ringkasan: Serangan lompat URL boleh dihalang melalui penyesuaianhttp.Client
的CheckRedirect
.
Seperti yang kita semua tahu, untuk memulakan permintaan HTTP, anda perlu terlebih dahulu meminta perkhidmatan DNS untuk mendapatkan alamat IP yang sepadan dengan nama domain. Jika penyerang mempunyai perkhidmatan DNS yang boleh dikawal, dia boleh memintas strategi pertahanan sebelumnya melalui pengikatan dan serangan semula DNS.
Proses khusus adalah seperti gambar di bawah.
Apabila mengesahkan sama ada sumber itu sah, pelayan melakukan resolusi DNS pertama dan memperoleh IP bukan intranet dengan TTL 0. Nilai IP yang diselesaikan dan dapatkan IP bukan intranet boleh digunakan untuk permintaan seterusnya. Memandangkan Pelayan DNS penyerang menetapkan TTL kepada 0, resolusi DNS perlu dilakukan semula apabila permintaan dimulakan secara rasmi. Pada masa ini, Pelayan DNS mengembalikan alamat intranet Memandangkan ia telah memasuki peringkat permintaan sumber dan tiada langkah pertahanan, penyerang boleh mendapatkan sumber intranet.
Sebagai sebutan tambahan, Lao Xu secara khusus melihat sebahagian daripada kod sumber resolusi DNS dalam Go dan mendapati bahawa Go tidak menyimpan cache hasil DNS, jadi walaupun TTL bukan 0, terdapat risiko pengikat semula 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
方法里。
Dua penyelesaian pertahanan di atas bukan sahaja boleh menghalang serangan pengikat semula DNS, tetapi juga menghalang kaedah serangan lain. Malah, Lao Xu juga mengesyorkan pilihan kedua, iaitu penyelesaian satu-dan-selesai!
Ringkasan:
Seorang penyerang boleh melakukan serangan mengikat semula DNS melalui perkhidmatan DNSnya sendiri.
Serangan pengikat semula DNS boleh dihalang melalui penyesuaianhttp.Transport
.
Atas ialah kandungan terperinci Serangan dan pertahanan SSRF di Go. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!