Heim > Artikel > Backend-Entwicklung > Angriff und Verteidigung der SSRF in Go
SSRF wird im Englischen als Server Side Request Forgery
geschrieben, was als serverseitige Anforderungsfälschung übersetzt wird. Wenn es dem Angreifer nicht gelingt, Serverberechtigungen zu erhalten, nutzt er die Serverschwachstelle aus, um eine konstruierte Anfrage als Server an das Intranet zu senden, in dem sich der Server befindet. Was die Zugriffskontrolle von Intranet-Ressourcen betrifft, muss sich jeder darüber im Klaren sein.
Wenn die obige Aussage nicht leicht zu verstehen ist, dann wird Lao Xu nur ein praktisches Beispiel geben. Viele Schreibplattformen unterstützen mittlerweile das Hochladen von Bildern über URLs. Wenn der Server URLs nicht streng überprüft, können böswillige Angreifer möglicherweise auf Intranetressourcen zugreifen.
„Ein tausend Meilen langer Deich wird in einem Ameisennest einstürzen.“ Wir Programmierer sollten keine Lücken ignorieren, die Risiken verursachen können, und solche Lücken werden wahrscheinlich zu einem Sprungbrett für die Leistung anderer. Um nicht zum Sprungbrett zu werden, wird Lao Xu mit allen Lesern einen Blick auf die Offensiv- und Defensivrunden der SSRF werfen.
Warum das Wort „sich ständig ändern“ verwenden? Lao Xu wird vorerst nicht antworten, aber die Leser lesen bitte geduldig weiter. Im Folgenden verwendet Lao Xu 182.61.200.7
(www.baidu.com的一个IP地址)这个IP和各位读者一起复习一下IPv4的不同表示方式。
注意⚠️:点分混合制中,以点分割地每一部分均可以写作不同的进制(仅限于十、八和十六进制)。
上面仅是IPv4的不同表现方式,IPv6的地址也有三种不同表示方式。而这三种表现方式又可以有不同的写法。下面以IPv6中的回环地址0:0:0:0:0:0:0:1
als Beispiel.
Hinweis⚠️: Die führende 0 jedes Formulars. Ebenso wie die 0-Bit-komprimierte Darstellung und die eingebettete IPv4-Adressdarstellung kann auch eine IPv6-Adresse in unterschiedlichen Darstellungen geschrieben werden.
Nachdem er so viel geredet hat, kann Lao
Interne IP, glauben Sie, dass sie hier ist? Natürlich nicht! Ich weiß nicht, ob irgendein Leser davon gehört hat, xip.io
这个域名。xip
可以帮你做自定义的DNS解析,并且可以解析到任意IP地址(包括内网)。
我们通过xip
提供的域名解析,还可以将内网IP通过域名的方式进行访问。
关于内网IP的访问到这儿仍将继续!搞过Basic验证的应该都知道,可以通过http://user:passwd@hostname/
auf Ressourcen zuzugreifen. Wenn der Angreifer die Schreibweise ändert, kann er möglicherweise einige der weniger strengen Logiken umgehen, wie unten gezeigt.
In Bezug auf die Intranetadresse hat Lao
Lao Es wird niemanden geben, der reguläre Ausdrücke verwenden kann, um die obige Filterung durchzuführen. Wenn ja, hinterlassen Sie mir bitte eine Nachricht, damit ich daraus lernen kann.
Wir haben die verschiedenen Intranetadressen grundsätzlich verstanden, daher stellt sich nun die Frage, wie wir diese in eine IP umwandeln können, die wir beurteilen können. Zusammenfassend lassen sich die oben genannten Intranetadressen in drei Kategorien einteilen: 1. Es handelt sich um eine IP-Adresse selbst, deren Ausdruck jedoch nicht einheitlich ist. 2. Ein Domänenname, der auf die Intranet-IP verweist. 3. Eine Adresse, die eine Basisüberprüfung enthält Informationen und die Intranet-IP. Anhand dieser drei Arten von Merkmalen können Sie die Intranetadresse identifizieren und den Zugriff verweigern, indem Sie die folgenden Schritte ausführen, bevor Sie eine Anfrage initiieren.
Parsen Sie den Hostnamen in der Adresse.
Initiieren Sie die DNS-Auflösung und beziehen Sie die IP.
Stellen Sie fest, ob es sich bei der IP um eine Intranetadresse handelt.
Bei der Beurteilung der Intranetadresse in den obigen Schritten ignorieren Sie bitte nicht die IPv6-Loopback-Adresse und die eindeutige lokale IPv6-Adresse. Das Folgende ist die von Lao Xu verwendete Logik, um zu bestimmen, ob es sich bei der IP um eine Intranet-IP handelt.
// 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 }
Das Bild unten zeigt das Ergebnis der Befolgung der oben genannten Schritte, um festzustellen, ob es sich bei der Anfrage um eine Intranet-Anfrage handelt.
Zusammenfassung: URLs gibt es in verschiedenen Formen, und Sie können die DNS-Auflösung verwenden, um die standardisierte IP zu erhalten und so festzustellen, ob es sich um eine Intranetressource handelt.
Wenn böswillige Angreifer nur über unterschiedliche Arten des Schreibens von IP angreifen, können wir uns natürlich zurücklehnen und entspannen, aber dieser Kampf zwischen Speer und Schild hat gerade erst begonnen.
Lassen Sie uns die Verteidigungsstrategie in Runde 1 überprüfen. Die Feststellung, ob es sich bei der Anfrage um eine Intranetressource handelt, erfolgt, bevor die Anfrage offiziell initiiert wird. Wenn der Angreifer während des Anfrageprozesses über einen URL-Sprung auf die Intranetressource zugreift, kann er Runde 1 vollständig umgehen Strategie. Der spezifische Angriffsprozess ist wie folgt.
Wie in der Abbildung gezeigt, kann ein Angreifer durch URL-Sprung an Intranetressourcen gelangen. Bevor Lao Xu und seine Leser sich mit der Abwehr von URL-Jump-Angriffen befassen, werden sie zunächst den HTTP-Umleitungsstatuscode 3xx überprüfen.
Laut Wikipedia gibt es 9 3xx-Weiterleitungscodes im Bereich von 300 bis 308. Lao ;font-size : 11.9px;padding: 0.2em 0.4em;background-color: rgba(27, 31, 35, 0.05);border-radius: 3px;">http.ClientDie ausgegebene Anfrage unterstützt nur die folgenden Werte: Orientierungscode. http.Client
发出的请求仅支持如下几个重定向码。
301
301
: Die angeforderte Ressource wurde dauerhaft an einen neuen Speicherort verschoben; die Antwort kann zwischengespeichert werden; die Umleitungsanforderung muss eine GET-Anfrage sein.
302
:要求客户端执行临时重定向;只有在Cache-Control或Expires中进行指定的情况下,这个响应才是可缓存的;重定向请求一定是GET请求。
303
:当POST(或PUT / DELETE)请求的响应在另一个URI能被找到时可用此code,这个code存在主要是为了允许由脚本激活的POST请求输出重定向到一个新的资源;303响应禁止被缓存;重定向请求一定是GET请求。
307
:临时重定向;不可更改请求方法,如果原请求是POST,则重定向请求也是POST。
308
: Permanente Umleitung; die Anfragemethode kann nicht geändert werden. Wenn die ursprüngliche Anfrage POST ist, ist die umgeleitete Anfrage auch POST.
Das war's mit der Überprüfung des 3xx-Statuscodes. Lassen Sie uns die Diskussion über die Offensiv- und Defensivrunden der SSRF fortsetzen. Da das Springen von URLs auf der Serverseite Risiken mit sich bringen kann, können wir solche Risiken vollständig vermeiden, solange wir das Springen von URLs deaktivieren. Dies ist jedoch nicht möglich, obwohl dieser Ansatz Risiken vermeidet, es ist jedoch sehr wahrscheinlich, dass normale Anfragen versehentlich beschädigt werden. Wie kann man solche Angriffe verhindern?
看过老许“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解析,效率不佳。后文会结合其他攻击方式介绍更加有效率的防御措施。
Zusammenfassung: URL-Jump-Angriffe können durch Anpassung verhindert werdenhttp.Client
的CheckRedirect
.
Wie wir alle wissen, müssen Sie zum Initiieren einer HTTP-Anfrage zunächst den DNS-Dienst anfordern, um die dem Domänennamen entsprechende IP-Adresse zu erhalten. Verfügt der Angreifer über einen kontrollierbaren DNS-Dienst, kann er die bisherige Abwehrstrategie durch DNS-Rebinding und Angriff umgehen.
Der spezifische Prozess ist im Bild unten dargestellt.
Bei der Überprüfung, ob die Ressource legal ist, führte der Server die erste DNS-Auflösung durch und erhielt eine Nicht-Intranet-IP mit einer TTL von 0. Beurteilen Sie die aufgelöste IP und stellen Sie fest, dass Nicht-Intranet-IP für nachfolgende Anforderungen verwendet werden kann. Da der DNS-Server des Angreifers die TTL auf 0 setzt, muss die DNS-Auflösung erneut durchgeführt werden, wenn die Anfrage offiziell initiiert wird. Zu diesem Zeitpunkt gibt der DNS-Server die Intranetadresse zurück. Da er in die Ressourcenanforderungsphase eingetreten ist und keine Abwehrmaßnahmen vorhanden sind, kann der Angreifer an die Intranetressourcen gelangen.
Als zusätzliche Erwähnung: Lao .
在发起请求的过程中有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
方法里。
Die beiden oben genannten Verteidigungslösungen können nicht nur DNS-Rebinding-Angriffe, sondern auch andere Angriffsmethoden verhindern. Tatsächlich empfahl Lao Xu sogar die zweite Option, die eine einmalige Lösung darstellt!
Zusammenfassung:
Ein Angreifer kann einen DNS-Rebinding-Angriff über seinen eigenen DNS-Dienst durchführen.
DNS-Rebinding-Angriffe können durch Anpassung verhindert werdenhttp.Transport
.
Das obige ist der detaillierte Inhalt vonAngriff und Verteidigung der SSRF in Go. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!