SSRF s'écrit en anglais par Server Side Request Forgery
, qui se traduit par falsification de requête côté serveur. Lorsque l'attaquant ne parvient pas à obtenir les autorisations du serveur, il utilise la vulnérabilité du serveur pour envoyer une requête construite en tant que serveur à l'intranet où se trouve le serveur. Concernant le contrôle d’accès aux ressources intranet, chacun doit en être conscient.
Si la déclaration ci-dessus n'est pas facile à comprendre, alors Lao Xu donnera simplement un exemple pratique. De nombreuses plates-formes d'écriture prennent désormais en charge le téléchargement d'images via des URL. Si le serveur ne vérifie pas strictement les URL, des attaquants malveillants peuvent accéder aux ressources intranet.
"Une digue de mille kilomètres s'effondrera dans un nid de fourmis". Nous, les programmeurs, ne devons ignorer aucune faille pouvant entraîner des risques, et de telles failles sont susceptibles de devenir un tremplin pour la performance des autres. Afin de ne pas devenir un tremplin, Lao Xu examinera les tours offensifs et défensifs de SSRF avec tous les lecteurs.
Pourquoi utiliser le mot « en constante évolution » ? Lao Xu ne répondra pas pour l'instant, mais lecteurs, veuillez continuer à lire patiemment. Ci-dessous, Lao Xu utilise 182.61.200.7
(www.baidu.com的一个IP地址)这个IP和各位读者一起复习一下IPv4的不同表示方式。
注意⚠️:点分混合制中,以点分割地每一部分均可以写作不同的进制(仅限于十、八和十六进制)。
上面仅是IPv4的不同表现方式,IPv6的地址也有三种不同表示方式。而这三种表现方式又可以有不同的写法。下面以IPv6中的回环地址0:0:0:0:0:0:0:1
comme exemple.
Remarque⚠️ : Le premier 0 de chaque formulaire. De la même manière que la représentation compressée 0 bit et la représentation d'adresse IPv4 intégrée, une adresse IPv6 peut également être écrite dans différentes représentations.
Après avoir tant parlé, Lao Xu ne peut plus compter le nombre de façons différentes d'écrire une adresse IP. Veuillez faire quelques calculs si vous êtes bon en mathématiques.
IP interne, vous pensez que c'est par ici ? Bien sûr que non! Je ne sais pas si des lecteurs ont entendu parler de l'accès aux ressources xip.io
这个域名。xip
可以帮你做自定义的DNS解析,并且可以解析到任意IP地址(包括内网)。
我们通过xip
提供的域名解析,还可以将内网IP通过域名的方式进行访问。
关于内网IP的访问到这儿仍将继续!搞过Basic验证的应该都知道,可以通过http://user:passwd@hostname/
. Si l’attaquant change sa façon d’écrire, il pourra peut-être contourner certaines des logiques les moins rigoureuses, comme indiqué ci-dessous.
Concernant l'adresse intranet, Lao Xu a épuisé toutes ses réserves de connaissances pour résumer le contenu ci-dessus, alors Lao Xu a dit que l'adresse intranet en constante évolution n'est pas de trop !
Pour le moment, Lao Xu veut simplement demander, lorsque des attaquants malveillants utilisent ces différentes formes d'adresses intranet pour télécharger des images, comment les identifier et refuser l'accès ? Personne ne pourra vraiment utiliser des expressions régulières pour effectuer le filtrage ci-dessus. Si tel est le cas, laissez-moi un message et dites-le-moi afin que je puisse en tirer des leçons.
Nous avons essentiellement compris les différentes adresses intranet, la question est donc maintenant de savoir comment la convertir en une IP que nous pouvons juger. Pour résumer, les adresses intranet ci-dessus peuvent être divisées en trois catégories : 1. Il s'agit d'une adresse IP elle-même, mais son expression n'est pas uniforme ; 2. Un nom de domaine qui pointe vers l'adresse IP de l'intranet ; 3. Une adresse qui contient une vérification de base ; informations et l’adresse IP de l’intranet. En fonction de ces trois types de caractéristiques, vous pouvez identifier l'adresse intranet et refuser l'accès en suivant les étapes suivantes avant de lancer une demande.
Analysez le nom d'hôte dans l'adresse.
Initiez la résolution DNS et obtenez l'IP.
Déterminez si l'IP est une adresse intranet.
Lorsque vous évaluez l'adresse intranet dans les étapes ci-dessus, veuillez ne pas ignorer l'adresse de bouclage IPv6 et l'adresse locale unique IPv6. Voici la logique utilisée par Lao Xu pour déterminer si l'adresse IP est une adresse 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 }
L'image ci-dessous montre le résultat des étapes ci-dessus pour détecter si la demande est une demande intranet.
Résumé : les URL se présentent sous différentes formes et vous pouvez utiliser la résolution DNS pour obtenir l'adresse IP standardisée afin de déterminer s'il s'agit d'une ressource intranet.
Si les attaquants malveillants attaquent uniquement via différentes manières d'écrire l'IP, alors nous pouvons naturellement nous asseoir et nous détendre, mais cette bataille entre la lance et le bouclier ne fait que commencer.
Passons en revue la stratégie de défense du premier tour. Détecter si la demande est une ressource intranet avant que la demande ne soit officiellement lancée. Si l'attaquant accède à la ressource intranet via un saut d'URL pendant le processus de demande, il peut complètement contourner le premier tour. stratégie. Le processus d’attaque spécifique est le suivant.
Comme le montre la figure, un attaquant peut obtenir des ressources intranet via un saut d'URL. Avant de présenter comment se défendre contre les attaques par saut d'URL, Lao Xu et les lecteurs examineront d'abord le code d'état de redirection HTTP-3xx.
Selon Wikipédia, il existe 9 codes de redirection 3xx allant de 300 à 308. Lao Xu a jeté un regard particulier sur le code source de go et a découvert que le http.Client
La requête émise ne prend en charge que les valeurs suivantes : Code d'orientation. http.Client
发出的请求仅支持如下几个重定向码。
301
301
: La ressource demandée a été définitivement déplacée vers un nouvel emplacement ; la réponse peut être mise en cache ; la requête de redirection doit être une requête GET.
302
:要求客户端执行临时重定向;只有在Cache-Control或Expires中进行指定的情况下,这个响应才是可缓存的;重定向请求一定是GET请求。
303
:当POST(或PUT / DELETE)请求的响应在另一个URI能被找到时可用此code,这个code存在主要是为了允许由脚本激活的POST请求输出重定向到一个新的资源;303响应禁止被缓存;重定向请求一定是GET请求。
307
:临时重定向;不可更改请求方法,如果原请求是POST,则重定向请求也是POST。
308
: Redirection permanente ; la méthode de requête ne peut pas être modifiée. Si la requête d'origine est POST, la requête redirigée est également POST.
C'est tout pour la révision du code de statut 3xx, poursuivons la discussion sur les tours offensifs et défensifs de SSRF. Étant donné que le saut d'URL côté serveur peut comporter des risques, nous pouvons complètement éviter ces risques tant que nous désactivons le saut d'URL. Cependant, nous ne pouvons pas faire cela. Même si cette approche évite les risques, elle est également très susceptible d'endommager accidentellement les requêtes normales. Alors comment prévenir de telles attaques ?
看过老许“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解析,效率不佳。后文会结合其他攻击方式介绍更加有效率的防御措施。
Résumé : les attaques par saut d'URL peuvent être évitées grâce à la personnalisationhttp.Client
的CheckRedirect
.
Comme nous le savons tous, pour lancer une requête HTTP, vous devez d'abord solliciter le service DNS pour obtenir l'adresse IP correspondant au nom de domaine. Si l'attaquant dispose d'un service DNS contrôlable, il peut contourner la stratégie de défense précédente via la reconnexion DNS et l'attaque.
Le processus spécifique est tel qu'indiqué dans l'image ci-dessous.
Lors de la vérification si la ressource est légale, le serveur a effectué la première résolution DNS et a obtenu une IP non intranet avec un TTL de 0. Jugez l’adresse IP résolue et constatez que l’adresse IP non intranet peut être utilisée pour les demandes ultérieures. Étant donné que le serveur DNS de l'attaquant définit le TTL sur 0, la résolution DNS doit être effectuée à nouveau lorsque la demande est formellement lancée. À ce stade, le serveur DNS renvoie l'adresse intranet. Puisqu'il est entré dans la phase de demande de ressources et qu'il n'existe aucune mesure défensive, l'attaquant peut obtenir les ressources intranet.
En guise de mention supplémentaire, Lao Xu a spécifiquement examiné une partie du code source de la résolution DNS dans Go et a constaté que Go ne met pas en cache les résultats DNS, donc même si le TTL n'est pas 0, il existe un risque de rebinding 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
方法里。
Les deux solutions de défense ci-dessus peuvent non seulement empêcher les attaques de rebinding DNS, mais également empêcher d'autres méthodes d'attaque. En fait, Lao Xu a même recommandé la deuxième option, qui est une solution unique !
Résumé :
Un attaquant peut mener une attaque de rebinding DNS via son propre service DNS.
Les attaques de rereliure DNS peuvent être évitées grâce à la personnalisationhttp.Transport
.
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!