Go의 SSRF 공격과 방어

풀어 주다: 2023-07-24 14:05:16
앞으로
833명이 탐색했습니다.
SSRF란 무엇입니까

SSRF는 영어로 Server Side Request Forgery로 표기되며 이는 서버 측 요청 위조로 번역됩니다. 공격자는 서버 권한 획득에 실패하면 서버 취약점을 이용하여 서버가 위치한 인트라넷에 서버로 구성된 요청을 보냅니다. 인트라넷 리소스에 대한 액세스 제어에 대해서는 모든 사람이 알고 있어야 합니다.

Go의 SSRF 공격과 방어

위의 설명이 이해하기 쉽지 않다면 Lao Xu는 실용적인 예를 제시할 것입니다. 이제 많은 작성 플랫폼이 URL을 통한 이미지 업로드를 지원합니다. 서버가 URL을 엄격하게 확인하지 않으면 악의적인 공격자가 인트라넷 리소스에 액세스할 수 있습니다.

"개미집에 천 리 제방이 무너진다." 우리 프로그래머들은 위험을 초래할 수 있는 허점을 무시해서는 안 되며, 그러한 허점은 다른 사람들의 성과에 디딤돌이 될 가능성이 높습니다. 디딤돌이 되지 않기 위해 라오쉬는 독자 여러분과 함께 SSRF의 공격과 수비 라운드를 살펴보겠습니다.

1라운드: 끊임없이 변하는 인트라넷 주소

왜 "계속 변하는"이라는 단어를 사용하나요? Lao Xu는 지금은 답변하지 않겠지만 독자들은 인내심을 가지고 읽어보시기 바랍니다. 아래에서 Lao Xu는 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 공격과 방어

참고⚠️: 각 양식의 선행 0입니다. 0비트 압축 표현 및 내장된 IPv4 주소 표현과 마찬가지로 IPv6 주소도 다른 표현으로 기록될 수 있습니다.

말을 너무 많이 한 후에 Lao Xu는 더 이상 IP를 작성할 수 있는 방법을 셀 수 없습니다. 수학을 잘한다면 계산을 해보세요.

내부 IP, 여기 있을 것 같나요? 당연히 아니지! xip.io这个域名。xip可以帮你做自定义的DNS解析,并且可以解析到任意IP地址(包括内网)。

Go의 SSRF 공격과 방어

我们通过xip提供的域名解析,还可以将内网IP通过域名的方式进行访问。

关于内网IP的访问到这儿仍将继续!搞过Basic验证的应该都知道,可以通过http://user:passwd@hostname/ 리소스 액세스에 대해 들어본 독자가 있는지 모르겠습니다. 공격자가 작성 방식을 변경하면 아래와 같이 덜 엄격한 논리 중 일부를 우회할 수 있습니다.

Go의 SSRF 공격과 방어

인트라넷 주소와 관련하여 Lao Xu는 위 내용을 요약하기 위해 모든 지식을 소진했기 때문에 Lao Xu는 끊임없이 변화하는 인트라넷 주소가 그리 많지 않다고 말했습니다!

지금 Lao Xu는 악의적인 공격자가 이러한 다양한 형태의 인트라넷 주소를 사용하여 이미지를 업로드할 때 이를 어떻게 식별하고 액세스를 거부하는지 묻고 싶습니다. 정규식을 사용하여 위의 필터링을 완료할 수 있는 사람은 실제로 없을 것입니다. 그렇다면 제가 배울 수 있도록 메시지를 남겨주세요.

우리는 기본적으로 다양한 인트라넷 주소를 이해했으니, 이제 문제는 이를 우리가 판단할 수 있는 IP로 변환하는 방법입니다. 위의 인트라넷 주소는 크게 3가지로 분류할 수 있습니다. 1. IP 주소 자체이지만 표현이 균일하지 않습니다. 2. 인트라넷 IP를 가리키는 도메인 이름 3. 기본 검증이 포함된 주소입니다. 정보와 인트라넷 IP. 이러한 세 가지 유형의 특성을 기반으로 요청을 시작하기 전에 다음 단계에 따라 인트라넷 주소를 식별하고 액세스를 거부할 수 있습니다.

  1. 주소의 HostName을 구문 분석합니다.

  2. DNS 확인을 시작하고 IP를 얻습니다.

  3. 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
}
로그인 후 복사

아래 그림은 요청이 인트라넷 요청인지 감지하기 위해 위의 단계를 수행한 결과를 보여줍니다.

Go의 SSRF 공격과 방어

요약: URL은 다양한 형태로 제공되며 DNS 확인을 사용하여 표준화된 IP를 얻어 인트라넷 리소스인지 확인할 수 있습니다.

Round 2: URL Jump

악의적인 공격자가 IP를 쓰는 다양한 방법으로만 공격한다면 우리는 자연스럽게 편안히 앉아 있을 수 있지만 창과 방패의 전투는 이제 막 시작되었습니다.

1차 방어 전략을 검토해 보겠습니다. 요청이 공식적으로 시작되기 전에 해당 요청이 인트라넷 리소스인지 여부를 탐지하는 것은 공격자가 요청 과정에서 URL 점프를 통해 인트라넷 리소스에 액세스할 경우 1차를 완전히 우회할 수 있습니다. 전략. 구체적인 공격 과정은 다음과 같다.

Go의 SSRF 공격과 방어

그림과 같이 공격자는 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跳转攻击就变得十分容易了。

  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解析,效率不佳。后文会结合其他攻击方式介绍更加有效率的防御措施。

요약: URL 점프 공격은 사용자 정의http.ClientCheckRedirect를 통해 방지할 수 있습니다.

3단계: DNS 리바인딩

우리 모두 알고 있듯이 HTTP 요청을 시작하려면 먼저 DNS 서비스에 요청하여 도메인 이름에 해당하는 IP 주소를 얻어야 합니다. 공격자가 제어 가능한 DNS 서비스를 보유하고 있다면 DNS 리바인딩 및 공격을 통해 이전 방어 전략을 우회할 수 있습니다.

구체적인 과정은 아래 사진과 같습니다.

Go의 SSRF 공격과 방어

리소스가 합법적인지 확인할 때 서버는 첫 번째 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方法里。

Go의 SSRF 공격과 방어

위의 두 가지 방어 솔루션은 DNS 리바인딩 공격뿐만 아니라 다른 공격 방법도 방지할 수 있습니다. 실제로 Lao Xu는 일회성 솔루션인 두 번째 옵션도 권장했습니다!

요약:

  1. 공격자는 자체 DNS 서비스를 통해 DNS 리바인딩 공격을 수행할 수 있습니다.

  2. DNS 리바인딩 공격은 사용자 정의를 통해 방지할 수 있습니다http.Transport.

위 내용은 Go의 SSRF 공격과 방어의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

관련 라벨:
원천:Go语言进阶学习
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