土日の勉強ノート

AI、機械学習、最適化、Pythonなどについて、技術調査、技術書の理解した内容、ソフトウェア/ツール作成について書いていきます

OWASP ZAPの自動スキャン結果の分析と対策:SQLインジェクション

ハッキング・ラボのつくりかた 完全版 仮想環境におけるハッカー体験学習」と「体系的に学ぶ 安全なWebアプリケーションの作り方 第2版 脆弱性が生まれる原理と対策の実践」(通称:徳丸本)を参考に、セキュリティの勉強を進めています。

前回は、以前 に行った OWASP ZAP の自動脆弱性スキャンの結果の「クロスサイトスクリプティング(XSS)」について、分析と対策までやりました。

今回は、SQLインジェクションを見ていきます。

それでは、やっていきます。

参考文献

はじめに

「セキュリティ」の記事一覧です。良かったら参考にしてください。

セキュリティの記事一覧

徳丸本の環境構築については、以下の第9回でやりました。

daisuke20240310.hatenablog.com

また、徳丸本が用意してくれている、脆弱なアプリケーション Bad Todo の準備については、以下の第12回でやりました。今回は、この環境を使ってやっていきます。

daisuke20240310.hatenablog.com

SQLインジェクションの検出結果の確認

まずは、脆弱性スキャンの指摘内容を細かく見ていきます。

SQLインジェクションの脆弱性の分析(認証回避)

SQLインジェクションの先頭から見ていきます。1つ目は、Authentication Bypass とあります。ログインには認証が必要ですが、それを回避できる SQLインジェクションが有効だった、ということだと思います。

SQLインジェクションの認証回避の指摘
SQLインジェクションの認証回避の指摘

ログイン画面の id のところに、ユーザID を入力(daisuke)の代わりに、daisuke' AND '1'='1' -- を入力すると、パスワードを入力しなくても、ログインできたということでしょうか。

指摘の2つ目は、1つ目と同じ内容でした。

指摘の3つ目は、同じ個所ですが、攻撃方法が違うようでした。対策内容は同じになりそうです。

SQLインジェクションの他の指摘
SQLインジェクションの他の指摘

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 のロゴを使わせていただきました。ありがとうございます。

最後になりましたが、エンジニアグループのランキングに参加中です。

気楽にポチッとよろしくお願いいたします🙇

今回は以上です!

最後までお読みいただき、ありがとうございました。