「」と「」(通称:徳丸本)を参考に、セキュリティの勉強を進めています。
前回は、以前 に行った OWASP ZAP の自動脆弱性スキャンの結果の「パストラバーサル」について、分析と対策までやりました。
今回は、クロスサイトスクリプティング(XSS)を見ていきます。
それでは、やっていきます。
※2024/8/16:全体を書き直し
当初の記事は、XSS(DOMベース)を勘違いしていたので、全面的に書き直しました。
- 参考文献
- はじめに
- クロスサイトスクリプティングの検出結果の確認(Reflected)
- 自動脆弱性スキャンの再実行
- クロスサイトスクリプティングの検出結果の確認(DOMベース)
- 自動脆弱性スキャンの再実行
- おわりに
参考文献
はじめに
「セキュリティ」の記事一覧です。良かったら参考にしてください。
・第2回:Ghidraで始めるリバースエンジニアリング(使い方編)
・第3回:VirtualBoxにParrotOS(OVA)をインストールする
・第4回:tcpdumpを理解して出力を正しく見れるようにする
・第5回:nginx(エンジンエックス)を理解する
・第6回:Python+Flask(WSGI+Werkzeug+Jinja2)を動かしてみる
・第7回:Python+FlaskのファイルをCython化してみる
・第8回:shadowファイルを理解してパスワードを解読してみる
・第9回:安全なWebアプリケーションの作り方(徳丸本)の環境構築
・第10回:Vue.jsの2.xと3.xをVue CLIを使って動かしてみる(ビルドも行う)
・第11回:Vue.jsのソースコードを確認する(ビルド後のソースも見てみる)
・第12回:徳丸本:OWASP ZAPの自動脆弱性スキャンをやってみる
・第13回:徳丸本:セッション管理を理解してセッションID漏洩で成りすましを試す
・第14回:OWASP ZAPの自動スキャン結果の分析と対策:パストラバーサル
・第15回:OWASP ZAPの自動スキャン結果の分析と対策:クロスサイトスクリプティング(XSS) ← 今回
徳丸本の環境構築については、以下の第9回でやりました。
daisuke20240310.hatenablog.com
また、徳丸本が用意してくれている、脆弱なアプリケーション Bad Todo の準備については、以下の第12回でやりました。今回は、この環境を使ってやっていきます。
daisuke20240310.hatenablog.com
クロスサイトスクリプティングの検出結果の確認(Reflected)
まず、先頭にある反射型のクロスサイトスクリプティングの検出について見ていきます。GET と POST の2件があります。
クロスサイトスクリプティング(Reflected)の脆弱性の分析(GET)
GETリクエストの方の指摘内容を見てみます。

以下の検索ボックスで、適切なエスケープ処理が行われていないため、任意のスクリプトが実行できるというもののようです。

