Docker+firewalldを使ってSSRF攻撃を防ぐ

  • URLをコピーしました!

こんにちは、インフラチームテックリードの櫻井です。

今回はDockerとfirewalldを使って内部ネットワークへのアクセスを制限し、SSRF攻撃を防ぐ方法について紹介します。

目次

SSRF攻撃とは

SSRF(Server Side Request Forgery)攻撃はWebアプリケーションに対する攻撃の一種で、公開されたサーバーを経由して公開されていない内部ネットワークのサーバーにアクセスする手法です。

SSRFの概略図

具体例

例えば以下のように外部から指定されたURLにcurlでリクエストを行い、その結果を出力するプログラムがあるとします(このプログラムにはXSS脆弱性も含まれていますが今回は割愛します)。

<?php

$ch = curl_init();

curl_setopt_array($ch, [
    CURLOPT_URL => 'http://' . $_REQUEST['url'],
    CURLOPT_RETURNTRANSFER => true
]);

echo curl_exec($ch);


このプログラムは何も対策がされていないため example.com のようなドメインだけではなく、 192.168.1.10 のようなプライベートIPにもリクエストを送信することができてしまいます。

例えばEC2インスタンスには 169.254.169.254 にリンクローカルアドレスが割り当てられており、先ほどのプログラムを通じてこのIPアドレスにリクエストを送ることでインスタンスのメタデータや一時的な認証情報などを盗むことができてしまいます。

あわせて読みたい
インスタンスアイデンティティドキュメント - Amazon Elastic Compute Cloud インスタンスアイデンティティドキュメントを使用して、インスタンスの属性を検証します。

実際はここまで分かりやすいパターンはあまりないと思いますが、OSSが内部ネットワークにも通信できることを知らずに使ってしまっていたというパターンは十分ありうると思います。

対策パターン

このようなSSRF攻撃を防ぐための対策には大きく分けて3種類あります。

  1. 外部からの入力をリクエストのURLに使用しない
  2. 正規表現などを使ってリクエスト送信先をバリデーションする
  3. ファイアウォールを使って内部ネットワークへのアウトバウンド通信を制限する

1. 外部からの入力をリクエストのURLに使用しない

この方法は外部からの入力をリクエストのURLに使用しないように、そもそも仕様を考え直したり機能を削除するという方法です。

SSRFの対策は難しいことが多いので、可能であればこのようにするのが一番安全かつシンプルな対策です。

しかしアプリケーションの要件によってはそのようにできない場合もあるため、その場合は2または3の対策を行う必要があります。

2. 正規表現などを使ってリクエスト送信先をバリデーションする

この方法は正規表現などのパターンマッチングを使って外部からの入力をチェックし、内部ネットワークへのリクエストをブロックするという対策です。

この方法は導入は比較的容易である反面、漏れ無く正確にパターンを記述するのは難しく、正規表現が複雑になるとデバッグやメンテナンスも難しくなります。

また自分で正規表現を作成せずWAFのマネージドルールを使って外部からの入力値をチェックするという方法もあります。

3. ファイアウォールを使って内部ネットワークへのアウトバウンド通信を制限する

この方法はLinuxのfirewalldやAWSのセキュリティグループなどの機能を利用して、指定した範囲のIPアドレスへのアウトバウンド通信を制限する方法です。

この方法はポートやCIDRを指定して柔軟かつ分かりやすく記述することができますが、サーバー単位で設定が適用されてしまうため、特定の機能でのみ内部ネットワークへの通信を制限するということができません。

しかしDocker上でその機能を動かし、Dockerの仮想ネットワークに対してファイアウォールの設定を適用することで、特定の機能のみ内部ネットワークへのアウトバウンド通信を制限することができるようになります。

また、Docker上で動かすことでファイルシステムへのアクセスも必要最小限に制限することができ、ディレクトリトラバーサル脆弱性の対策にもなります。

今回はこのDockerコンテナからのアウトバウンド通信をfirewalldで制限する方法について紹介したいと思います。

Dockerとfirewalldの設定手順

次に実際にDockerとfirewalldの具体的な設定手順について説明します。

今回のホストサーバーはAlmaLinux8+Apache+PHPの構成ですが、Nginxや他のプログラミング言語を使った構成にも応用できると思います。

Dockerとfirewalldをインストールする

まず初めにDockerとfirewalldをインストールする必要があります。

OSによってインストール方法は異なりますが、例えばRHEL8系のOSなら以下のコマンドでインストールできます。

$ sudo dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo
$ sudo dnf install docker-ce firewalld

Dockerのiptablesを無効化する

Dockerはデフォルトでiptablesを上書きする機能があるため、これを無効化します。

具体的には /etc/docker/daemon.json に以下の設定を追加します。

{
  "iptables": false
}

Dockerとfirewalldを起動する

次にインストールしたDockerとfirewalldを起動し、自動起動設定もしておきます。

$ sudo systemctl start docker
$ sudo systemctl enable docker
$ sudo systemctl start firewalld
$ sudo systemctl enable firewalld

firewalldのインバウンド通信を許可する

次に起動したfirewalldに対してインバウンド通信の許可設定を追加します。

今回はTCP通信のための80番ポートを許可します。

$ sudo firewall-cmd --permanent --add-port 80/tcp

firewalldのIPマスカレードを設定する

次にDockerからインターネットにアクセスするためにfirewalldにIPマスカレードを設定します。

$ sudo firewall-cmd --permanent --add-masquerade

