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"
}

上のようなJSONトークンが返ってくる。

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

まとめ

なんかうまくいきそう。