「」と「」(通称:徳丸本)を参考に、セキュリティの勉強を進めています。
前回は、以前 に行った OWASP ZAP の自動脆弱性スキャンの結果の「クロスサイトスクリプティング(XSS)」について、分析と対策までやりました。
今回は、SQLインジェクションを見ていきます。
それでは、やっていきます。
参考文献
はじめに
「セキュリティ」の記事一覧です。良かったら参考にしてください。
・第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)
・第16回:OWASP ZAPの自動スキャン結果の分析と対策:SQLインジェクション ← 今回
徳丸本の環境構築については、以下の第9回でやりました。
daisuke20240310.hatenablog.com
また、徳丸本が用意してくれている、脆弱なアプリケーション Bad Todo の準備については、以下の第12回でやりました。今回は、この環境を使ってやっていきます。
daisuke20240310.hatenablog.com
SQLインジェクションの検出結果の確認
まずは、脆弱性スキャンの指摘内容を細かく見ていきます。
SQLインジェクションの脆弱性の分析(認証回避)
SQLインジェクションの先頭から見ていきます。1つ目は、Authentication Bypass とあります。ログインには認証が必要ですが、それを回避できる SQLインジェクションが有効だった、ということだと思います。

ログイン画面の id のところに、ユーザID を入力(daisuke)の代わりに、daisuke' AND '1'='1' -- を入力すると、パスワードを入力しなくても、ログインできたということでしょうか。
指摘の2つ目は、1つ目と同じ内容でした。
指摘の3つ目は、同じ個所ですが、攻撃方法が違うようでした。対策内容は同じになりそうです。

SQLインジェクションの脆弱性の再現(認証回避)
まずは、試してみます。ユーザID には攻撃の文字列を入力して、パスワードを適当な文字列を入れて、ログインしてみます。

うまくいってない気がします。

logindo.php のソースコードを確認します。SQL文が2回実行されています。この2回をうまくやるとログインできそうです。
<?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'); $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()); } ?>
以下の2文です。攻撃は daisuke' AND '1'='1' -- です。
<?php $sql = "SELECT id, userid FROM users WHERE userid='$userid'"; $sqlstm = "SELECT id, userid, super FROM users WHERE userid='$userid' AND pwd='$pwd'";
まず、攻撃で使われている -- は、SQLではコメントの開始を意味します。以降を無視させたいというわけです。
つまり、パスワードのところをコメントアウトして、代わりに AND '1'='1' を入れておくということのようです。
しかし、攻撃はうまくいっていません。だいぶ悩んだのですが、分かりました。攻撃したとき、ブラウザにエラーが表示されています。これは文法エラーが出てしまっています。これを解消する必要があります。
では、どこに問題があるのかというと、SQL のコメントは、-- の後に半角スペースが必要らしいです(MySQL だけらしい?)。知らんがな、と言いたくなるような内容ですが、データベースを扱うには常識なんでしょうか、だいぶ時間がかかってしまいました。
では、気を取り直して、daisuke' AND '1'='1' -- で、攻撃を行います。見た目が一緒なので分かりにくいですが、-- の後に、半角スペースを入れいています。

適当なパスワードで、ログインできました。ユーザ名が変な表示になっていますが、普通に操作できます。自分の作った Todo を削除してみると、普通に削除できました。

SQLインジェクションの脆弱性の対策(認証回避)
それでは対策を考えていきます。
SQLインジェクションの対策は、プレースホルダを使えばいいとのことです。
プレースホルダには、静的プレースホルダと動的プレースホルダがあり、徳丸本では、動的プレースホルダでも、SQLインジェクションは対策できるが、静的プレースホルダを推奨していました。
静的プレースホルダは実装が増えるので、今回は動的プレースホルダで対策を行いたいと思います。
2文ある SQL文のうち、最初の SQL文の対策です。まず、対策前です。
<?php // 対策前 $sql = "SELECT id, userid FROM users WHERE userid='$userid'"; $sth = $dbh->query($sql); $row = $sth->fetch(PDO::FETCH_ASSOC); $sth = null;
次に、対策後です。? がプレースホルダで、SQL文を確定させた後に、bindValue() で、値を設定するので、安全ということらしいです。プレースホルダを使った場合、なぜか、query() でエラーが出るようになったので、prepare() と execute() を使う方に変更しました。
<?php // 対策後 $sql = $dbh->prepare("SELECT id, userid FROM users WHERE userid = ?"); $sql->bindValue(1, $userid, PDO::PARAM_STR); $sql->execute(); $row = $sql->fetch(PDO::FETCH_ASSOC); $sql = null;
続いて、もう1つの SQL文です。まず、対策前です。
<?php // 対策前 $sqlstm = "SELECT id, userid, super FROM users WHERE userid='$userid' AND pwd='$pwd'"; $sth = $dbh->query($sqlstm); $row = $sth->fetch(PDO::FETCH_ASSOC);
次に、対策後です。bindValue() の第1引数は、何番目のプレースホルダかを指定(1始まり)で、第2引数が変数、第3引数が変数の型です。
<?php // 対策後 $sqlstm = $dbh->prepare("SELECT id, userid, super FROM users WHERE userid = ? AND pwd = ?"); $sqlstm->bindValue(1, $userid, PDO::PARAM_STR); $sqlstm->bindValue(2, $pwd, PDO::PARAM_STR); $sqlstm->execute(); $row = $sqlstm->fetch(PDO::FETCH_ASSOC);
とりあえずの対策は出来たと思います。普通に動作することを確認しました。
logindo.php の修正点を貼っておきます。
--- todo.org/logindo.php 2018-08-15 15:29:23.000000000 +0900 +++ todo.change/logindo.php 2024-08-15 17:12:00.000000000 +0900 @@ -1,20 +1,27 @@ <?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'); $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; + $sql = $dbh->prepare("SELECT id, userid FROM users WHERE userid = ?"); + $sql->bindValue(1, $userid, PDO::PARAM_STR); + $sql->execute(); + $row = $sql->fetch(PDO::FETCH_ASSOC); + $sql = 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); + $sqlstm = $dbh->prepare("SELECT id, userid, super FROM users WHERE userid = ? AND pwd = ?"); + $sqlstm->bindValue(1, $userid, PDO::PARAM_STR); + $sqlstm->bindValue(2, $pwd, PDO::PARAM_STR); + $sqlstm->execute(); + $row = $sqlstm->fetch(PDO::FETCH_ASSOC); if (! empty($row)) { + error_log("row[id]=" . $row['id'] . ", row[super]=" . $row['super']); $_SESSION['login'] = true; $user = new User($row['id'], $userid, $row['super']); setcookie('USER', serialize($user), 0, '/');
自動脆弱性スキャンの再実行
では、対策したので、自動脆弱性スキャンを再実行してみたいと思います。
SQLインジェクションの赤フラグは無くなりました。赤のフラグはリスク高ですが、あと3つになりました。うち、2つはよく分からないので、あとは、External Redirect だけです。

External Redirect は、徳丸本では、オープンリダイレクト脆弱性として説明されています。こちらの対策は次回としたいと思います。
おわりに
今回は、SQLインジェクションの脆弱性について、再現と対策を行いました。今回はすっきりと指摘が消えてくれて良かったです。
次回は、オープンリダイレクトを見ていきたいと思います。
今回は wasbook で使われている MySQL のロゴを使わせていただきました。ありがとうございます。
最後になりましたが、エンジニアグループのランキングに参加中です。
気楽にポチッとよろしくお願いいたします🙇
今回は以上です!
最後までお読みいただき、ありがとうございました。