Dockerコンテナからインターネットへ通信するためのルールを設定する

次にDockerコンテナのネットワーク からインターネットへ通信するためのルールを設定します。

Dockerはデフォルトだと docker0 インターフェースの bridge ネットワークを経由して外部と通信するため、これに対してfirewalldのルールを設定します。

bridge ネットワークのCIDRは以下のコマンドから 172.17.0.0/16 であることが確認できます。

$ sudo docker network inspect bridge
[
    {
        "Name": "bridge",
         ...
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                   "Subnet": "172.17.0.0/16",
                   "Gateway": "172.17.0.1"
                }
            ]
        },
       ...
    }
]

以下のコマンドで 172.17.0.0/16 からインターネットへの通信を行うためのNATルールを設定します。

$ sudo firewall-cmd --permanent --direct --add-rule ipv4 nat POSTROUTING 1 ! -o docker0 -s 172.17.0.0/16 -j MASQUERADE

DockerコンテナからプライベートIPへの通信を拒否する

次にDockerコンテナが経由するdocker0インターフェースからプライベートIPへの通信を拒否するルールを設定します。

プライベートIPの一覧は

あわせて読みたい

にあるものを使用しました。

$ sudo firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 1 -i docker0 -d 0.0.0.0/8 -j REJECT
$ sudo firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 1 -i docker0 -d 10.0.0.0/8 -j REJECT
$ sudo firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 1 -i docker0 -d 100.64.0.0/10 -j REJECT
$ sudo firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 1 -i docker0 -d 127.0.0.0/8 -j REJECT
$ sudo firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 1 -i docker0 -d 169.254.0.0/16 -j REJECT
$ sudo firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 1 -i docker0 -d 172.16.0.0/12 -j REJECT
$ sudo firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 1 -i docker0 -d 192.0.0.0/24 -j REJECT
$ sudo firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 1 -i docker0 -d 192.0.2.0/24 -j REJECT
$ sudo firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 1 -i docker0 -d 192.88.99.0/24 -j REJECT
$ sudo firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 1 -i docker0 -d 192.168.0.0/16 -j REJECT
$ sudo firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 1 -i docker0 -d 198.18.0.0/15 -j REJECT
$ sudo firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 1 -i docker0 -d 198.51.100.0/24 -j REJECT
$ sudo firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 1 -i docker0 -d 203.0.113.0/24 -j REJECT
$ sudo firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 1 -i docker0 -d 224.0.0.0/4 -j REJECT
$ sudo firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 1 -i docker0 -d 240.0.0.0/4 -j REJECT
$ sudo firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 1 -i docker0 -d 255.255.255.255/32 -j REJECT

firewalldをリロードして設定を反映する

次にここまで設定してきたfirewalldの設定を反映します。

$ sudo firewall-cmd --reload

apacheユーザーをdockerグループに追加する

今回はapacheユーザーで実行されているPHP経由でDockerを呼び出すため、apacheユーザーをdockerグループに追加します。

追加した後はApacheを再起動して変更を反映する必要があります。

$ sudo gpasswd -a apache docker
$ sudo systemctl restart httpd

Dockerコンテナ経由で実行する形にプログラムを変更する

最後に対象のプログラムをDockerから実行する形に変更します。

使用するDockerイメージは軽量で使いやすいものなら何でも良いですが、今回はcurlがインストールされている curlimages/curl を使用してみました。

また今回のようにシェルのコマンドの一部に外部からの入力を使う場合は、OSコマンドインジェクションを防ぐために必ず escapeshellarg() を使ってエスケープしておく必要があります。

OSコマンドインジェクションの対策としてはこのように escapeshellarg() でエスケープする方法の他にも proc_open() を使って標準入力から渡す方法や、PHP7.4以降なら proc_open() の command パラメータにコマンドの引数を含めたarrayを渡す方法などがあります。

<?php

$command = "docker run --rm curlimages/curl curl -s http://" . escapeshellarg($_REQUEST['url']);

passthru($command);

firewalldとDockerの設定を完了した状態でこのように変更したプログラムに ?url=169.254.169.254 をつけてアクセスしてみると、具体例 のときのプログラムとは違ってプライベートIPへのリクエストが拒否されていることが分かると思います。

まとめ

今回はDocker+firewalldを使ってプライベートIPへのアクセスを制限し、SSRF攻撃を防ぐ方法について紹介しました。

SSRF攻撃は他の脆弱性に比べて完全な対策が難しいことが多いですが、今回紹介した方法も一つの手段として参考にしていただけると幸いです。

参考

あわせて読みたい
SSRF(Server Side Request Forgery)徹底入門 SSRF(Server Side Request Forgery)という脆弱性ないし攻撃手法が最近注目されています。以下は、ここ3ヶ月にSSRFについて言及された記事です。 EC2上のAWS CLIで使われ...
あわせて読みたい
インスタンスアイデンティティドキュメント - Amazon Elastic Compute Cloud インスタンスアイデンティティドキュメントを使用して、インスタンスの属性を検証します。
GitHub
GitHub - mizzy/docker-firewalld-sample Contribute to mizzy/docker-firewalld-sample development by creating an account on GitHub.
あわせて読みたい
  • URLをコピーしました!

この記事を書いた人

2018年に京都大学を卒業後、PR TIMESに新卒入社。
現在は開発チームのサーバーサイドエンジニア兼インフラチームのテックリードを担当しています。

目次