SSRF는 영어로 Server Side Request Forgery
로 표기되며 이는 서버 측 요청 위조로 번역됩니다. 공격자는 서버 권한 획득에 실패하면 서버 취약점을 이용하여 서버가 위치한 인트라넷에 서버로 구성된 요청을 보냅니다. 인트라넷 리소스에 대한 액세스 제어에 대해서는 모든 사람이 알고 있어야 합니다.
위의 설명이 이해하기 쉽지 않다면 Lao Xu는 실용적인 예를 제시할 것입니다. 이제 많은 작성 플랫폼이 URL을 통한 이미지 업로드를 지원합니다. 서버가 URL을 엄격하게 확인하지 않으면 악의적인 공격자가 인트라넷 리소스에 액세스할 수 있습니다.
"개미집에 천 리 제방이 무너진다." 우리 프로그래머들은 위험을 초래할 수 있는 허점을 무시해서는 안 되며, 그러한 허점은 다른 사람들의 성과에 디딤돌이 될 가능성이 높습니다. 디딤돌이 되지 않기 위해 라오쉬는 독자 여러분과 함께 SSRF의 공격과 수비 라운드를 살펴보겠습니다.
왜 "계속 변하는"이라는 단어를 사용하나요? Lao Xu는 지금은 답변하지 않겠지만 독자들은 인내심을 가지고 읽어보시기 바랍니다. 아래에서 Lao Xu는 182.61.200.7
(www.baidu.com的一个IP地址)这个IP和各位读者一起复习一下IPv4的不同表示方式。
注意⚠️:点分混合制中,以点分割地每一部分均可以写作不同的进制(仅限于十、八和十六进制)。
上面仅是IPv4的不同表现方式,IPv6的地址也有三种不同表示方式。而这三种表现方式又可以有不同的写法。下面以IPv6中的回环地址0:0:0:0:0:0:0:1
를 예로 사용합니다.
참고⚠️: 각 양식의 선행 0입니다. 0비트 압축 표현 및 내장된 IPv4 주소 표현과 마찬가지로 IPv6 주소도 다른 표현으로 기록될 수 있습니다.
말을 너무 많이 한 후에 Lao Xu는 더 이상 IP를 작성할 수 있는 방법을 셀 수 없습니다. 수학을 잘한다면 계산을 해보세요.
내부 IP, 여기 있을 것 같나요? 당연히 아니지! xip.io
这个域名。xip
可以帮你做自定义的DNS解析,并且可以解析到任意IP地址(包括内网)。
我们通过xip
提供的域名解析,还可以将内网IP通过域名的方式进行访问。
关于内网IP的访问到这儿仍将继续!搞过Basic验证的应该都知道,可以通过http://user:passwd@hostname/
리소스 액세스에 대해 들어본 독자가 있는지 모르겠습니다. 공격자가 작성 방식을 변경하면 아래와 같이 덜 엄격한 논리 중 일부를 우회할 수 있습니다.
인트라넷 주소와 관련하여 Lao Xu는 위 내용을 요약하기 위해 모든 지식을 소진했기 때문에 Lao Xu는 끊임없이 변화하는 인트라넷 주소가 그리 많지 않다고 말했습니다!
지금 Lao Xu는 악의적인 공격자가 이러한 다양한 형태의 인트라넷 주소를 사용하여 이미지를 업로드할 때 이를 어떻게 식별하고 액세스를 거부하는지 묻고 싶습니다. 정규식을 사용하여 위의 필터링을 완료할 수 있는 사람은 실제로 없을 것입니다. 그렇다면 제가 배울 수 있도록 메시지를 남겨주세요.
우리는 기본적으로 다양한 인트라넷 주소를 이해했으니, 이제 문제는 이를 우리가 판단할 수 있는 IP로 변환하는 방법입니다. 위의 인트라넷 주소는 크게 3가지로 분류할 수 있습니다. 1. IP 주소 자체이지만 표현이 균일하지 않습니다. 2. 인트라넷 IP를 가리키는 도메인 이름 3. 기본 검증이 포함된 주소입니다. 정보와 인트라넷 IP. 이러한 세 가지 유형의 특성을 기반으로 요청을 시작하기 전에 다음 단계에 따라 인트라넷 주소를 식별하고 액세스를 거부할 수 있습니다.
주소의 HostName을 구문 분석합니다.
DNS 확인을 시작하고 IP를 얻습니다.
IP가 인트라넷 주소인지 확인합니다.
위 단계에서 인트라넷 주소를 판단할 때 IPv6 루프백 주소와 IPv6 고유 로컬 주소를 무시하지 마십시오. 다음은 Lao Xu가 해당 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 }
아래 그림은 요청이 인트라넷 요청인지 감지하기 위해 위의 단계를 수행한 결과를 보여줍니다.
요약: URL은 다양한 형태로 제공되며 DNS 확인을 사용하여 표준화된 IP를 얻어 인트라넷 리소스인지 확인할 수 있습니다.
악의적인 공격자가 IP를 쓰는 다양한 방법으로만 공격한다면 우리는 자연스럽게 편안히 앉아 있을 수 있지만 창과 방패의 전투는 이제 막 시작되었습니다.
1차 방어 전략을 검토해 보겠습니다. 요청이 공식적으로 시작되기 전에 해당 요청이 인트라넷 리소스인지 여부를 탐지하는 것은 공격자가 요청 과정에서 URL 점프를 통해 인트라넷 리소스에 액세스할 경우 1차를 완전히 우회할 수 있습니다. 전략. 구체적인 공격 과정은 다음과 같다.
그림과 같이 공격자는 URL 점프를 통해 인트라넷 자원을 획득할 수 있습니다. URL 점프 공격을 방어하는 방법을 소개하기 전에 Lao Xu와 독자는 먼저 HTTP 리디렉션 상태 코드-3xx를 검토합니다.
Wikipedia에 따르면 300에서 308 사이의 9개의 3xx 리디렉션 코드가 있습니다. Lao Xu는 go의 소스 코드를 특별히 살펴보고 공식 http.Client
발행된 요청은 다음을 지원합니다. 다음 값: 방향 코드. http.Client
发出的请求仅支持如下几个重定向码。
301
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跳转攻击就变得十分容易了。
根据前一次请求的响应直接拒绝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解析,效率不佳。后文会结合其他攻击方式介绍更加有效率的防御措施。
요약: URL 점프 공격은 사용자 정의http.Client
的CheckRedirect
를 통해 방지할 수 있습니다.
우리 모두 알고 있듯이 HTTP 요청을 시작하려면 먼저 DNS 서비스에 요청하여 도메인 이름에 해당하는 IP 주소를 얻어야 합니다. 공격자가 제어 가능한 DNS 서비스를 보유하고 있다면 DNS 리바인딩 및 공격을 통해 이전 방어 전략을 우회할 수 있습니다.
구체적인 과정은 아래 사진과 같습니다.
리소스가 합법적인지 확인할 때 서버는 첫 번째 DNS 확인을 수행하고 TTL이 0인 비인트라넷 IP를 얻었습니다. 해결된 IP를 판단하고 후속 요청에 인트라넷이 아닌 IP를 사용할 수 있는지 확인합니다. 공격자의 DNS 서버는 TTL을 0으로 설정하므로 정식으로 요청이 시작되면 DNS 확인을 다시 수행해야 합니다. 이때 DNS 서버는 인트라넷 주소를 반환하게 되는데, 리소스 요청 단계에 진입했고 어떠한 방어수단도 없기 때문에 공격자는 인트라넷 리소스를 탈취할 수 있다.
추가로 언급하자면 Lao Xu는 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
方法里。
위의 두 가지 방어 솔루션은 DNS 리바인딩 공격뿐만 아니라 다른 공격 방법도 방지할 수 있습니다. 실제로 Lao Xu는 일회성 솔루션인 두 번째 옵션도 권장했습니다!
요약:
공격자는 자체 DNS 서비스를 통해 DNS 리바인딩 공격을 수행할 수 있습니다.
DNS 리바인딩 공격은 사용자 정의를 통해 방지할 수 있습니다http.Transport
.
위 내용은 Go의 SSRF 공격과 방어의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!