前回:やられアプリ BadTodo - 17 認証(パスワードの強度・ログアウト) - demandosigno
BadTodo - 3.4 SQLインジェクション ID・パスワードの取得で確認したように、BadTodoはDB内にパスワードが平文で保存されているという問題点があります。
パスワードはハッシュ化されて保存されていることが多いです。その理由は次回に回しますが、先にハッシュ化の方法をメモしておきます。
PHPでパスワードハッシュを作る関数
password_hash, password_verify はPHP 5 >= 5.5.0で利用可能ですが、BadTodoのPHPは 5.3.3のため使えません。
password_hash("test", PASSWORD_DEFAULT)
結果 ↓
Fatal error: Call to undefined function password_hash()
password_verify($input_pass, $hashed_pass)
結果 ↓
Fatal error: Call to undefined function password_verify()
よって crypt の方を先に試し、password_hash, password_verify は後半で試します。
ただし、PHP 5.3.7 未満の crypt や crypt_blowfish には脆弱性があるためメモ程度に留めておきます。
JVNDB - PHP の crypt 関数における認証を回避される脆弱性
JVNDB - PHP で使用される crypt_blowfish におけるクリアテキストのパスワードを容易に推測される脆弱性
crypt
crypt("パスワード", "ソルト")
(PHP 4, PHP 5, PHP 7, PHP 8)
PHP: crypt - Manual
ハッシュ方式は、salt 引数によって決まります。
crypt('test');
PHP 8.0.0 より前のバージョンでは、salt パラメータは必須ではありませんでした。salt を省略した場合は標準の 2 文字 (DES) の salt を自動生成します。salt を省略すると crypt() が作るハッシュが弱いものになるため、このパラメータを省略した場合には E_NOTICE が発生します。
(Fatal error: Uncaught ArgumentCountError: crypt() expects exactly 2 arguments, 1 given)
ハッシュ形式の一例:
例1:CRYPT_STD_DES : 標準の DES ベースのハッシュで、アルファベット "./0-9A-Za-z" からなる 2 文字の salt を使用するもの。 salt として無効な文字を使うと crypt() は失敗します。
$password = 'test'
$hashed_password = crypt($password, "aa");
$hashed_password は aaqPiZY5xR5l.
例2:CRYPT_SHA512 : SHA-512 ハッシュに $6$ で始まる 16 文字の salt を組み合わせたもの。
前提:DBに保存してあるハッシュ化されたパスワードがあるとする。Linux端末上で
# openssl passwd -6 -salt=kdBX2lbhkgjOSs.t test で生成される(-6 = $6$ = SHA-512。testが生の値)
$hashed_password = '$6$kdBX2lbhkgjOSs.t$1VwBMMg5U/apz1dKb19oXHMW8wXy/kA0XwfpPWYy7xp4aB5uBGGzzYeiE0uUYv.QX9eiroROLk8RdHMTN6GTD/' (3つ目の$より後ろがパスワード。太字部分16文字がcryptに入力される新たなソルトとして使われる。ハッシュ化するためのソルトに、ハッシュ化されたパスワードを利用するということ)
<?php // クエリパラメータから取得 $userid = $_GET['userid']; $input_pass = $_GET['password']; // 表示して確認 echo "Input Userid:" . $userid . "<br>Input Password:" . $input_pass . "<br>"; // DB内に保存してある想定のハッシュ化されたパスワード。今回はハードコーディングしておく $hashed_pass = "\$6\$kdBX2lbhkgjOSs.t\$1VwBMMg5U/apz1dKb19oXHMW8wXy/kA0XwfpPWYy7xp4aB5uBGGzzYeiE0uUYv.QX9eiroROLk8RdHMTN6GTD/"; // 表示して確認 echo "crypt(\$input_pass, \$hashed_pass):" . "crypt('" . $input_pass . "', '" . $hashed_pass . "')" . "<br>"; echo "Hashed Password :" . $hashed_pass . "<br>"; // 入力された生パスワードをハッシュ化してDB内のパスと照合 if (crypt($input_pass, $hashed_pass) == $hashed_pass) { echo 'Password is valid!'; } else { echo crypt($input_pass, $hashed_pass) . "<br>"; echo 'Invalid password.'; }
その他、
CRYPT_MD5 - $1$ ではじまる 12 文字の salt を使用する MD5 ハッシュ。
$1$12doKmjG$5CwUdecskAuxOb7k24yzz/
$1$ は MD5 ハッシュ値を意味して $1$12doKmjG$ までがソルト
CRYPT_BLOWFISH - Blowfish ハッシュ。salt の形式は、$2y$+2桁のコストパラメータ+$"./0-9A-Za-z"からなる22文字。この範囲外の文字を salt に使うと、crypt() は長さゼロの文字列を返します。
crypt('test', '$2y$04$1234567890123456789012');
$2rcByx51ejoM
$crypt = crypt('test', '$2y$04$1234567890123456789012');
$2y$04$123456789012345678901upHHr.tvk59gEcVReva1VSf3jvDcJ8we
password_hash() は crypt() のシンプルなラッパーであり、既存のパスワードハッシュと互換性があります。 password_hash() を使うことを推奨します。
PHP 8.2 で試す
PHP: password_hash - Manual : PHPでパスワードハッシュを作る関数
password_hash("入力パスワード", "アルゴリズム", [オプション])
(PHP 5 >= 5.5.0, PHP 7, PHP 8)
ハッシュアルゴリズムは幾つか用意されていますが、PASSWORD_DEFAULT を使うと出力の一例は次のようになります。
入力
password_hash("test", PASSWORD_DEFAULT)
出力 ↓
$2y$10$2e1JFJU80lBqrHAT2N78Qes/cl31WwDF0KPg/ziOzy4uUONs4IZmi
オプションの PASSWORD_DEFAULT は定数で、PHP8現在この定数の値は PASSWORD_BCRYPT です。新しくてより強力なアルゴリズムが PHPに追加されれば、 この定数もそれにあわせて変わっていきます。
PHP: 定義済み定数 - Manual
PASSWORD_BCRYPT (=PASSWORD_DEFAULT) を使うと、CRYPT_BLOWFISH アルゴリズムで新たなパスワードハッシュを作ります。
これは標準の crypt() 互換のハッシュで、識別子 "$2y$" を使った場合の結果を作ります。 その結果は、常に 60 文字の文字列になります。結果をデータベースに格納するときにはカラム幅を 60 文字以上にできるようなカラムを使うことをお勧めします (DEFAULTの長さは将来的に変わる可能性もあるため)255 文字くらいが適切でしょう。*1
CRYPT_BLOWFISH には "$2x$"や"$2a$"もありますが、潜在的に弱いハッシュでであり、新しくハッシュを生成する場合は "$2y$" を使うべきです。*2
ですので、先の出力例は $2y$ の計60文字で出力されます。$10$の「10」はストレッチングの回数に関するパラメータです(10回という意味ではありません)。その後に続く2e1JF...はソルトとハッシュ値です。
PHP: password_verify - Manual
password_verify("入力されたパスワード", "password_hash() で作ったハッシュ値")
パスワードがハッシュにマッチするかどうかを調べる(PHP 5 >= 5.5.0, PHP 7, PHP 8)。password_verify側にはアルゴリズム指定はなく、ハッシュ値からアルゴリズムを自動的に読み取る。
PHP: password_needs_rehash - Manual
password_needs_rehash("password_hash() が作ったハッシュ値", アルゴリズム定数, オプション)
指定したハッシュがオプションにマッチするかどうかを調べる。(ハッシュアルゴリズムやオプションが変更されたかを確認します)(PHP 5 >= 5.5.0, PHP 7, PHP 8)
# php -v PHP 8.2.10 (cli) (built: Sep 20 2023 18:21:31) (NTS) Copyright (c) The PHP Group Zend Engine v4.2.10, Copyright (c) Zend Technologies # php -r "echo password_hash('a', PASSWORD_DEFAULT), PHP_EOL;" $2y$10$oVEPTIY1lAL92rHW7QXcsepvBy7ac/zO6hXuENNxnLDV9QIxTR31q
<?php $userid = $_GET['userid']; $input_pass = $_GET['password']; echo "Input Userid:" . $userid . "<br>Input Password:" . $input_pass . "<br>"; $algorithm = PASSWORD_DEFAULT; $hashed_DB_pass = '$2y$10$tiqIBDcjkQNIoN12P4c8ZODWwWaKPOBZPU54IPGxZ.YNpje0Jpr9a'; $hashed_input_pass = password_hash($input_pass, PASSWORD_DEFAULT); echo 'Hashed Password from Input Password:<br>' . $hashed_input_pass . '<br>'; // 格納されたハッシュを、平文のパスワードに対して検証します if (password_verify($input_pass, $hashed_DB_pass)) { // ハッシュアルゴリズムやオプションが変更されたかを確認します var_dump(password_needs_rehash($hashed_DB_pass, $algorithm)); echo '<br>'; if (password_needs_rehash($hashed_DB_pass, $algorithm)) { // 変更された場合は新しいハッシュを計算して、古いものを置き換えます $newHash = password_hash($password, $algorithm, $options); // ユーザーのレコードを $newHash で更新します } echo 'Password is valid!'; } else { echo 'Invalid password.'; }
DBのpwdカラムの桁数を増やす
[前回]指摘したように、BadTodoは初期状態で6文字しかパスワードに登録できないため増やします。
MariaDB [todo]> Desc users; +--------+--------------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +--------+--------------+------+-----+---------+-------+ | id | int(11) | NO | | NULL | | | userid | varchar(64) | NO | | NULL | | | pwd | varchar(6) | NO | | NULL | | | email | varchar(64) | NO | | NULL | | | icon | varchar(128) | NO | | NULL | | | super | int(11) | NO | | 0 | | +--------+--------------+------+-----+---------+-------+ # pwdカラムを255桁に変更 > ALTER TABLE users MODIFY pwd varchar(255); > desc users; +--------+--------------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +--------+--------------+------+-----+---------+-------+ | id | int(11) | NO | | NULL | | | userid | varchar(64) | NO | | NULL | | | pwd | varchar(255) | YES | | NULL | | | email | varchar(64) | NO | | NULL | | | icon | varchar(128) | NO | | NULL | | | super | int(11) | NO | | 0 | | +--------+--------------+------+-----+---------+-------+ # 変更された
MySQL :: MySQL 8.0 リファレンスマニュアル :: 13.1.9 ALTER TABLE ステートメント