パスワードがハッシュ値で保存されているサイトのSQLインジェクションによる認証回避の練習問題

# SQLインジェクションによる認証回避

SQLインジェクションによる影響として、情報が漏洩するとか、データが勝手に更新されてしまうなどとともに、認証回避の例がよく紹介されます（[私の本](https://amzn.to/46xDPqP)でも取り上げています）。

典型的な例は下記のとおりです。

```PHP
// $id と $password は外部からの入力
$sql = "SELECT * FROM users WHERE id='$id' AND password='$password'";
```

このケースで、\\$idに適当な値、$passwordとして「'OR'a'='a」を入力すると、生成されるSQL文は下記となります。

```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関数（[マニュアル](https://www.php.net/manual/ja/function.password-verify.php)）がこのチェックをしています。なので、「'OR'a'='a」などで認証回避できません。

# 練習問題を作ってみた

この条件でSQLインジェクションによる認証回避の練習問題を作ってみました。以下はログイン処理の中身です。

```PHP:login.php
<?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());
}
```
データベースの中身
```SQL:init.sql
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のリポジトリ](https://github.com/ockeghem/SQLi-to-bypass-auth)を用意しました。以下のコマンドでインストールできます。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/ にアクセスすると以下のログインフォームが表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/22718/80e24c09-5175-e765-305b-48278b1172f7.png)
攻撃の結果、以下の状態（ログイン成功:admin）となれば攻撃成功です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/22718/1b526fe0-e941-c3f1-a68f-db193e3cc0d5.png)

想定正解については、数日後に公開したいと思います。
2023/9/20追記。解答編を書きました。

https://qiita.com/ockeghem/items/c5818819a87ee22c1681

# 発展問題（2023/9/18追記）
adminでログインできたという方向けに発展問題を。

- ソースコードの情報は使ってよい想定ですが、ソースコードの内容に依存しない解法も考えてください
- SQLインジェクションを使ってSQLiteのバージョンを表示してください
- SQLインジェクションを使ってテーブル情報（テーブル名や列名など）を表示してください

※ これから解かれる人もいるので、X(Twitter)やブックマーク、この記事のコメント欄などに解答やヒントは書かないでください。ご自身のブログやQiita等に書くのはかまいません。