SSRF は英語で Server Side Request Forgery
と綴られ、サーバーサイドと翻訳されます。捏造を要求します。攻撃者はサーバー権限の取得に失敗すると、サーバーの脆弱性を利用して、サーバーとして構築されたリクエストをサーバーが配置されているイントラネットに送信します。イントラネット リソースのアクセス制御については、全員が認識する必要があります。
#上記の説明が理解しにくい場合は、老徐が実際的な例を挙げます。現在、多くの書き込みプラットフォームが URL を介した画像のアップロードをサポートしていますが、サーバーが URL を厳密に検証しない場合、悪意のある攻撃者がイントラネット リソースにアクセスできる可能性があります。
「アリの巣で千マイルの堤防が決壊する。」 私たちプログラマーはリスクを引き起こす可能性のある抜け穴を無視すべきではありませんし、そのような抜け穴は他の人のパフォーマンスの踏み台になる可能性があります。踏み台にならないよう、老徐はSSRFの攻防を読者の皆さんと一緒に見守っていきます。
なぜ「常に変化する」という言葉を使うのでしょうか?老徐は今のところ答えませんが、読者は辛抱強く読み続けてください。次に、Lao Xu は IP 182.61.200.7
(www.baidu.com の IP アドレス) を使用して、読者と一緒に IPv4 のさまざまな表現を確認します。
注意⚠️: ドット混合システムでは、ドットで区切られた各部分を異なる基数で記述することができます (10 進数、8 進数、および 16 進数に制限されます)。 。
上記は IPv4 の表現の違いだけであり、IPv6 アドレスにも 3 つの異なる表現方法があります。そして、これら 3 つの表現方法はさまざまな方法で書くことができます。以下では、例として IPv6 のループバック アドレス 0:0:0:0:0:0:0:1
を取り上げます。
注⚠️: 16 進表記の各 X の先頭の 0 は省略できるので、部分的に省略しても構いません。その部分は省略しないでください。 、それにより、IPv6 アドレスをさまざまな形式で書き込みます。 0 ビット圧縮表現および埋め込み IPv4 アドレス表現と同様に、IPv6 アドレスもさまざまな表現で記述することができます。
これだけ話した後、Lao Xu は IP を何通りに記述できるかを数えることができなくなりました。数学が得意な方は計算してみてください。
イントラネット IP ここで終わりだと思いますか?もちろん違います!読者の中で xip.io
というドメイン名を聞いたことがある人がいるかどうかはわかりません。 xip
は、カスタマイズされた DNS 解決を行うのに役立ち、任意の IP アドレス (イントラネットを含む) に解決できます。
xip
が提供するドメイン名解決を使用しており、ドメイン名を通じてイントラネット IP にアクセスすることもできます。
イントラネット IP へのアクセスはここで継続されます。基本認証を行ったことがある人なら誰でも、http://user:passwd@hostname/
を通じてリソースにアクセスできることを知っているはずです。攻撃者が記述方法を変更すると、以下に示すように、厳密性の低いロジックの一部をバイパスできる可能性があります。
イントラネット アドレスに関しては、老徐は知識のすべてを使い果たして上記の内容を要約したため、刻々と変化するイントラネット アドレスについてはあまり多くを語ることはできません。
現時点では、Lao Xu が聞きたいのは、悪意のある攻撃者がこれらのさまざまな形式のイントラネット アドレスを使用して画像をアップロードした場合、どのようにそれらを識別してアクセスを拒否するのかということです。正規表現を使用して上記のフィルタリングを実行できる人は実際にはいないでしょう。その場合は、メッセージを残して私に教えてください。勉強させていただきます。
さまざまなイントラネット アドレスについては基本的に理解できたので、問題はそれを判断できる IP にどのように変換するかです。まとめると、上記のイントラネット アドレスは、1. IP アドレスそのものであるが表現が統一されていない、2. イントラネット IP を指すドメイン名、3. 基本検証を含むアドレスの 3 つのカテゴリに分類できます。情報とイントラネット IP。これら 3 種類の特性に基づいて、要求を開始する前に次の手順に従ってイントラネット アドレスを特定し、アクセスを拒否できます。
アドレス内のホスト名を解析します。
DNS 解決を開始し、IP を取得します。
IP がイントラネット アドレスであるかどうかを確認します。
上記の手順では、イントラネット アドレスを判断する際に、IPv6 ループバック アドレスと IPv6 固有ローカル アドレスを無視しないでください。以下は、IP がイントラネット IP であるかどうかを判断するために Lao Xu が使用するロジックです。
// 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
リクエストが次のリダイレクト コードのみをサポートしていることを発見しました。
301
: 要求されたリソースは新しい場所に永続的に移動されました。応答はキャッシュ可能です。リダイレクト要求は GET 要求である必要があります。
302
: クライアントは一時的なリダイレクトを実行する必要があります。この応答は、Cache-Control または Expires で指定されている場合にのみキャッシュ可能です。リダイレクト要求は GET 要求である必要があります。
303
: このコードは、POST (または PUT / DELETE) リクエストへの応答が別の URI で見つかる場合に使用できます。このコードは主に、スクリプトによってアクティブ化された 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解析,效率不佳。后文会结合其他攻击方式介绍更加有效率的防御措施。
概要: http.Client
の CheckRedirect
をカスタマイズすることで、URL ジャンプ攻撃を防ぐことができます。
ご存知のとおり、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
方法里。
上記の 2 つの防御ソリューションは、DNS リバインディング攻撃を防ぐだけでなく、他の攻撃方法も防ぐことができます。実際、老徐は 1 回で完了する解決策である 2 番目の選択肢を推奨しました。
概要:
攻撃者は、独自の DNS サービスを通じて DNS 再バインド攻撃を実行できます。
DNS 再バインド攻撃は、http.Transport
をカスタマイズすることで防止できます。
以上がGo における SSRF の攻撃と防御の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。