「ハッキング・ラボのつくりかた 完全版 仮想環境におけるハッカー体験学習」と「体系的に学ぶ 安全なWebアプリケーションの作り方 第2版 脆弱性が生まれる原理と対策の実践」(通称:徳丸本)を参考に、セキュリティの勉強を進めています。
前回は、以前 に行った OWASP ZAP の自動脆弱性スキャンの結果の「クロスサイトスクリプティング(XSS)」について、分析と対策までやりました。
今回は、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 のロゴを使わせていただきました。ありがとうございます。
最後になりましたが、エンジニアグループのランキングに参加中です。
気楽にポチッとよろしくお願いいたします🙇
今回は以上です!
最後までお読みいただき、ありがとうございました。