SSRFをProxyで防御する
SSRF
危険性
<?php require_once('./htmlpurifier/library/HTMLPurifier.includes.php'); $purifier = new HTMLPurifier(); $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $_GET['url']); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $html = curl_exec($ch); $aws_container_credentials_relative_uri = getenv('AWS_CONTAINER_CREDENTIALS_RELATIVE_URI'); error_log("container_credentials : {$aws_container_credentials_relative_uri}"); echo $purifier->purify($html);
このようなプログラムをECSにデプロイして検証した(SSRF徹底入門 のコードを利用させていただきました)。
当たり前だが、
curl 'http://169.254.170.2:8080/ssrf.php?url=http://169.254.170.2/v2/credentials/x-1111-xxxx-1111-x' | jq . curl 'http://3137331111:8080/ssrf.php?url=http://2852039170/v2/credentials/x-1111-xxxx-1111-x' | jq .
で普通にトークンが抜ける。下は、IPv4アドレスの10進数表記。これも普通に通る。
{ "RoleArn": "arn:aws:iam::452509140030:role/ssrf", "AccessKeyId": "xxxx", "SecretAccessKey": "xxxx", "Token": "xxxx", o [ "Expiration": "2020-02-06T12:01:03Z" }
Fargateにおける危険性
Fargateにおいてクレデンシャルを取得するためのパスは環境変数 $AWS_CONTAINER_CREDENTIALS_RELATIVE_URI に格納されている。 上記のコードではあえて、この値を標準出力にアウトプットしている。
本来なら、環境変数をネットワーク越しに取得できるような脆弱性が作り込まれていない限り、クレデンシャルは抜けない。 Fargateにおいては AWSのクレデンシャルに関しては そんなに気にすることもなさそう。
Proxyによる防御
ユーザーから入力されたURLにアクセスする部分で、そのアクセスをProxy経由にすることで防御する。
Proxyはプライベートアドレスとリンクローカルアドレスへのアクセスを禁止する。
ProxyにはSquidを利用して、設定は下記のようになる。
access_log stdio:/proc/self/fd/1 combined coredump_dir /var/cache/squid error_directory /etc/squid/errors # 宛先 acl localhost_dst dst localhost acl private dst 10.0.0.0/8 acl private dst 172.16.0.0/12 acl private dst 192.168.0.0/16 # ipv4 link local acl linklocal dst 169.254.0.0/16 # Allow Ports acl SSL_ports port 443 acl Safe_ports port 80 acl Safe_ports port 443 acl Safe_ports port 1025-65535 acl CONNECT method CONNECT # Rule # deny http_access deny !Safe_ports http_access deny CONNECT !SSL_ports # 宛先がローカルアドレスであれば拒否 http_access deny localhost_dst # 宛先がプライベートアドレスであれば拒否 http_access deny private # 宛先がリンクローカルアドレスであれば拒否 http_access deny linklocal # allow http_access allow localhost http_access allow all http_port 0.0.0.0:3128 acl NOCACHE src all cache deny NOCACHE
防御できているかを検証
ECSに上記したSSRF脆弱性のあるPHPアプリをデプロイし、防御用のProxyをサイドカーで動作させる。
そうした状態で、
curl 'http://xxx:8080/ssrf.php?url=http://ifconfig.me'
curl 'http://xxx:8080/ssrf.php?url=http://169.254.170.2'
curl 'http://xxx:8080/ssrf.php?url=http://2852039170/'
を実行する。最初のURLはリクエストに成功するはずであり、次とその次URLは失敗するはずである。
最初の結果は下記のようになり、リクエストに成功している(TCP_MISSはキャッシュミスしたということ、キャッシュは無効にしているので常にミス)
GET http://ifconfig.me/ HTTP/1.1" 200 517 "-" "-" TCP_MISS:HIER_DIRECT
次のリクエストは、下記のようになり失敗している。ステータスコード 403 が脆弱性のあるPHPアプリに返っているはずである。
GET http://169.254.170.2/ HTTP/1.1" 403 323 "-" "-" TCP_DENIED:HIER_NONE
最後のリクエストは、下記のように失敗している。10進数のIPv4アドレスが解釈され、通常の形式で表示されていることがわかる。
GET http://169.254.170.2/ HTTP/1.1" 403 323 "-" "-" TCP_DENIED:HIER_NONE
まとめ
なんかうまくいきそう。