zkat’s diary

技術ブログ

クライアント証明書による認証があるウェブサイトでCSRF(Cross Site Request Forgery)は発生するか確認する

結論

発生します。

理由

CSRFは、セッション管理の不備を悪用した攻撃だからです。 認証方式として、クライアント証明書を使っていても、その不備を補うことはできません。

CSRF一般の話になりますが、攻撃者は対象のウェブサイトにアクセス権限(クライアント証明書)を持っている必要は ない(正当な権限をもつユーザーにリクエストを強要しているだけ)ためです。1

実際にやってみる

準備1: クライアント証明書とサーバー証明書を発行する

サーバー証明書とクライアント証明書がそれぞれ必要になりますので、 プライベート認証局を作り、そこから発行します。

まず、認証局(ルート)を作成します。
ここで作成した、DevelopmentPurposePrivateRootCA1.crtをブラウザのトラストストアにインストールしておきます。

$ openssl genrsa -des3 -out DevelopmentPurposePrivateRootCA1.key 2048
$ openssl req -x509 -new -nodes \
-key DevelopmentPurposePrivateRootCA1.key \
-sha256 -days 90 -out DevelopmentPurposePrivateRootCA1.crt \
-subj "/C=JP/ST=Tokyo/L=Chiyoda/O=Dummy Company/OU=Dummy Division/CN=DevelopmentPurposePrivateRootCA1"

つづいて、サーバー証明書を先ほどの認証局から発行します。
ここで発行したサーバー証明書(example.jp.crt)と秘密鍵(private-server-rsa-key)は、次に説明するhttpsサーバー(Nginx)の設定の時に使います。
発行先のドメイン(common name)には、example.jpを指定することとします。

# CSRを作成する
$ openssl genrsa 2048 > ./private-server-rsa-key
$ openssl req -new -key ./private-server-rsa-key -sha256 -out ./server-csr
# CSRをもとに、サーバー証明書を発行する
$ cat << __EOF__ > ./signing_config
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names

[alt_names]
DNS.1 = example.jp
__EOF__
$ openssl x509 -req -in ./server-csr -CA DevelopmentPurposePrivateRootCA1.crt \
-CAkey DevelopmentPurposePrivateRootCA1.key \
-CAcreateserial \
-out example.jp.crt -days 90 -sha256 -extfile ./signing_config

同じように、クライアント証明書を発行します。 発行したクライアント証明書(client.crt.p12)をブラウザにimportしておきます。

# クライアント証明書用のCSRをつくる
$ openssl genrsa 2048 > ./private-client-rsa-key
$ openssl req -new -key ./private-client-rsa-key -sha256 -out ./client-csr
# クライアント証明書を発行する
$ cat << __EOF__ > ./signing_config
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
extendedKeyUsage = clientAuth
__EOF__
$  openssl x509 -req -in ./client-csr \
-CA DevelopmentPurposePrivateRootCA1.crt \
-CAkey DevelopmentPurposePrivateRootCA1.key \
-CAcreateserial \
-out client.crt -days 90 -sha256 -extfile ./signing_config
$ openssl pkcs12 -export -out client.crt.p12 -inkey ./private-client-rsa-key -in client.crt

証明書の準備、めんどくさいですね。

準備2: Nginxを立ち上げる

以下のような設定にてNginxでhttpsのサーバーを立ち上げます。 裏側のアプリ(CSRFあり)をPythonのFlaskで作ろうとしているので、uWSGIの設定を入れています。 example.jpとして立ち上げたので、アクセス元のブラウザがexample.jpを名前解決できるように、hostsファイルなどに設定を追加しておく必要がありますが、 割愛します。

server {
        server_name example.jp;
        listen 443;

        ssl on;
        ssl_protocols TLSv1.1 TLSv1.2;
        ssl_ciphers HIGH:!aNULL:!MD5;

        ssl_certificate /path/to/example.jp.crt;
        ssl_certificate_key /path/to/private-server-rsa-key;

        ssl_verify_client on;
        ssl_client_certificate /path/to/DevelopmentPurposeRootCA1.crt;

        # Add index.php to the list if you are using PHP
        index index.html index.htm index.nginx-debian.html;

        location / {
                include uwsgi_params;
                uwsgi_pass unix:/tmp/csrfapp.sock;

        }

}

準備3: CSRF脆弱性があるアプリを立ち上げる

なんでもよいですが、CSRF脆弱性があるアプリを立ち上げます。 このアプリは、buy_confirmからbuy_completeに遷移するときに、画面遷移の正当性を確認していないので、
CSRF脆弱性があります。

CSRFを起こす

まず、対象のウェブサイトにログインしておきます。先ほど発行したクライアント証明書を提示します。

f:id:zkat:20200609015401j:plain f:id:zkat:20200609015452j:plain

つづいて、ログイン状態を保持したまま、悪意のあるウェブサイトのボタンをクリックしてみます。 悪意のあるウェブサイトは例えば以下のようになっています。

<html>
当選おめでとうございます!
以下をクリックして、賞金を獲得!
<form action="https://example.jp/buy_complete" method="POST">
    <input type="hidden" name="amount" value=100>
    <input type="submit" value="アクセス">
</form>
</html>

f:id:zkat:20200609015818j:plain

これにアクセスすると、CSRFが発生し、ユーザーの意図しない操作が実行されてしまいます。

f:id:zkat:20200609020016j:plain

防ぎ方

防ぎ方は、クライアント証明書があるか無いかなどは関係なく、様々なウェブサイトで説明されている通りです。 操作を完了する直前の画面で、攻撃者に知られることのない情報を埋め込み、画面遷移した際にその値が保持されているかチェックすることで、防ぐことができます。


  1. ただ、一般的には攻撃者がCSRFを準備する為のフェーズがあるかと思いますが、アクセス権限がないとその作業が容易にできないと思われる為、攻撃者がアクセス権限を持っている状況でないと発生しずらいとは思います。