問題の todolist.php のソースコードの抜粋は以下です。
$key には、検索ボックスに入力された文字列が入ります。確かに、エスケープ処理が行われずに、データベースにアクセスしてるのと、検索ボックスの value に設定しています。前者は、SQLインジェクションの対策になるので、ここでは後者について再現と対策をしていきます。
<?php require_once('./common.php'); $id = $user->get_id(); if (empty($id)) $id = -1; $reqid = filter_input(INPUT_GET, 'id'); $key = filter_input(INPUT_GET, 'key'); if (empty($reqid)) $reqid = -1; try { $dbh = dblogin(); $sql = "SELECT todos.id, users.userid, todo, c_date, due_date, done, org_filename, real_filename, public FROM todos INNER JOIN users ON users.id=todos.owner AND (todos.owner=? OR ?) AND (todos.owner = ? OR todos.public > 0 OR ? > 0)"; if (! empty($key)) { $sql .= " AND todo LIKE '%$key%'"; } $sth = $dbh->prepare($sql); $sth->execute(array($reqid, $reqid < 0, $id, $user->is_super())); ?><html> <head> <link rel="stylesheet" type="text/css" href="css/common.css"> <script src="../js/jquery-1.8.3.js"></script> <title>一覧</title> </head> <body> <div id="top"> <?php $menu = 1; require "menu.php"; ?> <div id="search"> <form action="" method="get"> <input type="text" name="key" value="<?php echo $key; ?>"> <input type="submit" value="検索"> </form> </div>
クロスサイトスクリプティング(Reflected)の脆弱性の再現(GET)
実際に、エスケープ処理が無いと、どうなるかをやってみたいと思います。
書籍でも紹介されている XSS の攻撃方法を試したいと思います。
検索ボックスに、"><script>alert(document.cookie)</script> を入力します。これは、input のタグを終了させて、JavaScript を追加しています。入力したら、検索ボタンをクリックします。
Cookie の内容が表示されました。脆弱性の再現が出来ました。

これだけだと、自分の Cookie の値が見れるだけなので、あんまり意味はありませんが、書籍に書かれているように、罠サイトに誘導するなどと、組み合わせることで、他者のセッションID を取得できたりするようです。
クロスサイトスクリプティング(Reflected)の脆弱性の対策(GET)
とにかく、エスケープ処理が無いことが問題だと思うので、対策してみます。
PHP では、htmlspecialchars関数を使って、エスケープ処理が行えます。common.php に htmlspecialchars関数が簡単に使えるラッパー関数が用意されています(以下は、common.php の e関数)が、echo が入ってるので、ここでは使いにくいです。
function e($s)
{
echo htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}
htmlspecialchars関数を使った XSS の対策は以下です。
--- todo.org/todolist.php 2018-08-16 12:03:14.000000000 +0900 +++ todo.change/todolist.php 2024-08-16 19:40:02.000000000 +0900 @@ -27,7 +27,7 @@ <?php $menu = 1; require "menu.php"; ?> <div id="search"> <form action="" method="get"> - <input type="text" name="key" value="<?php echo $key; ?>"> + <input type="text" name="key" value="<?php echo htmlspecialchars($key); ?>"> <input type="submit" value="検索"> </form> </div>
クロスサイトスクリプティング(Reflected)の脆弱性の分析(POST)
次に、POSTリクエストの方を見てみます。logindo.php にも XSS の脆弱性があるようです。

現在の logindo.php のソースコードです。
3つの入力がありますが、いずれもエスケープ処理がありません。
<?php require_once './common.php'; if (! isset($_POST['userid']) || ! isset($_POST['pwd']) || ! isset($_POST['url'])) { exit; } error_log("userid=" . $_POST['userid'] . ", pwd=" . $_POST['pwd'] . ", url=" . $_POST['url']); try { $dbh = dblogin(); $userid = filter_input(INPUT_POST, 'userid'); $pwd = substr($_POST['pwd'], 0, 6); $url = filter_input(INPUT_POST, 'url'); $sql = "SELECT id, userid FROM users WHERE userid='$userid'"; $sth = $dbh->query($sql); $row = $sth->fetch(PDO::FETCH_ASSOC); $sth = null; if (! empty($row)) { $sqlstm = "SELECT id, userid, super FROM users WHERE userid='$userid' AND pwd='$pwd'"; $sth = $dbh->query($sqlstm); $row = $sth->fetch(PDO::FETCH_ASSOC); if (! empty($row)) { $_SESSION['login'] = true; $user = new User($row['id'], $userid, $row['super']); setcookie('USER', serialize($user), 0, '/'); header('Location: ' . $url . '?' . SID); } else { e("パスワードが違います"); exit; } } else { e("そのユーザーは登録されていません"); exit; } } catch (PDOException $e) { die('接続に失敗しました: ' . $e->getMessage()); } ?><body> ログイン成功しました<br> 自動的に遷移しない場合は以下のリンクをクリックして下さい。 <a href="<?php echo "$url?" . SID; ?>">todo一覧に遷移</a> </body>
1点目は、$sql = "SELECT id, userid FROM users WHERE userid='$userid'"; のところです。おそらく、ここで今回の指摘は出てそうです。この対策は、SQLインジェクションの対策になりそうなので、次回に対応したいと思います。
2点目は、$sqlstm = "SELECT id, userid, super FROM users WHERE userid='$userid' AND pwd='$pwd'"; で、こちらも、SQLインジェクションの対策になりそうです。
3点目は、<a href="<?php echo "$url?" . SID; ?>">todo一覧に遷移</a> で、エスケープ処理がありませんが、指摘も出てないようです。ここの再現をやってみたいと思います。
クロスサイトスクリプティング(Reflected)の脆弱性の再現(POST)
単純にやってみても再現できませんでした。ログインは、ログイン処理が終わると、Todo の一覧へリダイレクトしているためです。alert関数が実行されず(実行されたけどリダイレクトされて分からないだけ?)、Todo の一覧に遷移してしまいました。
そこで、再現を確認するため、header('Location: ' . $url . '?' . SID); をコメントアウトすることにしました。
少し小細工をしましたが、実際に再現させてみます。
まず、再現させるため、ログインしている場合は、いったんログアウトします。ログイン画面に遷移させて、id とパスワードを入力し、Burp Suite で Intercept を有効にして、ログインボタンをクリックします。
すると、POSTリクエストが送信される前に、Burp Suite が止めてくれます。POST するデータを以下のように書き換えます。もともとは、userid=daisuke&pwd=useruser&url=todolist.php でした。書き換えができたら、Forwardボタンをクリックします。

すると、リダイレクトは無効化したので、Cookie の値が表示され、再現することができました。

というわけで、ここのエスケープ処理が無かった部分について、OWASP ZAP が指摘したわけではないですが、問題部分ということで修正を行いたいと思います。
クロスサイトスクリプティング(Reflected)の脆弱性の対策(POST)
では、対策します。修正点は以下です。
--- todo.org/logindo.php 2018-08-15 15:29:23.000000000 +0900 +++ todo.change/logindo.php 2024-08-14 22:20:21.000000000 +0900 @@ -1,5 +1,8 @@ <?php require_once './common.php'; + if (! isset($_POST['userid']) || ! isset($_POST['pwd']) || ! isset($_POST['url'])) { + exit; + } try { $dbh = dblogin(); $userid = filter_input(INPUT_POST, 'userid'); @@ -33,5 +36,5 @@ ?><body> ログイン成功しました<br> 自動的に遷移しない場合は以下のリンクをクリックして下さい。 -<a href="<?php echo "$url?" . SID; ?>">todo一覧に遷移</a> +<a href="<?php echo e("$url?" . SID); ?>">todo一覧に遷移</a> </body>
自動脆弱性スキャンの再実行
では、自動診断を実行してみます。
クロスサイトスクリプティング(Reflected)の GET の方は、指摘されないようになりました。POST が残ってしまっていますが、このときはアラートをクリアし忘れたからなのか、次に再診断を行った結果を見ると POST の方も指摘されなくなっていました。

クロスサイトスクリプティングの検出結果の確認(DOMベース)
次は「クロスサイトスクリプティング(DOMベース)」を見ていきます。
クロスサイトスクリプティング(DOMベース)の脆弱性の分析
3件の指摘が上がっています。

徳丸本の説明を読むと、XSS の DOMベースは、JavaScript に原因がある脆弱性とのことです。todolist.php の JavaScript を見ていきます。
1つ目の関数「checkOrClearAll()」は、Todoリスト(表)のタイトルにあるチェックボックスに反応する関数で、全体をチェックON にしたり、チェックOFF にしたりできます。
2つ目の関数「check()」は、なかなか難しく、ちゃんと説明できませんが、この todolist.php に対して、#(ハッシュ、ページ内リンク)を付けて呼び出す(例:https://example.jp/todo/todolist.php?#all)と、対象の Todo がチェックON になるという機能です。例のように、#all でページを開くと、現在の全ての Todo のチェックが ON になります。
3つ目の関数は、あまり分かってないのですが、ページ内でハッシュが変更された場合は、window.addEventListener("hashchange", check, false); が反応して、チェックが変化し、3つ目の関数は、最初にこのページがロードしたときに反応するための関数かな、と思います。
<script> window.addEventListener("hashchange", check, false); function checkOrClearAll(checkbox) { $('input[name="id[]"]').prop('checked', checkbox.checked); } function check() { var checklist = decodeURIComponent (location.hash.slice(1)); if (checklist === 'all') { $('input[name="id[]"]').prop('checked', true); } else { var a = checklist.split(','); a.map(function(id) { $('input[name="id[]"][value="' + id + '"]').prop('checked', true); }); } } $(function() { check(); }); </script>
理解は十分ではないですが、自動スキャンの指摘のように、https://example.jp/todo/todolist.php#<img src='random.gif' onerror=alert(5397)> を URL として指定すると、任意の JavaScript が実行できるということだと思います。
クロスサイトスクリプティング(DOMベース)の脆弱性の再現
では、実際にブラウザに、上のようなアドレスを直接設定して再現させてみたいと思います。
alert関数が実行されました。再現できたようです。

クロスサイトスクリプティング(DOMベース)の脆弱性の対策
対策としては、やはりエスケープ処理を行うことになります。JavaScript にはエスケープ処理を行う関数は用意されてないからなのか、徳丸本では、エスケープ処理を実装した関数を提供してくれています。
以下は、ソースコードの 4h/4h-002a.html です。
<body> アクセス解析サンプル <script> function escape_html(s){ return s.replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, "> ") .replace(/"/g, """) .replace(/'/g, "'"); } var url = decodeURIComponent(location.href); document.write('<img src="http://api.example.net/4h/4h-003.php?' + escape_html(url) + '">'); </script> </body>
この関数を使わせてもらうことにします。
以下が修正内容になります。エスケープ処理をした変数を使うようにしました。
--- todo.org/todolist.php 2018-08-16 12:03:14.000000000 +0900 +++ todo.change/todolist.php 2024-08-16 21:09:50.000000000 +0900 @@ -27,7 +27,7 @@ <?php $menu = 1; require "menu.php"; ?> <div id="search"> <form action="" method="get"> - <input type="text" name="key" value="<?php echo $key; ?>"> + <input type="text" name="key" value="<?php echo htmlspecialchars($key); ?>"> <input type="submit" value="検索"> </form> </div> @@ -89,8 +89,17 @@ $('input[name="id[]"]').prop('checked', checkbox.checked); } +function escape_html(s){ + return s.replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, "> ") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + function check() { var checklist = decodeURIComponent (location.hash.slice(1)); + checklist = escape_html(checklist); if (checklist === 'all') { $('input[name="id[]"]').prop('checked', true); } else {
自動脆弱性スキャンの再実行
では、自動診断を実行してみます。クロスサイトスクリプティング(DOMベース)の3件の指摘が無くなりました。ただ、XSS(DOMベース)の指摘は、何度か自動脆弱性スキャンをしてると、出たり消えたりするんですよね、、少し長い目で見る必要がありそうです。

おわりに
今回は、クロスサイトスクリプティングの脆弱性について、再現と対策を行いました。当初は、DOMベースの理解が出来ておらず、おかしな記事になっていましたが、全面的に書き直しました、すいません。
次回は、SQLインジェクションを見ていきたいと思います。
今回の記事にはあまり関係ないですが、wasbook では、nginx と apache が使われてます。今回は、apache のロゴを使わせていただきました。ありがとうございます。
最後になりましたが、エンジニアグループのランキングに参加中です。
気楽にポチッとよろしくお願いいたします🙇
今回は以上です!
最後までお読みいただき、ありがとうございました。