SQLインジェクションによる認証回避
SQLインジェクションによる影響として、情報が漏洩するとか、データが勝手に更新されてしまうなどとともに、認証回避の例がよく紹介されます(私の本でも取り上げています)。
典型的な例は下記のとおりです。
// $id と $password は外部からの入力
$sql = "SELECT * FROM users WHERE id='$id' AND password='$password'";
このケースで、\$idに適当な値、$passwordとして「'OR'a'='a」を入力すると、生成されるSQL文は下記となります。
SELECT * FROM users WHERE id='XXX' AND password=''OR'a'='a'
ANDとORでは、ANDの方が先に計算されます。id='XXX' AND password='' は偽ですが、'a'='a'は常に真なので、WHERE句全体は常に真になります。これでパスワードを知らなくてもログインできるというわけです。
パスワードがハッシュ値で保存されているとどうなる?
しかし、今どきはパスワードはハッシュ値で保存することになっています。なのでログイン処理は以下のようになっているはずです(PHPの場合)。
$sql = "SELECT * FROM users WHERE userid = '$userid'";
$stmt = $pdo->query($sql);
$user = $stmt->fetch();
if ($user && password_verify($password, $user['password'])) {
echo "ログイン成功:" . htmlspecialchars($user['userid']);
} else {
// ログイン失敗
この処理ですと、SQLクエリの結果、ユーザーがヒットして、かつユーザのパスワードハッシュ値が、入力値から計算したハッシュ値と一致していなければなりません。PHPのpassword_verify関数(マニュアル)がこのチェックをしています。なので、「'OR'a'='a」などで認証回避できません。
練習問題を作ってみた
この条件でSQLインジェクションによる認証回避の練習問題を作ってみました。以下はログイン処理の中身です。
<?php
$userid = $_POST['userid'];
$password = $_POST['password'];
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
];
try {
$pdo = new PDO("sqlite:../db.sqlite3", null, null, $options);
$sql = "SELECT * FROM users WHERE userid = '$userid'";
$stmt = $pdo->query($sql);
$user = $stmt->fetch();
if ($user && password_verify($password, $user['password'])) {
echo "ログイン成功:" . htmlspecialchars($user['userid']);
} else {
echo "ログイン失敗";
}
} catch (Exception $e) {
echo "エラー:" . htmlspecialchars($e->getMessage());
}
データベースの中身
CREATE TABLE users (id int, userid varchar(64), password varchar(128), email varchar(128), PRIMARY KEY (id));
INSERT INTO users VALUES (1, 'admin', '$2y$10$7sTu7b3VW6cMreA6QrdYkOQpPP9hSiXLDpuAzElGHios2N/y5Ct2C', 'admin@example.jp');
INSERT INTO users VALUES (2, 'alice', '$2y$10$fxOHmQr3NB4xfvkxOnTTqudbLGUQj4LCgScg9vmsNnpTpRuG4lWMW', 'alice@example.jp');
ログインフォームを含めた完全なソースコードは、GitHubのリポジトリを用意しました。以下のコマンドでインストールできます。Windows、Mac、Linuxに対応しているはずです。
$ git clone https://github.com/ockeghem/SQLi-to-bypass-auth.git
$ cd SQLi-to-bypass-auth
$ docker compose up -d
http://localhost:7890/ にアクセスすると以下のログインフォームが表示されます。
攻撃の結果、以下の状態(ログイン成功:admin)となれば攻撃成功です。
想定正解については、数日後に公開したいと思います。
2023/9/20追記。解答編を書きました。
発展問題(2023/9/18追記)
adminでログインできたという方向けに発展問題を。
- ソースコードの情報は使ってよい想定ですが、ソースコードの内容に依存しない解法も考えてください
- SQLインジェクションを使ってSQLiteのバージョンを表示してください
- SQLインジェクションを使ってテーブル情報(テーブル名や列名など)を表示してください
※ これから解かれる人もいるので、X(Twitter)やブックマーク、この記事のコメント欄などに解答やヒントは書かないでください。ご自身のブログやQiita等に書くのはかまいません。