mysql_real_escape_string()
関数を使ってもSQLインジェクションの可能性はありますか?
このサンプル状況を検討してください。 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コードを通過する可能性のある注入を知っていますか?
次のようなクエリを考えます。
$iId = mysql_real_escape_string("1 OR 1=1");
$sSql = "SELECT * FROM table WHERE id = $iId";
mysql_real_escape_string()
はこれに対してあなたを守りません。 クエリ内で変数の周りに一重引用符(' '
)を使用しているという事実が、これに対してあなたを保護しています。 以下もオプションです。
$iId = (int)"1 OR 1=1";
$sSql = "SELECT * FROM table WHERE id = $iId";
短い答えははい、はい、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");
特定の状況では、それは複数の行を返します。ここで何が起こっているのかを分析しましょう:
文字セットの選択
mysql_query('SET NAMES gbk');
この攻撃が機能するためには、接続でサーバーが期待するエンコードが必要です。エンコードはASCIIのように'
をエンコードする必要があります。つまり0x27
and ASCII \
すなわち0x5c
。結局のところ、MySQL 5.6ではデフォルトで、big5
、cp932
、gb2312
、gbk
およびsjis
という5つのエンコーディングがサポートされています。ここでgbk
を選択します。
ここで、SET NAMES
の使用に注意することが非常に重要です。これにより、文字セットサーバー上が設定されます。 C API関数mysql_set_charset()
の呼び出しを使用した場合、問題ありません(2006年以降のMySQLリリース)。しかし、その理由についてはあと1分で...
ペイロード
このインジェクションに使用するペイロードは、バイトシーケンス0xbf27
で始まります。 gbk
では、これは無効なマルチバイト文字です。 latin1
では、文字列¿'
です。 latin1
andgbk
では、0x27
自体が'
文字であることに注意してください。
このペイロードを選択したのは、addslashes()
を呼び出した場合、\
文字の前にASCII 0x5c
、つまり'
を挿入するためです。そこで、gbk
の2文字のシーケンスである0xbf5c27
を作成しました:0xbf5c
の後に0x27
が続きます。または、言い換えると、valid文字の後にエスケープされていない'
が続きます。ただし、addslashes()
は使用していません。次のステップに進みます...
mysql_real_escape_string()
mysql_real_escape_string()
へのC API呼び出しは、接続文字セットを知っているという点でaddslashes()
と異なります。そのため、サーバーが予期している文字セットに対して適切にエスケープを実行できます。ただし、この時点まで、クライアントは接続にlatin1
を使用していると考えています。 serverにgbk
を使用していると伝えましたが、clientはまだlatin1
であると考えています。
したがって、mysql_real_escape_string()
を呼び出すとバックスラッシュが挿入され、「エスケープ」されたコンテンツに'
文字がぶら下がります!実際、gbk
文字セットの$var
を見ると、次のように表示されます。
縗 'OR 1 = 1/*
これは 正確に何 攻撃に必要です。
クエリ
この部分は単なる形式ですが、レンダリングされたクエリは次のとおりです。
SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1
おめでとうございます、あなたはmysql_real_escape_string()
を使用してプログラムを攻撃することに成功しました...
ひどくなる。 PDO
のデフォルトは、MySQLで準備されたステートメントをemulatingにします。つまり、クライアント側では、基本的に(Cライブラリ内の)mysql_real_escape_string()
を介してsprintfを実行します。つまり、次のようにするとインジェクションが成功します。
$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がネイティブに準備できないステートメントをエミュレートします:マニュアルでは listed ですが、適切なものを選択するように注意してくださいサーバーのバージョン)。
最初に、SET NAMES gbk
の代わりにmysql_set_charset('gbk')
を使用していた場合、これをすべて防止できたと言いました。 2006年以降、MySQLのリリースを使用している場合、それは事実です。
以前のMySQLリリースを使用している場合、mysql_real_escape_string()
の bug は、ペイロード内の文字などの無効なマルチバイト文字が、クライアントがエスケープしても1バイトとして扱われることを意味しました接続エンコーディングが正しく通知されていたため、この攻撃は引き続き成功します。このバグはMySQLで修正されました 4.1.2 、 5.0.22 および 5.1.11 。
しかし、最悪の部分は、PDO
は5.3.6までmysql_set_charset()
のC APIを公開しなかったため、以前のバージョンではcannot考えられるすべてのコマンドに対してこの攻撃を防ぎます。現在、 DSNパラメーター として公開されています。
最初に述べたように、この攻撃が機能するには、脆弱な文字セットを使用してデータベース接続をエンコードする必要があります。 utf8mb4
は脆弱ではないでありながら、すべてのUnicode文字をサポートできます。 MySQL 5.5.3以降でのみ利用可能です。代替手段は utf8
で、これも脆弱ではなく、Unicode全体をサポートできます Basic Multilingual Plane 。
または、 NO_BACKSLASH_ESCAPES
SQLモードを有効にすることができます。これにより、(特に)mysql_real_escape_string()
の操作が変更されます。このモードを有効にすると、0x27
は0x2727
ではなく0x5c27
に置き換えられるため、エスケーププロセスcannotは、以前に存在しなかった脆弱なエンコーディングで有効な文字を作成します(つまり0xbf27
はまだ0xbf27
など)-サーバーは引き続き文字列を無効として拒否します。ただし、このSQLモードの使用から生じる可能性のある別の脆弱性については、 @ eggyal's answer を参照してください。
次の例は安全です。
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_set_charset()
/$mysqli->set_charset()
/PDOのDSN charsetパラメーター(PHP≥5.3.6)または
utf8
/latin1
/ascii
/などのみを使用します)あなたは100%安全です。
そうでなければ、あなたは脆弱ですmysql_real_escape_string()
...を使用していても.
実は、それを通過できるものは何もありません、%
ワイルドカード以外に。攻撃者としてLIKE
ステートメントを使用していて、それを除外しない場合はログインとして%
だけを入力することができ、ユーザーのパスワードをブルートフォースする必要があります。データはそのようにクエリ自体に干渉することができないため、100%安全にするためにプリペアドステートメントを使用することをお勧めします。しかし、そのような単純なクエリでは、おそらく$login = preg_replace('/[^a-zA-Z0-9_]/', '', $login);
のようなことをするほうが効率的でしょう。
私はこれに直面しています、そして私はあなたがPDOで働くことをお勧めしますが、場合によってはこの方法を試してみたいかもしれません。それはうまくいくし、とても簡単です。なぜ人々はそれを無視したのだろうか。
コードサンプル//私のフレームワークを使う Moorexa
それは豊富なORMとそれ以上のものをサポートしています。しかし私は生のSQL文を書く人々に代わるものを確実にするためにこのテストケースを実行しなければなりませんでした。
例です。
// checking from a user table
$check = DB::table('api_users')->get(['username' => "admin' or password='1'"])->run();
// expected output
SELECT * FROM api_users WHERE username='admin' or password='1'
//sql generated output
SELECT * FROM api_users WHERE username='admin\' or password=\'1\''
// lets try something heavy
$check = DB::table($table)->get(['username' => "admin' or 1=1 UNION SELECT password FROM api_users where id=1"])->run();
// expected output
SELECT * FROM api_users WHERE username='admin' or 1=1 UNION SELECT password FROM api_users where id=1
// this would pass and fail
SELECT * FROM api_users WHERE username='admin\' or 1=1 UNION SELECT password FROM api_users where id=1'
要点は何ですか。
選択クエリを実行するコード例を紹介します。
// let's assume. would all work
$input = ['username' => "moorexa"]; //or $_POST or $_GET
$sql = 'SELECT * FROM '.$table.' ';
$safe = "";
// let's grab the user input from the array
foreach ($input as $key => $val)
{
switch($val)
{
case is_string($val):
$safe .= $key .'=\''.addslashes($val).'\' AND ';
break;
case is_int($val):
$safe .= $key .'='.((int) $val).' AND ';
break;
case is_float($val):
case is_double($val):
$safe .= $key .'='.(double) $val.' AND ';
break;
default:
// this failed
}
}
$safe = rtrim($safe, "AND ");
$sql .= ' WHERE '. $safe .' ';
// now sql contains a valid statement. and would only fail when terms are not met.
// Hope you can apply this and also use more test cases.