はじめに
実務にて、ある Web サイトが SQL Injection によって数百万円の被害を生じた事件があったので、誰かが同じ轍を踏まないように SQL Injection の手口と対策について情報を纏めました。検証用のテーブルとソースコードをコピペ(コピー&ペースト)するだけで簡単にクラッキングの体験と対策ができます。
検証環境
SQL Injection とは
Web サイトのフォームやクエリー文字列に、SQL 文(データベースを操作する命令文)が書き変わってしまう値を入力して、データベースを不正に操作する攻撃手法です。
脅威
個人情報が漏洩した場合、銀行口座、クレジットカード情報、保険証番号などの不正利用、或いはブラックマーケットで売買される可能性があります。
システム情報が漏洩した場合、ログファイル改竄、バックドア設置、Web ページや送信メールを改竄してマルウェアが配布されるような被害を受ける可能性があります。
また、開発側は情報漏洩による損害賠償請求を受ける可能性があり、会社と社会に対して責任を取ることになります。
手口
百考は一行に如かず。クラッカーになったつもりでユーザーログイン画面をクラッキングします。
他人様のユーザーログイン画面をクラッキングすると不正アクセス禁止法違反で逮捕されてしまうので、MAMP などのローカル開発環境にユーザーログイン画面を自作して検証します。
まずは、ユーザーのテーブル(users)を作成します。
次は、ユーザーログイン画面(form.php)を作成します。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0">
</head>
<body>
<form action="login.php" method="post">
Email:<input type="email" name="email" required><br>
Password:<input type="password" name="password" required><br>
<input type="submit" value="ログイン">
</form>
</body>
</html>
ブラウザ側の表示です。
最後に、脆弱性があるユーザーログイン機能(login.php)を作って完成です。
<?php
const DB_HOST = 'localhost';
const DB_NAME = 'test';
const DB_CHARSET = 'utf8mb4';
const DB_USER = 'root';
const DB_PASS = 'root';
$dsn = 'mysql:dbname=' . DB_NAME . ';host=' . DB_HOST . ';charset=' . DB_CHARSET;
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false
];
$email = $_POST['email'] ?? '';
$password = $_POST['password'] ?? '';
$message = '';
try {
$pdo = new PDO($dsn, DB_USER, DB_PASS, $options);
$statement = $pdo->prepare('
SELECT *
FROM users
WHERE email = \'' . $email . '\'
AND password = \'' . $password . '\'
');
$statement->execute();
$member = $statement->fetch();
if ($member) $message = 'ログインに成功しました!';
else $message = 'ログインに失敗しました!';
} catch (PDOException $e) {
$message = 'エラーが発生しました。';
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0">
</head>
<body>
<p><?= $message ?></p>
<a href="form.php">戻る</a>
</body>
</html>
それでは、クラッキングしてみます。
手始めに、Wappalyzer や Chrome DevTools などを利用してシステムが何の言語やライブラリで作られているか解析をします。
結果、PHP でユーザーログイン機能が作られていることがわかったので、セキュリティ意識の低いエンジニアがプログラムを組んでいた場合の攻撃を試します。
ユーザーログイン画面の Email に例示用のメールアドレス(test@example.com)を入力して、Password に下記の文字列を入力します
※ --
(コメント構文)の後に半角スペースが必要です。
' OR 1 = 1 --
ブラウザ側の表示です。
そして、ログインボタンを押下すると不正にログインすることができました。
仕掛けは単純です。
ユーザーログイン機能の PDO::prepare を注意深く見ると、ログインフォームの入力値がそのまま SQL 文として解釈される作りになっています。
$email = $_POST['email'] ?? '';
$password = $_POST['password'] ?? '';
$statement = $pdo->prepare('
SELECT *
FROM users
WHERE email = \'' . $email . '\'
AND password = \'' . $password . '\'
');
つまり、ログインフォームの入力値で SQL 文の検索条件(WHERE 句)を書き換えてしまえば不正にログインできることがわかります。
先ほどの入力値によって PDO::prepare で解釈される SQL 文は下記のように組み立てられます。
SELECT *
FROM users
WHERE email = 'test@example.com'
AND password = ''
OR 1 = 1 -- '
検索条件(WHERE 句)に、必ず true になる OR
条件が追加されて、--
(コメント構文)で以降を無視しています。つまり、メールアドレスとパスワードが一致していなくてもログインに成功します。
また、PDO::ATTR_EMULATE_PREPARES(プリペアドステートメントのエミュレーションを有効または無効にする) が true だった場合、より凶悪な攻撃が可能です。
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
// PDO::ATTR_EMULATE_PREPARES => false
];
例えば、ログインフォームの Password に下記の文字列を入力すると、全てのユーザーを抹消できます。
' OR 1 = 1;TRUNCATE TABLE users --
これは、;
(セミコロン)で SQL 文を一度終わらせて、続けて新しい SQL 文(全てのユーザーを削除)を追加しています。
このように、複数のクエリーを実行できる設定であれば、任意の SQL 文を組み立てて多種多様な攻撃が可能になります。
対策
とても簡単です。
ログインフォームの入力値を PDOStatement::bindValue でバインドさせるだけです。
$statement = $pdo->prepare('
SELECT *
FROM users
WHERE email = \':email\'
AND password = \':$password\'
');
$statement->bindValue(':email', $email, PDO::PARAM_STR);
$statement->bindValue(':$password', $password, PDO::PARAM_STR);
$statement->execute();
たったこれだけです、解釈をさせてから実行する。
仕組みについては、PHP マニュアルにある プリペアドステートメントおよびストアドプロシージャ に詳しい情報があります。
また、プリペアドステートメントは SQL 文を解釈と実行に分けて負荷分散できるので、大規模なバッチ処理に利用することでパフォーマンスの向上に期待できます。
以上です。
おわりに
クライアントに「どう落とし前つけるんじゃ!」と言われても冷静に情報を整理して対応しましょう。