関数を使用した場合でも SQL インジェクションの可能性はありますかmysql_real_escape_string()
?
次のサンプル状況を考えてみましょう。SQL は PHP で次のように構築されます。
$login = mysql_real_escape_string(GetFromPost('login'));
$password = mysql_real_escape_string(GetFromPost('password'));
$sql = "SELECT * FROM table WHERE login='$login' AND password='$password'";
そのようなコードは依然として危険であり、関数を使用してもハッキングされる可能性があると多くの人が言っているのを聞いたことがありますmysql_real_escape_string()
。しかし、可能性のあるエクスプロイトは思いつきません。
典型的な注射は次のようになります:
aaa' OR 1=1 --
動作しない。
上記の PHP コードを通過する可能性のあるインジェクションについてご存知ですか?
ベストアンサー1
簡単に答えると、はい、回避する方法がありますmysql_real_escape_string()
。#非常にわかりにくいエッジケースの場合!!!
長い答えはそう簡単ではありません。それは攻撃に基づいていますここで実証。
攻撃
それでは、まずは攻撃の様子から見ていきましょう...
mysql_query('SET NAMES gbk');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");
状況によっては、1 行以上が返されることがあります。ここで何が起こっているのか分析してみましょう。
文字セットの選択
mysql_query('SET NAMES gbk');
この攻撃が成功するには、サーバーが接続時に想定しているエンコーディングが
'
ASCII でエンコードされていること (つまり )0x27
、および最終バイトが ASCII である文字が含まれていること (つまり )\
が必要0x5c
です。MySQL 5.6 では、デフォルトで 、 、 、 の 5 つのエンコーディングがサポートされています。big5
ここcp932
でgb2312
はgbk
選択sjis
しますgbk
。ここで、 の使用に注意することが非常に重要です。これは、サーバー上の
SET NAMES
文字セットを設定します。C API 関数 の呼び出しを使用すれば、問題ありません (2006 年以降の MySQL リリースの場合)。ただし、その理由については後ほど詳しく説明します...mysql_set_charset()
ペイロード
このインジェクションに使用するペイロードは、バイト シーケンス で始まります
0xbf27
。 ではgbk
、これは無効なマルチバイト文字です。 ではlatin1
、これは文字列 です¿'
。latin1
およびgbk
では、0x27
それ自体はリテラル'
文字であることに注意してください。このペイロードを選択したのは、これを呼び出すと、文字の前に
addslashes()
ASCII が挿入されるから\
です。その結果、 になり、これは2 つの文字シーケンス の後に が続きます。つまり、有効な文字の後にエスケープされていない が続きます。ただし、 は使用しません。それでは、次のステップに進みます...0x5c
'
0xbf5c27
gbk
0xbf5c
0x27
'
addslashes()
mysql_real_escape_string()
への C API 呼び出しは、接続文字セットを認識している点
mysql_real_escape_string()
で と異なりますaddslashes()
。そのため、サーバーが期待する文字セットに対して適切にエスケープを実行できます。ただし、この時点では、クライアントは接続にまだ を使用していると考えています。これは、他の方法を指示していないためです。サーバーにを使用しているlatin1
ことを伝えましたが、クライアントは依然としてであると考えています。gbk
latin1
したがって、 の呼び出しによって
mysql_real_escape_string()
バックスラッシュが挿入され、「エスケープされた」コンテンツに自由にぶら下がっている文字が存在します。実際、文字セットを'
見ると、次のようになります。$var
gbk
縗' OR 1=1 /*
それはまさに攻撃には必要です。
クエリ
この部分は単なる形式的なものですが、レンダリングされたクエリは次のとおりです。
SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1
おめでとうございます。... を使用してプログラムへの攻撃に成功しましたmysql_real_escape_string()
。
悪い人
さらに悪いことに、 MySQL で準備されたステートメントをエミュレートするPDO
のがデフォルトです。つまり、クライアント側では基本的に sprintf が実行されます(C ライブラリ内)。つまり、次の処理はインジェクションに成功します。mysql_real_escape_string()
$pdo->query('SET NAMES gbk');
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));
ここで注目すべきは、エミュレートされた準備済みステートメントを無効にすることでこれを防ぐことができるということです。
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
これは通常、真の準備されたステートメント(つまり、クエリとは別のパケットでデータが送信される)になります。ただし、PDOは暗黙的に後退するMySQLがネイティブに準備できないステートメントをエミュレートします。リストされているマニュアルに記載されていますが、適切なサーバー バージョンを選択するように注意してください。
ぶさいく
冒頭で、mysql_set_charset('gbk')
の代わりにを使っていれば、これらすべてを回避できたはずだと言いましたSET NAMES gbk
。これは、2006 年以降の MySQL リリースを使用している場合に当てはまります。
以前のMySQLリリースを使用している場合は、バグではmysql_real_escape_string()
、ペイロード内の無効なマルチバイト文字は、クライアントが接続エンコーディングを正しく通知されていたとしても、エスケープの目的で1バイトとして扱われ、この攻撃は成功していました。このバグはMySQLで修正されました。4.1.20、5.0.22そして5.1.11。
しかし最悪なのは、 5.3.6PDO
までC APIが公開されていなかったためmysql_set_charset()
、以前のバージョンではあらゆるコマンドに対してこの攻撃を防ぐことができなかったことです。DSNパラメータ。
救いの恵み
冒頭で述べたように、この攻撃が機能するには、データベース接続が脆弱な文字セットを使用してエンコードされている必要があります。utf8mb4
脆弱性はなく、すべてのUnicode文字をサポートしているので、代わりにそれを使用することを選択できますが、MySQL 5.5.3以降でのみ利用可能です。代替案としては、utf8
これも脆弱性がなく、Unicode全体をサポートできる。基本的な多言語面。
あるいは、NO_BACKSLASH_ESCAPES
SQL モードは、(他の機能の中でも) の動作を変更しますmysql_real_escape_string()
。このモードを有効にすると、はではなく に0x27
置き換えられ、したがって、エスケープ処理では、脆弱なエンコーディングで以前に存在しなかった有効な文字を作成できません(つまり、はまだなど)。そのため、サーバーは文字列を無効として拒否します。ただし、0x2727
0x5c27
0xbf27
0xbf27
@eggyal の回答この SQL モードの使用によって発生する可能性のある別の脆弱性について説明します。
安全な例
次の例は安全です:
mysql_query('SET NAMES utf8');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");
サーバーが期待しているためutf8
...
mysql_set_charset('gbk');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");
クライアントとサーバーが一致するように文字セットを適切に設定したためです。
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$pdo->query('SET NAMES gbk');
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));
エミュレートされた準備済みステートメントをオフにしたためです。
$pdo = new PDO('mysql:host=localhost;dbname=testdb;charset=gbk', $user, $password);
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));
文字セットを適切に設定したためです。
$mysqli->query('SET NAMES gbk');
$stmt = $mysqli->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$param = "\xbf\x27 OR 1=1 /*";
$stmt->bind_param('s', $param);
$stmt->execute();
MySQLi は常に真の準備済みステートメントを実行するためです。
まとめ
もし、あんたが:
- 最新バージョンの MySQL (5.1 以降、5.5 以降、5.6 など)と PDO の DSN 文字セット パラメータ (PHP ≥ 5.3.6) を
mysql_set_charset()
使用します。$mysqli->set_charset()
または
- 接続エンコーディングに脆弱な文字セットを使用しないでください(
utf8
/latin1
/ascii
/ などのみを使用します)
あなたは100%安全です。
そうでなければ、使用しているにもかかわらずmysql_real_escape_string()
脆弱になります...